commit 8f31d4ed4157542aa4e1bc4e2267befde5499941 Author: one Date: Thu Mar 6 11:32:45 2025 +0700 init: sudah ganti logo, hilangin setting, dan investigational use dialog diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 0000000..d10bb62 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,6 @@ +# Browsers that we support + +> 1% +IE 11 +not dead +not op_mini all diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..8079e8e --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,495 @@ +version: 2.1 + +orbs: + codecov: codecov/codecov@1.0.5 + cypress: cypress-io/cypress@3.4.2 + +defaults: &defaults + docker: + - image: cimg/node:20.18.1 + environment: + TERM: xterm + QUICK_BUILD: true + working_directory: ~/repo + +commands: + install_bun: + steps: + - restore_cache: + keys: + - bun-cache-v2-{{ arch }}-latest + - run: + name: Install Bun + command: | + if [ ! -d "$HOME/.bun" ]; then + curl -fsSL https://bun.sh/install | bash + fi + echo 'export BUN_INSTALL="$HOME/.bun"' >> $BASH_ENV + echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> $BASH_ENV + source $BASH_ENV + - save_cache: + key: bun-cache-v2-{{ arch }}-latest + paths: + - ~/.bun + +jobs: + UNIT_TESTS: + <<: *defaults + resource_class: large + steps: + - install_bun + - run: node --version + - checkout + - run: + name: Install Dependencies + command: bun install --no-save + # RUN TESTS + - run: + name: 'JavaScript Test Suite' + command: bun run test:unit:ci + # platform/app + - run: + name: 'VIEWER: Combine report output' + command: | + viewerCov="/home/circleci/repo/platform/app/coverage" + touch "${viewerCov}/reports" + cat "${viewerCov}/clover.xml" >> "${viewerCov}/reports" + echo "\<<\<<\<< EOF" >> "${viewerCov}/reports" + cat "${viewerCov}/lcov.info" >>"${viewerCov}/reports" + echo "\<<\<<\<< EOF" >> "${viewerCov}/reports" + - codecov/upload: + file: '/home/circleci/repo/platform/app/coverage/reports' + flags: 'viewer' + # PLATFORM/CORE + - run: + name: 'CORE: Combine report output' + command: | + coreCov="/home/circleci/repo/platform/core/coverage" + touch "${coreCov}/reports" + cat "${coreCov}/clover.xml" >> "${coreCov}/reports" + echo "\<<\<<\<< EOF" >> "${coreCov}/reports" + cat "${coreCov}/lcov.info" >> "${coreCov}/reports" + echo "\<<\<<\<< EOF" >> "${coreCov}/reports" + - codecov/upload: + file: '/home/circleci/repo/platform/core/coverage/reports' + flags: 'core' + + BUILD: + <<: *defaults + resource_class: large + steps: + # Checkout code and ALL Git Tags + - checkout + - install_bun + - run: + name: Install Dependencies + command: bun install --no-save + # Build & Test + - run: + name: 'Perform the versioning before build' + command: bun ./version.mjs + - run: + name: 'Build the OHIF Viewer' + command: bun run build + no_output_timeout: 45m + - run: + name: 'Upload SourceMaps, Send Deploy Notification' + command: | + # export FILE_1=$(find ./build/static/js -type f -name "2.*.js" -exec basename {} \;) + # export FILE_MAIN=$(find ./build/static/js -type f -name "main.*.js" -exec basename {} \;) + # export FILE_RUNTIME_MAIN=$(find ./build/static/js -type f -name "runtime~main.*.js" -exec basename {} \;) + # curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_1.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_1 + # curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_MAIN + # curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_RUNTIME_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_RUNTIME_MAIN + curl --request POST https://api.rollbar.com/api/1/deploy/ -F access_token=$ROLLBAR_TOKEN -F environment=$GOOGLE_STORAGE_BUCKET -F revision=$CIRCLE_SHA1 -F local_username=CircleCI + # Persist :+1: + - persist_to_workspace: + root: ~/repo + paths: + - platform/app/dist + - Dockerfile + - version.txt + - commit.txt + - version.json + + BUILD_PACKAGES_QUICK: + <<: *defaults + resource_class: large + steps: + - install_bun + # Checkout code and ALL Git Tags + - checkout + - attach_workspace: + at: ~/repo + - run: + name: Install Dependencies + command: bun install --frozen-lockfile + - run: + name: Avoid hosts unknown for github + command: | + rm -rf ~/.ssh + mkdir ~/.ssh/ + echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config + git config --global user.email "danny.ri.brown+ohif-bot@gmail.com" + git config --global user.name "ohif-bot" + - run: + name: Authenticate with NPM registry + command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc + - run: + name: build half of the packages (to avoid out of memory in circleci) + command: | + bun run build:package-all + - run: + name: build the other half of the packages + command: | + bun run build:package-all-1 + + NPM_PUBLISH: + <<: *defaults + resource_class: large + steps: + - install_bun + # Checkout code and ALL Git Tags + - checkout + - attach_workspace: + at: ~/repo + - run: + name: Install Dependencies + command: bun install --no-save + - run: + name: Avoid hosts unknown for github + command: | + rm -rf ~/.ssh + mkdir ~/.ssh/ + echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config + git config --global user.email "danny.ri.brown+ohif-bot@gmail.com" + git config --global user.name "ohif-bot" + - run: + name: Authenticate with NPM registry + command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc + - run: + name: build half of the packages (to avoid out of memory in circleci) + command: | + bun run build:package-all + - run: + name: build the other half of the packages + command: | + bun run build:package-all-1 + - run: + name: increase min time out + command: | + npm config set fetch-retry-mintimeout 20000 + - run: + name: increase max time out + command: | + npm config set fetch-retry-maxtimeout 120000 + - run: + name: publish package versions + command: | + bun ./publish-version.mjs + - run: + name: Again set the NPM registry (was deleted in the version script) + command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc + - run: + name: publish package dist + command: | + bun ./publish-package.mjs + - persist_to_workspace: + root: ~/repo + paths: + - . + + DOCKER_RELEASE_PUBLISH: + <<: *defaults + resource_class: large + steps: + - attach_workspace: + at: ~/repo + - setup_remote_docker: + docker_layer_caching: false + - run: + name: Build Docker image for amd64 + command: | + # This file will exist if a new version was published by + # our command in the previous job. + if [[ ! -e version.txt ]]; then + exit 0 + else + # Remove npm config + rm -f ./.npmrc + # Set our version number using vars + export IMAGE_VERSION=$(cat version.txt) + export IMAGE_VERSION_FULL=v$IMAGE_VERSION + echo $IMAGE_VERSION + echo $IMAGE_VERSION_FULL + # Build our amd64 image, auth, and push + docker build --platform linux/amd64 --tag ohif/app:$IMAGE_VERSION_FULL-amd64 --tag ohif/app:latest-amd64 . + echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin + docker push ohif/app:$IMAGE_VERSION_FULL-amd64 + docker push ohif/app:latest-amd64 + fi + - persist_to_workspace: + root: ~/repo + paths: + - . + + DOCKER_RELEASE_PUBLISH_ARM: + <<: *defaults + resource_class: arm.large + steps: + - attach_workspace: + at: ~/repo + - setup_remote_docker: + docker_layer_caching: false + - run: + name: Build Docker image for arm64 + command: | + # This file will exist if a new version was published by + # our command in the previous job. + if [[ ! -e version.txt ]]; then + exit 0 + else + # Remove npm config + rm -f ./.npmrc + # Set our version number using vars + export IMAGE_VERSION=$(cat version.txt) + export IMAGE_VERSION_FULL=v$IMAGE_VERSION + echo $IMAGE_VERSION + echo $IMAGE_VERSION_FULL + # Build our arm64 image, auth, and push + docker build --platform linux/arm64 --tag ohif/app:$IMAGE_VERSION_FULL-arm64 --tag ohif/app:latest-arm64 . + echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin + docker push ohif/app:$IMAGE_VERSION_FULL-arm64 + docker push ohif/app:latest-arm64 + fi + - persist_to_workspace: + root: ~/repo + paths: + - . + + DOCKER_BETA_PUBLISH: + <<: *defaults + resource_class: large + steps: + - attach_workspace: + at: ~/repo + - setup_remote_docker: + docker_layer_caching: false + - run: + name: Build Docker image for amd64 (Beta) + command: | + echo $(ls -l) + + # This file will exist if a new version was published by + # our command in the previous job. + if [[ ! -e version.txt ]]; then + echo "don't have version txt" + exit 0 + else + echo "Building and pushing Docker image from the master branch (beta releases)" + rm -f ./.npmrc + + # Set our version number using vars + export IMAGE_VERSION=$(cat version.txt) + export IMAGE_VERSION_FULL=v$IMAGE_VERSION + echo $IMAGE_VERSION + echo $IMAGE_VERSION_FULL + # Build our amd64 image, auth, and push + docker build --platform linux/amd64 --tag ohif/app:$IMAGE_VERSION_FULL-amd64 --tag ohif/app:latest-beta-amd64 . + echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin + docker push ohif/app:$IMAGE_VERSION_FULL-amd64 + docker push ohif/app:latest-beta-amd64 + fi + + DOCKER_BETA_PUBLISH_ARM: + <<: *defaults + resource_class: arm.large + steps: + - attach_workspace: + at: ~/repo + - setup_remote_docker: + docker_layer_caching: false + - run: + name: Build Docker image for arm64 (Beta) + command: | + echo $(ls -l) + + # This file will exist if a new version was published by + # our command in the previous job. + if [[ ! -e version.txt ]]; then + echo "don't have version txt" + exit 0 + else + echo "Building and pushing ARM64 Docker image from the master branch (beta releases)" + rm -f ./.npmrc + # Set our version number using vars + export IMAGE_VERSION=$(cat version.txt) + export IMAGE_VERSION_FULL=v$IMAGE_VERSION + echo $IMAGE_VERSION + echo $IMAGE_VERSION_FULL + # Build our arm64 image, auth, and push + docker build --platform linux/arm64 --tag ohif/app:$IMAGE_VERSION_FULL-arm64 --tag ohif/app:latest-beta-arm64 . + echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin + docker push ohif/app:$IMAGE_VERSION_FULL-arm64 + docker push ohif/app:latest-beta-arm64 + fi + + CYPRESS: + <<: *defaults + resource_class: large + parallelism: 8 + steps: + - install_bun + - run: + name: Install System Dependencies + command: | + sudo apt-get update + sudo apt-get install -y xvfb libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 + - run: + name: Start Xvfb + command: Xvfb :99 -screen 0 1920x1080x24 & + background: true + - run: + name: Export Display Variable + command: export DISPLAY=:99 + - cypress/install: + install-command: bun install --no-save + package-manager: yarn + - cypress/run-tests: + cypress-command: | + npx wait-on@latest http://localhost:3000 && cd platform/app && npx cypress run --record --parallel + start-command: bun run test:data && bun run test:e2e:serve + + DOCKER_MULTIARCH_MANIFEST: + <<: *defaults + resource_class: large + steps: + - attach_workspace: + at: ~/repo + - setup_remote_docker: + docker_layer_caching: false + - run: + name: Create and push multi-architecture manifest + command: | + # This file will exist if a new version was published by + # our command in the previous job. + if [[ ! -e version.txt ]]; then + exit 0 + else + echo "Building and pushing multi-architecture manifest from the master branch (release releases)" + rm -f ./.npmrc + # Set our version number using vars + export IMAGE_VERSION=$(cat version.txt) + export IMAGE_VERSION_FULL=v$IMAGE_VERSION + echo $IMAGE_VERSION + echo $IMAGE_VERSION_FULL + echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin + + # Create and push manifest for specific version + docker manifest create ohif/app:$IMAGE_VERSION_FULL \ + --amend ohif/app:$IMAGE_VERSION_FULL-amd64 \ + --amend ohif/app:$IMAGE_VERSION_FULL-arm64 + docker manifest push ohif/app:$IMAGE_VERSION_FULL + + # Create and push manifest for "latest" tag + docker manifest create ohif/app:latest \ + --amend ohif/app:latest-amd64 \ + --amend ohif/app:latest-arm64 + docker manifest push ohif/app:latest + fi + + DOCKER_BETA_MULTIARCH_MANIFEST: + <<: *defaults + resource_class: large + steps: + - attach_workspace: + at: ~/repo + - setup_remote_docker: + docker_layer_caching: false + - run: + name: Create and push multi-architecture manifest (Beta) + command: | + echo $(ls -l) + + # This file will exist if a new version was published by + # our command in the previous job. + if [[ ! -e version.txt ]]; then + exit 0 + else + echo "Building and pushing multi-architecture manifest from the master branch (beta releases)" + rm -f ./.npmrc + # Set our version number using vars + export IMAGE_VERSION=$(cat version.txt) + export IMAGE_VERSION_FULL=v$IMAGE_VERSION + echo $IMAGE_VERSION + echo $IMAGE_VERSION_FULL + echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin + + # Create and push manifest for specific beta version + docker manifest create ohif/app:$IMAGE_VERSION_FULL \ + --amend ohif/app:$IMAGE_VERSION_FULL-amd64 \ + --amend ohif/app:$IMAGE_VERSION_FULL-arm64 + docker manifest push ohif/app:$IMAGE_VERSION_FULL + + # Create and push manifest for "latest-beta" tag + docker manifest create ohif/app:latest-beta \ + --amend ohif/app:latest-beta-amd64 \ + --amend ohif/app:latest-beta-arm64 + docker manifest push ohif/app:latest-beta + fi + +workflows: + PR_CHECKS: + jobs: + - BUILD_PACKAGES_QUICK: + filters: + branches: + ignore: master + - UNIT_TESTS + - CYPRESS: + name: 'Cypress Tests' + context: cypress + + # viewer-dev.ohif.org + DEPLOY_MASTER: + jobs: + - BUILD: + filters: + branches: + only: master + - NPM_PUBLISH: + requires: + - BUILD + - DOCKER_BETA_PUBLISH: + requires: + - NPM_PUBLISH + - DOCKER_BETA_PUBLISH_ARM: + requires: + - DOCKER_BETA_PUBLISH + - DOCKER_BETA_MULTIARCH_MANIFEST: + requires: + - DOCKER_BETA_PUBLISH_ARM + + # viewer.ohif.org + DEPLOY_RELEASE: + jobs: + - BUILD: + filters: + branches: + only: /^release\/.*/ + - HOLD_FOR_APPROVAL: + type: approval + requires: + - BUILD + - NPM_PUBLISH: + requires: + - HOLD_FOR_APPROVAL + - DOCKER_RELEASE_PUBLISH: + requires: + - NPM_PUBLISH + - DOCKER_RELEASE_PUBLISH_ARM: + requires: + - DOCKER_RELEASE_PUBLISH + - DOCKER_MULTIARCH_MANIFEST: + requires: + - DOCKER_RELEASE_PUBLISH_ARM diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000..ab57ad1 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,6 @@ +[codespell] +skip = .git,*.pdf,*.svg,yarn.lock,*.min.js,locales +# ignore words ending with โ€ฆ and some camelcased variables and names +ignore-regex = \b\S+โ€ฆ\S*|\b(doubleClick|afterAll|PostgresSQL)\b|\bWee, L\.|.*te.*Telugu.* +# some odd variables +ignore-words-list = datea,ser,childrens diff --git a/.docker/README.md b/.docker/README.md new file mode 100644 index 0000000..ca8d4fb --- /dev/null +++ b/.docker/README.md @@ -0,0 +1,43 @@ +# Docker compose files + +This folder contains docker-compose files used to spin up OHIF-Viewer with +different options such as locally or with any PAS you desire to + +## Public Server + +## Local Orthanc + +### Build + +`$ docker-compose -f docker-compose-orthanc.yml build` + +### Run + +Starts containers and leaves them running in the background. + +`$ docker-compose -f docker-compose-orthanc.yml up -d` + +then, access the application at [http://localhost](http://localhost) + +**remember that you have to access orthanc application and include your studies +there** + +## Local Dcm4chee + +#### build + +`$ docker-compose -f docker-compose-dcm4chee.yml build` + +#### run + +`$ docker-compose -f docker-compose-dcm4chee.yml up -d` + +then, access the application at [http://localhost](http://localhost) + +**remember that you have to access dcm4chee application and include your studies +there** You can use the following command to import your studies into dcm4che + +`$ docker run -v {YOUR_STUDY_FOLDER}:/tmp --rm --network=docker_dcm4che_default dcm4che/dcm4che-tools:5.14.0 storescu -cDCM4CHEE@arc:11112 /tmp` + +**make sure that your Docker network name is docker_dcm4chee_default or change +it to the right one** diff --git a/.docker/Viewer-v3.x/default.conf.template b/.docker/Viewer-v3.x/default.conf.template new file mode 100644 index 0000000..bbee324 --- /dev/null +++ b/.docker/Viewer-v3.x/default.conf.template @@ -0,0 +1,21 @@ +server { + gzip_static always; + gzip_proxied expired no-cache no-store private auth; + gunzip on; + listen ${PORT} default_server; + listen [::]:${PORT} default_server; + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + add_header Cross-Origin-Resource-Policy same-origin; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + } + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/.docker/Viewer-v3.x/default.ssl.conf.template b/.docker/Viewer-v3.x/default.ssl.conf.template new file mode 100644 index 0000000..04a36da --- /dev/null +++ b/.docker/Viewer-v3.x/default.ssl.conf.template @@ -0,0 +1,20 @@ +server { + listen ${SSL_PORT} ssl http2 default_server; + listen [::]:${SSL_PORT} ssl http2 default_server; + ssl_certificate /etc/ssl/certs/ssl-certificate.crt; + ssl_certificate_key /etc/ssl/private/ssl-private-key.key; + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + add_header Cross-Origin-Resource-Policy same-origin; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + } + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/.docker/Viewer-v3.x/entrypoint.sh b/.docker/Viewer-v3.x/entrypoint.sh new file mode 100644 index 0000000..c0801a0 --- /dev/null +++ b/.docker/Viewer-v3.x/entrypoint.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +if [ -n "$SSL_PORT" ] + then + envsubst '${SSL_PORT}:${PORT}' < /usr/src/default.ssl.conf.template > /etc/nginx/conf.d/default.conf + else + envsubst '${PORT}' < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf +fi + + +if [ -n "$APP_CONFIG" ]; then + echo "$APP_CONFIG" > /usr/share/nginx/html${PUBLIC_URL}app-config.js + echo "Using custom APP_CONFIG environment variable" +else + echo "Not using custom APP_CONFIG" +fi + +if [ -f /usr/share/nginx/html${PUBLIC_URL}app-config.js ]; then + if [ -s /usr/share/nginx/html${PUBLIC_URL}app-config.js ]; then + echo "Detected non-empty app-config.js. Ensuring .gz file is updated..." + rm -f /usr/share/nginx/html${PUBLIC_URL}app-config.js.gz + gzip /usr/share/nginx/html${PUBLIC_URL}app-config.js + touch /usr/share/nginx/html${PUBLIC_URL}app-config.js + echo "Compressed app-config.js to app-config.js.gz" + else + echo "app-config.js is empty. Skipping compression." + fi +else + echo "No app-config.js file found. Skipping compression." +fi + + + +if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ] + then + # If CLIENT_ID is specified, use the google.js configuration with the modified ID + if [ -n "$CLIENT_ID" ] + then + echo "Google Cloud Healthcare \$CLIENT_ID has been provided: " + echo "$CLIENT_ID" + echo "Updating config..." + + # - Use SED to replace the CLIENT_ID that is currently in google.js + sed -i -e "s/YOURCLIENTID.apps.googleusercontent.com/$CLIENT_ID/g" /usr/share/nginx/html/google.js + fi + + # If HEALTHCARE_API_ENDPOINT is specified, use the google.js configuration with the modified endpoint + if [ -n "$HEALTHCARE_API_ENDPOINT" ] + then + echo "Google Cloud Healthcare \$HEALTHCARE_API_ENDPOINT has been provided: " + echo "$HEALTHCARE_API_ENDPOINT" + echo "Updating config..." + + # - Use SED to replace the HEALTHCARE_API_ENDPOINT that is currently in google.js + sed -i -e "s+https://healthcare.googleapis.com/v1+$HEALTHCARE_API_ENDPOINT+g" /usr/share/nginx/html/google.js + fi + + # - Copy google.js to overwrite app-config.js + cp /usr/share/nginx/html/google.js /usr/share/nginx/html/app-config.js +fi + +echo "Starting Nginx to serve the OHIF Viewer on ${PUBLIC_URL}" + +exec "$@" diff --git a/.docker/compressDist.sh b/.docker/compressDist.sh new file mode 100644 index 0000000..4aaf932 --- /dev/null +++ b/.docker/compressDist.sh @@ -0,0 +1,4 @@ +find platform/app/dist -name "*.js" -exec gzip -9 "{}" \; -exec touch "{}" \; +find platform/app/dist -name "*.map" -exec gzip -9 "{}" \; -exec touch "{}" \; +find platform/app/dist -name "*.css" -exec gzip -9 "{}" \; -exec touch "{}" \; +find platform/app/dist -name "*.svg" -exec gzip -9 "{}" \; -exec touch "{}" \; diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f906eff --- /dev/null +++ b/.dockerignore @@ -0,0 +1,37 @@ +# Reduces size of context and hides +# files from Docker (can't COPY or ADD these) + +# Note that typically the Docker context for various OHIF containers is the +# directory of this file (i.e. the root of the source). As such, this is +# the .dockerignore file for ALL Docker containers that are built. For example, +# the Docker containers built from the recipes in ./platform/app/.recipes will +# have this file as their .dockerignore. + +# Output +**/dist/ +**/build/ + +# Dependencies +**/node_modules/ + +# Root +README.md +Dockerfile +dockerfile + +# Misc. Config +.git +.DS_Store +.gitignore +.vscode +.circleci + +# Unnecessary things to pull into container +.circleci/ +.github/ +.netlify/ +.scripts/ +.vscode/ +coverage/ +platform/docs/ +testdata/ diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..b722f6a --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +config/** +docs/** +img/** +node_modules diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..67f24ea --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,31 @@ +{ + "plugins": ["@typescript-eslint", "import", "eslint-plugin-tsdoc", "prettier"], + "extends": [ + "react-app", + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "parser": "@typescript-eslint/parser", + "env": { + "jest": true + }, + "settings": { + "react": { + "version": "detect" + } + }, + "rules": { + // Enforce consistent brace style for all control statements for readability + "curly": "error", + "import/no-anonymous-default-export": "off" + }, + "globals": { + "cy": true, + "before": true, + "context": true, + "Cypress": true, + "assert": true + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d877674 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Set the default behavior, +# in case people don't have core.autocrlf set. +* text=auto +# Declares that files will always have CRLF line ends +*.sh text eol=lf diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..3685b64 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: https://giving.massgeneral.org/ohif diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..36a22e9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,84 @@ +name: 'Bug report' +description: Create a report to help us improve +title: '[Bug] ' +labels: ['Community: Report :bug:', 'Awaiting Reproduction'] + +body: + - type: markdown + attributes: + value: | + ๐Ÿ‘‹ Hello, and thank you for contributing to our project! Your support is greatly appreciated. + + ๐Ÿ” Before proceeding, please make sure to read our [Rules of Conduct](https://github.com/OHIF/Viewers/blob/master/CODE_OF_CONDUCT.md) and familiarize yourself with our [development process](https:/docs.ohif.org/development/our-process). + + โ“ If you're here to seek general support or ask a question, we encourage you to visit our [community discussion board](https://community.ohif.org/) + + ๐Ÿž For bug reports, please complete the following template in as much detail as possible. This will help us reproduce and address the issue efficiently. + + ๐Ÿงช Finally, ensure that you're using the latest version of the software and check if your issue has already been reported to avoid duplicates. + + - type: textarea + id: bug_description + attributes: + label: Describe the Bug + description: 'A clear and concise description of what the bug is.' + validations: + required: true + + - type: textarea + id: reproduction_steps + attributes: + label: Steps to Reproduce + description: 'Please describe the steps to reproduce the issue.' + placeholder: "1. First step\n2. Second step\n3. ..." + validations: + required: true + + - type: textarea + id: current_behavior + attributes: + label: The current behavior + description: + 'A clear and concise description of what happens instead of the expected behavior.' + validations: + required: true + + - type: textarea + id: expected_behavior + attributes: + label: The expected behavior + description: 'A clear and concise description of what you expected to happen.' + validations: + required: true + + - type: input + id: os + attributes: + label: 'OS' + description: 'Your operating system.' + placeholder: 'e.g., Windows 10, macOS 10.15.4' + validations: + required: true + - type: input + id: node-version + attributes: + label: 'Node version' + description: 'Your Node.js version.' + placeholder: 'e.g., 20.18.1' + validations: + required: true + - type: input + id: browser + attributes: + label: 'Browser' + description: 'Your browser.' + placeholder: 'e.g., Chrome 83.0.4103.116, Firefox 77.0.1, Safari 13.1.1' + validations: + required: true + + - type: markdown + attributes: + value: > + > :warning: Reports we cannot reproduce are at risk of being marked stale and > closed. The + more information you can provide, the more likely we are to look > into and address your + issue. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..bd22745 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: ๐Ÿค— Support Question + url: https://community.ohif.org/ + about: Please use our forum if you have questions or need help. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..0331ba9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,34 @@ +name: Feature request +description: Create a feature request +labels: ['Community: Request :hand:'] +title: '[Feature Request] ' +body: + - type: markdown + attributes: + value: | + ๐Ÿ‘‹ Hello and thank you for your interest in our project! + + ๐Ÿ” Before you proceed, please read our [Rules of Conduct](https://github.com/OHIF/Viewers/blob/master/CODE_OF_CONDUCT.md). + + ๐Ÿš€ If your request is specific to your needs, consider contributing it yourself! Read our [contributing guides](https://docs.ohif.org/development/contributing) to get started. + + ๐Ÿ–Š๏ธ Please provide as much detail as possible for your feature request. Mock-up screenshots, workflow or logic flow diagrams are very helpful. Discuss how your requested feature would interact with existing features. + + โฑ๏ธ Lastly, tell us why we should prioritize your feature. What impact would it have? + + - type: textarea + attributes: + label: 'What feature or change would you like to see made?' + description: + 'Please include as much detail as possible including possibly mock up screen shots, workflow + or logic flow diagrams etc.' + placeholder: '...' + validations: + required: true + - type: textarea + attributes: + label: 'Why should we prioritize this feature?' + description: 'Discuss if and how the requested feature interacts with existing features.' + placeholder: '...' + validations: + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..6a1753d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,93 @@ + + + + + + + +### Context + + + +### Changes & Results + + + +### Testing + + + +### Checklist + +#### PR + + + +- [] My Pull Request title is descriptive, accurate and follows the + semantic-release format and guidelines. + +#### Code + +- [] My code has been well-documented (function documentation, inline comments, + etc.) + +#### Public Documentation Updates + + + +- [] The documentation page has been updated as necessary for any public API + additions or removals. + +#### Tested Environment + +- [] OS: +- [] Node version: +- [] Browser: + + + +[blog]: https://circleci.com/blog/triggering-trusted-ci-jobs-on-untrusted-forks/ +[script]: https://github.com/jklukas/git-push-fork-to-upstream-branch + diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..28535cc --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,25 @@ +# GitHub App: Stale +# https://github.com/apps/stale +# +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 180 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 60 +# Issues with these labels will never be considered stale +exemptLabels: + - 'Bug: Verified :bug:' + - 'PR: Awaiting Review ๐Ÿ‘€' + - 'Announcement ๐ŸŽ‰' + - 'IDC:priority' + - 'IDC:candidate' + - 'IDC:collaboration' + - 'Community: Request :hand:' + - 'Community: Report :bug:' +# Label to use when marking an issue as stale +staleLabel: 'Stale :baguette_bread:' +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had recent activity. It will + be closed if no further activity occurs. Thank you for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..9bc4e59 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,76 @@ +name: Playwright Tests +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] +jobs: + playwright-tests: + timeout-minutes: 60 + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright:v1.50.0-noble + strategy: + fail-fast: false + matrix: + shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + shardTotal: [10] + steps: + - uses: actions/checkout@v4 + - name: Install unzip + run: apt-get update && apt-get install -y unzip + - uses: oven-sh/setup-bun@v2 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Install dependencies + run: bun install --frozen-lockfile + - name: Set up Git safe directory + run: git config --global --add safe.directory /__w/Viewers/Viewers + - name: Run Playwright tests + run: + export NODE_OPTIONS="--max_old_space_size=8192" && bun x playwright test --shard=${{ + matrix.shardIndex }}/${{ matrix.shardTotal }} + + - name: Upload blob report to GitHub Actions Artifacts + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: blob-report-${{ matrix.shardIndex }} + path: blob-report + retention-days: 1 + + merge-reports: + if: ${{ !cancelled() }} + needs: [playwright-tests] + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright:v1.48.1-focal + steps: + - name: Install unzip + run: apt-get update && apt-get install -y unzip + - uses: oven-sh/setup-bun@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Install dependencies + run: bun install --frozen-lockfile + - name: Set up Git safe directory + run: git config --global --add safe.directory /__w/Viewers/Viewers + - name: Download blob reports from GitHub Actions Artifacts + uses: actions/download-artifact@v4 + with: + path: all-blob-reports + pattern: blob-report-* + merge-multiple: true + + - name: Merge into HTML Report + run: bun x playwright merge-reports --reporter html ./all-blob-reports + + - name: Upload HTML report + uses: actions/upload-artifact@v4 + with: + name: html-report--attempt-${{ github.run_attempt }} + path: playwright-report + retention-days: 14 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af800d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +# Packages +node_modules + +# Output +build +dist +docs/_book +src/version.js +junit.xml +coverage/ +.docz/ +.yarn/ +.nx/ +addOns/yarn.lock + +# YALC (for Erik) +.yalc +yalc.lock +*.dcm +# Logging, System files, misc. +.idea/ +.npm +npm-debug.log +package-lock.json +yarn-error.log +.DS_Store +.env +*.code-workspace +.directory + +# Common Example Data Directories +sampledata/ +example/deps/ +docker/dcm4che/dcm4che-arc + +# Cypress test results +videos/ + + +# Locize settings +.locize + +# autogenerated files +platform/app/src/pluginImports.js +/Viewers.iml +platform/app/.recipes/Nginx-Dcm4Chee/logs/* +platform/app/.recipes/OpenResty-Orthanc/logs/* +.vercel + +.vs + +# PlayWright + +node_modules/ +tests/test-results/ +tests/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f787a67 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "testdata"] + path = testdata + url = https://github.com/OHIF/viewer-testdata-dicomweb.git + branch = main diff --git a/.netlify/build-deploy-preview.sh b/.netlify/build-deploy-preview.sh new file mode 100755 index 0000000..4aafd66 --- /dev/null +++ b/.netlify/build-deploy-preview.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Set directory to location of this script +# https://stackoverflow.com/a/3355423/1867984 +cd "$(dirname "$0")" +cd .. # Up to project root + +# Helpful to verify which versions we're using +echo 'My yarn version is... ' + +yarn -v +node -v + +# Build && Move PWA Output +yarn run build:ci +mkdir -p ./.netlify/www/pwa +mv platform/app/dist/* .netlify/www/pwa -v +echo 'Web application built and copied' + +# Build && Move Docusaurus Output (for the docs themselves) +cd platform/docs +yarn install +yarn run build +cd ../.. +mkdir -p ./.netlify/www/docs +mv platform/docs/build/* .netlify/www/docs -v +echo 'Docs built (docusaurus) and copied' + +# Cache all of the node_module dependencies in +# extensions, modules, and platform packages +yarn run lerna:cache +echo 'Nothing left to see here. Go home, folks.' diff --git a/.netlify/deploy-workflow/_redirects b/.netlify/deploy-workflow/_redirects new file mode 100644 index 0000000..801b28f --- /dev/null +++ b/.netlify/deploy-workflow/_redirects @@ -0,0 +1,5 @@ +# Specific to our non-deploy-preview deploys +# Confgure redirects using netlify.toml + +# PWA Redirect +/* /index.html 200 diff --git a/.netlify/package.json b/.netlify/package.json new file mode 100644 index 0000000..ea40d1a --- /dev/null +++ b/.netlify/package.json @@ -0,0 +1,15 @@ +{ + "name": "root", + "private": true, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "scripts": { + "deploy": "netlify deploy --prod --dir ./../platform/app/dist" + }, + "devDependencies": { + "netlify-cli": "^2.21.0" + } +} diff --git a/.netlify/www/_redirects b/.netlify/www/_redirects new file mode 100644 index 0000000..af175b3 --- /dev/null +++ b/.netlify/www/_redirects @@ -0,0 +1,10 @@ +# Specific to our deploy-preview +# Our docs are published using CircleCI + GitBook +# Confgure redirects using netlify.toml + +# PWA Demo +/pwa/* /pwa/index.html 200 +# UI Demo +/ui/* /ui/index.html 200 +# UI Demo +/docs/* /docs/index.html 200 diff --git a/.netlify/www/index.html b/.netlify/www/index.html new file mode 100644 index 0000000..c817960 --- /dev/null +++ b/.netlify/www/index.html @@ -0,0 +1,21 @@ + + + OHIF Viewer: Deploy Preview + + + +

Index of Previews

+ + + + diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..f3f52b4 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +20.9.0 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..dd44972 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +*.md diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..c7eeb08 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,12 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"], + "trailingComma": "es5", + "printWidth": 100, + "proseWrap": "always", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "arrowParens": "avoid", + "singleAttributePerLine": true, + "endOfLine": "auto" +} diff --git a/.scripts/dev.sh b/.scripts/dev.sh new file mode 100755 index 0000000..ba4ef9b --- /dev/null +++ b/.scripts/dev.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# https://github.com/shelljs/shelljs +# https://github.com/shelljs/shelljs#exclude-options +PROJECT=$1 + +if [ -z "$PROJECT" ] +then + # Default + npx lerna run dev:viewer +else + eval "npx lerna run dev:$PROJECT" +fi + +read -p 'Press [Enter] key to continue...' diff --git a/.scripts/dicom-json-generator.js b/.scripts/dicom-json-generator.js new file mode 100644 index 0000000..e24dddf --- /dev/null +++ b/.scripts/dicom-json-generator.js @@ -0,0 +1,273 @@ +/* + * This script uses nodejs to generate a JSON file from a DICOM study folder. + * You need to have dcmjs installed in your project. + * The JSON file can be used to load the study into the OHIF Viewer. You can get more detail + * in the DICOM JSON Data source on docs.ohif.org + * + * Usage: node dicom-json-generator.js + * + * params: + * - studyFolder: path to the study folder which contains the DICOM files + * - urlPrefix: prefix to the url that will be used to load the study into the viewer. For instance + * we use https://ohif-assets.s3.us-east-2.amazonaws.com/dicom-json/data as the urlPrefix for the + * example since the data is hosted on S3 and each study is in a folder. So the url in the generated + * json file for the first instance of the first series of the first study will be + * dicomweb:https://ohif-assets.s3.us-east-2.amazonaws.com/dicom-json/data/Series1/Instance1 + * + * as you see the dicomweb is a prefix that is used to load the data into the viewer, which is suited when + * the .dcm file is hosted statically and can be accessed via a URL (like our example above) + * However, you can specify a new scheme bellow. + * + * - outputJSONPath: path to the output JSON file + * - scheme: default dicomweb if not provided + */ +const dcmjs = require('dcmjs'); +const path = require('path'); +const fs = require('fs').promises; + +const args = process.argv.slice(2); +const [studyDirectory, urlPrefix, outputPath, scheme = 'dicomweb'] = args; + +if (args.length < 3 || args.length > 4) { + console.error( + 'Usage: node dicom-json-generator.js [scheme]' + ); + process.exit(1); +} + +const model = { + studies: [], +}; + +async function convertDICOMToJSON(studyDirectory, urlPrefix, outputPath, scheme) { + try { + const files = await recursiveReadDir(studyDirectory); + console.debug('Processing...'); + + for (const file of files) { + if (!file.includes('.DS_Store') && !file.includes('.xml')) { + const arrayBuffer = await fs.readFile(file); + const dicomDict = dcmjs.data.DicomMessage.readFile(arrayBuffer.buffer); + const instance = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomDict.dict); + + instance.fileLocation = createImageId(file, urlPrefix, studyDirectory, scheme); + processInstance(instance); + } + } + + console.log('Successfully loaded data'); + + model.studies.forEach(study => { + study.NumInstances = findInstancesNumber(study); + study.Modalities = findModalities(study).join('/'); + }); + + await fs.writeFile(outputPath, JSON.stringify(model, null, 2)); + console.log('JSON saved'); + } catch (error) { + console.error(error); + } +} + +async function recursiveReadDir(dir) { + let results = []; + const list = await fs.readdir(dir); + for (const file of list) { + const filePath = path.resolve(dir, file); + const stat = await fs.stat(filePath); + if (stat.isDirectory()) { + const res = await recursiveReadDir(filePath); + results = results.concat(res); + } else { + results.push(filePath); + } + } + return results; +} + +function createImageId(fileLocation, urlPrefix, studyDirectory, scheme) { + const relativePath = path.relative(studyDirectory, fileLocation); + const normalizedPath = path.normalize(relativePath).replace(/\\/g, '/'); + return `${scheme}:${urlPrefix}${normalizedPath}`; +} + +function processInstance(instance) { + const { StudyInstanceUID, SeriesInstanceUID } = instance; + let study = getStudy(StudyInstanceUID); + + if (!study) { + study = createStudyMetadata(StudyInstanceUID, instance); + model.studies.push(study); + } + + let series = getSeries(StudyInstanceUID, SeriesInstanceUID); + + if (!series) { + series = createSeriesMetadata(instance); + study.series.push(series); + } + + const instanceMetaData = + instance.NumberOfFrames > 1 + ? createInstanceMetaDataMultiFrame(instance) + : createInstanceMetaData(instance); + + series.instances.push(...[].concat(instanceMetaData)); +} + +function getStudy(StudyInstanceUID) { + return model.studies.find(study => study.StudyInstanceUID === StudyInstanceUID); +} + +function getSeries(StudyInstanceUID, SeriesInstanceUID) { + const study = getStudy(StudyInstanceUID); + return study + ? study.series.find(series => series.SeriesInstanceUID === SeriesInstanceUID) + : undefined; +} + +const findInstancesNumber = study => { + let numInstances = 0; + study.series.forEach(aSeries => { + numInstances = numInstances + aSeries.instances.length; + }); + return numInstances; +}; + +const findModalities = study => { + let modalities = new Set(); + study.series.forEach(aSeries => { + modalities.add(aSeries.Modality); + }); + return Array.from(modalities); +}; + +function createStudyMetadata(StudyInstanceUID, instance) { + return { + StudyInstanceUID, + StudyDescription: instance.StudyDescription, + StudyDate: instance.StudyDate, + StudyTime: instance.StudyTime, + PatientName: instance.PatientName, + PatientID: instance.PatientID || '1234', // this is critical to have + AccessionNumber: instance.AccessionNumber, + PatientAge: instance.PatientAge, + PatientSex: instance.PatientSex, + PatientWeight: instance.PatientWeight, + series: [], + }; +} +function createSeriesMetadata(instance) { + return { + SeriesInstanceUID: instance.SeriesInstanceUID, + SeriesDescription: instance.SeriesDescription, + SeriesNumber: instance.SeriesNumber, + SeriesTime: instance.SeriesTime, + Modality: instance.Modality, + SliceThickness: instance.SliceThickness, + instances: [], + }; +} +function commonMetaData(instance) { + return { + Columns: instance.Columns, + Rows: instance.Rows, + InstanceNumber: instance.InstanceNumber, + SOPClassUID: instance.SOPClassUID, + AcquisitionNumber: instance.AcquisitionNumber, + PhotometricInterpretation: instance.PhotometricInterpretation, + BitsAllocated: instance.BitsAllocated, + BitsStored: instance.BitsStored, + PixelRepresentation: instance.PixelRepresentation, + SamplesPerPixel: instance.SamplesPerPixel, + PixelSpacing: instance.PixelSpacing, + HighBit: instance.HighBit, + ImageOrientationPatient: instance.ImageOrientationPatient, + ImagePositionPatient: instance.ImagePositionPatient, + FrameOfReferenceUID: instance.FrameOfReferenceUID, + ImageType: instance.ImageType, + Modality: instance.Modality, + SOPInstanceUID: instance.SOPInstanceUID, + SeriesInstanceUID: instance.SeriesInstanceUID, + StudyInstanceUID: instance.StudyInstanceUID, + WindowCenter: instance.WindowCenter, + WindowWidth: instance.WindowWidth, + RescaleIntercept: instance.RescaleIntercept, + RescaleSlope: instance.RescaleSlope, + }; +} + +function conditionalMetaData(instance) { + return { + ...(instance.ConceptNameCodeSequence && { + ConceptNameCodeSequence: instance.ConceptNameCodeSequence, + }), + ...(instance.SeriesDate && { SeriesDate: instance.SeriesDate }), + ...(instance.ReferencedSeriesSequence && { + ReferencedSeriesSequence: instance.ReferencedSeriesSequence, + }), + ...(instance.SharedFunctionalGroupsSequence && { + SharedFunctionalGroupsSequence: instance.SharedFunctionalGroupsSequence, + }), + ...(instance.PerFrameFunctionalGroupsSequence && { + PerFrameFunctionalGroupsSequence: instance.PerFrameFunctionalGroupsSequence, + }), + ...(instance.ContentSequence && { ContentSequence: instance.ContentSequence }), + ...(instance.ContentTemplateSequence && { + ContentTemplateSequence: instance.ContentTemplateSequence, + }), + ...(instance.CurrentRequestedProcedureEvidenceSequence && { + CurrentRequestedProcedureEvidenceSequence: instance.CurrentRequestedProcedureEvidenceSequence, + }), + ...(instance.CodingSchemeIdentificationSequence && { + CodingSchemeIdentificationSequence: instance.CodingSchemeIdentificationSequence, + }), + ...(instance.RadiopharmaceuticalInformationSequence && { + RadiopharmaceuticalInformationSequence: instance.RadiopharmaceuticalInformationSequence, + }), + ...(instance.ROIContourSequence && { + ROIContourSequence: instance.ROIContourSequence, + }), + ...(instance.StructureSetROISequence && { + StructureSetROISequence: instance.StructureSetROISequence, + }), + ...(instance.ReferencedFrameOfReferenceSequence && { + ReferencedFrameOfReferenceSequence: instance.ReferencedFrameOfReferenceSequence, + }), + ...(instance.CorrectedImage && { CorrectedImage: instance.CorrectedImage }), + ...(instance.Units && { Units: instance.Units }), + ...(instance.DecayCorrection && { DecayCorrection: instance.DecayCorrection }), + ...(instance.AcquisitionDate && { AcquisitionDate: instance.AcquisitionDate }), + ...(instance.AcquisitionTime && { AcquisitionTime: instance.AcquisitionTime }), + ...(instance.PatientWeight && { PatientWeight: instance.PatientWeight }), + ...(instance.NumberOfFrames && { NumberOfFrames: instance.NumberOfFrames }), + ...(instance.FrameTime && { FrameTime: instance.FrameTime }), + ...(instance.EncapsulatedDocument && { EncapsulatedDocument: instance.EncapsulatedDocument }), + ...(instance.SequenceOfUltrasoundRegions && { + SequenceOfUltrasoundRegions: instance.SequenceOfUltrasoundRegions, + }), + }; +} + +function createInstanceMetaData(instance) { + const metadata = { + ...commonMetaData(instance), + ...conditionalMetaData(instance), + }; + return { metadata, url: instance.fileLocation }; +} + +function createInstanceMetaDataMultiFrame(instance) { + const instances = []; + const commonData = commonMetaData(instance); + const conditionalData = conditionalMetaData(instance); + + for (let i = 1; i <= instance.NumberOfFrames; i++) { + const metadata = { ...commonData, ...conditionalData }; + const result = { metadata, url: instance.fileLocation + `?frame=${i}` }; + instances.push(result); + } + return instances; +} + +convertDICOMToJSON(studyDirectory, urlPrefix, outputPath, scheme); diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..1e74a79 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,13 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "streetsidesoftware.code-spell-checker", + "dbaeumer.vscode-eslint", + "mikestead.dotenv", + "bungcip.better-toml", + "silvenon.mdx", + "gruntfuggly.todo-tree", + "wayou.vscode-todo-highlight", + "bradlc.vscode-tailwindcss" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f085796 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "pwa-chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:3000", + "webRoot": "${workspaceFolder}" + } + // { + // "name": "Debug Jest Tests", + // "type": "node", + // "request": "launch", + // "runtimeArgs": [ + // "--inspect-brk", + // "${workspaceRoot}/node_modules/.bin/jest", + // "--runInBand" + // ], + // "console": "integratedTerminal", + // "internalConsoleOptions": "neverOpen", + // "port": 9229 + // } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3bd5e24 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,114 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.rulers": [80, 120], + // === + // Spacing + // === + "editor.insertSpaces": true, + "editor.tabSize": 2, + "editor.trimAutoWhitespace": true, + "files.trimTrailingWhitespace": true, + "files.eol": "\n", + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + // === + // Event Triggers + // === + "editor.formatOnSave": true, + "eslint.run": "onSave", + "jest.autoRun": "off", + "prettier.disableLanguages": ["html"], + "prettier.endOfLine": "lf", + "workbench.colorCustomizations": {}, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "cSpell.userWords": [ + "aabb", + "architectured", + "attrname", + "Barksy", + "browserslist", + "bulkdata", + "Cacheable", + "cfun", + "clonedeep", + "Colormap", + "Colormaps", + "Comlink", + "cornerstonejs", + "Crosshairs", + "datasource", + "dcmjs", + "decache", + "decached", + "decaching", + "deepmerge", + "Dicom", + "dicomweb", + "DISPLAYSETS", + "glwindow", + "grababble", + "grabbable", + "Hounsfield", + "Interactable", + "Interactor", + "istyle", + "kitware", + "labelmap", + "labelmaps", + "livewire", + "Mergeable", + "multiframe", + "nifti", + "ofun", + "OHIF", + "polylines", + "POLYSEG", + "prapogation", + "precisionmetrics", + "prefetch", + "Prescaled", + "pydicom", + "Radiopharmaceutical", + "rasterizing", + "reconstructable", + "Rehydratable", + "renderable", + "resampler", + "resemblejs", + "reslice", + "resliced", + "Reslices", + "roadmap", + "ROADMAPS", + "Segmentations", + "semibold", + "sitk", + "SUBRESOLUTION", + "suvbsa", + "suvbw", + "suvlbm", + "textbox", + "thresholded", + "thresholding", + "timepoint", + "timepoints", + "TMTV", + "TOOLGROUP", + "tqdm", + "transferables", + "typedoc", + "unsubscriptions", + "uuidv", + "viewplane", + "viewports", + "Voxel", + "Voxels", + "Vtkjs", + "wado", + "wadors", + "wadouri", + "workerpool" + ] +} diff --git a/.webpack/helpers/excludeNodeModulesExcept.js b/.webpack/helpers/excludeNodeModulesExcept.js new file mode 100644 index 0000000..ce4d3c3 --- /dev/null +++ b/.webpack/helpers/excludeNodeModulesExcept.js @@ -0,0 +1,22 @@ +const path = require('path'); + +function excludeNodeModulesExcept(modules) { + var pathSep = path.sep; + if (pathSep == '\\') + // must be quoted for use in a regexp: + pathSep = '\\\\'; + var moduleRegExps = modules.map(function (modName) { + return new RegExp('node_modules' + pathSep + modName); + }); + + return function (modulePath) { + if (/node_modules/.test(modulePath)) { + for (var i = 0; i < moduleRegExps.length; i++) + if (moduleRegExps[i].test(modulePath)) return false; + return true; + } + return false; + }; +} + +module.exports = excludeNodeModulesExcept; diff --git a/.webpack/rules/cssToJavaScript.js b/.webpack/rules/cssToJavaScript.js new file mode 100644 index 0000000..0cd60e9 --- /dev/null +++ b/.webpack/rules/cssToJavaScript.js @@ -0,0 +1,29 @@ +const autoprefixer = require('autoprefixer'); +const path = require('path'); +const tailwindcss = require('tailwindcss'); +const tailwindConfigPath = path.resolve('../../platform/app/tailwind.config.js'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const devMode = process.env.NODE_ENV !== 'production'; + +const cssToJavaScript = { + test: /\.css$/, + use: [ + //'style-loader', + devMode ? 'style-loader' : MiniCssExtractPlugin.loader, + { loader: 'css-loader', options: { importLoaders: 1 } }, + { + loader: 'postcss-loader', + options: { + postcssOptions: { + verbose: true, + plugins: [ + [tailwindcss(tailwindConfigPath)], + [autoprefixer('last 2 version', 'ie >= 11')], + ], + }, + }, + }, + ], +}; + +module.exports = cssToJavaScript; diff --git a/.webpack/rules/loadShaders.js b/.webpack/rules/loadShaders.js new file mode 100644 index 0000000..49dbef5 --- /dev/null +++ b/.webpack/rules/loadShaders.js @@ -0,0 +1,10 @@ +/** + * This is exclusively used by `vtk.js` to bundle glsl files. + */ +const loadShaders = { + test: /\.glsl$/i, + include: /vtk\.js[\/\\]Sources/, + loader: 'shader-loader', +}; + +module.exports = loadShaders; diff --git a/.webpack/rules/loadWebWorkers.js b/.webpack/rules/loadWebWorkers.js new file mode 100644 index 0000000..a4ab284 --- /dev/null +++ b/.webpack/rules/loadWebWorkers.js @@ -0,0 +1,17 @@ +/** + * This allows us to include web workers in our bundle, and VTK.js + * web workers in our bundle. While this increases bundle size, it + * cuts down on the number of includes we need for `script tag` usage. + */ +const loadWebWorkers = { + test: /\.worker\.js$/, + include: /vtk\.js[\/\\]Sources/, + use: [ + { + loader: 'worker-loader', + options: { inline: true, fallback: false }, + }, + ], +}; + +module.exports = loadWebWorkers; diff --git a/.webpack/rules/stylusToJavaScript.js b/.webpack/rules/stylusToJavaScript.js new file mode 100644 index 0000000..1d83f95 --- /dev/null +++ b/.webpack/rules/stylusToJavaScript.js @@ -0,0 +1,10 @@ +const stylusToJavaScript = { + test: /\.styl$/, + use: [ + { loader: 'style-loader' }, // 3. Style nodes from JS Strings + { loader: 'css-loader' }, // 2. CSS to CommonJS + { loader: 'stylus-loader' }, // 1. Stylus to CSS + ], +}; + +module.exports = stylusToJavaScript; diff --git a/.webpack/rules/transpileJavaScript.js b/.webpack/rules/transpileJavaScript.js new file mode 100644 index 0000000..e6eb8d5 --- /dev/null +++ b/.webpack/rules/transpileJavaScript.js @@ -0,0 +1,45 @@ +const excludeNodeModulesExcept = require('./../helpers/excludeNodeModulesExcept.js'); + +function transpileJavaScript(mode) { + const exclude = + mode === 'production' + ? excludeNodeModulesExcept([ + // 'dicomweb-client', + // https://github.com/react-dnd/react-dnd/blob/master/babel.config.js + 'react-dnd', + // https://github.com/dcmjs-org/dcmjs/blob/master/.babelrc + // https://github.com/react-dnd/react-dnd/issues/1342 + // 'dcmjs', // contains: loglevelnext + // https://github.com/shellscape/loglevelnext#browser-support + // 'loglevelnext', + // https://github.com/dcmjs-org/dicom-microscopy-viewer/issues/35 + // 'dicom-microscopy-viewer', + // https://github.com/openlayers/openlayers#supported-browsers + // 'ol', --> Should be fine + ]) + : excludeNodeModulesExcept([]); + + return { + // Include mjs, ts, tsx, js, and jsx files. + test: /\.(mjs|ts|js)x?$/, + // These are packages that are not transpiled to our lowest supported + // JS version (currently ES5). Most of these leverage ES6+ features, + // that we need to transpile to a different syntax. + exclude: [/(codecs)/, /(dicomicc)/, exclude], + loader: 'babel-loader', + options: { + // Find babel.config.js in monorepo root + // https://babeljs.io/docs/en/options#rootmode + rootMode: 'upward', + envName: mode, + cacheCompression: false, + // Note: This was causing a lot of issues with yarn link of the cornerstone + // only set this to true if you don't have a yarn link to external libs + // otherwise expect the lib changes not to be reflected in the dev server + // as it will be cached + cacheDirectory: false, + }, + }; +} + +module.exports = transpileJavaScript; diff --git a/.webpack/webpack.base.js b/.webpack/webpack.base.js new file mode 100644 index 0000000..025c8bd --- /dev/null +++ b/.webpack/webpack.base.js @@ -0,0 +1,232 @@ +// ~~ ENV +const dotenv = require('dotenv'); +// +const path = require('path'); +const fs = require('fs'); + +const webpack = require('webpack'); + +// ~~ PLUGINS +// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +const TerserJSPlugin = require('terser-webpack-plugin'); + +// ~~ PackageJSON +// const vtkRules = require('vtk.js/Utilities/config/dependency.js').webpack.core +// .rules; +// ~~ RULES +// const loadShadersRule = require('./rules/loadShaders.js'); +const loadWebWorkersRule = require('./rules/loadWebWorkers.js'); +const transpileJavaScriptRule = require('./rules/transpileJavaScript.js'); +const cssToJavaScript = require('./rules/cssToJavaScript.js'); +// Only uncomment for old v2 stylus +// const stylusToJavaScript = require('./rules/stylusToJavaScript.js'); +const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); + +// ~~ ENV VARS +const NODE_ENV = process.env.NODE_ENV; +const QUICK_BUILD = process.env.QUICK_BUILD; +const BUILD_NUM = process.env.CIRCLE_BUILD_NUM || '0'; + +// read from ../version.txt +const VERSION_NUMBER = fs.readFileSync(path.join(__dirname, '../version.txt'), 'utf8') || ''; + +const COMMIT_HASH = fs.readFileSync(path.join(__dirname, '../commit.txt'), 'utf8') || ''; + +// +dotenv.config(); + +const defineValues = { + /* Application */ + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), + 'process.env.NODE_DEBUG': JSON.stringify(process.env.NODE_DEBUG), + 'process.env.DEBUG': JSON.stringify(process.env.DEBUG), + 'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'), + 'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM), + 'process.env.VERSION_NUMBER': JSON.stringify(VERSION_NUMBER), + 'process.env.COMMIT_HASH': JSON.stringify(COMMIT_HASH), + /* i18n */ + 'process.env.USE_LOCIZE': JSON.stringify(process.env.USE_LOCIZE || ''), + 'process.env.LOCIZE_PROJECTID': JSON.stringify(process.env.LOCIZE_PROJECTID || ''), + 'process.env.LOCIZE_API_KEY': JSON.stringify(process.env.LOCIZE_API_KEY || ''), + 'process.env.REACT_APP_I18N_DEBUG': JSON.stringify(process.env.REACT_APP_I18N_DEBUG || ''), +}; + +// Only redefine updated values. This avoids warning messages in the logs +if (!process.env.APP_CONFIG) { + defineValues['process.env.APP_CONFIG'] = ''; +} + +module.exports = (env, argv, { SRC_DIR, ENTRY }) => { + const mode = NODE_ENV === 'production' ? 'production' : 'development'; + const isProdBuild = NODE_ENV === 'production'; + const isQuickBuild = QUICK_BUILD === 'true'; + + const config = { + mode: isProdBuild ? 'production' : 'development', + devtool: isProdBuild ? 'source-map' : 'cheap-module-source-map', + entry: ENTRY, + optimization: { + // splitChunks: { + // // include all types of chunks + // chunks: 'all', + // }, + //runtimeChunk: 'single', + minimize: isProdBuild, + sideEffects: false, + }, + output: { + // clean: true, + publicPath: '/', + }, + context: SRC_DIR, + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + cache: { + type: 'filesystem', + }, + module: { + noParse: [/(dicomicc)/], + rules: [ + ...(isProdBuild + ? [] + : [ + { + test: /\.[jt]sx?$/, + exclude: /node_modules/, + loader: 'babel-loader', + options: { + plugins: isProdBuild ? [] : ['react-refresh/babel'], + }, + }, + ]), + { + test: /\.svg?$/, + oneOf: [ + { + use: [ + { + loader: '@svgr/webpack', + options: { + svgoConfig: { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false, + }, + }, + }, + ], + }, + prettier: false, + svgo: true, + titleProp: true, + }, + }, + ], + issuer: { + and: [/\.(ts|tsx|js|jsx|md|mdx)$/], + }, + }, + ], + }, + transpileJavaScriptRule(mode), + loadWebWorkersRule, + // loadShadersRule, + { + test: /\.m?js/, + resolve: { + fullySpecified: false, + }, + }, + cssToJavaScript, + // Note: Only uncomment the following if you are using the old style of stylus in v2 + // Also you need to uncomment this platform/app/.webpack/rules/extractStyleChunks.js + // stylusToJavaScript, + { + test: /\.wasm/, + type: 'asset/resource', + }, + { + test: /\.(png|jpe?g|gif|svg)$/i, + use: [ + { + loader: 'file-loader', + options: { + name: 'assets/images/[name].[ext]', + }, + }, + ], + }, + ], //.concat(vtkRules), + }, + resolve: { + mainFields: ['module', 'browser', 'main'], + alias: { + // Viewer project + '@': path.resolve(__dirname, '../platform/app/src'), + '@components': path.resolve(__dirname, '../platform/app/src/components'), + '@hooks': path.resolve(__dirname, '../platform/app/src/hooks'), + '@routes': path.resolve(__dirname, '../platform/app/src/routes'), + '@state': path.resolve(__dirname, '../platform/app/src/state'), + 'dicom-microscopy-viewer': + 'dicom-microscopy-viewer/dist/dynamic-import/dicomMicroscopyViewer.min.js', + }, + // Which directories to search when resolving modules + modules: [ + // Modules specific to this package + path.resolve(__dirname, '../node_modules'), + // Hoisted Yarn Workspace Modules + path.resolve(__dirname, '../../../node_modules'), + path.resolve(__dirname, '../platform/app/node_modules'), + path.resolve(__dirname, '../platform/ui/node_modules'), + SRC_DIR, + ], + // Attempt to resolve these extensions in order. + extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '*'], + // symlinked resources are resolved to their real path, not their symlinked location + symlinks: true, + fallback: { + fs: false, + path: false, + zlib: false, + buffer: require.resolve('buffer'), + }, + }, + plugins: [ + new webpack.DefinePlugin(defineValues), + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + }), + ...(isProdBuild ? [] : [new ReactRefreshWebpackPlugin({ overlay: false })]), + // Uncomment to generate bundle analyzer + // new BundleAnalyzerPlugin(), + ], + }; + + if (isProdBuild) { + config.optimization.minimizer = [ + new TerserJSPlugin({ + parallel: true, + terserOptions: {}, + }), + ]; + } + + if (isQuickBuild) { + config.optimization.minimize = false; + config.devtool = false; + } + + return config; +}; diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2777690 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3944 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + + +### Bug Fixes + +* broken activateViewportBeforeInteraction behavior ([#4810](https://github.com/OHIF/Viewers/issues/4810)) ([fdb073c](https://github.com/OHIF/Viewers/commit/fdb073c216013477c8545db34d254a9ad328fe48)) + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + + +### Bug Fixes + +* **sr:** sr hydration and load was not working, Screenshot Comparison, and Testing ([#4814](https://github.com/OHIF/Viewers/issues/4814)) ([9233143](https://github.com/OHIF/Viewers/commit/9233143b9da5850080365e1526e24b44e9910075)) + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + + +### Features + +* **hotkeys:** Migrate hotkeys to customization service and fix issues with overrides ([#4777](https://github.com/OHIF/Viewers/issues/4777)) ([3e6913b](https://github.com/OHIF/Viewers/commit/3e6913b097569280a5cc2fa5bbe4add52f149305)) + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + + +### Bug Fixes + +* Changes to address hang/crash on jump to instance ([#4679](https://github.com/OHIF/Viewers/issues/4679)) ([e480e84](https://github.com/OHIF/Viewers/commit/e480e841bb5da5281a7c4624a60d5964d690ebb8)) +* **rt:** jump to segment discards the configured width ([#4799](https://github.com/OHIF/Viewers/issues/4799)) ([afd528b](https://github.com/OHIF/Viewers/commit/afd528b8d72c3d8360b54bb52604758e83ada863)) + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + + +### Bug Fixes + +* Delay for all series thumbnails on fetching thumbnail ([#4802](https://github.com/OHIF/Viewers/issues/4802)) ([bda98b0](https://github.com/OHIF/Viewers/commit/bda98b0beebde6294a522b5c7e0ca76724020a2f)) + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + + +### Bug Fixes + +* combine frame instance ([#4792](https://github.com/OHIF/Viewers/issues/4792)) ([55f0b54](https://github.com/OHIF/Viewers/commit/55f0b54db1e81a99f9e2d92b1d6d78dfb02762f0)) + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + + +### Bug Fixes + +* icon is not defined ([#4794](https://github.com/OHIF/Viewers/issues/4794)) ([b7cd0c6](https://github.com/OHIF/Viewers/commit/b7cd0c6027debcbfa573bc8068bc2e87928af9a5)) + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + + +### Bug Fixes + +* **button:** fix for className ([#4604](https://github.com/OHIF/Viewers/issues/4604)) ([125f11f](https://github.com/OHIF/Viewers/commit/125f11fc737f70ec9324798245787f44198e3bd4)) + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + + +### Bug Fixes + +* cache thumbnail in display set ([#4782](https://github.com/OHIF/Viewers/issues/4782)) ([2410c6a](https://github.com/OHIF/Viewers/commit/2410c6a50904c1235993900e837876cc26af019b)) + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + + +### Bug Fixes + +* lodash dependencies ([#4791](https://github.com/OHIF/Viewers/issues/4791)) ([4e16099](https://github.com/OHIF/Viewers/commit/4e16099ad3ab777b09f6ac8f181025cfd656ab6b)) + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + + +### Features + +* improve dicom tag browser with nested rows ([#4451](https://github.com/OHIF/Viewers/issues/4451)) ([0b5836c](https://github.com/OHIF/Viewers/commit/0b5836ca1a908e152336752672b196f0d533f4f9)) + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + + +### Bug Fixes + +* depandabot ([#4786](https://github.com/OHIF/Viewers/issues/4786)) ([d8a6e79](https://github.com/OHIF/Viewers/commit/d8a6e79df008139f7f2f45054b73baf8cd52fb40)) +* right panel for the create mode cli command ([#4788](https://github.com/OHIF/Viewers/issues/4788)) ([5712e91](https://github.com/OHIF/Viewers/commit/5712e91ca1d939ff3c36615d3cf1a1f6f0051c4f)) + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + + +### Features + +* add viewport overlays to microscopy mode ([#4776](https://github.com/OHIF/Viewers/issues/4776)) ([084a10f](https://github.com/OHIF/Viewers/commit/084a10f7835acab6a851922850c474bc9c7b864b)) + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + + +### Bug Fixes + +* add commandsManager to MoreDropdownMenu onClick props ([#4765](https://github.com/OHIF/Viewers/issues/4765)) ([bbf1a19](https://github.com/OHIF/Viewers/commit/bbf1a19676b2b345a0f911dde319e5ffefe29fa6)) + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + + +### Bug Fixes + +* **core:** Address 3D reconstruction and Android compatibility issues and clean up 4D data mode ([#4762](https://github.com/OHIF/Viewers/issues/4762)) ([149d6d0](https://github.com/OHIF/Viewers/commit/149d6d049cd333b9e5846576b403ff387558a66f)) + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + + +### Bug Fixes + +* changing colormap for second volume in fusion viewport ([#4746](https://github.com/OHIF/Viewers/issues/4746)) ([8996df8](https://github.com/OHIF/Viewers/commit/8996df80e45c97ad5847ebbd19291cd772a2163d)) + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + + +### Features + +* **ui:** Add support for Custom Modal component in Modal Service ([#4752](https://github.com/OHIF/Viewers/issues/4752)) ([2c183aa](https://github.com/OHIF/Viewers/commit/2c183aa4a777d7b5a0417ebcc8576a0fc2631ad2)) + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + + +### Features + +* **ui:** customization option for viewport notification ([#4638](https://github.com/OHIF/Viewers/issues/4638)) ([8acbd76](https://github.com/OHIF/Viewers/commit/8acbd760d801dcaf624c5d9fb636a029201b91e1)) + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + + +### Bug Fixes + +* **context-menu:** Fixing regression introduced by PR [#4727](https://github.com/OHIF/Viewers/issues/4727) ([#4760](https://github.com/OHIF/Viewers/issues/4760)) ([12d3db2](https://github.com/OHIF/Viewers/commit/12d3db2dbc80438df60139c67e9bcf0a610532d6)) + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + + +### Bug Fixes + +* **measurement label auto-completion:** Customization of measurement label auto-completion fails for measurements following arrow annotations. ([#4739](https://github.com/OHIF/Viewers/issues/4739)) ([e035ef1](https://github.com/OHIF/Viewers/commit/e035ef1dcc72ecbe2a757e3b814551d768d7e610)) + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + + +### Bug Fixes + +* **store-segmentation:** storing segmentations was hitting the wrong command resulting in an undefined datasource ([#4755](https://github.com/OHIF/Viewers/issues/4755)) ([9b8e5cf](https://github.com/OHIF/Viewers/commit/9b8e5cfd1a6121a58991c0f75660a2fd9913a4e7)) + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + + +### Features + +* Add customization support for more UI components ([#4634](https://github.com/OHIF/Viewers/issues/4634)) ([f15eb44](https://github.com/OHIF/Viewers/commit/f15eb44b4cf49de1b73a22512571cec02effaef3)) + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + + +### Bug Fixes + +* Remove non-functional Tailwind class for SegmentationPanel ([#4745](https://github.com/OHIF/Viewers/issues/4745)) ([32017d1](https://github.com/OHIF/Viewers/commit/32017d15a4c11e0cb7717c13da022a01ee152ba5)) + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + + +### Bug Fixes + +* publish dependency ([#4753](https://github.com/OHIF/Viewers/issues/4753)) ([0cb6106](https://github.com/OHIF/Viewers/commit/0cb6106d0fa576348ca3324685d5768b6ec80572)) + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + + +### Bug Fixes + +* typo in pet_series_module ([#4748](https://github.com/OHIF/Viewers/issues/4748)) ([f10683c](https://github.com/OHIF/Viewers/commit/f10683c667ea8f20c8d3e99ee0fb206522757b71)) + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + + +### Features + +* **customization:** enable custom onDropHandler for viewportGrid ([#4641](https://github.com/OHIF/Viewers/issues/4641)) ([054b262](https://github.com/OHIF/Viewers/commit/054b262e9cbeb0f44de65d05641efe1e8944a4f5)) + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + + +### Bug Fixes + +* **dependencies:** Update dcmjs library and improve documentation links ([#4741](https://github.com/OHIF/Viewers/issues/4741)) ([d554f02](https://github.com/OHIF/Viewers/commit/d554f02f7cdb876e4132fb94e3b3df8d11b7bb5c)) + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + + +### Features + +* **side-panels:** Added resize handle interactive feedback for side panels ([#4734](https://github.com/OHIF/Viewers/issues/4734)) ([6abb095](https://github.com/OHIF/Viewers/commit/6abb095b8a39c5ae4f8df8852b3ddb3249ec463f)) + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + + +### Bug Fixes + +* for initialImageIndex mismatch issue for loading SR after disabling prompts ([#4732](https://github.com/OHIF/Viewers/issues/4732)) ([8e3e208](https://github.com/OHIF/Viewers/commit/8e3e2085d45eba230d0210849b65a6e609c9d81a)) + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + + +### Bug Fixes + +* **context menu:** Context menus for edge-proximate measurements are partially obscured. ([#4727](https://github.com/OHIF/Viewers/issues/4727)) ([61699d0](https://github.com/OHIF/Viewers/commit/61699d00b6ce1e53631fd8c01e783701e01a7373)) + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + + +### Features + +* **static-wado:** add support for case-insensitive searching ([#4603](https://github.com/OHIF/Viewers/issues/4603)) ([ac6e674](https://github.com/OHIF/Viewers/commit/ac6e674b4d094f942556d045178011bbf3f81796)) + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + + +### Bug Fixes + +* **docs:** image in customization ([#4735](https://github.com/OHIF/Viewers/issues/4735)) ([28fb921](https://github.com/OHIF/Viewers/commit/28fb92108988c3304344690792947c847bad72a6)) + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + + +### Features + +* delete active annotation using backspace/delete key ([#4722](https://github.com/OHIF/Viewers/issues/4722)) ([d6f0092](https://github.com/OHIF/Viewers/commit/d6f0092a3236cecb5d04ec46c8ad01600500831e)) + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + + +### Features + +* **customization:** new customization service api ([#4688](https://github.com/OHIF/Viewers/issues/4688)) ([55ad8ef](https://github.com/OHIF/Viewers/commit/55ad8efbabc3fabd8031fc08927b2f92ae5aec69)) + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + + +### Features + +* **panels:** responsive thumbnails based on panel size ([#4723](https://github.com/OHIF/Viewers/issues/4723)) ([d9abc3d](https://github.com/OHIF/Viewers/commit/d9abc3da8d94d6c5ab0cc5af25a5f61849905a35)) + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + + +### Bug Fixes + +* **seg:** sphere scissor on stack and cpu rendering reset properties was broken ([#4721](https://github.com/OHIF/Viewers/issues/4721)) ([f00d182](https://github.com/OHIF/Viewers/commit/f00d18292f02e8910215d913edfc994850a68d88)) + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + + +### Features + +* **resizable-side-panels:** Make the left and right side panels (optionally) resizable. ([#4672](https://github.com/OHIF/Viewers/issues/4672)) ([d90a4cf](https://github.com/OHIF/Viewers/commit/d90a4cfb16cc0daed9b905de9780f44cca1323f9)) + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + + +### Bug Fixes + +* **ui:** Update dependencies and add missing icons ([#4699](https://github.com/OHIF/Viewers/issues/4699)) ([cf97fa9](https://github.com/OHIF/Viewers/commit/cf97fa9b7b9687a9b73c1cf6926bc9fbc39b6512)) + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + + +### Bug Fixes + +* Inconsistent Handling of Patient Name Tag ([#4703](https://github.com/OHIF/Viewers/issues/4703)) ([8aedb2e](https://github.com/OHIF/Viewers/commit/8aedb2ec54a0ccf2550f745fed6f0b8aa184a860)) + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + + +### Bug Fixes + +* **rotation:** support cycling rotateViewportCCW ([#4533](https://github.com/OHIF/Viewers/issues/4533)) ([bf58707](https://github.com/OHIF/Viewers/commit/bf587070cccd344f5e8817e9954ba9f2e46b81bb)) + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + + +### Bug Fixes + +* **hp:** Display set should allow remembered updates ([#4707](https://github.com/OHIF/Viewers/issues/4707)) ([464148e](https://github.com/OHIF/Viewers/commit/464148ece66b48b583dc6e998ca4d11c66746f3a)) + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + + +### Bug Fixes + +* context menu icon ([#4696](https://github.com/OHIF/Viewers/issues/4696)) ([1993161](https://github.com/OHIF/Viewers/commit/19931614dc53da440718e512d39a87ca9118b96e)) + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + + +### Bug Fixes + +* **multiframe:** handling proxies properly ([#4693](https://github.com/OHIF/Viewers/issues/4693)) ([ec4b5a6](https://github.com/OHIF/Viewers/commit/ec4b5a6876cea77278e5cffaf4108eeeefdc57dc)) + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + + +### Bug Fixes + +* Having sop instance in a per-frame or shared attribute breaks load ([#4560](https://github.com/OHIF/Viewers/issues/4560)) ([cded082](https://github.com/OHIF/Viewers/commit/cded08261788143e0d5be57a55c927fd96aafb22)) + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + + +### Bug Fixes + +* bugs after multimonitor ([#4680](https://github.com/OHIF/Viewers/issues/4680)) ([c901a84](https://github.com/OHIF/Viewers/commit/c901a847af75d356509366c695ea46ff4f4bcdaf)) +* cs dicom sr commands module ([#4683](https://github.com/OHIF/Viewers/issues/4683)) ([2d611d0](https://github.com/OHIF/Viewers/commit/2d611d06ed759bbd1e83ccfac7dceeff9eb6238e)) + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + + +### Features + +* **multimonitor:** Add simple multi-monitor support to open another study([#4178](https://github.com/OHIF/Viewers/issues/4178)) ([07c628e](https://github.com/OHIF/Viewers/commit/07c628e689b28f831317a7c28d712509b69c6b13)) + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + + +### Bug Fixes + +* **toolbarService:** All header tools are enabled in volume3D viewport ([#4677](https://github.com/OHIF/Viewers/issues/4677)) ([9832dbe](https://github.com/OHIF/Viewers/commit/9832dbe653a196280a0de57460436b6600a6aaa8)) + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + + +### Features + +* **dev:** move to rsbuild for dev - faster ([#4674](https://github.com/OHIF/Viewers/issues/4674)) ([d4a4267](https://github.com/OHIF/Viewers/commit/d4a4267429c02916dd51f6aefb290d96dd1c3b04)) + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + + +### Bug Fixes + +* **docker:** run compression regardless of APP_CONFIG being present ( in cases such as volume mount) ([#4673](https://github.com/OHIF/Viewers/issues/4673)) ([a2d34c9](https://github.com/OHIF/Viewers/commit/a2d34c97edcdc34f14b4b99a1bca6c1b43e80006)) + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + + +### Bug Fixes + +* **icons:** icons missing for volume presets and others ([#4671](https://github.com/OHIF/Viewers/issues/4671)) ([01924b8](https://github.com/OHIF/Viewers/commit/01924b8bf27da045d39dfaeb126b73cb4efcdb08)) + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + + +### Bug Fixes + +* orthanc datasource dev ([#4663](https://github.com/OHIF/Viewers/issues/4663)) ([ebbc37d](https://github.com/OHIF/Viewers/commit/ebbc37d291ba9bfa11baf164bf673c6f0994014c)) + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + + +### Features + +* Start using group filtering to define measurements table layout ([#4501](https://github.com/OHIF/Viewers/issues/4501)) ([82440e8](https://github.com/OHIF/Viewers/commit/82440e88d5debe808f0b14281b77e430c2489779)) + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + + +### Bug Fixes + +* Execute HP onProtocolEnter callback after HPservice.run( ([#4589](https://github.com/OHIF/Viewers/issues/4589)) ([8e2c607](https://github.com/OHIF/Viewers/commit/8e2c60790437d4df583a236c99e856d21dbc0dfe)) + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + + +### Bug Fixes + +* Make StudyInstanceUID optional to retrieve a Series from DicomMetadataStore ([#4644](https://github.com/OHIF/Viewers/issues/4644)) ([aef68d1](https://github.com/OHIF/Viewers/commit/aef68d18b82455ee485fef70df4ee7ba2c775417)) + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + + +### Bug Fixes + +* Inconsistencies and update the style setting on load for embedded styles from codingValues ([#4599](https://github.com/OHIF/Viewers/issues/4599)) ([e0088ec](https://github.com/OHIF/Viewers/commit/e0088ec91807fa6a8e11e1e6942f51cedd080cc9)) + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + + +### Bug Fixes + +* **circleci:** Update workflow dependencies for Docker publishing stages ([#4661](https://github.com/OHIF/Viewers/issues/4661)) ([66eebbe](https://github.com/OHIF/Viewers/commit/66eebbed0f27975082bf8997f4637c5de169af79)) +* **circleci:** Update workflow dependencies for Docker publishing stages ([#4662](https://github.com/OHIF/Viewers/issues/4662)) ([38edde5](https://github.com/OHIF/Viewers/commit/38edde5e37e8408b853c3712d240bd0423464701)) + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + + +### Bug Fixes + +* **circleci:** Update workflow dependencies for Docker publishing stages ([#4660](https://github.com/OHIF/Viewers/issues/4660)) ([3c6a5ef](https://github.com/OHIF/Viewers/commit/3c6a5ef69e9914bdc9f3fda1b149400881e2ce1e)) + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + + +### Bug Fixes + +* **Dockerfile:** Clear bun package manager cache before installation ([#4659](https://github.com/OHIF/Viewers/issues/4659)) ([5f6f528](https://github.com/OHIF/Viewers/commit/5f6f528fbf166a524f447708e1d0b4b8f3766d3c)) + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + + +### Bug Fixes + +* Convert Rows and Columns to numbers before comparison ([#4654](https://github.com/OHIF/Viewers/issues/4654)) ([#4656](https://github.com/OHIF/Viewers/issues/4656)) ([2f5076e](https://github.com/OHIF/Viewers/commit/2f5076ece8b3125c3426014efdf7fc6b498606d0)) + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + + +### Bug Fixes + +* **docker:** make multiarch work ([#4655](https://github.com/OHIF/Viewers/issues/4655)) ([8e12021](https://github.com/OHIF/Viewers/commit/8e120219390a91474fb634711ea8c3312a7d9539)) + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + + +### Bug Fixes + +* **docker:** publish manifest for multiarch and update cs3d ([#4650](https://github.com/OHIF/Viewers/issues/4650)) ([836e67a](https://github.com/OHIF/Viewers/commit/836e67a6ab8de66d8908c75856774318729544f4)) + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + + +### Bug Fixes + +* **toolbars:** Fix error when filtering out duplicate buttons for a button section. ([#4618](https://github.com/OHIF/Viewers/issues/4618)) ([28cf3a1](https://github.com/OHIF/Viewers/commit/28cf3a17fad5070ba00d0d5d27633237b499da7a)) + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + + +### Bug Fixes + +* year 2025 missing in date picker ([#4647](https://github.com/OHIF/Viewers/issues/4647)) ([4a8e8ca](https://github.com/OHIF/Viewers/commit/4a8e8cacf2fa5a7e2ed2429cba79edcd3f2a6d78)) + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + + +### Bug Fixes + +* **context menu:** Implemented closing of context menu on outside click ([#4627](https://github.com/OHIF/Viewers/issues/4627)) ([6b851df](https://github.com/OHIF/Viewers/commit/6b851dfc12f4cf617d02f683e0661feeebfbcf20)) +* **context menu:** restrict the context menu accessibility for locked and hidden annotations ([#4625](https://github.com/OHIF/Viewers/issues/4625)) ([e11ceb9](https://github.com/OHIF/Viewers/commit/e11ceb9d20fa5e680a0247f6ca7c27825daea6c5)) + + +### Features + +* Implemented CSV support for Arrow annotation. ([#4623](https://github.com/OHIF/Viewers/issues/4623)) ([55fe185](https://github.com/OHIF/Viewers/commit/55fe185c72500256452e25d2f2b17fc9faa99dff)) + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + + +### Bug Fixes + +* **3D rendering:** disabled light sliders when shade is off ([#4631](https://github.com/OHIF/Viewers/issues/4631)) ([5322064](https://github.com/OHIF/Viewers/commit/5322064e9eb66791bc598f82bdf4edd35e40be11)) + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + + +### Bug Fixes + +* Docker build time was very slow on a tiny change ([#4559](https://github.com/OHIF/Viewers/issues/4559)) ([7e43b2f](https://github.com/OHIF/Viewers/commit/7e43b2f768cfc3e08ecde9dfdae275194daece2b)) + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + + +### Bug Fixes + +* **tools:** enable additional tools in volume viewport ([#4620](https://github.com/OHIF/Viewers/issues/4620)) ([1992002](https://github.com/OHIF/Viewers/commit/1992002d2dced171c17b9a0163baf707fc551e3d)) + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + + +### Bug Fixes + +* **datasource:** attach auth headers for delete requests in the dicomweb datasource ([#4619](https://github.com/OHIF/Viewers/issues/4619)) ([8d0ed80](https://github.com/OHIF/Viewers/commit/8d0ed80e0c4570ab799281c29e487dbb39f47b95)) + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + + +### Bug Fixes + +* **segmentation:** black preview when loading a seg, and crash on opening panel ([#4602](https://github.com/OHIF/Viewers/issues/4602)) ([faf5515](https://github.com/OHIF/Viewers/commit/faf5515e4b93da58b673f1ae59ec345e30870446)) + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + + +### Bug Fixes + +* **SR:** Bring back the onModeEnter for the cornerstone-dicom-sr extension that was accidentally removed by PR [#4586](https://github.com/OHIF/Viewers/issues/4586) ([#4616](https://github.com/OHIF/Viewers/issues/4616)) ([2df8e1d](https://github.com/OHIF/Viewers/commit/2df8e1d5cd7a203bdde1cac6230b60a0b87bfcdd)) + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + + +### Bug Fixes + +* **icons:** Add Clipboard icon and update MetadataProvider for null checks ([#4615](https://github.com/OHIF/Viewers/issues/4615)) ([93d7076](https://github.com/OHIF/Viewers/commit/93d707690104ae099df6e08156e2efd8c1a6e076)) + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + + +### Features + +* **measurements:** Provide for the Load (SR) measurements button to optionally clear existing measurements prior to loading the SR. ([#4586](https://github.com/OHIF/Viewers/issues/4586)) ([4d3d5e7](https://github.com/OHIF/Viewers/commit/4d3d5e794cb99212eba06bf91dbb30a258725efe)) + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + + +### Bug Fixes + +* ohif icons in Header ([#4611](https://github.com/OHIF/Viewers/issues/4611)) ([52cf9b1](https://github.com/OHIF/Viewers/commit/52cf9b1e0398f966d4498dda83fd5ceae69262c6)) + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + + +### Bug Fixes + +* Documentation and default enabled for bulkdata load ([#4607](https://github.com/OHIF/Viewers/issues/4607)) ([d0ccdbd](https://github.com/OHIF/Viewers/commit/d0ccdbd68db1dcb190b5a288dd455f573eddc280)) + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + + +### Features + +* migrate icons to ui-next ([#4606](https://github.com/OHIF/Viewers/issues/4606)) ([4e2ae32](https://github.com/OHIF/Viewers/commit/4e2ae328744ed95589c2cdf7a531454a25bf88b5)) + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + + +### Bug Fixes + +* **seg:** jump to the first slice in SEG and RT that has data ([#4605](https://github.com/OHIF/Viewers/issues/4605)) ([9bf24d6](https://github.com/OHIF/Viewers/commit/9bf24d6dc58ed8f65c90899a17c11044b792cf40)) + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + + +### Bug Fixes + +* **tag-browser:** fix dicom tag browser not loading in segmentation mode in study panel ([#4601](https://github.com/OHIF/Viewers/issues/4601)) ([60fc7d6](https://github.com/OHIF/Viewers/commit/60fc7d6a112da99b47e26c5e3460b920bbc3c0b0)) + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + + +### Features + +* **node:** move to node 20 ([#4594](https://github.com/OHIF/Viewers/issues/4594)) ([1f04d6c](https://github.com/OHIF/Viewers/commit/1f04d6c1be729a26fe7bcda923770a1cd461053c)) + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + + +### Bug Fixes + +* **worklist:** selected patient changes randomly when clicked. ([#4592](https://github.com/OHIF/Viewers/issues/4592)) ([684267b](https://github.com/OHIF/Viewers/commit/684267b19b553817590b8760a188a56e17e5d2ec)) + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + + +### Bug Fixes + +* **sr:** correct jump to first image via viewRef ([#4576](https://github.com/OHIF/Viewers/issues/4576)) ([6ec04ca](https://github.com/OHIF/Viewers/commit/6ec04ca65ea2f0fe95eaf624652911b87a6f81e6)) + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + + +### Bug Fixes + +* **touch:** For viewport interactions use onPointerDown. ([#4572](https://github.com/OHIF/Viewers/issues/4572)) ([6160718](https://github.com/OHIF/Viewers/commit/6160718fd20db6bac6dd511183a30359d9420140)) + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + + +### Bug Fixes + +* **defaultRouteInit:** fixes 'madeInClient' parameter when 'makeDisplaySets' is called ([#4571](https://github.com/OHIF/Viewers/issues/4571)) ([7cc6c14](https://github.com/OHIF/Viewers/commit/7cc6c1484a551026af5f641254431c23b729c2c2)) + + +### Features + +* **extension:** added 'extensionManager' to 'onModeEnter' parameter list ([#4569](https://github.com/OHIF/Viewers/issues/4569)) ([f87c6cd](https://github.com/OHIF/Viewers/commit/f87c6cd2aa83007393302d4437b417150ed26e2e)) + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + + +### Bug Fixes + +* **CinePlayer:** always show cine player for dynamic data ([#4575](https://github.com/OHIF/Viewers/issues/4575)) ([b8e8bbe](https://github.com/OHIF/Viewers/commit/b8e8bbe482b66e8cbe9167d03e9d8dedd2d3b6c5)) + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + + +### Bug Fixes + +* **WorkflowSteps:** fixed how hooks are invoked + added support for 'onExit' hook ([#4568](https://github.com/OHIF/Viewers/issues/4568)) ([bca2022](https://github.com/OHIF/Viewers/commit/bca20223513c15720b4538533c0f6d38b839e045)) + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + + +### Bug Fixes + +* **cli:** publish 4D preclincial mode on NPM so it can be used in the OHIF cli commands ([#4557](https://github.com/OHIF/Viewers/issues/4557)) ([085590a](https://github.com/OHIF/Viewers/commit/085590a4ca64bebad9ef60503411e1a6dd4d93f9)) + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + + +### Bug Fixes + +* **multiframe:** metadata handling of NM studies and loading order ([#4554](https://github.com/OHIF/Viewers/issues/4554)) ([7624ccb](https://github.com/OHIF/Viewers/commit/7624ccb5e495c0a151227a458d8d5bfb8babb22c)) + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + + +### Features + +* **segmentation:** Enhance dropdown menu functionality in SegmentationTable ([#4553](https://github.com/OHIF/Viewers/issues/4553)) ([397fd85](https://github.com/OHIF/Viewers/commit/397fd856539cd3b949a9614a9ea32d0d04a90000)) + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + + +### Bug Fixes + +* **colorlut:** use the correct colorlut index and update vtk ([#4544](https://github.com/OHIF/Viewers/issues/4544)) ([b9c26e7](https://github.com/OHIF/Viewers/commit/b9c26e775a49044673473418dd5bdee2e5562ab9)) + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + + +### Bug Fixes + +* **modes:** don't attempt to retrieve a stage index if HPs are an array ([#4542](https://github.com/OHIF/Viewers/issues/4542)) ([44648ee](https://github.com/OHIF/Viewers/commit/44648eef92265f0a80c0c72ca1729d6eca6c4178)) + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + + +### Bug Fixes + +* Make the commands ordering the registration order of hte mode ([#4492](https://github.com/OHIF/Viewers/issues/4492)) ([edfaf72](https://github.com/OHIF/Viewers/commit/edfaf7248d217707e90d24642361a40c6f1a03ff)) + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + + +### Bug Fixes + +* **error-boundray:** prevent stack trace from overflowing and make it scrollable ([#4541](https://github.com/OHIF/Viewers/issues/4541)) ([27ae385](https://github.com/OHIF/Viewers/commit/27ae385fd7787bf34af00366c5d490ac33abeff9)) + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + + +### Bug Fixes + +* **viewport:** set a minimum width of 5px on viewports to prevent them from turning black/ going into an unrecoverable state. ([#4517](https://github.com/OHIF/Viewers/issues/4517)) ([32fe262](https://github.com/OHIF/Viewers/commit/32fe2623cfb5129a19ee07031dd50e79b530c7e0)) + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + + +### Bug Fixes + +* **ci:** address error in merge report step of the playwright workflow ([#4518](https://github.com/OHIF/Viewers/issues/4518)) ([d05db4c](https://github.com/OHIF/Viewers/commit/d05db4cb61b9f0492fd2498990f2ef0309cfaaa3)) + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + + +### Bug Fixes + +* avoid black images after hiding the viewports ([#4502](https://github.com/OHIF/Viewers/issues/4502)) ([ad8f205](https://github.com/OHIF/Viewers/commit/ad8f205e419d439bd8e51eff1101b2ef4c314214)) + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + + +### Bug Fixes + +* Have an addIcon that adds to both ui and ui-next ([#4490](https://github.com/OHIF/Viewers/issues/4490)) ([4a12523](https://github.com/OHIF/Viewers/commit/4a125236ddbf8a4a95fb9c5820f511d0224e663f)) +* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed)) +* **styles:** several fixes for different styles ([#4483](https://github.com/OHIF/Viewers/issues/4483)) ([a5f0376](https://github.com/OHIF/Viewers/commit/a5f03764d1fe07b55635c52c5dac38ab5e694ba1)) + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + + +### Bug Fixes + +* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30)) + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + + +### Bug Fixes + +* **style:** worklist shifting ([#4477](https://github.com/OHIF/Viewers/issues/4477)) ([8fb8b3b](https://github.com/OHIF/Viewers/commit/8fb8b3bfd1c887cd67fc058629d7aba598c76f9e)) + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + + +### Bug Fixes + +* **tmtv:** fix toggle one up weird behaviours ([#4473](https://github.com/OHIF/Viewers/issues/4473)) ([aa2b649](https://github.com/OHIF/Viewers/commit/aa2b649444eb4fe5422e72ea7830a709c4d24a90)) + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + + +### Bug Fixes + +* build ([#4471](https://github.com/OHIF/Viewers/issues/4471)) ([3d11ef2](https://github.com/OHIF/Viewers/commit/3d11ef28f213361ec7586809317bd219fa70e742)) + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + + +### Bug Fixes + +* **ui:** show ui notification on displaySet load error ([#4447](https://github.com/OHIF/Viewers/issues/4447)) ([4f20523](https://github.com/OHIF/Viewers/commit/4f20523109ecbb7ec5a6d5f2c97f7e73f81cda09)) + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + + +### Bug Fixes + +* **ui:** display error in ui while loading seg ([#4433](https://github.com/OHIF/Viewers/issues/4433)) ([2e96371](https://github.com/OHIF/Viewers/commit/2e96371b0631a9e5d411b0142300708ab8ba7d27)) + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + + +### Bug Fixes + +* **packages:** http-proxy-middleware vulnerability ([#4443](https://github.com/OHIF/Viewers/issues/4443)) ([0610425](https://github.com/OHIF/Viewers/commit/06104257402e872d447e59cb166184d9a3548f8b)) + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + + +### Features + +* **new-study-panel:** default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser ([#4417](https://github.com/OHIF/Viewers/issues/4417)) ([a7fd9fa](https://github.com/OHIF/Viewers/commit/a7fd9fa5bfff7a1b533d99cb96f7147a35fd528f)) + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + + +### Bug Fixes + +* **tmtv:** prevent fusion row in tmtv from getting inverted unexpectedly ([#4420](https://github.com/OHIF/Viewers/issues/4420)) ([33af9bb](https://github.com/OHIF/Viewers/commit/33af9bb021ff3a6c3b683d4df2c730413400ff8a)) + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + + +### Bug Fixes + +* **3d-viewport:** exception was being thrown and 3d viewpot not getting resized. ([#4366](https://github.com/OHIF/Viewers/issues/4366)) ([433cc80](https://github.com/OHIF/Viewers/commit/433cc8089db6aa218c9075bd0eeb7952a7e4f028)) +* **createReport:** early return on cancel in prompt ([#4243](https://github.com/OHIF/Viewers/issues/4243)) ([2ec4692](https://github.com/OHIF/Viewers/commit/2ec4692eaf2349e21b141a2c0b5b104ee10f7a28)) +* **dicomjson:** Update getUIDsFromImageID to work with json data source + update getDisplaySetImageUIDs to work with mixed sop class json ([#4322](https://github.com/OHIF/Viewers/issues/4322)) ([3dd0666](https://github.com/OHIF/Viewers/commit/3dd0666c0c090cbd66161f24bc9795f96abb3697)) +* **hp-presets:** select the active displaySet when toggling an HP preset, not a random one from the series panel ([#4365](https://github.com/OHIF/Viewers/issues/4365)) ([ace67b3](https://github.com/OHIF/Viewers/commit/ace67b3bbb6be4e8c78e613e20d3e10b93762bf7)) +* **sr:** load existing point, if there is 2nd point in renderableData (Fix rotation in arrow annotation) ([#4356](https://github.com/OHIF/Viewers/issues/4356)) ([7353f7f](https://github.com/OHIF/Viewers/commit/7353f7f069446f8484278c2cff5b09149cfa23eb)) +* **tools:** check if seriesNumber is an undefined properly ([#4338](https://github.com/OHIF/Viewers/issues/4338)) ([307b144](https://github.com/OHIF/Viewers/commit/307b14476be41b10b861d6a8474f7386b5107618)) +* **typo:** type in fourup preset ([#4426](https://github.com/OHIF/Viewers/issues/4426)) ([03aad4e](https://github.com/OHIF/Viewers/commit/03aad4eba24e33a266a6d91eaf74df52dc2a550e)) +* **updateIndex:** getNumberOfSlices is defined when used with a 3D viewport ([#4424](https://github.com/OHIF/Viewers/issues/4424)) ([d5bcf54](https://github.com/OHIF/Viewers/commit/d5bcf54e23ef68abd85c5f0ea671feca637c4f49)) + + +### Features + +* **hangingProtocols:** added selection of the HangingProtocol stage from the url ([#4310](https://github.com/OHIF/Viewers/issues/4310)) ([fa2435d](https://github.com/OHIF/Viewers/commit/fa2435d5e94e5f903404ca94687b086f90f8d1f8)) +* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3)) + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + + +### Bug Fixes + +* **auth:** oidc-react-issue ([#4410](https://github.com/OHIF/Viewers/issues/4410)) ([e849199](https://github.com/OHIF/Viewers/commit/e849199eb0a9ecba4f9845aa1e07df775d5ded9b)) + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + + +### Bug Fixes + +* **fossa:** update fossa to track licenses correctly ([#4411](https://github.com/OHIF/Viewers/issues/4411)) ([ec685ef](https://github.com/OHIF/Viewers/commit/ec685ef5b9c8bfa5bff3bbf869eb394548ae1cab)) + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + + +### Features + +* **tours:** freeze versions and add licensings doc ([#4407](https://github.com/OHIF/Viewers/issues/4407)) ([60a8d51](https://github.com/OHIF/Viewers/commit/60a8d5154a5d6d2b121bd93aeacf12d97ef9f8cb)) + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + + +### Bug Fixes + +* **Select:** select clear button ([#4398](https://github.com/OHIF/Viewers/issues/4398)) ([a11cd6d](https://github.com/OHIF/Viewers/commit/a11cd6d6cbe20d7d986430befb3398f910a03ada)) + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + + +### Bug Fixes + +* **dependent:** high priority vulnerabilities ([#4396](https://github.com/OHIF/Viewers/issues/4396)) ([b4f08ad](https://github.com/OHIF/Viewers/commit/b4f08adfb638e5df11bb77d3c1b128b5efdf77a7)) + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + + +### Bug Fixes + +* ๐Ÿ› Fix imports for ui-next ([#4394](https://github.com/OHIF/Viewers/issues/4394)) ([43efed2](https://github.com/OHIF/Viewers/commit/43efed207e0d8d13bcbf52fab14c1be034d22d0c)) + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + + +### Bug Fixes + +* **ui:** Fixed study component open and closed feedback in Studies panel ([#4384](https://github.com/OHIF/Viewers/issues/4384)) ([365d824](https://github.com/OHIF/Viewers/commit/365d824b98e03b87db294878abde6823abdcf409)) + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + + +### Features + +* **toolbar:** enable extensions to change toolbar button sections ([#4367](https://github.com/OHIF/Viewers/issues/4367)) ([1bfce0a](https://github.com/OHIF/Viewers/commit/1bfce0a03cbbb4cc1f69e8b5d1d72244b30d6b46)) + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + + +### Features + +* **studies-panel:** New OHIF study panel - under experimental flag ([#4254](https://github.com/OHIF/Viewers/issues/4254)) ([7a96406](https://github.com/OHIF/Viewers/commit/7a96406a116e46e62c396855fa64f434e2984b58)) + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + + +### Bug Fixes + +* Add kheops integration into OHIF v3 again ([#4345](https://github.com/OHIF/Viewers/issues/4345)) ([e1feffa](https://github.com/OHIF/Viewers/commit/e1feffa42553d6c8650a4aceb09f72c637126660)) + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + + +### Bug Fixes + +* ๐Ÿ› SeriesInstanceUID fallback + update retrieve metadata filtered to check for lazy ([#4346](https://github.com/OHIF/Viewers/issues/4346)) ([14498d4](https://github.com/OHIF/Viewers/commit/14498d4e9a6a57324b8be9f0b314f2901459dc4a)) + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + + +### Features + +* Add CS3D WSI and Video Viewports and add annotation navigation for MPR ([#4182](https://github.com/OHIF/Viewers/issues/4182)) ([7599ec9](https://github.com/OHIF/Viewers/commit/7599ec9421129dcade94e6fa6ec7908424ab3134)) + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + + +### Bug Fixes + +* **roundNumber:** handle negative numbers properly ([#4336](https://github.com/OHIF/Viewers/issues/4336)) ([7377db8](https://github.com/OHIF/Viewers/commit/7377db8d280a90515fe099cb580607450cb146a5)) + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + + +### Bug Fixes + +* unexpected mpr measurements ([#4332](https://github.com/OHIF/Viewers/issues/4332)) ([ab6e341](https://github.com/OHIF/Viewers/commit/ab6e341731652a4fa894fcb576eb23dc95aefa11)) + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + + +### Bug Fixes + +* **ui:** Tailwind build errors ([#4329](https://github.com/OHIF/Viewers/issues/4329)) ([8e7cc11](https://github.com/OHIF/Viewers/commit/8e7cc1152917f562ea7e6a5f3f7e492b300dc564)) + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + + +### Bug Fixes + +* **url:** series query param filtering ([#4328](https://github.com/OHIF/Viewers/issues/4328)) ([9b10303](https://github.com/OHIF/Viewers/commit/9b10303a2efa809096156d4a2322b2b46f160a91)) + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + + +### Features + +* **ui:** Created design and added core components for ui-next ([#4324](https://github.com/OHIF/Viewers/issues/4324)) ([9036418](https://github.com/OHIF/Viewers/commit/90364189b865514cc471786d2f91c270517e98fc)) + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + + +### Bug Fixes + +* customization types ([#4321](https://github.com/OHIF/Viewers/issues/4321)) ([72bef63](https://github.com/OHIF/Viewers/commit/72bef63ef6e63395ba18ff91a39294913966e9db)) + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + + +### Bug Fixes + +* ip SSRF improper categorization ([#4319](https://github.com/OHIF/Viewers/issues/4319)) ([aa0e5a5](https://github.com/OHIF/Viewers/commit/aa0e5a59379453bb8e6a4f286447576744ea6bf5)) + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + + +### Bug Fixes + +* **ui:** remove border-border class ([#4317](https://github.com/OHIF/Viewers/issues/4317)) ([d402ded](https://github.com/OHIF/Viewers/commit/d402ded8c36631f8009b7b15b2f1c7a02cd09f6c)) + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + + +### Bug Fixes + +* **dicom:** Update multiframe DICOM JSON parsing for correct image ID generation ([#4307](https://github.com/OHIF/Viewers/issues/4307)) ([16b7aa4](https://github.com/OHIF/Viewers/commit/16b7aa4f6538b81e5915e47b9209d74575895dfe)) + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + + +### Features + +* **pmap:** added support for parametric map ([#4284](https://github.com/OHIF/Viewers/issues/4284)) ([fc0064f](https://github.com/OHIF/Viewers/commit/fc0064fd9d8cdc8fde81b81f0e71fd5d077ca22b)) + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + + +### Features + +* **SR:** text structured report (TEXT, CODE, NUM, PNAME, DATE, TIME and DATETIME) ([#4287](https://github.com/OHIF/Viewers/issues/4287)) ([246ebab](https://github.com/OHIF/Viewers/commit/246ebab6ebf5431a704a1861a5804045b9644ba4)) + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + + +### Bug Fixes + +* the start/end command in TMTV for the ROIStartEndThreshold tools ([#4281](https://github.com/OHIF/Viewers/issues/4281)) ([38c19fa](https://github.com/OHIF/Viewers/commit/38c19fab77cdb21d14bdae35813d73f43012cbd7)) + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + + +### Features + +* **auth:** Add Authorization Code Flow and new Keycloak recipes with new video tutorials ([#4234](https://github.com/OHIF/Viewers/issues/4234)) ([aefa6d9](https://github.com/OHIF/Viewers/commit/aefa6d94dff82d34fa8358933fb1d5dec3f8246d)) + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + + +### Bug Fixes + +* Tests run against e2e config for both playwright and older tests ([#4283](https://github.com/OHIF/Viewers/issues/4283)) ([31271ae](https://github.com/OHIF/Viewers/commit/31271aeef727ec9cfa44fdf91f571a33b10cb3ab)) + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + + +### Bug Fixes + +* Cobb angle not working in basic-test mode and open contour ([#4280](https://github.com/OHIF/Viewers/issues/4280)) ([6fd3c7e](https://github.com/OHIF/Viewers/commit/6fd3c7e293fec851dd30e650c1347cc0bc7a99ee)) +* **image-orientation:** Prevent incorrect orientation marker display for single-slice US images ([#4275](https://github.com/OHIF/Viewers/issues/4275)) ([6d11048](https://github.com/OHIF/Viewers/commit/6d11048ca5ea66284948602613a63277083ec6a5)) +* webpack import bugs showing warnings on import ([#4265](https://github.com/OHIF/Viewers/issues/4265)) ([24c511f](https://github.com/OHIF/Viewers/commit/24c511f4bc04c4143bbd3d0d48029f41f7f36014)) + + +### Features + +* Add interleaved HTJ2K and volume progressive loading ([#4276](https://github.com/OHIF/Viewers/issues/4276)) ([a2084f3](https://github.com/OHIF/Viewers/commit/a2084f319b731d98b59485799fb80357094f8c38)) + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + + +### Bug Fixes + +* stdValue in TMTV mode ([#4278](https://github.com/OHIF/Viewers/issues/4278)) ([b2c6291](https://github.com/OHIF/Viewers/commit/b2c629123c5cf05afbeb19bd1424c327c1f5a606)) + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + + +### Features + +* **studyPrefetcher:** Study Prefetcher ([#4206](https://github.com/OHIF/Viewers/issues/4206)) ([2048b19](https://github.com/OHIF/Viewers/commit/2048b19484c0b1fae73f993cfaa814f861bbd230)) + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + + +### Bug Fixes + +* **cli:** missing js ([#4268](https://github.com/OHIF/Viewers/issues/4268)) ([f660f8e](https://github.com/OHIF/Viewers/commit/f660f8e970c0226b34a9de10e2c57429dcce6763)) + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + + +### Bug Fixes + +* **cli:** Fix the cli utilities which require full paths ([d09f8b5](https://github.com/OHIF/Viewers/commit/d09f8b5ba2dcc0c02beb405b8cfa79fbae5bdde8)), closes [#4267](https://github.com/OHIF/Viewers/issues/4267) + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + + +### Bug Fixes + +* **orthanc:** Correct bulkdata URL handling and add configuration example PDF ([#4262](https://github.com/OHIF/Viewers/issues/4262)) ([fdf883a](https://github.com/OHIF/Viewers/commit/fdf883ada880c0979acba8fdff9b542dc05b7706)) + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) +* **code:** remove console log ([#4248](https://github.com/OHIF/Viewers/issues/4248)) ([f3bbfff](https://github.com/OHIF/Viewers/commit/f3bbfff09b66ee020daf503656a2b58e763634a3)) +* **CustomViewportOverlay:** pass accurate data to Custom Viewport Functions ([#4224](https://github.com/OHIF/Viewers/issues/4224)) ([aef00e9](https://github.com/OHIF/Viewers/commit/aef00e91d63e9bc2de289cc6f35975e36547fb20)) +* **studybrowser:** Differentiate recent and all in study panel based on a provided time period ([#4242](https://github.com/OHIF/Viewers/issues/4242)) ([6f93449](https://github.com/OHIF/Viewers/commit/6f9344914951c204feaff48aaeb43cd7d727623d)) + + +### Features + +* customization service append and customize functionality should run once ([#4238](https://github.com/OHIF/Viewers/issues/4238)) ([e462fd3](https://github.com/OHIF/Viewers/commit/e462fd31f7944acfee34f08cfbc28cfd9de16169)) +* **HP:** Frame View HP ([#4235](https://github.com/OHIF/Viewers/issues/4235)) ([d5d8214](https://github.com/OHIF/Viewers/commit/d5d821464acb0f89fc9b189bd245a06c209d77b4)) +* **sort:** custom series sort in study panel ([#4214](https://github.com/OHIF/Viewers/issues/4214)) ([a433d40](https://github.com/OHIF/Viewers/commit/a433d406e2cac13f644203996c682260b54e8865)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + + +### Bug Fixes + +* Use correct external URL for rendered responses with relative URI ([#4236](https://github.com/OHIF/Viewers/issues/4236)) ([d8f6991](https://github.com/OHIF/Viewers/commit/d8f6991dbe72465080cfc5de39c7ea225702f2e0)) + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + + +### Bug Fixes + +* Re-enable hpScale module ([#4237](https://github.com/OHIF/Viewers/issues/4237)) ([2eab049](https://github.com/OHIF/Viewers/commit/2eab049d7993bb834f7736093941c175f16d61fc)) + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + + +### Bug Fixes + +* **cli:** version txt had a new line which it should not ([#4233](https://github.com/OHIF/Viewers/issues/4233)) ([097ef76](https://github.com/OHIF/Viewers/commit/097ef7665559a672d73e1babfc42afccc3cdd41d)) +* **pdf-viewport:** Allow Drag and Drop on PDF Viewport ([#4225](https://github.com/OHIF/Viewers/issues/4225)) ([729efb6](https://github.com/OHIF/Viewers/commit/729efb6d766e0f72f1fd8adefbca6fb46b355b2b)) + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + + +### Bug Fixes + +* **sr:** rendering issue by running loadSR before updateSR ([#4226](https://github.com/OHIF/Viewers/issues/4226)) ([6971287](https://github.com/OHIF/Viewers/commit/69712874603109aa4f655d47daf15d72167a49ff)) + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + + +### Features + +* Add customization merge, append or replace functionality ([#3871](https://github.com/OHIF/Viewers/issues/3871)) ([55dcfa1](https://github.com/OHIF/Viewers/commit/55dcfa1f6994a7036e7e594efb23673382a41915)) + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + + +### Bug Fixes + +* **window-level:** move window level region to more tools menu ([#4215](https://github.com/OHIF/Viewers/issues/4215)) ([33f4c18](https://github.com/OHIF/Viewers/commit/33f4c18f2687d30a250fe7183df3daae8394a984)) + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + + +### Bug Fixes + +* get direct url pixel data should be optional for video ([#4152](https://github.com/OHIF/Viewers/issues/4152)) ([649ffab](https://github.com/OHIF/Viewers/commit/649ffab4d97be875d42e1a3473a4354aac14e87d)) + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + + +### Bug Fixes + +* **seg:** maintain algorithm name and algorithm type when DICOM seg is exported or downloaded ([#4203](https://github.com/OHIF/Viewers/issues/4203)) ([a29e94d](https://github.com/OHIF/Viewers/commit/a29e94de803f79bbb3372d00ad8eb14b4224edc2)) + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + + +### Bug Fixes + +* **hydration:** Maintain the same slice that the user was on pre hydration in post hydration for SR and SEG. ([#4200](https://github.com/OHIF/Viewers/issues/4200)) ([430330f](https://github.com/OHIF/Viewers/commit/430330f7e384d503cb6fc695a7a9642ddfaac313)) + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + + +### Features + +* **window-level-region:** add window level region tool ([#4127](https://github.com/OHIF/Viewers/issues/4127)) ([ab1a18a](https://github.com/OHIF/Viewers/commit/ab1a18af5a5b0f9086c080ed81c8fda9bfaa975b)) + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + + +### Bug Fixes + +* **tmtv:** crosshairs should not have viewport indicators ([#4197](https://github.com/OHIF/Viewers/issues/4197)) ([f85da32](https://github.com/OHIF/Viewers/commit/f85da32f34389ef7cecae03c07e0af26468b52a6)) + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + + +### Bug Fixes + +* **seg:** should be able to navigate outside toolbox and come back later ([#4196](https://github.com/OHIF/Viewers/issues/4196)) ([93e7609](https://github.com/OHIF/Viewers/commit/93e760937f6587ba7481fcf3484ba9004ba49a62)) + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + + +### Bug Fixes + +* **docker:** docker build was broken because of imports ([#4192](https://github.com/OHIF/Viewers/issues/4192)) ([d7aa386](https://github.com/OHIF/Viewers/commit/d7aa386800153e0bb9eea6bbf36c696c57750ad8)) +* segmentation creation and segmentation mode viewport rendering ([#4193](https://github.com/OHIF/Viewers/issues/4193)) ([2174026](https://github.com/OHIF/Viewers/commit/217402678981f74293dff615f6b6812e54216d37)) + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + + +### Bug Fixes + +* **tmtv:** side panel crashing when activeToolOptions is not an array ([#4189](https://github.com/OHIF/Viewers/issues/4189)) ([19b5b1c](https://github.com/OHIF/Viewers/commit/19b5b1c15cb29ddf1cfd9b608815199bc838f8b2)) + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + + +### Bug Fixes + +* **queryparam:** set all query params to lowercase by default ([#4190](https://github.com/OHIF/Viewers/issues/4190)) ([e073d19](https://github.com/OHIF/Viewers/commit/e073d195fdec7f8bdb67e5e3dae522a0fd121ad2)) + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + + +### Bug Fixes + +* **contour:** set renderFill to false for contour ([#4186](https://github.com/OHIF/Viewers/issues/4186)) ([731340d](https://github.com/OHIF/Viewers/commit/731340d70ab23e116dd23e80b880bd8a28526f19)) + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + + +### Features + +* **hp:** Add displayArea option for Hanging protocols and example with Mamo([#3808](https://github.com/OHIF/Viewers/issues/3808)) ([18ac08e](https://github.com/OHIF/Viewers/commit/18ac08ed860d119721c52e4ffc270332259100b6)) + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + + +### Bug Fixes + +* **ultrasound:** Upgrade cornerstone3D version to resolve coloring issues ([#4181](https://github.com/OHIF/Viewers/issues/4181)) ([75a71db](https://github.com/OHIF/Viewers/commit/75a71db7f89840250ad1c2b35df5a35aceb8be7d)) + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + + +### Features + +* **measurements:** show untracked measurements in measurement panel under additional findings ([#4160](https://github.com/OHIF/Viewers/issues/4160)) ([18686c2](https://github.com/OHIF/Viewers/commit/18686c2caf13ede3e881303100bd4cc34b8b135f)) + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + + +### Bug Fixes + +* **rt:** dont convert to volume for RTSTRUCT ([#4157](https://github.com/OHIF/Viewers/issues/4157)) ([7745c09](https://github.com/OHIF/Viewers/commit/7745c092bb3edf0090f32fbbbae2f0776128d5a2)) + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + + +### Features + +* **types:** typed app config ([#4171](https://github.com/OHIF/Viewers/issues/4171)) ([8960b89](https://github.com/OHIF/Viewers/commit/8960b89911a9342d93bf1a62bec97a696f101fd4)) + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + + +### Bug Fixes + +* **crosshairs:** reset angle, position, and slabthickness for crosshairs when reset viewport tool is used ([#4113](https://github.com/OHIF/Viewers/issues/4113)) ([73d9e99](https://github.com/OHIF/Viewers/commit/73d9e99d5d6f38ab6c36f4471d54f18798feacb4)) + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + + +### Bug Fixes + +* dicom json for orthanc by Update package versions for [@cornerstonejs](https://github.com/cornerstonejs) dependencies ([#4165](https://github.com/OHIF/Viewers/issues/4165)) ([34c7d72](https://github.com/OHIF/Viewers/commit/34c7d72142847486b98c9c52469940083eeaf87e)) + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + + +### Bug Fixes + +* **HangingProtocol:** fix hp when unsupported series load first ([#4145](https://github.com/OHIF/Viewers/issues/4145)) ([b124c91](https://github.com/OHIF/Viewers/commit/b124c91d8fa0def262d1fee8f105295b02864129)) + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + + +### Features + +* **rt:** allow rendering of points in RT Struct ([#4128](https://github.com/OHIF/Viewers/issues/4128)) ([5903b07](https://github.com/OHIF/Viewers/commit/5903b0749aa41112d2e991bf53ed29b1fd7bd13f)) + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + + +### Bug Fixes + +* **segmentation:** Address issue where segmentation creation failed on layout change ([#4153](https://github.com/OHIF/Viewers/issues/4153)) ([29944c8](https://github.com/OHIF/Viewers/commit/29944c8512c35718af03c03ef82bc43675ee1872)) + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + + +### Features + +* **test:** Playwright testing integration ([#4146](https://github.com/OHIF/Viewers/issues/4146)) ([fe1a706](https://github.com/OHIF/Viewers/commit/fe1a706446cc33670bf5fab8451e8281b487fcd6)) + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + + +### Bug Fixes + +* **stack-invalidation:** Resolve stack invalidation if metadata invalidated ([#4147](https://github.com/OHIF/Viewers/issues/4147)) ([70bb6c4](https://github.com/OHIF/Viewers/commit/70bb6c46267b3733a665f12534b849c890ce54ad)) + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + + +### Bug Fixes + +* **select:** utilize react portals for select component ([#4144](https://github.com/OHIF/Viewers/issues/4144)) ([dce1e7d](https://github.com/OHIF/Viewers/commit/dce1e7d423cb64ec0d4be7362ecbfd52db47ef36)) + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + + +### Bug Fixes + +* **tmtv:** threshold was crashing the side panel ([#4119](https://github.com/OHIF/Viewers/issues/4119)) ([8d5c676](https://github.com/OHIF/Viewers/commit/8d5c676a5e1f3eda664071c8aece313de766bd59)) + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + + +### Bug Fixes + +* ๐Ÿ› Overflow scroll list menu based on screen hight ([#4123](https://github.com/OHIF/Viewers/issues/4123)) ([6bba2e7](https://github.com/OHIF/Viewers/commit/6bba2e70f80d8eacc57c0e765013d9c10adf5413)) + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + + +### Bug Fixes + +* **auth:** bind handleUnauthenticated to correct context ([#4120](https://github.com/OHIF/Viewers/issues/4120)) ([8fa339f](https://github.com/OHIF/Viewers/commit/8fa339f296fd7e844f3879cfd81e47dbff315e66)) +* **DicomJSONDataSource:** Fix series filtering ([#4092](https://github.com/OHIF/Viewers/issues/4092)) ([2de102c](https://github.com/OHIF/Viewers/commit/2de102c73c795cfb48b49005b10aa788444a45b7)) + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + + +### Bug Fixes + +* **bugs:** enhancements and bugs in several areas ([#4086](https://github.com/OHIF/Viewers/issues/4086)) ([730f434](https://github.com/OHIF/Viewers/commit/730f4349100f21b4489a21707dbb2dca9dbfbba2)) + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + + +### Bug Fixes + +* **rt:** enhanced RT support, utilize SVGs for rendering. ([#4074](https://github.com/OHIF/Viewers/issues/4074)) ([0156bc4](https://github.com/OHIF/Viewers/commit/0156bc426f1840ae0d090223e94a643726e856cb)) + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + + +### Bug Fixes + +* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f)) + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + + +### Bug Fixes + +* **bugs:** fix patient header for doc, track ball rotate resize observer and add segmentation button not being enabled on viewport data change ([#4068](https://github.com/OHIF/Viewers/issues/4068)) ([c09311d](https://github.com/OHIF/Viewers/commit/c09311d3b7df05fcd00a9f36a7233e9d7e5589d0)) + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + + +### Bug Fixes + +* **viewport-sync:** Enable re-sync image slices in a different position when needed ([#3984](https://github.com/OHIF/Viewers/issues/3984)) ([6ebd2cc](https://github.com/OHIF/Viewers/commit/6ebd2cc7cb70cd88fd01dc1e516077f27b201943)) + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + + +### Bug Fixes + +* **vewport:** Add missing blendmodes from cornerstonejs ([#4055](https://github.com/OHIF/Viewers/issues/4055)) ([3ec7e51](https://github.com/OHIF/Viewers/commit/3ec7e512169a07506388902acb5b2c118093fa50)) +* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951)) + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + + +### Bug Fixes + +* **hp:** Fails to display any layouts in the layout selector if first layout has multiple stages ([#4058](https://github.com/OHIF/Viewers/issues/4058)) ([f0ed3fd](https://github.com/OHIF/Viewers/commit/f0ed3fd7b99b0e4e00b261ceb9888ba94726719c)) + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + + +### Features + +* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb)) + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + + +### Bug Fixes + +* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af)) + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + + +### Bug Fixes + +* **bugs:** and replace seriesInstanceUID and seriesInstanceUIDs URL with seriesInstanceUIDs ([#4049](https://github.com/OHIF/Viewers/issues/4049)) ([da7c1a5](https://github.com/OHIF/Viewers/commit/da7c1a5d8c54bfa1d3f97bbc500386bf76e7fd9d)) + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes - final ([#4048](https://github.com/OHIF/Viewers/issues/4048)) ([170bb96](https://github.com/OHIF/Viewers/commit/170bb96983082c39b22b7352e0c54aacf3e73b02)) + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes - more ([#4043](https://github.com/OHIF/Viewers/issues/4043)) ([3754c22](https://github.com/OHIF/Viewers/commit/3754c224b4dab28182adb0a41e37d890942144d8)) + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + + +### Bug Fixes + +* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025)) + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637)) + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + + +### Features + +* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13)) + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + + +### Bug Fixes + +* **general:** enhancements and bug fixes ([#4018](https://github.com/OHIF/Viewers/issues/4018)) ([2b83393](https://github.com/OHIF/Viewers/commit/2b83393f91cb16ea06821d79d14ff60f80c29c90)) + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + + +### Bug Fixes + +* **dicom-video:** Update get direct func for dicom json to use url if present and fix config argument ([#4017](https://github.com/OHIF/Viewers/issues/4017)) ([4f99244](https://github.com/OHIF/Viewers/commit/4f99244d864427d69be6f863cb7a6a78411adb12)) + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + + +### Bug Fixes + +* **MetaDataProvider:** Fix tag in GeneralImageModule ([#4000](https://github.com/OHIF/Viewers/issues/4000)) ([e9c30a1](https://github.com/OHIF/Viewers/commit/e9c30a108e2dd14a8b137b81e5b832cc167bc3d1)) + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + + +### Bug Fixes + +* Microscopy bulkdata and image retrieve ([#3894](https://github.com/OHIF/Viewers/issues/3894)) ([7fac49b](https://github.com/OHIF/Viewers/commit/7fac49b4492b4bd5e9ece8e2e2b0fa2faa840d7f)) + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + + +### Features + +* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2)) + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + + +### Bug Fixes + +* **cornerstone-dicom-sr:** Freehand SR hydration support ([#3996](https://github.com/OHIF/Viewers/issues/3996)) ([5645ac1](https://github.com/OHIF/Viewers/commit/5645ac1b271e1ed8c57f5d71100809362447267e)) + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + + +### Features + +* **advanced-roi-tools:** new tools and icon updates and overlay bug fixes ([#4014](https://github.com/OHIF/Viewers/issues/4014)) ([cea27d4](https://github.com/OHIF/Viewers/commit/cea27d438d1de2c1ec90cbaefdc2b31a1d9980a1)) + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + + +### Features + +* **measurement:** Add support measurement label autocompletion ([#3855](https://github.com/OHIF/Viewers/issues/3855)) ([56b1eae](https://github.com/OHIF/Viewers/commit/56b1eae6356a6534960df1196bdd1e95b0a9a470)) + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + + +### Bug Fixes + +* **presentation-state:** Iterate over map properly to restore the presentation state ([#4013](https://github.com/OHIF/Viewers/issues/4013)) ([fa38e6a](https://github.com/OHIF/Viewers/commit/fa38e6a07a259d8cb33277922884e722414ac548)) + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + + +### Features + +* **segmentation:** Enhanced segmentation panel design for TMTV ([#3988](https://github.com/OHIF/Viewers/issues/3988)) ([9f3235f](https://github.com/OHIF/Viewers/commit/9f3235ff096636aafa88d8a42859e8dc85d9036d)) + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + + +### Features + +* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1)) + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + + +### Bug Fixes + +* **new layout:** address black screen bugs ([#4008](https://github.com/OHIF/Viewers/issues/4008)) ([158a181](https://github.com/OHIF/Viewers/commit/158a1816703e0ad66cae08cb9bd1ffb93bbd8d43)) + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + + +### Features + +* **layout:** new layout selector with 3D volume rendering ([#3923](https://github.com/OHIF/Viewers/issues/3923)) ([617043f](https://github.com/OHIF/Viewers/commit/617043fe0da5de91fbea4ac33a27f1df16ae1ca6)) + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + + +### Features + +* **worklist:** new investigational use text ([#3999](https://github.com/OHIF/Viewers/issues/3999)) ([45b68e8](https://github.com/OHIF/Viewers/commit/45b68e841dcb9e28a2ea991c37ee7ac4a8c5b71e)) + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + + +### Features + +* **worklist:** New worklist buttons and tooltips ([#3989](https://github.com/OHIF/Viewers/issues/3989)) ([9bcd1ae](https://github.com/OHIF/Viewers/commit/9bcd1ae6f51d61786cc1e99624f396b56a47cd69)) + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + + +### Bug Fixes + +* **SR display:** and the token based navigation ([#3995](https://github.com/OHIF/Viewers/issues/3995)) ([feed230](https://github.com/OHIF/Viewers/commit/feed2304c124dc2facc7a7371ed9851548c223c5)) + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + + +### Features + +* **delete measurement:** icon for measurement table ([#3775](https://github.com/OHIF/Viewers/issues/3775)) ([f7fe91c](https://github.com/OHIF/Viewers/commit/f7fe91c5f6c4f05f3f3f5f640d3a119bd40a5870)) + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + + +### Bug Fixes + +* **cli:** mode creation template ([#3876](https://github.com/OHIF/Viewers/issues/3876)) ([#3981](https://github.com/OHIF/Viewers/issues/3981)) ([e485d68](https://github.com/OHIF/Viewers/commit/e485d68fd4619ce7187113cbe59e47f9523dbcc8)) + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + + +### Bug Fixes + +* **docs:** Minor typos in hpModule.md ([#3962](https://github.com/OHIF/Viewers/issues/3962)) ([4cdfdae](https://github.com/OHIF/Viewers/commit/4cdfdae8149166cf9dc91a55c0d7f2a224e55d8f)) + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + + +### Bug Fixes + +* **demo:** Deploy issue ([#3951](https://github.com/OHIF/Viewers/issues/3951)) ([21e8a2b](https://github.com/OHIF/Viewers/commit/21e8a2bd0b7cc72f90a31e472d285d761be15d30)) + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + + +### Features + +* **errorboundary:** format stack trace properly ([#3931](https://github.com/OHIF/Viewers/issues/3931)) ([0eac386](https://github.com/OHIF/Viewers/commit/0eac386a31a5d6965536360aa65a44769c1a5740)) + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + + +### Bug Fixes + +* ๐Ÿ› Sort merge results based on default data source (input) ([#3903](https://github.com/OHIF/Viewers/issues/3903)) ([5bba98e](https://github.com/OHIF/Viewers/commit/5bba98ed848bdf46b5ba4fc4708527cced3308b5)) + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + + +### Bug Fixes + +* catch errors in getPTImageIdInstanceMetadata ([#3897](https://github.com/OHIF/Viewers/issues/3897)) ([a47aeb8](https://github.com/OHIF/Viewers/commit/a47aeb8bd729dcb8d2cfc13b27a31b0dd88f11ad)) + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + + +### Bug Fixes + +* **viewport-sync:** remember synced viewports bw stack and volume and RENAME StackImageSync to ImageSliceSync ([#3849](https://github.com/OHIF/Viewers/issues/3849)) ([e4a116b](https://github.com/OHIF/Viewers/commit/e4a116b074fcb85c8cbcc9db44fdec565f3386db)) + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + + +### Bug Fixes + +* is same orientaiton ([#3905](https://github.com/OHIF/Viewers/issues/3905)) ([31b837f](https://github.com/OHIF/Viewers/commit/31b837fa90f631d4984482c6e952373fbb8bdbfc)) + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + + +### Bug Fixes + +* ๐Ÿ› Check merge key for merge data source ([#3901](https://github.com/OHIF/Viewers/issues/3901)) ([911d672](https://github.com/OHIF/Viewers/commit/911d67283536b2fe7930948f2819ea0ad66e2a32)) + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + + +### Bug Fixes + +* Update CS3D to fix second render ([#3892](https://github.com/OHIF/Viewers/issues/3892)) ([d00a86b](https://github.com/OHIF/Viewers/commit/d00a86b022742ea089d246d06cfd691f43b64412)) + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + + +### Features + +* **hp:** enable OHIF to run with partial metadata for large studies at the cost of less effective hanging protocol ([#3804](https://github.com/OHIF/Viewers/issues/3804)) ([0049f4c](https://github.com/OHIF/Viewers/commit/0049f4c0303f0b6ea995972326fc8784259f5a47)) + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + + +### Features + +* **transferSyntax:** prefer server transcoded transfer syntax for all images ([#3883](https://github.com/OHIF/Viewers/issues/3883)) ([1456a49](https://github.com/OHIF/Viewers/commit/1456a493d66c90c787b022256c9f2846afb115fc)) + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + + +### Bug Fixes + +* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6)) + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + + +### Features + +* Add on mode init hook ([#3882](https://github.com/OHIF/Viewers/issues/3882)) ([f58725c](https://github.com/OHIF/Viewers/commit/f58725ce40685f7297181ef98d81bc28420c8291)) + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + + +### Features + +* **ui:** sidePanel expandedWidth ([#3728](https://github.com/OHIF/Viewers/issues/3728)) ([61bf22c](https://github.com/OHIF/Viewers/commit/61bf22c6f80e764bdf5c3b56bb0124a95aa0f793)) + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + + +### Features + +* improve disableEditing flag ([#3875](https://github.com/OHIF/Viewers/issues/3875)) ([2049c09](https://github.com/OHIF/Viewers/commit/2049c0936c86f819604c243d3dc7b3fe971b5b2c)) + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + + +### Bug Fixes + +* convert radian to degree value for mip rotation ([#3881](https://github.com/OHIF/Viewers/issues/3881)) ([bf846c9](https://github.com/OHIF/Viewers/commit/bf846c94c378f04b9f44dcd71be3f056dbcfe0b5)) +* PDF display request in v3 ([#3878](https://github.com/OHIF/Viewers/issues/3878)) ([9865030](https://github.com/OHIF/Viewers/commit/98650302c7575f0aea386e32cfc4112c378035e6)) + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + + +### Bug Fixes + +* colormap for stack viewports via HangingProtocol ([#3866](https://github.com/OHIF/Viewers/issues/3866)) ([e8858f3](https://github.com/OHIF/Viewers/commit/e8858f3eb55552f695af4a55980f9ae2e9af7291)) + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + + +### Bug Fixes + +* **icon-style:** Ensure consistent icon dimensions ([#3727](https://github.com/OHIF/Viewers/issues/3727)) ([6ca13c0](https://github.com/OHIF/Viewers/commit/6ca13c0a4cb5a95bbb52b0db902b5dbf72f8aa6e)) + + +### Features + +* **overlay:** add inline binary overlays ([#3852](https://github.com/OHIF/Viewers/issues/3852)) ([0177b62](https://github.com/OHIF/Viewers/commit/0177b625ba86760168bc4db58c8a109aa9ee83cb)) + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + + +### Features + +* **customizationService:** Enable saving and loading of private tags in SRs ([#3842](https://github.com/OHIF/Viewers/issues/3842)) ([e1f55e6](https://github.com/OHIF/Viewers/commit/e1f55e65f2d2a34136ad5d0b1ada77d337a0ea23)) + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + + +### Bug Fixes + +* address and improve system vulnerabilities ([#3851](https://github.com/OHIF/Viewers/issues/3851)) ([805c532](https://github.com/OHIF/Viewers/commit/805c53270f243ec61f142a3ffa0af500021cd5ec)) + + +### Features + +* **config:** Add activateViewportBeforeInteraction parameter for viewport interaction customization ([#3847](https://github.com/OHIF/Viewers/issues/3847)) ([f707b4e](https://github.com/OHIF/Viewers/commit/f707b4ebc996f379cd30337badc06b07e6e35ac5)) +* **i18n:** enhanced i18n support ([#3761](https://github.com/OHIF/Viewers/issues/3761)) ([d14a8f0](https://github.com/OHIF/Viewers/commit/d14a8f0199db95cd9e85866a011b64d6bf830d57)) + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + + +### Features + +* **HP:** Added new 3D hanging protocols to be used in the new layout selector ([#3844](https://github.com/OHIF/Viewers/issues/3844)) ([59576d6](https://github.com/OHIF/Viewers/commit/59576d695d4d26601d35c43f73d602f0b12d72bf)) + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + + +### Bug Fixes + +* **auth:** fix the issue with oauth at a non root path ([#3840](https://github.com/OHIF/Viewers/issues/3840)) ([6651008](https://github.com/OHIF/Viewers/commit/6651008fbb35dabd5991c7f61128e6ef324012df)) + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + + +### Bug Fixes + +* **SM:** drag and drop is now fixed for SM ([#3813](https://github.com/OHIF/Viewers/issues/3813)) ([f1a6764](https://github.com/OHIF/Viewers/commit/f1a67647aed635437b188cea7cf5d5a8fb974bbe)) + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + + +### Bug Fixes + +* **cine:** Set cine disabled on mode exit. ([#3812](https://github.com/OHIF/Viewers/issues/3812)) ([924affa](https://github.com/OHIF/Viewers/commit/924affa7b5d420c2f91522a075cecbb3c78e8f52)) + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + + +### Bug Fixes + +* Update the CS3D packages to add the most recent HTJ2K TSUIDS ([#3806](https://github.com/OHIF/Viewers/issues/3806)) ([9d1884d](https://github.com/OHIF/Viewers/commit/9d1884d7d8b6b2a1cdc26965a96995838aa72682)) + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + + +### Features + +* Merge Data Source ([#3788](https://github.com/OHIF/Viewers/issues/3788)) ([c4ff2c2](https://github.com/OHIF/Viewers/commit/c4ff2c2f09546ce8b72eab9c5e7beed611e3cab0)) + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + + +### Features + +* **events:** broadcast series summary metadata ([#3798](https://github.com/OHIF/Viewers/issues/3798)) ([404b0a5](https://github.com/OHIF/Viewers/commit/404b0a5d535182d1ae44e33f7232db500a7b2c16)) + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + + +### Bug Fixes + +* **DICOM Overlay:** The overlay data wasn't being refreshed on change ([#3793](https://github.com/OHIF/Viewers/issues/3793)) ([00e7519](https://github.com/OHIF/Viewers/commit/00e751933ac6d611a34773fa69594243f1b99082)) + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + + +### Bug Fixes + +* **metadata:** to handle cornerstone3D update for htj2k ([#3783](https://github.com/OHIF/Viewers/issues/3783)) ([8c8924a](https://github.com/OHIF/Viewers/commit/8c8924af373d906773f5db20defe38628cacd4a0)) + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + + +### Features + +* **docs:** Added various training videos to support the OHIF CLI tools ([#3794](https://github.com/OHIF/Viewers/issues/3794)) ([d83beb7](https://github.com/OHIF/Viewers/commit/d83beb7c62c1d5be19c54e08d23883f112147fe1)) + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + + +### Features + +* **url:** Add SeriesInstanceUIDs wado query param ([#3746](https://github.com/OHIF/Viewers/issues/3746)) ([b694228](https://github.com/OHIF/Viewers/commit/b694228dd535e4b97cb86a1dc085b6e8716bdaf3)) + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + + +### Bug Fixes + +* ๐Ÿ› Run error handler for failed image requests ([#3773](https://github.com/OHIF/Viewers/issues/3773)) ([3234014](https://github.com/OHIF/Viewers/commit/323401418e7ccab74655ba02f990bbe0ed4e523b)) + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + + +### Bug Fixes + +* **overlay:** Overlays aren't shown on undefined origin ([#3781](https://github.com/OHIF/Viewers/issues/3781)) ([fd1251f](https://github.com/OHIF/Viewers/commit/fd1251f751d8147b8a78c7f4d81c67ba69769afa)) + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + + +### Features + +* **dicomJSON:** Add Loading Other Display Sets and JSON Metadata Generation script ([#3777](https://github.com/OHIF/Viewers/issues/3777)) ([43b1c17](https://github.com/OHIF/Viewers/commit/43b1c17209502e4876ad59bae09ed9442eda8024)) + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + + +### Bug Fixes + +* **path:** upgrade docusaurus for security ([#3780](https://github.com/OHIF/Viewers/issues/3780)) ([8bbcd0e](https://github.com/OHIF/Viewers/commit/8bbcd0e692e25917c1b6dd94a39fac834c812fca)) + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + + +### Bug Fixes + +* **arrow:** ArrowAnnotate text key cause validation error ([#3771](https://github.com/OHIF/Viewers/issues/3771)) ([8af1046](https://github.com/OHIF/Viewers/commit/8af10468035f1f59e0a21e579d50ad63c8cbf7ad)) + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + + +### Features + +* add VolumeViewport rotation ([#3776](https://github.com/OHIF/Viewers/issues/3776)) ([442f99d](https://github.com/OHIF/Viewers/commit/442f99d5eb2ceece7def20e14da59af1dd7d8442)) + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + + +### Features + +* **hp callback:** Add viewport ready callback ([#3772](https://github.com/OHIF/Viewers/issues/3772)) ([bf252bc](https://github.com/OHIF/Viewers/commit/bf252bcec2aae3a00479fdcb732110b344bcf2c0)) + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + + +### Bug Fixes + +* **thumbnail:** Avoid multiple promise creations for thumbnails ([#3756](https://github.com/OHIF/Viewers/issues/3756)) ([b23eeff](https://github.com/OHIF/Viewers/commit/b23eeff93745769e67e60c33d75293d6242c5ec9)) + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + + +### Features + +* **i18n:** enhanced i18n support ([#3730](https://github.com/OHIF/Viewers/issues/3730)) ([330e11c](https://github.com/OHIF/Viewers/commit/330e11c7ff0151e1096e19b8ffdae7d64cae280e)) + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + + +### Bug Fixes + +* **measurement service:** Implemented correct check of schema keys in _isValidMeasurment. ([#3750](https://github.com/OHIF/Viewers/issues/3750)) ([db39585](https://github.com/OHIF/Viewers/commit/db395852b6fc6cd5c265a9282e5eee5bd6f951b7)) + + +### Features + +* **filters:** save worklist query filters to session storage so that they persist between navigation to the viewer and back ([#3749](https://github.com/OHIF/Viewers/issues/3749)) ([2a15ef0](https://github.com/OHIF/Viewers/commit/2a15ef0e44b7b4d8bbf5cb9363db6e523201c681)) + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + + +### Bug Fixes + +* **toolbar:** allow customizable toolbar for active viewport and allow active tool to be deactivated via a click ([#3608](https://github.com/OHIF/Viewers/issues/3608)) ([dd6d976](https://github.com/OHIF/Viewers/commit/dd6d9768bbca1d3cc472e8c1e6d85822500b96ef)) + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + + +### Bug Fixes + +* **sr:** dcm4chee requires the patient name for an SR to match what is in the original study ([#3739](https://github.com/OHIF/Viewers/issues/3739)) ([d98439f](https://github.com/OHIF/Viewers/commit/d98439fe7f3825076dbc87b664a1d1480ff414d3)) + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + + +### Bug Fixes + +* **recipes:** package.json script orthanc:up docker-compose path ([#3741](https://github.com/OHIF/Viewers/issues/3741)) ([49514ae](https://github.com/OHIF/Viewers/commit/49514aedfe0498b5bd505193106a9745a6a5b5e6)) + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + + +### Bug Fixes + +* **cine:** Use the frame rate specified in DICOM and optionally auto play cine ([#3735](https://github.com/OHIF/Viewers/issues/3735)) ([d9258ec](https://github.com/OHIF/Viewers/commit/d9258eca70587cf4dc18be4e56c79b16bae73d6d)) + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + + +### Bug Fixes + +* **calibration:** No calibration popup caused by perhaps an unused code optimization for production builds ([#3736](https://github.com/OHIF/Viewers/issues/3736)) ([93d798d](https://github.com/OHIF/Viewers/commit/93d798db99c0dee53ef73c376f8a74ac3049cf3f)) + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + + +### Bug Fixes + +* **display messages:** broken after timings ([#3719](https://github.com/OHIF/Viewers/issues/3719)) ([157b88c](https://github.com/OHIF/Viewers/commit/157b88c909d3289cb89ace731c1f9a19d40797ac)) + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + + +### Bug Fixes + +* **export:** wrong export for the tmtv RT function ([#3715](https://github.com/OHIF/Viewers/issues/3715)) ([a3f2a1a](https://github.com/OHIF/Viewers/commit/a3f2a1a7b0d16bfcc0ecddc2ab731e54c5e377c8)) + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + + +### Bug Fixes + +* **i18n:** display set(s) are two words for English messages ([#3711](https://github.com/OHIF/Viewers/issues/3711)) ([c3a5847](https://github.com/OHIF/Viewers/commit/c3a5847dcd3dce4f1c8d8b11af95f79e3f93f70d)) + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + + +### Bug Fixes + +* **modules:** add stylus loader as an option to be uncommented ([#3710](https://github.com/OHIF/Viewers/issues/3710)) ([7c57f67](https://github.com/OHIF/Viewers/commit/7c57f67844b790fc6e47ac3f9708bf9d576389c8)) + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + + +### Bug Fixes + +* **segmentation:** Various fixes for segmentation mode and other ([#3709](https://github.com/OHIF/Viewers/issues/3709)) ([a9a6ad5](https://github.com/OHIF/Viewers/commit/a9a6ad50eae67b43b8b34efc07182d788cacdcfe)) + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + + +### Bug Fixes + +* **voi:** should publish voi change event on reset ([#3707](https://github.com/OHIF/Viewers/issues/3707)) ([52f34c6](https://github.com/OHIF/Viewers/commit/52f34c64d014f433ec1661a39b47e7fb27f15332)) + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + + +### Bug Fixes + +* **modality unit:** fix the modality unit per target via upgrade of cs3d ([#3706](https://github.com/OHIF/Viewers/issues/3706)) ([0a42d57](https://github.com/OHIF/Viewers/commit/0a42d573bbca7f2551a831a46d3aa6b56674a580)) + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + + +### Bug Fixes + +* **segmentation:** do not use SAB if not specified ([#3705](https://github.com/OHIF/Viewers/issues/3705)) ([4911e47](https://github.com/OHIF/Viewers/commit/4911e4796cef5e22cb7cc0ca73dc5c956bc75339)) + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + + +### Features + +* **Segmentation:** download RTSS from Labelmap([#3692](https://github.com/OHIF/Viewers/issues/3692)) ([40673f6](https://github.com/OHIF/Viewers/commit/40673f64b36b1150149c55632aa1825178a39e65)) + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + + +### Bug Fixes + +* **bugs:** fixing lots of bugs regarding release candidate ([#3700](https://github.com/OHIF/Viewers/issues/3700)) ([8bc12a3](https://github.com/OHIF/Viewers/commit/8bc12a37d0353160ae5ea4624dc0b244b7d59c07)) + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + + +### Bug Fixes + +* **segmentation scroll:** and hydration bugs ([#3701](https://github.com/OHIF/Viewers/issues/3701)) ([1fd98d9](https://github.com/OHIF/Viewers/commit/1fd98d922094d10fe0c6e9df726314ec9fce49e8)) + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + + +### Bug Fixes + +* **measurement and microscopy:** various small fixes for measurement and microscopy side panel ([#3696](https://github.com/OHIF/Viewers/issues/3696)) ([c1d5ee7](https://github.com/OHIF/Viewers/commit/c1d5ee7e3f7f4c0c6bed9ae81eba5519741c5155)) + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + + +### Features + +* **locale:** add German translations - community PR ([#3697](https://github.com/OHIF/Viewers/issues/3697)) ([ebe8f71](https://github.com/OHIF/Viewers/commit/ebe8f71da22c1d24b58f889c5d803951e19817b6)) + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + + +### Features + +* **locale:** Added Turkish language support (tr-TR) - Community PR ([#3695](https://github.com/OHIF/Viewers/issues/3695)) ([745050a](https://github.com/OHIF/Viewers/commit/745050a28ec7c2ef2e9a4d4e590040050b2177b2)) + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + + +### Bug Fixes + +* **translation:** Side panel translate fix ([#3156](https://github.com/OHIF/Viewers/issues/3156)) ([29748d4](https://github.com/OHIF/Viewers/commit/29748d46a14d23817dbe196e0f64363fc61a8aed)) + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + + +### Bug Fixes + +* **cli:** Add npm packaged mode not working ([#3689](https://github.com/OHIF/Viewers/issues/3689)) ([28cec04](https://github.com/OHIF/Viewers/commit/28cec04ff43b81e218c3e9addef4665b3833a6fe)) + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + + +### Features + +* **debug:** Add timing information about time to first image/all images, and query time ([#3681](https://github.com/OHIF/Viewers/issues/3681)) ([108383b](https://github.com/OHIF/Viewers/commit/108383b9ef51e4bef82d9c932b9bc7aa5354e799)) + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + + +### Features + +* **displayArea:** add display area to hanging protocol ([#3691](https://github.com/OHIF/Viewers/issues/3691)) ([5e7fe91](https://github.com/OHIF/Viewers/commit/5e7fe91617d7399f85702d82e7bfa028b8010a89)) + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + + +### Bug Fixes + +* **editing:** regression bug in disable editing ([#3687](https://github.com/OHIF/Viewers/issues/3687)) ([4dc2acd](https://github.com/OHIF/Viewers/commit/4dc2acdefa872dd1d8df47f465e9e9656f95f67f)) + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + + +### Bug Fixes + +* **typescript error:** Change pubSubServiceInterface file type to typescript ([#3546](https://github.com/OHIF/Viewers/issues/3546)) ([eb22328](https://github.com/OHIF/Viewers/commit/eb22328fc05d06fc4411805e7a30f826659d796a)) + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + + +### Bug Fixes + +* **dicom overlay:** Handle special cases of ArrayBuffer for various DICOM overlay attributes. ([#3684](https://github.com/OHIF/Viewers/issues/3684)) ([e36a604](https://github.com/OHIF/Viewers/commit/e36a6043315e900eeb6ce183772c7f852f478e96)) +* **StackSync:** Miscellaneous fixes for stack image sync ([#3663](https://github.com/OHIF/Viewers/issues/3663)) ([8a335bd](https://github.com/OHIF/Viewers/commit/8a335bd03d14ba87d65d7468d93f74040aa828d9)) + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + + +### Bug Fixes + +* **config:** support more values for the useSharedArrayBuffer ([#3688](https://github.com/OHIF/Viewers/issues/3688)) ([1129c15](https://github.com/OHIF/Viewers/commit/1129c155d2c7d46c98a5df7c09879aa3d459fa7e)) + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + + +### Bug Fixes + +* **no sab:** should work when shared array buffer is not required ([#3686](https://github.com/OHIF/Viewers/issues/3686)) ([a67d72d](https://github.com/OHIF/Viewers/commit/a67d72de85238b369a18c010bf6d147daefc6df5)) + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + + +### Bug Fixes + +* **cli:** various fixes for adding custom modes and extensions ([#3683](https://github.com/OHIF/Viewers/issues/3683)) ([dc73b18](https://github.com/OHIF/Viewers/commit/dc73b187484da029a2664bb1302f30137c973b8c)) + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + + +### Bug Fixes + +* **toggleOneUp:** fixed one up for main tmtv layout ([#3677](https://github.com/OHIF/Viewers/issues/3677)) ([86f54d0](https://github.com/OHIF/Viewers/commit/86f54d0d07042750a863ae876aa8dd5fb16029a5)) + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Bug Fixes + +* **react-select:** update react select package ([#3622](https://github.com/OHIF/Viewers/issues/3622)) ([04ca10d](https://github.com/OHIF/Viewers/commit/04ca10d8779dd15454920002f3d48afa8830de8a)) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) +* **SidePanel:** new side panel tab look-and-feel ([#3657](https://github.com/OHIF/Viewers/issues/3657)) ([85c899b](https://github.com/OHIF/Viewers/commit/85c899b399e2521480724be145538993721b9378)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + + +### Performance Improvements + +* **memory:** add 16 bit texture via configuration - reduces memory by half ([#3662](https://github.com/OHIF/Viewers/issues/3662)) ([2bd3b26](https://github.com/OHIF/Viewers/commit/2bd3b26a6aa54b211ef988f3ad64ef1fe5648bab)) + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + + +### Bug Fixes + +* **mpr:** Return the original/raw hanging protocol when fetching and preserving the current active protocol. ([#3670](https://github.com/OHIF/Viewers/issues/3670)) ([221dedd](https://github.com/OHIF/Viewers/commit/221dedde5dd4df086276406a9fa2da1cc23b4eb1)) + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + + +### Bug Fixes + +* **keyCloak:** fix openresty keycloak deployment recipe ([#3655](https://github.com/OHIF/Viewers/issues/3655)) ([2d7721c](https://github.com/OHIF/Viewers/commit/2d7721cb581f55dc49e3baeca2411b18dd78ad74)) + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + + +### Bug Fixes + +* **DicomJson:** retrieve.series.metadata method should be async ([#3659](https://github.com/OHIF/Viewers/issues/3659)) ([2737903](https://github.com/OHIF/Viewers/commit/2737903386cf97399473e0fa64fe53ad14da155a)) + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + + +### Bug Fixes + +* **measurements:** Update the calibration tool to match changes in CS3D ([#3505](https://github.com/OHIF/Viewers/issues/3505)) ([38af311](https://github.com/OHIF/Viewers/commit/38af3112ec1f94f36c0ef64ff1cf9d21c0981c81)) + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + + +### Bug Fixes + +* **health imaging:** studies not loading from healthimaging if imagepositionpatient is missing ([#3646](https://github.com/OHIF/Viewers/issues/3646)) ([74e62a1](https://github.com/OHIF/Viewers/commit/74e62a176374f720080d4e777972f70e7f2d8b2b)) + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + + +### Bug Fixes + +* **suv:** import calculate-suv library version that prevents SUV calculation for a zero PatientWeight ([#3638](https://github.com/OHIF/Viewers/issues/3638)) ([0d10f46](https://github.com/OHIF/Viewers/commit/0d10f46b885fe54ec3dae1848134da658eb6280a)) + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + + +### Bug Fixes + +* **hotkeys:** preserve hotkeys if changed, and reduce re-rendering ([#3635](https://github.com/OHIF/Viewers/issues/3635)) ([94f7cfb](https://github.com/OHIF/Viewers/commit/94f7cfb08e3490488394efc42ef089ebe55e86be)) + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + + +### Features + +* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f)) + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + + +### Bug Fixes + +* **nginx archive recipe:** Fixes to various configuration files. ([#3624](https://github.com/OHIF/Viewers/issues/3624)) ([3ce7225](https://github.com/OHIF/Viewers/commit/3ce72254b390f32c9aa207a0589e688805e2659d)) + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + + +### Features + +* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd)) + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + + +### Features + +* **data source UI config:** Popup the configuration dialogue whenever a data source is not fully configured ([#3620](https://github.com/OHIF/Viewers/issues/3620)) ([adedc8c](https://github.com/OHIF/Viewers/commit/adedc8c382e18a2e86a569e3d023cc55a157363f)) + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + + +### Bug Fixes + +* **OpenIdConnectRoutes:** fix handleUnauthenticated ([#3617](https://github.com/OHIF/Viewers/issues/3617)) ([35fc30c](https://github.com/OHIF/Viewers/commit/35fc30c5359d8199cc38ffa670c08687d2672f11)) + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + + +### Bug Fixes + +* **PT Metadata:** Allow for PatientWeight to be missing from the metadata ([#3621](https://github.com/OHIF/Viewers/issues/3621)) ([44f101d](https://github.com/OHIF/Viewers/commit/44f101d3f2b3204b67e31f4e4939062e65a246ee)) + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package ohif-monorepo-root + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + + +### Features + +* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1)) + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + + +### Bug Fixes + +* **memory leak:** array buffer was sticking around in volume viewports ([#3611](https://github.com/OHIF/Viewers/issues/3611)) ([65b49ae](https://github.com/OHIF/Viewers/commit/65b49aeb1b5f38224e4892bdf32453500ee351f8)) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..1e7be83 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at danny.ri.brown+OHIFcoc@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3863cd4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1 @@ +See our contributing guidelines at [`https://docs.ohif.org`](https://docs.ohif.org/development/contributing.html) diff --git a/DATACITATION.md b/DATACITATION.md new file mode 100644 index 0000000..eff3cd8 --- /dev/null +++ b/DATACITATION.md @@ -0,0 +1,297 @@ +# OHIF public demo data sets + +The OHIF Viewer's public demo page, available at https://viewer.ohif.org/, uses publicly anonymized demo datasets. +These datasets were mostly obtained from the [NIH NCI Imaging Data Commons](https://datacommons.cancer.gov/repository/imaging-data-commons) +and [NIH NCI TCIA](https://www.cancerimagingarchive.net/). Before listing the datasets, +we would like to extend a special thank you to all groups who have made their datasets publicly available. +Without them, we would not have been able to create this demo page. + +Please find below the list of datasets used on the demo page, along with their respective citations. + + +## Platforms + +### NIH NCI IDC + +- Fedorov, A., Longabaugh, W.J., Pot, D., Clunie, D.A., Pieper, S., Aerts, H.J., Homeyer, A., Lewis, R., Akbarzadeh, A., Bontempi, D. and Clifford, W., 2021. NCI imaging data commons. Cancer research, 81(16), p.4188. + +### NIH NCI TCIA + +- Clark, K., Vendt, B., Smith, K., Freymann, J., Kirby, J., Koppel, P., Moore, S., Phillips, S., Maffitt, D., Pringle, M., Tarbox, L., & Prior, F. (2013). The Cancer Imaging Archive (TCIA): Maintaining and Operating a Public Information Repository. Journal of Digital Imaging, 26(6), 1045โ€“1057. https://doi.org/10.1007/s10278-013-9622-7 + + + + +## Datasets +Below you can find the StudyInstanceUID of the studies that are used in the demo page along with their citations. + +### 1.3.6.1.4.1.14519.5.2.1.267424821384663813780850856506829388886 + +Segmentation of Vestibular Schwannoma from Magnetic Resonance Imaging: An Open Annotated Dataset and Baseline Algorithm (Vestibular-Schwannoma-SEG) + +- Shapey, J., Kujawa, A., Dorent, R., Wang, G., Bisdas, S., Dimitriadis, A., Grishchuck, D., Paddick, I., Kitchen, N., Bradford, R., Saeed, S., Ourselin, S., & Vercauteren, T. (2021). Segmentation of Vestibular Schwannoma from Magnetic Resonance Imaging: An Open Annotated Dataset and Baseline Algorithm [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/TCIA.9YTJ-5Q73 + +- Shapey, J., Kujawa, A., Dorent, R., Wang, G., Dimitriadis, A., Grishchuk, D., Paddick, I., Kitchen, N., Bradford, R., Saeed, S. R., Bisdas, S., Ourselin, S., & Vercauteren, T. (2021). Segmentation of vestibular schwannoma from MRI, an open annotated dataset and baseline algorithm. In Scientific Data (Vol. 8, Issue 1). Springer Science and Business Media LLC. https://doi.org/10.1038/s41597-021-01064-w + + +### 1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463 +### 1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339 + + +ACRIN-NSCLC-FDG-PET (ACRIN 6668) + +- Kinahan, P., Muzi, M., Bialecki, B., Herman, B., & Coombs, L. (2019). Data from the ACRIN 6668 Trial NSCLC-FDG-PET (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/tcia.2019.30ilqfcl + +- Machtay, M., Duan, F., Siegel, B. A., Snyder, B. S., Gorelick, J. J., Reddin, J. S., Munden, R., Johnson, D. W., Wilf, L. H., DeNittis, A., Sherwin, N., Cho, K. H., Kim, S., Videtic, G., Neumann, D. R., Komaki, R., Macapinlac, H., Bradley, J. D., & Alavi, A. (2013). Prediction of Survival by [18F]Fluorodeoxyglucose Positron Emission Tomography in Patients With Locally Advanced Nonโ€“Small-Cell Lung Cancer Undergoing Definitive Chemoradiation Therapy: Results of the ACRIN 6668/RTOG 0235 Trial. In Journal of Clinical Oncology (Vol. 31, Issue 30, pp. 3823โ€“3830). American Society of Clinical Oncology (ASCO). https://doi.org/10.1200/jco.2012.47.5947 + + +### 2.25.103659964951665749659160840573802789777 + +The Cancer Genome Atlas Glioblastoma Multiforme Collection (TCGA-GBM) + +- Scarpace, L., Mikkelsen, T., Cha, S., Rao, S., Tekchandani, S., Gutman, D., Saltz, J. H., Erickson, B. J., Pedano, N., Flanders, A. E., Barnholtz-Sloan, J., Ostrom, Q., Barboriak, D., & Pierce, L. J. (2016). The Cancer Genome Atlas Glioblastoma Multiforme Collection (TCGA-GBM) (Version 4) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2016.RNYFUYE9 + + +### 1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458 + +Abdominal or pelvic enhanced CT images within 10 days before surgery of 230 patients with stage II colorectal cancer (StageII-Colorectal-CT) + + +- Tong T., Li M. (2022) Abdominal or pelvic enhanced CT images within 10 days before surgery of 230 patients with stage II colorectal cancer (StageII-Colorectal-CT) [Dataset]. The Cancer Imaging Archive. DOI: https://doi.org/10.7937/p5k5-tg43 + +- Li, M., Gong, J., Bao, Y., Huang, D., Peng, J., & Tong, T. (2022). Special issue โ€œThe advance of solid tumor research in Chinaโ€: Prognosis prediction for stage II colorectal cancer by fusing computed tomography radiomics and deepโ€learning features of primary lesions and peripheral lymph nodes. In International Journal of Cancer. Wiley. https://doi.org/10.1002/ijc.34053 + + +### 1.3.6.1.4.1.14519.5.2.1.3023.4024.215308722288168917637555384485 + +The Cancer Genome Atlas Sarcoma Collection (TCGA-SARC) + +- Roche, C., Bonaccio, E., & Filippini, J. (2016). The Cancer Genome Atlas Sarcoma Collection (TCGA-SARC) (Version 3) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2016.CX6YLSUX + + + +### 1.3.6.1.4.1.14519.5.2.1.4792.2001.105216574054253895819671475627 + +BREAST-DIAGNOSIS + + +- Bloch, B. Nicolas, Jain, Ashali, & Jaffe, C. Carl. (2015). BREAST-DIAGNOSIS [Data set]. The Cancer Imaging Archive. http://doi.org/10.7937/K9/TCIA.2015.SDNRQXXR + + + + +### 1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785 + +Multimodality annotated HCC cases with and without advanced imaging segmentation (HCC-TACE-Seg) + + +- Moawad, A. W., Fuentes, D., Morshid, A., Khalaf, A. M., Elmohr, M. M., Abusaif, A., Hazle, J. D., Kaseb, A. O., Hassan, M., Mahvash, A., Szklaruk, J., Qayyom, A., & Elsayes, K. (2021). Multimodality annotated HCC cases with and without advanced imaging segmentation [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/TCIA.5FNA-0924 + +- Morshid, A., Elsayes, K. M., Khalaf, A. M., Elmohr, M. M., Yu, J., Kaseb, A. O., Hassan, M., Mahvash, A., Wang, Z., Hazle, J. D., & Fuentes, D. (2019). A Machine Learning Model to Predict Hepatocellular Carcinoma Response to Transcatheter Arterial Chemoembolization. Radiology: Artificial Intelligence, 1(5), e180021. https://doi.org/10.1148/ryai.2019180021 + + + +### 1.3.6.1.4.1.14519.5.2.1.1188.2803.137585363493444318569098508293 + +Ultrasound data of a variety of liver masses (B-mode-and-CEUS-Liver) + +- Eisenbrey, J., Lyshchik, A., & Wessner, C. (2021). Ultrasound data of a variety of liver masses [Data set]. The Cancer Imaging Archive. DOI: https://doi.org/10.7937/TCIA.2021.v4z7-tc39 + + + +### 1.3.6.1.4.1.32722.99.99.62087908186665265759322018723889952421 + +NSCLC-Radiomics + +- Aerts, H. J. W. L., Wee, L., Rios Velazquez, E., Leijenaar, R. T. H., Parmar, C., Grossmann, P., Carvalho, S., Bussink, J., Monshouwer, R., Haibe-Kains, B., Rietveld, D., Hoebers, F., Rietbergen, M. M., Leemans, C. R., Dekker, A., Quackenbush, J., Gillies, R. J., Lambin, P. (2019). Data From NSCLC-Radiomics (version 4) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2015.PF0M9REI + + +- Aerts, H. J. W. L., Velazquez, E. R., Leijenaar, R. T. H., Parmar, C., Grossmann, P., Carvalho, S., Bussink, J., Monshouwer, R., Haibe-Kains, B., Rietveld, D., Hoebers, F., Rietbergen, M. M., Leemans, C. R., Dekker, A., Quackenbush, J., Gillies, R. J., Lambin, P. (2014, June 3). Decoding tumour phenotype by noninvasive imaging using a quantitative radiomics approach. Nature Communications. Nature Publishing Group. https://doi.org/10.1038/ncomms5006 (link) + + +### 1.3.6.1.4.1.14519.5.2.1.3671.4754.298665348758363466150039312520 + +QIN-PROSTATE-Repeatability + +- Fedorov, A; Schwier, M; Clunie, D; Herz, C; Pieper, S; Kikinis, R; Tempany, C; Fennessy, F. (2018). Data From QIN-PROSTATE-Repeatability. The Cancer Imaging Archive. DOI: 10.7937/K9/TCIA.2018.MR1CKGND + + +- Fedorov A, Vangel MG, Tempany CM, Fennessy FM. Multiparametric Magnetic Resonance Imaging of the Prostate: Repeatability of Volume and Apparent Diffusion Coefficient Quantification. Investigative Radiology. 52, 538โ€“546 (2017). DOI: 10.1097/RLI.0000000000000382 + +- Fedorov, A., Schwier, M., Clunie, D., Herz, C., Pieper, S., Kikinis,R., Tempany, C. & Fennessy, F. An annotated test-retest collection of prostate multiparametric MRI. Scientific Data 5, 180281 (2018). DOI: + +### 2.25.141277760791347900862109212450152067508 + +The Clinical Proteomic Tumor Analysis Consortium Clear Cell Renal Cell Carcinoma Collection (CPTAC-CCRCC) + +- National Cancer Institute Clinical Proteomic Tumor Analysis Consortium (CPTAC). (2018). The Clinical Proteomic Tumor Analysis Consortium Clear Cell Renal Cell Carcinoma Collection (CPTAC-CCRCC) (Version 10) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2018.OBLAMN27 + +- The CPTAC program requests that publications using data from this program include the following statement: โ€œData used in this publication were generated by the National Cancer Institute Clinical Proteomic Tumor Analysis Consortium (CPTAC).โ€ + + +### 2.25.275741864483510678566144889372061815320 + +National Lung Screening Trial + +- National Lung Screening Trial Research Team. (2013). Data from the National Lung Screening Trial (NLST) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/TCIA.HMQ8-J677 + +- National Lung Screening Trial Research Team*; Aberle DR, Adams AM, Berg CD, Black WC, Clapp JD, Fagerstrom RM, Gareen IF, Gatsonis C, Marcus PM, Sicks JD (2011). Reduced Lung-Cancer Mortality with Low-Dose Computed Tomographic Screening. New England Journal of Medicine, 365(5), 395โ€“409. https://doi.org/10.1056/nejmoa1102873 + + +### 1.3.6.1.4.1.14519.5.2.1.99.1071.26968527900428638961173806140069 + +Stony Brook University COVID-19 Positive Cases (COVID-19-NY-SBU) + +- Saltz, J., Saltz, M., Prasanna, P., Moffitt, R., Hajagos, J., Bremer, E., Balsamo, J., & Kurc, T. (2021). Stony Brook University COVID-19 Positive Cases [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/TCIA.BBAG-2923 + + +### 2.16.840.1.114362.1.11972228.22789312658.616067305.306.2 + +https://data.kitware.com/ + + +### 1.2.276.0.7230010.3.1.2.296485376.1.1665793212.499772 +### 2.25.269859997690759739055099378767846712697 +### 1.3.6.1.4.1.14519.5.2.1.5099.8010.217836670708542506360829799868 +### 1.3.6.1.4.1.14519.5.2.1.4792.2001.232252967813565730694525674696 +### 1.3.6.1.4.1.14519.5.2.1.4792.2001.105216574054253895819671475627 +### 1.3.6.1.4.1.5962.99.1.1117.5035.1620319789811.1.2.1 +### 1.3.6.1.4.1.5962.99.1.1123.9231.1620326176300.1.2.1 +### 1.3.6.1.4.1.5962.99.1.1126.3483.1620329455972.1.2.1 + +https://github.com/ImagingInformatics/hackathon-images + +### 2.16.124.113543.6004.101.103.20021117.162333.1 +### 2.16.124.113543.6004.101.103.20021117.190619.1 +### 2.16.124.113543.6004.101.103.20021117.123455.1 +### 2.16.124.113543.6004.101.103.20021117.061159.1 + +https://www.aapm.org/ + + +### 1.2.840.113619.2.30.1.1762295590.1623.978668949.886 + + +### 1.2.276.0.7230010.3.1.2.447481088.1.1669202398.851612 + +Custom data SPECT, specifically I123-FP-CIT (DaTSCAN) SPECT, evaluates the dopaminergic system to diagnose Parkinson's disease, especially when tremor symptoms are unclear. It helps distinguish Parkinson's disease from treatment-related tremor. + + + +### 1.3.6.1.4.1.9328.50.1.54652 + +https://www.cancerimagingarchive.net/collection/rider-pilot/ + +Lung Image Database Consortium (LIDC). (2023) RIDER Pilot [Data set]. The Cancer Imaging Archive (TCIA). https://doi.org/10.7937/m87f-mz83 + +### 1.3.6.1.4.1.14519.5.2.1.331759366792756327296606233801322964986 + +Mayr, N., Yuh, W. T. C., Bowen, S., Harkenrider, M., Knopp, M. V., Lee, E. Y.-P., Leung, E., Lo, S. S., Small Jr., W., & Wolfson, A. H. (2023). Cervical Cancer โ€“ Tumor Heterogeneity: Serial Functional and Molecular Imaging Across the Radiation Therapy Course in Advanced Cervical Cancer (Version 1) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/ERZ5-QZ59 + +https://www.cancerimagingarchive.net/collection/cc-tumor-heterogeneity/ + +### 1.3.6.1.4.1.14519.5.2.1.297577087050970310787702792940607009472 + +Eslick, E. M., Kipritidis, J., Gradinscak, D., Stevens, M. J., Bailey, D. L., Harris, B., Booth, J. T., & Keall, P. J. (2022). CT Ventilation as a functional imaging modality for lung cancer radiotherapy (CT-vs-PET-Ventilation-Imaging) (Version 1) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/3ppx-7s22 + +https://www.cancerimagingarchive.net/collection/ct-vs-pet-ventilation-imaging/ + + +### 1.3.6.1.4.1.14519.5.2.1.2103.7010.634114621738943599785009586807 +### 1.3.6.1.4.1.14519.5.2.1.2103.7010.135953723682765205394176991681 + +Huang, W., Tudorica, A., Chui, S., Kemmer, K., Naik, A., Troxell, M., Oh, K., Roy, N., Afzal, A., & Holtorf, M. (2014). Variations of dynamic contrast-enhanced magnetic resonance imaging in evaluation of breast cancer therapy response: a multicenter data analysis challenge (QIN Breast DCE-MRI) (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/k9/tcia.2014.a2n1ixox + +https://www.cancerimagingarchive.net/collection/qin-breast-dce-mri/ + + +### 1.3.6.1.4.1.14519.5.2.1.1.24766180081901755714059656629507905556 + + +Cancer Moonshot Biobank. (2023). Cancer Moonshoot Biobank โ€“ Acute Myeloid Leukemia (CMB-AML) (Version 4) [Dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/PCTE-6M66 + +https://www.cancerimagingarchive.net/collection/cmb-aml/ + +### 1.3.6.1.4.1.14519.5.2.1.3098.5025.285242291560760827564488897577 + +https://www.cancerimagingarchive.net/collection/anti-pd-1_lung/ + +Madhavi, P., Patel, S., & Tsao, A. S. (2019). Data from Anti-PD-1 Immunotherapy Lung [Data set]. The Cancer Imaging Archive. DOI: 10.7937/tcia.2019.zjjwb9ip + +### 1.3.6.1.4.1.14519.5.2.1.1.84416332615988066829602832830236187384 + +https://www.cancerimagingarchive.net/collection/cmb-pca/ + +Cancer Moonshot Biobank. (2022). Cancer Moonshot Biobank โ€“ Prostate Cancer Collection (CMB-PCA) (Version 7) [Dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/25T7-6Y12 + +### 1.3.6.1.4.1.32722.99.99.239341353911714368772597187099978969331 + +Aerts, H. J. W. L., Wee, L., Rios Velazquez, E., Leijenaar, R. T. H., Parmar, C., Grossmann, P., Carvalho, S., Bussink, J., Monshouwer, R., Haibe-Kains, B., Rietveld, D., Hoebers, F., Rietbergen, M. M., Leemans, C. R., Dekker, A., Quackenbush, J., Gillies, R. J., Lambin, P. (2014). Data From NSCLC-Radiomics (version 4) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2015.PF0M9REI + +https://www.cancerimagingarchive.net/collection/nsclc-radiomics/ + +### 1.3.6.1.4.1.14519.5.2.1.7085.2626.494695569589117268722281491772 + +https://www.cancerimagingarchive.net/collection/cptac-ucec/ + + +National Cancer Institute Clinical Proteomic Tumor Analysis Consortium (CPTAC). (2019). The Clinical Proteomic Tumor Analysis Consortium Uterine Corpus Endometrial Carcinoma Collection (CPTAC-UCEC) (Version 12) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2018.3R3JUISW + +### 1.3.6.1.4.1.14519.5.2.1.207544490797667703011829289839681390478 + +https://www.cancerimagingarchive.net/collection/remind/ + +Juvekar, P., Dorent, R., Kรถgl, F., Torio, E., Barr, C., Rigolo, L., Galvin, C., Jowkar, N., Kazi, A., Haouchine, N., Cheema, H., Navab, N., Pieper, S., Wells, W. M., Bi, W. L., Golby, A., Frisken, S., & Kapur, T. (2023). The Brain Resection Multimodal Imaging Database (ReMIND) (Version 1) [dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/3RAG-D070 + +### 1.3.12.2.1107.5.1.4.60175.30000008042114404745300000010 + +Gavrielides, M. A., Kinnard, L. M., Myers, K. J., Peregoy, J., Pritchard, W. F., Zeng, R., Esparza, J., Karanian, J., & Petrick, N. (2015). Data From Phantom FDA [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/k9/TCIA.2015.orbjkmux + +https://www.cancerimagingarchive.net/collection/phantom-fda/ + + +### 1.3.6.1.4.1.14519.5.2.1.6834.5010.992793141464713669479982159310 + +https://www.cancerimagingarchive.net/collection/4d-lung/ + + +Hugo, G. D., Weiss, E., Sleeman, W. C., Balik, S., Keall, P. J., Lu, J., & Williamson, J. F. (2016). Data from 4D Lung Imaging of NSCLC Patients (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/K9/TCIA.2016.ELN8YGLE + + +### 1.3.6.1.4.1.9328.50.17.15423521354819720574322014551955370036 + +https://www.cancerimagingarchive.net/collection/rider-lung-pet-ct/ + +Muzi P, Wanner M, & Kinahan P. (2015). Data From RIDER Lung PET-CT. The Cancer Imaging Archive. https://doi.org/10.7937/k9/tcia.2015.ofip7tvm + +### 1.3.6.1.4.1.14519.5.2.1.9823.1001.134394060407147891170882809392 + +https://www.cancerimagingarchive.net/collection/prostate-mri/ + +Choyke P, Turkbey B, Pinto P, Merino M, Wood B. (2016). Data From PROSTATE-MRI. The Cancer Imaging Archive. http://doi.org/10.7937/K9/TCIA.2016.6046GUDv + +### 1.3.6.1.4.1.14519.5.2.1.191696062987463500085282581898315738844 + +https://www.cancerimagingarchive.net/collection/upenn-gbm/ + +Bakas, S., Sako, C., Akbari, H., Bilello, M., Sotiras, A., Shukla, G., Rudie, J. D., Flores Santamaria, N., Fathi Kazerooni, A., Pati, S., Rathore, S., Mamourian, E., Ha, S. M., Parker, W., Doshi, J., Baid, U., Bergman, M., Binder, Z. A., Verma, R., โ€ฆ Davatzikos, C. (2021). Multi-parametric magnetic resonance imaging (mpMRI) scans for de novo Glioblastoma (GBM) patients from the University of Pennsylvania Health System (UPENN-GBM) (Version 2) [Data set]. The Cancer Imaging Archive. https://doi.org/10.7937/TCIA.709X-DN49 + +### 1.3.6.1.4.1.14519.5.2.1.4792.2001.921758700577562664959693695481 + +https://www.cancerimagingarchive.net/collection/breast-diagnosis/ + +Bloch, B. Nicolas, Jain, Ashali, & Jaffe, C. Carl. (2015). BREAST-DIAGNOSIS [Data set]. The Cancer Imaging Archive. http://doi.org/10.7937/K9/TCIA.2015.SDNRQXXR + + +### 1.3.6.1.4.1.14519.5.2.1.1620.1225.189514895974227080410265976065 + +Comstock, C. E., Gatsonis, C., Newstead, G. M., Snyder, B. S., Gareen, I. F., Bergin, J. T., Rahbar, H., Sung, J. S., Jacobs, C., Harvey, J. A., Nicholson, M. H., Ward, R. C., Holt, J., Prather, A., Miller, K. D., Schnall, M. D., & Kuhl, C. K. (2023). Abbreviated Breast MRI and Digital Tomosynthesis Mammography in Screening Women With Dense Breasts (EA1141) (Version 1) [dataset]. The Cancer Imaging Archive. https://doi.org/10.7937/2BAS-HR33 + +https://www.cancerimagingarchive.net/collection/ea1141/ + +### 1.2.276.0.7230010.3.1.2.2155604110.4180.1021041295.21 + +From OFFIS DICOM-Team + +https://www.offis.de/ +OFFIS DICOM-Team diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a3f8be3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,103 @@ +# syntax=docker/dockerfile:1.7-labs +# This dockerfile is used to publish the `ohif/app` image on dockerhub. +# +# It's a good example of how to build our static application and package it +# with a web server capable of hosting it as static content. +# +# docker build +# -------------- +# If you would like to use this dockerfile to build and tag an image, make sure +# you set the context to the project's root directory: +# https://docs.docker.com/engine/reference/commandline/build/ +# +# +# SUMMARY +# -------------- +# This dockerfile has two stages: +# +# 1. Building the React application for production +# 2. Setting up our Nginx (Alpine Linux) image w/ step one's output +# + + +# syntax=docker/dockerfile:1.7-labs +# This dockerfile is used to publish the `ohif/app` image on dockerhub. +# +# It's a good example of how to build our static application and package it +# with a web server capable of hosting it as static content. +# +# docker build +# -------------- +# If you would like to use this dockerfile to build and tag an image, make sure +# you set the context to the project's root directory: +# https://docs.docker.com/engine/reference/commandline/build/ +# +# +# SUMMARY +# -------------- +# This dockerfile is used as an input for a second stage to make things run faster. +# + + +# Stage 1: Build the application +# docker build -t ohif/viewer:latest . +# Copy Files +FROM node:20.18.1-slim as builder + +RUN apt-get update && apt-get install -y build-essential python3 + + +RUN mkdir /usr/src/app +WORKDIR /usr/src/app +RUN npm install -g bun +# RUN npm install -g lerna@7.4.2 +ENV PATH=/usr/src/app/node_modules/.bin:$PATH + +# Do an initial install and then a final install +COPY package.json yarn.lock preinstall.js lerna.json ./ +COPY --parents ./addOns/package.json ./addOns/*/*/package.json ./extensions/*/package.json ./modes/*/package.json ./platform/*/package.json ./ +# Run the install before copying the rest of the files + +RUN bun pm cache rm +RUN bun install +# Copy the local directory +COPY --link --exclude=yarn.lock --exclude=package.json --exclude=Dockerfile . . +# Do a second install to finalize things after the copy +RUN bun run show:config +RUN bun install + +# Build here +# After install it should hopefully be stable until the local directory changes +ENV QUICK_BUILD true +# ENV GENERATE_SOURCEMAP=false +ARG APP_CONFIG=config/default.js +ARG PUBLIC_URL=/ + +RUN bun run show:config +RUN bun run build + +# Precompress files +RUN chmod u+x .docker/compressDist.sh +RUN ./.docker/compressDist.sh + +# Stage 3: Bundle the built application into a Docker container +# which runs Nginx using Alpine Linux +FROM nginxinc/nginx-unprivileged:1.27-alpine as final +#RUN apk add --no-cache bash +ARG PORT=80 +ENV PORT=${PORT} +ARG PUBLIC_URL=/ +ENV PUBLIC_URL=${PUBLIC_URL} +RUN rm /etc/nginx/conf.d/default.conf +USER nginx +COPY --chown=nginx:nginx .docker/Viewer-v3.x /usr/src +RUN chmod 777 /usr/src/entrypoint.sh +COPY --from=builder /usr/src/app/platform/app/dist /usr/share/nginx/html${PUBLIC_URL} +COPY --from=builder /usr/src/app/platform/app/dist/index.html /usr/share/nginx/html +# In entrypoint.sh, app-config.js might be overwritten, so chmod it to be writeable. +# The nginx user cannot chmod it, so change to root. +USER root +RUN chown -R nginx:nginx /usr/share/nginx/html +USER nginx +ENTRYPOINT ["/usr/src/entrypoint.sh"] +CMD ["nginx", "-g", "daemon off;"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..19e20dd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2ab0a29 --- /dev/null +++ b/README.md @@ -0,0 +1,337 @@ + + +
+

OHIF Medical Imaging Viewer

+

The OHIF Viewer is a zero-footprint medical image viewer +provided by the Open Health Imaging Foundation (OHIF). It is a configurable and extensible progressive web application with out-of-the-box support for image archives which support DICOMweb.

+
+ + + + +
+ ๐Ÿ“ฐ Join OHIF Newsletter ๐Ÿ“ฐ +
+
+ ๐Ÿ“ฐ Join OHIF Newsletter ๐Ÿ“ฐ +
+ + + +
+ +[![NPM version][npm-version-image]][npm-url] +[![MIT License][license-image]][license-url] +[![This project is using Percy.io for visual regression testing.][percy-image]](percy-url) + + + + + + + + + + + +| | | | +| :-: | :--- | :--- | +| Measurement tracking | Measurement Tracking | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5) | +| Segmentations | Labelmap Segmentations | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046) | +| Hanging Protocols | Fusion and Custom Hanging protocols | [Demo](https://viewer.ohif.org/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463) | +| Volume Rendering | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) | +| PDF | PDF | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.317377619501274872606137091638706705333) | +| RTSTRUCT | RT STRUCT | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.5962.99.1.2968617883.1314880426.1493322302363.3.0) | +| 4D | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) | +| VIDEO | Video | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.96975534054447904995905761963464388233) | +| microscopy | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.141277760791347900862109212450152067508) | + +## About + +The OHIF Viewer can retrieve +and load images from most sources and formats; render sets in 2D, 3D, and +reconstructed representations; allows for the manipulation, annotation, and +serialization of observations; supports internationalization, OpenID Connect, +offline use, hotkeys, and many more features. + +Almost everything offers some degree of customization and configuration. If it +doesn't support something you need, we accept pull requests and have an ever +improving Extension System. + +## Why Choose Us + +### Community & Experience + +The OHIF Viewer is a collaborative effort that has served as the basis for many +active, production, and FDA Cleared medical imaging viewers. It benefits from +our extensive community's collective experience, and from the sponsored +contributions of individuals, research groups, and commercial organizations. + +### Built to Adapt + +After more than 8-years of integrating with many companies and organizations, +The OHIF Viewer has been rebuilt from the ground up to better address the +varying workflow and configuration needs of its many users. All of the Viewer's +core features are built using it's own extension system. The same extensibility +that allows us to offer: + +- 2D and 3D medical image viewing +- Multiplanar Reconstruction (MPR) +- Maximum Intensity Project (MIP) +- Whole slide microscopy viewing +- PDF and Dicom Structured Report rendering +- Segmentation rendering as labelmaps and contours +- User Access Control (UAC) +- Context specific toolbar and side panel content +- and many others + +Can be leveraged by you to customize the viewer for your workflow, and to add +any new functionality you may need (and wish to maintain privately without +forking). + +### Support + +- [Report a Bug ๐Ÿ›](https://github.com/OHIF/Viewers/issues/new?assignees=&labels=Community%3A+Report+%3Abug%3A%2CAwaiting+Reproduction&projects=&template=bug-report.yml&title=%5BBug%5D+) +- [Request a Feature ๐Ÿš€](https://github.com/OHIF/Viewers/issues/new?assignees=&labels=Community%3A+Request+%3Ahand%3A&projects=&template=feature-request.yml&title=%5BFeature+Request%5D+) +- [Ask a Question ๐Ÿค—](community.ohif.org) +- [Slack Channel](https://join.slack.com/t/cornerstonejs/shared_invite/zt-1r8xb2zau-dOxlD6jit3TN0Uwf928w9Q) + +For commercial support, academic collaborations, and answers to common +questions; please use [Get Support](https://ohif.org/get-support/) to contact +us. + + +## Developing + +### Branches + +#### `master` branch - The latest dev (beta) release + +- `master` - The latest dev release + +This is typically where the latest development happens. Code that is in the master branch has passed code reviews and automated tests, but it may not be deemed ready for production. This branch usually contains the most recent changes and features being worked on by the development team. It's often the starting point for creating feature branches (where new features are developed) and hotfix branches (for urgent fixes). + +Each package is tagged with beta version numbers, and published to npm such as `@ohif/ui@3.6.0-beta.1` + +### `release/*` branches - The latest stable releases +Once the `master` branch code reaches a stable, release-ready state, we conduct a comprehensive code review and QA testing. Upon approval, we create a new release branch from `master`. These branches represent the latest stable version considered ready for production. + +For example, `release/3.5` is the branch for version 3.5.0, and `release/3.6` is for version 3.6.0. After each release, we wait a few days to ensure no critical bugs. If any are found, we fix them in the release branch and create a new release with a minor version bump, e.g., 3.5.1 in the `release/3.5` branch. + +Each package is tagged with version numbers and published to npm, such as `@ohif/ui@3.5.0`. Note that `master` is always ahead of the `release` branch. We publish docker builds for both beta and stable releases. + +Here is a schematic representation of our development workflow: + +![alt text](platform/docs/docs/assets/img/github-readme-branches-Jun2024.png) + + + + + +### Requirements + +- [Yarn 1.20.0+](https://yarnpkg.com/en/docs/install) +- [Node 18+](https://nodejs.org/en/) +- Yarn Workspaces should be enabled on your machine: + - `yarn config set workspaces-experimental true` + +### Getting Started + +1. [Fork this repository][how-to-fork] +2. [Clone your forked repository][how-to-clone] + - `git clone https://github.com/YOUR-USERNAME/Viewers.git` +3. Navigate to the cloned project's directory +4. Add this repo as a `remote` named `upstream` + - `git remote add upstream https://github.com/OHIF/Viewers.git` +5. `yarn install` to restore dependencies and link projects + +#### To Develop + +_From this repository's root directory:_ + +```bash +# Enable Yarn Workspaces +yarn config set workspaces-experimental true + +# Restore dependencies +yarn install +``` + +## Commands + +These commands are available from the root directory. Each project directory +also supports a number of commands that can be found in their respective +`README.md` and `package.json` files. + +| Yarn Commands | Description | +| ---------------------------- | ------------------------------------------------------------- | +| **Develop** | | +| `dev` | Default development experience for Viewer | +| `dev:fast` | Our experimental fast dev mode that uses rsbuild instead of webpack | +| `test:unit` | Jest multi-project test runner; overall coverage | +| **Deploy** | | +| `build`\* | Builds production output for our PWA Viewer | | + +\* - For more information on different builds, check out our [Deploy +Docs][deployment-docs] + +## Project + +The OHIF Medical Image Viewing Platform is maintained as a +[`monorepo`][monorepo]. This means that this repository, instead of containing a +single project, contains many projects. If you explore our project structure, +you'll see the following: + +```bash +. +โ”œโ”€โ”€ extensions # +โ”‚ โ”œโ”€โ”€ _example # Skeleton of example extension +โ”‚ โ”œโ”€โ”€ default # basic set of useful functionalities (datasources, panels, etc) +โ”‚ โ”œโ”€โ”€ cornerstone # image rendering and tools w/ Cornerstone3D +โ”‚ โ”œโ”€โ”€ cornerstone-dicom-sr # DICOM Structured Report rendering and export +โ”‚ โ”œโ”€โ”€ cornerstone-dicom-sr # DICOM Structured Report rendering and export +โ”‚ โ”œโ”€โ”€ cornerstone-dicom-seg # DICOM Segmentation rendering and export +โ”‚ โ”œโ”€โ”€ cornerstone-dicom-rt # DICOM RTSTRUCT rendering +โ”‚ โ”œโ”€โ”€ cornerstone-microscopy # Whole Slide Microscopy rendering +โ”‚ โ”œโ”€โ”€ dicom-pdf # PDF rendering +โ”‚ โ”œโ”€โ”€ dicom-video # DICOM RESTful Services +โ”‚ โ”œโ”€โ”€ measurement-tracking # Longitudinal measurement tracking +โ”‚ โ”œโ”€โ”€ tmtv # Total Metabolic Tumor Volume (TMTV) calculation +| + +โ”‚ +โ”œโ”€โ”€ modes # +โ”‚ โ”œโ”€โ”€ _example # Skeleton of example mode +โ”‚ โ”œโ”€โ”€ basic-dev-mode # Basic development mode +โ”‚ โ”œโ”€โ”€ longitudinal # Longitudinal mode (measurement tracking) +โ”‚ โ”œโ”€โ”€ tmtv # Total Metabolic Tumor Volume (TMTV) calculation mode +โ”‚ โ””โ”€โ”€ microscopy # Whole Slide Microscopy mode +โ”‚ +โ”œโ”€โ”€ platform # +โ”‚ โ”œโ”€โ”€ core # Business Logic +โ”‚ โ”œโ”€โ”€ i18n # Internationalization Support +โ”‚ โ”œโ”€โ”€ ui # React component library +โ”‚ โ”œโ”€โ”€ docs # Documentation +โ”‚ โ””โ”€โ”€ viewer # Connects platform and extension projects +โ”‚ +โ”œโ”€โ”€ ... # misc. shared configuration +โ”œโ”€โ”€ lerna.json # MonoRepo (Lerna) settings +โ”œโ”€โ”€ package.json # Shared devDependencies and commands +โ””โ”€โ”€ README.md # This file +``` + +## Acknowledgments + +To acknowledge the OHIF Viewer in an academic publication, please cite + +> _Open Health Imaging Foundation Viewer: An Extensible Open-Source Framework +> for Building Web-Based Imaging Applications to Support Cancer Research_ +> +> Erik Ziegler, Trinity Urban, Danny Brown, James Petts, Steve D. Pieper, Rob +> Lewis, Chris Hafey, and Gordon J. Harris +> +> _JCO Clinical Cancer Informatics_, no. 4 (2020), 336-345, DOI: +> [10.1200/CCI.19.00131](https://www.doi.org/10.1200/CCI.19.00131) +> +> Open-Access on Pubmed Central: +> https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7259879/ + +or, for v1, please cite: + +> _LesionTracker: Extensible Open-Source Zero-Footprint Web Viewer for Cancer +> Imaging Research and Clinical Trials_ +> +> Trinity Urban, Erik Ziegler, Rob Lewis, Chris Hafey, Cheryl Sadow, Annick D. +> Van den Abbeele and Gordon J. Harris +> +> _Cancer Research_, November 1 2017 (77) (21) e119-e122 DOI: +> [10.1158/0008-5472.CAN-17-0334](https://www.doi.org/10.1158/0008-5472.CAN-17-0334) + +**Note:** If you use or find this repository helpful, please take the time to +star this repository on GitHub. This is an easy way for us to assess adoption +and it can help us obtain future funding for the project. + +This work is supported primarily by the National Institutes of Health, National +Cancer Institute, Informatics Technology for Cancer Research (ITCR) program, +under a +[grant to Dr. Gordon Harris at Massachusetts General Hospital (U24 CA199460)](https://projectreporter.nih.gov/project_info_description.cfm?aid=8971104). + +[NCI Imaging Data Commons (IDC) project](https://imaging.datacommons.cancer.gov/) supported the development of new features and bug fixes marked with ["IDC:priority"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Apriority), +["IDC:candidate"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Acandidate) or ["IDC:collaboration"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Acollaboration). NCI Imaging Data Commons is supported by contract number 19X037Q from +Leidos Biomedical Research under Task Order HHSN26100071 from NCI. [IDC Viewer](https://learn.canceridc.dev/portal/visualization) is a customized version of the OHIF Viewer. + +This project is tested with BrowserStack. Thank you for supporting open-source! + +## License + +MIT ยฉ [OHIF](https://github.com/OHIF) + + + + + +[lerna-image]: https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg +[lerna-url]: https://lerna.js.org/ +[netlify-image]: https://api.netlify.com/api/v1/badges/32708787-c9b0-4634-b50f-7ca41952da77/deploy-status +[netlify-url]: https://app.netlify.com/sites/ohif-dev/deploys +[all-contributors-image]: https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square +[circleci-image]: https://circleci.com/gh/OHIF/Viewers.svg?style=svg +[circleci-url]: https://circleci.com/gh/OHIF/Viewers +[codecov-image]: https://codecov.io/gh/OHIF/Viewers/branch/master/graph/badge.svg +[codecov-url]: https://codecov.io/gh/OHIF/Viewers/branch/master +[prettier-image]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square +[prettier-url]: https://github.com/prettier/prettier +[semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg +[semantic-url]: https://github.com/semantic-release/semantic-release + +[npm-url]: https://npmjs.org/package/@ohif/app +[npm-downloads-image]: https://img.shields.io/npm/dm/@ohif/app.svg?style=flat-square +[npm-version-image]: https://img.shields.io/npm/v/@ohif/app.svg?style=flat-square +[docker-pulls-img]: https://img.shields.io/docker/pulls/ohif/viewer.svg?style=flat-square +[docker-image-url]: https://hub.docker.com/r/ohif/app +[license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square +[license-url]: LICENSE +[percy-image]: https://percy.io/static/images/percy-badge.svg +[percy-url]: https://percy.io/Open-Health-Imaging-Foundation/OHIF-Viewer + +[monorepo]: https://en.wikipedia.org/wiki/Monorepo +[how-to-fork]: https://help.github.com/en/articles/fork-a-repo +[how-to-clone]: https://help.github.com/en/articles/fork-a-repo#step-2-create-a-local-clone-of-your-fork +[ohif-architecture]: https://docs.ohif.org/architecture/index.html +[ohif-extensions]: https://docs.ohif.org/architecture/index.html +[deployment-docs]: https://docs.ohif.org/deployment/ +[react-url]: https://reactjs.org/ +[pwa-url]: https://developers.google.com/web/progressive-web-apps/ +[ohif-viewer-url]: https://www.npmjs.com/package/@ohif/app +[configuration-url]: https://docs.ohif.org/configuring/ +[extensions-url]: https://docs.ohif.org/extensions/ + +[platform-core]: platform/core/README.md +[core-npm]: https://www.npmjs.com/package/@ohif/core +[platform-i18n]: platform/i18n/README.md +[i18n-npm]: https://www.npmjs.com/package/@ohif/i18n +[platform-ui]: platform/ui/README.md +[ui-npm]: https://www.npmjs.com/package/@ohif/ui +[platform-viewer]: platform/app/README.md +[viewer-npm]: https://www.npmjs.com/package/@ohif/app + +[extension-cornerstone]: extensions/cornerstone/README.md +[cornerstone-npm]: https://www.npmjs.com/package/@ohif/extension-cornerstone +[extension-dicom-html]: extensions/dicom-html/README.md +[html-npm]: https://www.npmjs.com/package/@ohif/extension-dicom-html +[extension-dicom-microscopy]: extensions/dicom-microscopy/README.md +[microscopy-npm]: https://www.npmjs.com/package/@ohif/extension-dicom-microscopy +[extension-dicom-pdf]: extensions/dicom-pdf/README.md +[pdf-npm]: https://www.npmjs.com/package/@ohif/extension-dicom-pdf +[extension-vtk]: extensions/vtk/README.md +[vtk-npm]: https://www.npmjs.com/package/@ohif/extension-vtk + + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FOHIF%2FViewers.svg?type=large&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_large&issueType=license) diff --git a/addOns/README.md b/addOns/README.md new file mode 100644 index 0000000..216a65e --- /dev/null +++ b/addOns/README.md @@ -0,0 +1,3 @@ +# External Dependencies + +This module contains optional dependencies and external dependencies for including in OHIF, such as the DICOM Microscopy Viewer component. diff --git a/addOns/externals/devDependencies/CHANGELOG.md b/addOns/externals/devDependencies/CHANGELOG.md new file mode 100644 index 0000000..7fa46d8 --- /dev/null +++ b/addOns/externals/devDependencies/CHANGELOG.md @@ -0,0 +1,1428 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + + +### Bug Fixes + +* **ui:** Update dependencies and add missing icons ([#4699](https://github.com/OHIF/Viewers/issues/4699)) ([cf97fa9](https://github.com/OHIF/Viewers/commit/cf97fa9b7b9687a9b73c1cf6926bc9fbc39b6512)) + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + + +### Features + +* **dev:** move to rsbuild for dev - faster ([#4674](https://github.com/OHIF/Viewers/issues/4674)) ([d4a4267](https://github.com/OHIF/Viewers/commit/d4a4267429c02916dd51f6aefb290d96dd1c3b04)) + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + + +### Bug Fixes + +* **colorlut:** use the correct colorlut index and update vtk ([#4544](https://github.com/OHIF/Viewers/issues/4544)) ([b9c26e7](https://github.com/OHIF/Viewers/commit/b9c26e775a49044673473418dd5bdee2e5562ab9)) + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @externals/devDependencies + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) diff --git a/addOns/externals/devDependencies/package.json b/addOns/externals/devDependencies/package.json new file mode 100644 index 0000000..a17f709 --- /dev/null +++ b/addOns/externals/devDependencies/package.json @@ -0,0 +1,98 @@ +{ + "name": "@externals/devDependencies", + "description": "External dev dependencies - put dev build dependencies here", + "version": "3.10.0-beta.111", + "license": "MIT", + "private": true, + "engines": { + "node": ">=12", + "yarn": ">=1.19.1" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "@kitware/vtk.js": "32.1.1", + "clsx": "^2.1.1", + "core-js": "^3.2.1", + "moment": "^2.9.4" + }, + "peerDependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", + "@rsbuild/core": "^1.1.13", + "@rsbuild/plugin-node-polyfill": "1.2.0", + "@rsbuild/plugin-react": "^1.1.0", + "@svgr/webpack": "^8.1.0", + "@swc/helpers": "^0.5.15", + "@types/jest": "^27.5.0", + "@typescript-eslint/eslint-plugin": "^6.3.0", + "@typescript-eslint/parser": "^6.3.0", + "autoprefixer": "^10.4.4", + "babel-eslint": "9.x", + "babel-loader": "^8.2.4", + "babel-plugin-module-resolver": "^5.0.0", + "clean-webpack-plugin": "^3.0.0", + "copy-webpack-plugin": "^9.0.1", + "cross-env": "^5.2.0", + "css-loader": "^6.8.1", + "dotenv": "^8.1.0", + "eslint": "^8.39.0", + "eslint-config-prettier": "^7.2.0", + "eslint-config-react-app": "^6.0.0", + "eslint-plugin-cypress": "^2.12.1", + "eslint-plugin-flowtype": "^7.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-promise": "^5.2.0", + "eslint-plugin-react": "^7.29.4", + "eslint-plugin-react-hooks": "^4.4.0", + "eslint-plugin-tsdoc": "^0.2.11", + "eslint-webpack-plugin": "^2.5.3", + "execa": "^8.0.1", + "extract-css-chunks-webpack-plugin": "^4.5.4", + "html-webpack-plugin": "^5.3.2", + "husky": "^3.0.0", + "jest": "^29.5.0", + "jest-canvas-mock": "^2.1.0", + "jest-environment-jsdom": "^29.5.0", + "jest-junit": "^6.4.0", + "lerna": "^7.2.0", + "lint-staged": "^9.0.2", + "mini-css-extract-plugin": "^2.1.0", + "optimize-css-assets-webpack-plugin": "^6.0.1", + "postcss": "^8.3.5", + "postcss-import": "^14.0.2", + "postcss-loader": "^6.1.1", + "postcss-preset-env": "^7.4.3", + "prettier": "^3.0.3", + "prettier-plugin-tailwindcss": "^0.5.4", + "react-refresh": "^0.14.2", + "semver": "^7.5.1", + "serve": "^14.2.4", + "shader-loader": "^1.3.1", + "shx": "^0.3.3", + "source-map-loader": "^4.0.1", + "start-server-and-test": "^1.10.0", + "style-loader": "^1.0.0", + "stylus": "^0.59.0", + "stylus-loader": "^7.1.3", + "terser-webpack-plugin": "^5.1.4", + "typescript": "5.5.4", + "unused-webpack-plugin": "2.4.0", + "webpack": "5.94.0", + "webpack-bundle-analyzer": "^4.8.0", + "webpack-cli": "^4.7.2", + "webpack-dev-server": "4.7.3", + "webpack-hot-middleware": "^2.25.0", + "webpack-merge": "^5.7.3", + "workbox-webpack-plugin": "^6.1.5", + "worker-loader": "^3.0.8" + }, + "scripts": { + "build": "Included as direct dependency" + } +} diff --git a/addOns/externals/dicom-microscopy-viewer/CHANGELOG.md b/addOns/externals/dicom-microscopy-viewer/CHANGELOG.md new file mode 100644 index 0000000..f1a97c3 --- /dev/null +++ b/addOns/externals/dicom-microscopy-viewer/CHANGELOG.md @@ -0,0 +1,1419 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @externals/dicom-microscopy-viewer + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) diff --git a/addOns/externals/dicom-microscopy-viewer/package.json b/addOns/externals/dicom-microscopy-viewer/package.json new file mode 100644 index 0000000..f95ee52 --- /dev/null +++ b/addOns/externals/dicom-microscopy-viewer/package.json @@ -0,0 +1,9 @@ +{ + "name": "@externals/dicom-microscopy-viewer", + "description": "External reference to dicom-microscopy-viewer", + "version": "3.10.0-beta.111", + "license": "MIT", + "dependencies": { + "dicom-microscopy-viewer": "^0.46.1" + } +} diff --git a/addOns/package.json b/addOns/package.json new file mode 100644 index 0000000..02508ca --- /dev/null +++ b/addOns/package.json @@ -0,0 +1,51 @@ +{ + "name": "ohif-monorepo-root", + "private": true, + "packageManager": "yarn@1.22.22", + "workspaces": { + "packages": [ + "../platform/i18n", + "../platform/core", + "../platform/ui", + "../platform/ui-next", + "../platform/app", + "../extensions/*", + "../modes/*", + "../addOns/externals/*" + ], + "nohoist": [ + "**/html-minifier-terser" + ] + }, + "scripts": { + "preinstall": "cd .. && node preinstall.js" + }, + "devDependencies": { + "@babel/core": "7.24.7", + "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-proposal-object-rest-spread": "^7.17.3", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.16.7", + "@babel/plugin-transform-regenerator": "^7.16.7", + "@babel/plugin-transform-runtime": "7.24.7", + "@babel/plugin-transform-typescript": "^7.13.0", + "@babel/preset-env": "7.24.7", + "@babel/preset-react": "^7.16.7", + "@babel/preset-typescript": "^7.13.0" + }, + "resolutions": { + "**/@babel/runtime": "^7.20.13", + "commander": "8.3.0", + "dcmjs": "0.38.0", + "dicomweb-client": ">=0.10.4", + "nth-check": "^2.1.1", + "trim-newlines": "^5.0.0", + "glob-parent": "^6.0.2", + "trim": "^1.0.0", + "package-json": "^8.1.0", + "typescript": "5.5.4", + "sharp": "^0.32.6" + } +} diff --git a/aliases.config.js b/aliases.config.js new file mode 100644 index 0000000..e828c9f --- /dev/null +++ b/aliases.config.js @@ -0,0 +1,8 @@ +/* Used by webpack, babel and eslint */ + +const path = require('path'); + +module.exports = { + '@codinsky/parse-js': path.resolve(__dirname, 'packages/parse/src'), + '@codinsky/curate': path.resolve(__dirname, 'packages/curate/src'), +}; diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..aaf2f30 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,57 @@ +// https://babeljs.io/docs/en/options#babelrcroots +const { extendDefaultPlugins } = require('svgo'); + +module.exports = { + babelrcRoots: ['./platform/*', './extensions/*', './modes/*'], + presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'], + plugins: [ + ['@babel/plugin-proposal-class-properties', { loose: true }], + '@babel/plugin-transform-typescript', + ['@babel/plugin-proposal-private-property-in-object', { loose: true }], + ['@babel/plugin-proposal-private-methods', { loose: true }], + '@babel/plugin-transform-class-static-block', + ], + env: { + test: { + presets: [ + [ + // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2 + '@babel/preset-env', + { + modules: 'commonjs', + debug: false, + }, + ], + '@babel/preset-react', + '@babel/preset-typescript', + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-regenerator', + '@babel/transform-destructuring', + '@babel/plugin-transform-runtime', + '@babel/plugin-transform-typescript', + '@babel/plugin-transform-class-static-block', + ], + }, + production: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + development: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + }, +}; diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..c877b63 --- /dev/null +++ b/bun.lock @@ -0,0 +1,7767 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "ohif-monorepo-root", + "dependencies": { + "execa": "^8.0.1", + }, + "devDependencies": { + "@babel/core": "7.24.7", + "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-proposal-object-rest-spread": "^7.17.3", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.16.7", + "@babel/plugin-transform-regenerator": "^7.16.7", + "@babel/plugin-transform-runtime": "7.24.7", + "@babel/plugin-transform-typescript": "^7.13.0", + "@babel/preset-env": "7.24.7", + "@babel/preset-react": "^7.16.7", + "@babel/preset-typescript": "^7.13.0", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "0.6.9", + }, + "optionalDependencies": { + "@percy/cypress": "^3.1.1", + "@playwright/test": "^1.48.0", + "cypress": "^14.0.0", + "cypress-file-upload": "^5.0.8", + }, + }, + "addOns/externals/devDependencies": { + "name": "@externals/devDependencies", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@kitware/vtk.js": "32.1.1", + "clsx": "^2.1.1", + "core-js": "^3.2.1", + "moment": "^2.9.4", + }, + "devDependencies": { + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", + "@rsbuild/core": "^1.1.13", + "@rsbuild/plugin-node-polyfill": "1.2.0", + "@rsbuild/plugin-react": "^1.1.0", + "@svgr/webpack": "^8.1.0", + "@swc/helpers": "^0.5.15", + "@types/jest": "^27.5.0", + "@typescript-eslint/eslint-plugin": "^6.3.0", + "@typescript-eslint/parser": "^6.3.0", + "autoprefixer": "^10.4.4", + "babel-eslint": "9.x", + "babel-loader": "^8.2.4", + "babel-plugin-module-resolver": "^5.0.0", + "clean-webpack-plugin": "^3.0.0", + "copy-webpack-plugin": "^9.0.1", + "cross-env": "^5.2.0", + "css-loader": "^6.8.1", + "dotenv": "^8.1.0", + "eslint": "^8.39.0", + "eslint-config-prettier": "^7.2.0", + "eslint-config-react-app": "^6.0.0", + "eslint-plugin-cypress": "^2.12.1", + "eslint-plugin-flowtype": "^7.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-promise": "^5.2.0", + "eslint-plugin-react": "^7.29.4", + "eslint-plugin-react-hooks": "^4.4.0", + "eslint-plugin-tsdoc": "^0.2.11", + "eslint-webpack-plugin": "^2.5.3", + "execa": "^8.0.1", + "extract-css-chunks-webpack-plugin": "^4.5.4", + "html-webpack-plugin": "^5.3.2", + "husky": "^3.0.0", + "jest": "^29.5.0", + "jest-canvas-mock": "^2.1.0", + "jest-environment-jsdom": "^29.5.0", + "jest-junit": "^6.4.0", + "lerna": "^7.2.0", + "lint-staged": "^9.0.2", + "mini-css-extract-plugin": "^2.1.0", + "optimize-css-assets-webpack-plugin": "^6.0.1", + "postcss": "^8.3.5", + "postcss-import": "^14.0.2", + "postcss-loader": "^6.1.1", + "postcss-preset-env": "^7.4.3", + "prettier": "^3.0.3", + "prettier-plugin-tailwindcss": "^0.5.4", + "react-refresh": "^0.14.2", + "semver": "^7.5.1", + "serve": "^14.2.4", + "shader-loader": "^1.3.1", + "shx": "^0.3.3", + "source-map-loader": "^4.0.1", + "start-server-and-test": "^1.10.0", + "style-loader": "^1.0.0", + "stylus": "^0.59.0", + "stylus-loader": "^7.1.3", + "terser-webpack-plugin": "^5.1.4", + "typescript": "5.5.4", + "unused-webpack-plugin": "2.4.0", + "webpack": "5.94.0", + "webpack-bundle-analyzer": "^4.8.0", + "webpack-cli": "^4.7.2", + "webpack-dev-server": "4.7.3", + "webpack-hot-middleware": "^2.25.0", + "webpack-merge": "^5.7.3", + "workbox-webpack-plugin": "^6.1.5", + "worker-loader": "^3.0.8", + }, + "peerDependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + }, + }, + "addOns/externals/dicom-microscopy-viewer": { + "name": "@externals/dicom-microscopy-viewer", + "version": "3.10.0-beta.108", + "dependencies": { + "dicom-microscopy-viewer": "^0.46.1", + }, + }, + "extensions/cornerstone": { + "name": "@ohif/extension-cornerstone", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@cornerstonejs/adapters": "^2.19.14", + "@cornerstonejs/core": "^2.19.14", + "@cornerstonejs/tools": "^2.19.14", + "@icr/polyseg-wasm": "^0.4.0", + "@itk-wasm/morphological-contour-interpolation": "1.1.0", + "@kitware/vtk.js": "32.1.1", + "html2canvas": "^1.4.1", + "lodash.compact": "^3.0.1", + "lodash.debounce": "^4.0.8", + "lodash.flatten": "^4.4.0", + "lodash.merge": "^4.6.2", + "lodash.zip": "^4.2.0", + "shader-loader": "^1.3.1", + "worker-loader": "^3.0.8", + }, + "peerDependencies": { + "@cornerstonejs/codec-charls": "^1.2.3", + "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", + "@cornerstonejs/codec-openjpeg": "^1.2.4", + "@cornerstonejs/codec-openjph": "^2.4.5", + "@cornerstonejs/dicom-image-loader": "^2.19.14", + "@ohif/core": "3.10.0-beta.108", + "@ohif/ui": "3.10.0-beta.108", + "dcmjs": "*", + "dicom-parser": "^1.8.21", + "hammerjs": "^2.0.8", + "prop-types": "^15.6.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-resize-detector": "^10.0.1", + }, + }, + "extensions/cornerstone-dicom-pmap": { + "name": "@ohif/extension-cornerstone-dicom-pmap", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@cornerstonejs/adapters": "^2.19.14", + "@cornerstonejs/core": "^2.19.14", + "@kitware/vtk.js": "32.1.1", + "react-color": "^2.19.3", + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.108", + "@ohif/extension-cornerstone": "3.10.0-beta.108", + "@ohif/extension-default": "3.10.0-beta.108", + "@ohif/i18n": "3.10.0-beta.108", + "prop-types": "^15.6.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^12.2.2", + "react-router": "^6.8.1", + "react-router-dom": "^6.8.1", + }, + }, + "extensions/cornerstone-dicom-rt": { + "name": "@ohif/extension-cornerstone-dicom-rt", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "react-color": "^2.19.3", + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.108", + "@ohif/extension-cornerstone": "3.10.0-beta.108", + "@ohif/extension-default": "3.10.0-beta.108", + "@ohif/i18n": "3.10.0-beta.108", + "prop-types": "^15.6.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^10.11.0", + "react-router": "^6.23.1", + "react-router-dom": "^6.23.1", + }, + }, + "extensions/cornerstone-dicom-seg": { + "name": "@ohif/extension-cornerstone-dicom-seg", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@cornerstonejs/adapters": "^2.19.14", + "@cornerstonejs/core": "^2.19.14", + "@kitware/vtk.js": "32.1.1", + "react-color": "^2.19.3", + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.108", + "@ohif/extension-cornerstone": "3.10.0-beta.108", + "@ohif/extension-default": "3.10.0-beta.108", + "@ohif/i18n": "3.10.0-beta.108", + "prop-types": "^15.6.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^12.2.2", + "react-router": "^6.23.1", + "react-router-dom": "^6.23.1", + }, + }, + "extensions/cornerstone-dicom-sr": { + "name": "@ohif/extension-cornerstone-dicom-sr", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@cornerstonejs/adapters": "^2.19.14", + "@cornerstonejs/core": "^2.19.14", + "@cornerstonejs/tools": "^2.19.14", + "classnames": "^2.3.2", + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.108", + "@ohif/extension-cornerstone": "3.10.0-beta.108", + "@ohif/extension-measurement-tracking": "3.10.0-beta.108", + "@ohif/ui": "3.10.0-beta.108", + "dcmjs": "*", + "dicom-parser": "^1.8.9", + "hammerjs": "^2.0.8", + "prop-types": "^15.6.2", + "react": "^18.3.1", + }, + }, + "extensions/cornerstone-dynamic-volume": { + "name": "@ohif/extension-cornerstone-dynamic-volume", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@cornerstonejs/core": "^2.19.14", + "@cornerstonejs/tools": "^2.19.14", + "classnames": "^2.3.2", + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.108", + "@ohif/extension-cornerstone": "3.10.0-beta.108", + "@ohif/extension-default": "3.10.0-beta.108", + "@ohif/i18n": "3.10.0-beta.108", + "@ohif/ui": "3.10.0-beta.108", + "dcmjs": "*", + "dicom-parser": "^1.8.21", + "hammerjs": "^2.0.8", + "prop-types": "^15.6.2", + "react": "^18.3.1", + }, + }, + "extensions/default": { + "name": "@ohif/extension-default", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@cornerstonejs/calculate-suv": "^1.1.0", + "lodash.get": "^4.4.2", + "lodash.uniqby": "^4.7.0", + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.108", + "@ohif/i18n": "3.10.0-beta.108", + "dcmjs": "*", + "dicomweb-client": "^0.10.4", + "prop-types": "^15.6.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^12.2.2", + "react-window": "^1.8.9", + "webpack": "5.89.0", + "webpack-merge": "^5.7.3", + }, + }, + "extensions/dicom-microscopy": { + "name": "@ohif/extension-dicom-microscopy", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@cornerstonejs/codec-charls": "^1.2.3", + "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", + "@cornerstonejs/codec-openjpeg": "^1.2.4", + "colormap": "^2.3", + "lodash.debounce": "^4.0.8", + "mathjs": "^12.4.2", + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.108", + "@ohif/extension-default": "3.10.0-beta.108", + "@ohif/i18n": "3.10.0-beta.108", + "@ohif/ui": "3.10.0-beta.108", + "prop-types": "^15.6.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^10.11.0", + "react-router": "^6.23.1", + "react-router-dom": "^6.23.1", + }, + }, + "extensions/dicom-pdf": { + "name": "@ohif/extension-dicom-pdf", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "classnames": "^2.3.2", + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.108", + "@ohif/ui": "3.10.0-beta.108", + "dcmjs": "*", + "dicom-parser": "^1.8.9", + "hammerjs": "^2.0.8", + "prop-types": "^15.6.2", + "react": "^18.3.1", + }, + }, + "extensions/dicom-video": { + "name": "@ohif/extension-dicom-video", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "classnames": "^2.3.2", + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.108", + "@ohif/ui": "3.10.0-beta.108", + "dcmjs": "*", + "dicom-parser": "^1.8.9", + "hammerjs": "^2.0.8", + "prop-types": "^15.6.2", + "react": "^18.3.1", + }, + }, + "extensions/measurement-tracking": { + "name": "@ohif/extension-measurement-tracking", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@ohif/ui": "3.10.0-beta.108", + "@xstate/react": "^3.2.2", + "xstate": "^4.10.0", + }, + "peerDependencies": { + "@cornerstonejs/core": "^2.19.14", + "@cornerstonejs/tools": "^2.19.14", + "@ohif/core": "3.10.0-beta.108", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.108", + "@ohif/extension-default": "3.10.0-beta.108", + "@ohif/ui": "3.10.0-beta.108", + "classnames": "^2.3.2", + "dcmjs": "*", + "lodash.debounce": "^4.0.8", + "prop-types": "^15.6.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "webpack": "5.89.0", + "webpack-merge": "^5.7.3", + }, + }, + "extensions/test-extension": { + "name": "@ohif/extension-test", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "classnames": "^2.3.2", + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.108", + "@ohif/ui": "3.10.0-beta.108", + "dcmjs": "0.38.0", + "dicom-parser": "^1.8.9", + "hammerjs": "^2.0.8", + "prop-types": "^15.6.2", + "react": "^18.3.1", + }, + }, + "extensions/tmtv": { + "name": "@ohif/extension-tmtv", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "classnames": "^2.3.2", + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.108", + "@ohif/ui": "3.10.0-beta.108", + "dcmjs": "*", + "dicom-parser": "^1.8.9", + "hammerjs": "^2.0.8", + "prop-types": "^15.6.2", + "react": "^18.3.1", + }, + }, + "modes/basic-dev-mode": { + "name": "@ohif/mode-basic-dev-mode", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "i18next": "^17.0.3", + }, + "devDependencies": { + "webpack": "5.94.0", + "webpack-merge": "^5.7.3", + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.108", + "@ohif/extension-cornerstone": "3.10.0-beta.108", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.108", + "@ohif/extension-default": "3.10.0-beta.108", + "@ohif/extension-dicom-pdf": "3.10.0-beta.108", + "@ohif/extension-dicom-video": "3.10.0-beta.108", + }, + }, + "modes/basic-test-mode": { + "name": "@ohif/mode-test", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "i18next": "^17.0.3", + }, + "devDependencies": { + "webpack": "5.94.0", + "webpack-merge": "^5.7.3", + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.108", + "@ohif/extension-cornerstone": "3.10.0-beta.108", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.108", + "@ohif/extension-default": "3.10.0-beta.108", + "@ohif/extension-dicom-pdf": "3.10.0-beta.108", + "@ohif/extension-dicom-video": "3.10.0-beta.108", + "@ohif/extension-measurement-tracking": "3.10.0-beta.108", + "@ohif/extension-test": "3.10.0-beta.108", + }, + }, + "modes/longitudinal": { + "name": "@ohif/mode-longitudinal", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "i18next": "^17.0.3", + }, + "devDependencies": { + "webpack": "5.94.0", + "webpack-merge": "^5.7.3", + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.108", + "@ohif/extension-cornerstone": "3.10.0-beta.108", + "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.108", + "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.108", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.108", + "@ohif/extension-default": "3.10.0-beta.108", + "@ohif/extension-dicom-pdf": "3.10.0-beta.108", + "@ohif/extension-dicom-video": "3.10.0-beta.108", + "@ohif/extension-measurement-tracking": "3.10.0-beta.108", + }, + }, + "modes/microscopy": { + "name": "@ohif/mode-microscopy", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "i18next": "^17.0.3", + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.108", + "@ohif/extension-dicom-microscopy": "3.10.0-beta.108", + }, + }, + "modes/preclinical-4d": { + "name": "@ohif/mode-preclinical-4d", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + }, + "devDependencies": { + "webpack": "5.94.0", + "webpack-merge": "^5.7.3", + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.108", + "@ohif/extension-cornerstone": "3.10.0-beta.108", + "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.108", + "@ohif/extension-cornerstone-dynamic-volume": "3.10.0-beta.108", + "@ohif/extension-default": "3.10.0-beta.108", + "@ohif/extension-tmtv": "3.10.0-beta.108", + }, + }, + "modes/segmentation": { + "name": "@ohif/mode-segmentation", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "i18next": "^17.0.3", + }, + "devDependencies": { + "@babel/core": "7.24.7", + "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-proposal-object-rest-spread": "^7.17.3", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.16.7", + "@babel/plugin-transform-regenerator": "^7.16.7", + "@babel/plugin-transform-runtime": "7.24.7", + "@babel/plugin-transform-typescript": "^7.13.0", + "@babel/preset-env": "7.23.2", + "@babel/preset-react": "^7.16.7", + "@babel/preset-typescript": "^7.13.0", + "@svgr/webpack": "^8.1.0", + "babel-eslint": "^10.1.0", + "babel-loader": "^8.0.0-beta.4", + "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^10.2.0", + "cross-env": "^7.0.3", + "dotenv": "^14.1.0", + "eslint": "^8.39.0", + "eslint-loader": "^2.0.0", + "webpack": "5.94.0", + "webpack-cli": "^4.7.2", + "webpack-merge": "^5.7.3", + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.108", + "@ohif/extension-cornerstone": "3.10.0-beta.108", + "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.108", + "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.108", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.108", + "@ohif/extension-default": "3.10.0-beta.108", + "@ohif/extension-dicom-pdf": "3.10.0-beta.108", + "@ohif/extension-dicom-video": "3.10.0-beta.108", + }, + }, + "modes/tmtv": { + "name": "@ohif/mode-tmtv", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "i18next": "^17.0.3", + }, + "devDependencies": { + "webpack": "5.94.0", + "webpack-merge": "^5.7.3", + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.108", + "@ohif/extension-cornerstone": "3.10.0-beta.108", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.108", + "@ohif/extension-default": "3.10.0-beta.108", + "@ohif/extension-dicom-pdf": "3.10.0-beta.108", + "@ohif/extension-dicom-video": "3.10.0-beta.108", + "@ohif/extension-measurement-tracking": "3.10.0-beta.108", + }, + }, + "platform/app": { + "name": "@ohif/app", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@cornerstonejs/codec-charls": "^1.2.3", + "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", + "@cornerstonejs/codec-openjpeg": "^1.2.4", + "@cornerstonejs/codec-openjph": "^2.4.5", + "@cornerstonejs/dicom-image-loader": "^2.19.14", + "@emotion/serialize": "^1.1.3", + "@ohif/core": "3.10.0-beta.108", + "@ohif/extension-cornerstone": "3.10.0-beta.108", + "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.108", + "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.108", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.108", + "@ohif/extension-default": "3.10.0-beta.108", + "@ohif/extension-dicom-microscopy": "3.10.0-beta.108", + "@ohif/extension-dicom-pdf": "3.10.0-beta.108", + "@ohif/extension-dicom-video": "3.10.0-beta.108", + "@ohif/extension-test": "3.10.0-beta.108", + "@ohif/i18n": "3.10.0-beta.108", + "@ohif/mode-basic-dev-mode": "3.10.0-beta.108", + "@ohif/mode-longitudinal": "3.10.0-beta.108", + "@ohif/mode-microscopy": "3.10.0-beta.108", + "@ohif/mode-test": "3.10.0-beta.108", + "@ohif/ui": "3.10.0-beta.108", + "@ohif/ui-next": "3.10.0-beta.108", + "@svgr/webpack": "^8.1.0", + "@types/react": "^18.3.3", + "classnames": "^2.3.2", + "core-js": "*", + "cornerstone-math": "^0.1.9", + "dcmjs": "*", + "detect-gpu": "^4.0.16", + "dicom-parser": "^1.8.9", + "dotenv-webpack": "^1.7.0", + "file-loader": "^6.2.0", + "hammerjs": "^2.0.8", + "history": "^5.3.0", + "i18next": "^17.0.3", + "i18next-browser-languagedetector": "^3.0.1", + "lodash.isequal": "4.5.0", + "oidc-client": "1.11.5", + "oidc-client-ts": "^3.0.1", + "prop-types": "^15.7.2", + "query-string": "^6.12.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-dropzone": "^10.1.7", + "react-i18next": "^12.2.2", + "react-resize-detector": "^10.0.1", + "react-router": "^6.23.1", + "react-router-dom": "^6.8.1", + "react-shepherd": "6.1.1", + "shepherd.js": "13.0.3", + "url-loader": "^4.1.1", + "zustand": "4.5.5", + }, + "devDependencies": { + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@types/node": "^20.12.12", + "identity-obj-proxy": "3.0.x", + "tailwindcss": "3.2.4", + }, + }, + "platform/cli": { + "name": "@ohif/cli", + "version": "3.10.0-beta.108", + "bin": { + "ohif-cli": "src/index.js" + }, + "dependencies": { + "@babel/core": "7.24.7", + "axios": "^0.28.0", + "chalk": "^5.0.0", + "execa": "^8.0.1", + "gitignore": "^0.7.0", + "inquirer": "^8.2.0", + "listr": "^0.14.3", + "mustache": "^4.2.0", + "ncp": "^2.0.0", + "node-fetch": "^3.1.1", + "pkg-install": "^1.0.0", + "registry-url": "^6.0.0", + "spdx-license-list": "^6.4.0", + "util": "^0.12.4", + "yarn-programmatic": "^0.1.2", + }, + }, + "platform/core": { + "name": "@ohif/core", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "dcmjs": "*", + "dicomweb-client": "^0.10.4", + "gl-matrix": "^3.4.3", + "immutability-helper": "^3.1.1", + "isomorphic-base64": "^1.0.2", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0", + "moment": "*", + "object-hash": "2.1.1", + "query-string": "^6.14.0", + "react-shepherd": "6.1.1", + "shepherd.js": "13.0.3", + "validate.js": "^0.12.0", + }, + "devDependencies": { + "webpack-merge": "*", + }, + "peerDependencies": { + "@cornerstonejs/codec-charls": "^1.2.3", + "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", + "@cornerstonejs/codec-openjpeg": "^1.2.4", + "@cornerstonejs/codec-openjph": "^2.4.5", + "@cornerstonejs/dicom-image-loader": "^2.19.14", + "@ohif/ui": "3.10.0-beta.108", + "cornerstone-math": "0.1.9", + "dicom-parser": "^1.8.21", + }, + }, + "platform/i18n": { + "name": "@ohif/i18n", + "version": "3.10.0-beta.108", + "dependencies": { + "@babel/runtime": "^7.20.13", + "i18next-locize-backend": "^2.0.0", + "locize-editor": "^2.0.0", + "locize-lastused": "^1.1.0", + }, + "devDependencies": { + "i18next": "^17.0.3", + "i18next-browser-languagedetector": "^3.0.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^12.2.2", + "webpack-merge": "^5.7.3", + }, + "peerDependencies": { + "i18next": "^17.0.3", + "i18next-browser-languagedetector": "^3.0.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^12.2.2", + }, + }, + "platform/ui": { + "name": "@ohif/ui", + "version": "3.10.0-beta.108", + "dependencies": { + "@testing-library/react": "^13.1.0", + "browser-detect": "^0.2.28", + "classnames": "^2.3.2", + "d3-array": "3", + "d3-axis": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-zoom": "3", + "lodash.clonedeep": "^4.5.0", + "lodash.debounce": "^4.0.8", + "lodash.merge": "^4.6.1", + "moment": "*", + "mousetrap": "^1.6.5", + "react": "^18.3.1", + "react-dates": "^21.8.0", + "react-dnd": "14.0.2", + "react-dnd-html5-backend": "14.0.0", + "react-dom": "^18.3.1", + "react-draggable": "^4.4.6", + "react-error-boundary": "^3.1.3", + "react-modal": "3.11.2", + "react-outside-click-handler": "^1.3.0", + "react-select": "5.7.4", + "react-test-renderer": "^18.3.1", + "react-window": "^1.8.9", + "react-with-direction": "^1.3.1", + "swiper": "^8.4.2", + "webpack": "5.94.0", + }, + "devDependencies": { + "@babel/core": "7.24.7", + "@storybook/addon-actions": "^7.6.10", + "@storybook/addon-docs": "^7.6.10", + "@storybook/addon-essentials": "^7.6.10", + "@storybook/addon-links": "^7.6.10", + "@storybook/cli": "^7.6.10", + "@storybook/react": "^7.6.10", + "@storybook/react-webpack5": "^7.6.10", + "@storybook/source-loader": "^7.6.10", + "autoprefixer": "^10.4.14", + "babel-loader": "^9.1.2", + "dotenv-webpack": "^8.0.1", + "postcss": "^8.4.23", + "postcss-loader": "^7.2.4", + "prop-types": "^15.8.1", + "remark-gfm": "^3.0.1", + "storybook": "^7.6.10", + "tailwindcss": "3.2.4", + }, + "peerDependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + }, + }, + "platform/ui-next": { + "name": "@ohif/ui-next", + "version": "3.10.0-beta.108", + "dependencies": { + "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-context-menu": "^2.2.4", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-scroll-area": "^1.1.0", + "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slider": "^1.2.0", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.0", + "@radix-ui/react-toggle": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.2", + "class-variance-authority": "^0.7.0", + "clsx": "*", + "cmdk": "^1.0.0", + "date-fns": "^3.6.0", + "framer-motion": "6.2.4", + "lucide-react": "^0.379.0", + "next-themes": "^0.3.0", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-resizable-panels": "^2.1.7", + "react-shepherd": "6.1.1", + "shepherd.js": "13.0.3", + "sonner": "^1.5.0", + "tailwind-merge": "^2.3.0", + "tailwindcss": "3.2.4", + "tailwindcss-animate": "^1.0.7", + }, + "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.16.7", + }, + }, + }, + "overrides": { + "trim-newlines": "^5.0.0", + "path-to-regexp": "0.1.12", + "glob-parent": "^6.0.2", + "rollup": "2.79.2", + "commander": "8.3.0", + "body-parser": "1.20.3", + "nth-check": "^2.1.1", + }, + "packages": { + "@adobe/css-tools": ["@adobe/css-tools@4.4.1", "", {}, "sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ=="], + + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + + "@apideck/better-ajv-errors": ["@apideck/better-ajv-errors@0.3.6", "", { "dependencies": { "json-schema": "^0.4.0", "jsonpointer": "^5.0.0", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA=="], + + "@aw-web-design/x-default-browser": ["@aw-web-design/x-default-browser@1.4.126", "", { "dependencies": { "default-browser-id": "3.0.0" }, "bin": { "x-default-browser": "bin/x-default-browser.js" } }, "sha512-Xk1sIhyNC/esHGGVjL/niHLowM0csl/kFO5uawBy4IrWwy0o1G8LGt3jP6nmWGz+USxeeqbihAmp/oVZju6wug=="], + + "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], + + "@babel/compat-data": ["@babel/compat-data@7.26.5", "", {}, "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg=="], + + "@babel/core": ["@babel/core@7.24.7", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", "@babel/generator": "^7.24.7", "@babel/helper-compilation-targets": "^7.24.7", "@babel/helper-module-transforms": "^7.24.7", "@babel/helpers": "^7.24.7", "@babel/parser": "^7.24.7", "@babel/template": "^7.24.7", "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g=="], + + "@babel/generator": ["@babel/generator@7.26.5", "", { "dependencies": { "@babel/parser": "^7.26.5", "@babel/types": "^7.26.5", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.26.5", "", { "dependencies": { "@babel/compat-data": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA=="], + + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.25.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/helper-replace-supers": "^7.25.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/traverse": "^7.25.9", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ=="], + + "@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.26.3", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong=="], + + "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="], + + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="], + + "@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.25.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-wrap-function": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.26.5", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/traverse": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.25.9", "", {}, "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="], + + "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.25.9", "", { "dependencies": { "@babel/template": "^7.25.9", "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g=="], + + "@babel/helpers": ["@babel/helpers@7.26.7", "", { "dependencies": { "@babel/template": "^7.25.9", "@babel/types": "^7.26.7" } }, "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A=="], + + "@babel/parser": ["@babel/parser@7.26.7", "", { "dependencies": { "@babel/types": "^7.26.7" }, "bin": "./bin/babel-parser.js" }, "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w=="], + + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g=="], + + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug=="], + + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/plugin-transform-optional-chaining": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g=="], + + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg=="], + + "@babel/plugin-proposal-class-properties": ["@babel/plugin-proposal-class-properties@7.18.6", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ=="], + + "@babel/plugin-proposal-object-rest-spread": ["@babel/plugin-proposal-object-rest-spread@7.20.7", "", { "dependencies": { "@babel/compat-data": "^7.20.5", "@babel/helper-compilation-targets": "^7.20.7", "@babel/helper-plugin-utils": "^7.20.2", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-transform-parameters": "^7.20.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg=="], + + "@babel/plugin-proposal-private-methods": ["@babel/plugin-proposal-private-methods@7.18.6", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA=="], + + "@babel/plugin-proposal-private-property-in-object": ["@babel/plugin-proposal-private-property-in-object@7.21.11", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-create-class-features-plugin": "^7.21.0", "@babel/helper-plugin-utils": "^7.20.2", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw=="], + + "@babel/plugin-syntax-async-generators": ["@babel/plugin-syntax-async-generators@7.8.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw=="], + + "@babel/plugin-syntax-bigint": ["@babel/plugin-syntax-bigint@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg=="], + + "@babel/plugin-syntax-class-properties": ["@babel/plugin-syntax-class-properties@7.12.13", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA=="], + + "@babel/plugin-syntax-class-static-block": ["@babel/plugin-syntax-class-static-block@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw=="], + + "@babel/plugin-syntax-dynamic-import": ["@babel/plugin-syntax-dynamic-import@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ=="], + + "@babel/plugin-syntax-export-namespace-from": ["@babel/plugin-syntax-export-namespace-from@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q=="], + + "@babel/plugin-syntax-flow": ["@babel/plugin-syntax-flow@7.26.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg=="], + + "@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.26.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg=="], + + "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.26.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A=="], + + "@babel/plugin-syntax-import-meta": ["@babel/plugin-syntax-import-meta@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g=="], + + "@babel/plugin-syntax-json-strings": ["@babel/plugin-syntax-json-strings@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA=="], + + "@babel/plugin-syntax-logical-assignment-operators": ["@babel/plugin-syntax-logical-assignment-operators@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig=="], + + "@babel/plugin-syntax-nullish-coalescing-operator": ["@babel/plugin-syntax-nullish-coalescing-operator@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ=="], + + "@babel/plugin-syntax-numeric-separator": ["@babel/plugin-syntax-numeric-separator@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug=="], + + "@babel/plugin-syntax-object-rest-spread": ["@babel/plugin-syntax-object-rest-spread@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA=="], + + "@babel/plugin-syntax-optional-catch-binding": ["@babel/plugin-syntax-optional-catch-binding@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q=="], + + "@babel/plugin-syntax-optional-chaining": ["@babel/plugin-syntax-optional-chaining@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg=="], + + "@babel/plugin-syntax-private-property-in-object": ["@babel/plugin-syntax-private-property-in-object@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg=="], + + "@babel/plugin-syntax-top-level-await": ["@babel/plugin-syntax-top-level-await@7.14.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ=="], + + "@babel/plugin-syntax-unicode-sets-regex": ["@babel/plugin-syntax-unicode-sets-regex@7.18.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg=="], + + "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg=="], + + "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-remap-async-to-generator": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw=="], + + "@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.25.9", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-remap-async-to-generator": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ=="], + + "@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.26.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ=="], + + "@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg=="], + + "@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.25.9", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q=="], + + "@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.26.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ=="], + + "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.25.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-compilation-targets": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-replace-supers": "^7.25.9", "@babel/traverse": "^7.25.9", "globals": "^11.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg=="], + + "@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/template": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA=="], + + "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ=="], + + "@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA=="], + + "@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw=="], + + "@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg=="], + + "@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.26.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ=="], + + "@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww=="], + + "@babel/plugin-transform-flow-strip-types": ["@babel/plugin-transform-flow-strip-types@7.26.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5", "@babel/plugin-syntax-flow": "^7.26.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-eGK26RsbIkYUns3Y8qKl362juDDYK+wEdPGHGrhzUl6CewZFo55VZ7hg+CyMFU4dd5QQakBN86nBMpRsFpRvbQ=="], + + "@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A=="], + + "@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.25.9", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA=="], + + "@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw=="], + + "@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ=="], + + "@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q=="], + + "@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA=="], + + "@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.25.9", "", { "dependencies": { "@babel/helper-module-transforms": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw=="], + + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.26.3", "", { "dependencies": { "@babel/helper-module-transforms": "^7.26.0", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ=="], + + "@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.25.9", "", { "dependencies": { "@babel/helper-module-transforms": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA=="], + + "@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.25.9", "", { "dependencies": { "@babel/helper-module-transforms": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw=="], + + "@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA=="], + + "@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ=="], + + "@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.26.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw=="], + + "@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q=="], + + "@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.25.9", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/plugin-transform-parameters": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg=="], + + "@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-replace-supers": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A=="], + + "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g=="], + + "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A=="], + + "@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g=="], + + "@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.25.9", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw=="], + + "@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.25.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw=="], + + "@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA=="], + + "@babel/plugin-transform-react-constant-elements": ["@babel/plugin-transform-react-constant-elements@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ncw2JFsJVuvfRsa2lSHiC55kETQVLSnsYGQ1JDDwkUeWGTL/8Tom8aLTnlqgoeuopWrbbGndrc9AlLYrIosrow=="], + + "@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ=="], + + "@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.25.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/plugin-syntax-jsx": "^7.25.9", "@babel/types": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw=="], + + "@babel/plugin-transform-react-jsx-development": ["@babel/plugin-transform-react-jsx-development@7.25.9", "", { "dependencies": { "@babel/plugin-transform-react-jsx": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw=="], + + "@babel/plugin-transform-react-pure-annotations": ["@babel/plugin-transform-react-pure-annotations@7.25.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg=="], + + "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "regenerator-transform": "^0.15.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg=="], + + "@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg=="], + + "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.24.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.24.7", "@babel/helper-plugin-utils": "^7.24.7", "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.10.1", "babel-plugin-polyfill-regenerator": "^0.6.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw=="], + + "@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng=="], + + "@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A=="], + + "@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA=="], + + "@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw=="], + + "@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.26.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.26.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/plugin-syntax-typescript": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5cJurntg+AT+cgelGP9Bt788DKiAw9gIMSMU2NJrLAilnj0m8WZWUNZPSLOmadYsujHutpgElO+50foX+ib/Wg=="], + + "@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q=="], + + "@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg=="], + + "@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA=="], + + "@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ=="], + + "@babel/preset-env": ["@babel/preset-env@7.24.7", "", { "dependencies": { "@babel/compat-data": "^7.24.7", "@babel/helper-compilation-targets": "^7.24.7", "@babel/helper-plugin-utils": "^7.24.7", "@babel/helper-validator-option": "^7.24.7", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", "@babel/plugin-syntax-import-assertions": "^7.24.7", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.24.7", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoped-functions": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.24.7", "@babel/plugin-transform-class-properties": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.24.7", "@babel/plugin-transform-classes": "^7.24.7", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.7", "@babel/plugin-transform-dotall-regex": "^7.24.7", "@babel/plugin-transform-duplicate-keys": "^7.24.7", "@babel/plugin-transform-dynamic-import": "^7.24.7", "@babel/plugin-transform-exponentiation-operator": "^7.24.7", "@babel/plugin-transform-export-namespace-from": "^7.24.7", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.24.7", "@babel/plugin-transform-json-strings": "^7.24.7", "@babel/plugin-transform-literals": "^7.24.7", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-member-expression-literals": "^7.24.7", "@babel/plugin-transform-modules-amd": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.7", "@babel/plugin-transform-modules-systemjs": "^7.24.7", "@babel/plugin-transform-modules-umd": "^7.24.7", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-new-target": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-object-super": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.7", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-property-literals": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-reserved-words": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-template-literals": "^7.24.7", "@babel/plugin-transform-typeof-symbol": "^7.24.7", "@babel/plugin-transform-unicode-escapes": "^7.24.7", "@babel/plugin-transform-unicode-property-regex": "^7.24.7", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.10.4", "babel-plugin-polyfill-regenerator": "^0.6.1", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ=="], + + "@babel/preset-flow": ["@babel/preset-flow@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-validator-option": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-EASHsAhE+SSlEzJ4bzfusnXSHiU+JfAYzj+jbw2vgQKgq5HrUr8qs+vgtiEL5dOH6sEweI+PNt2D7AqrDSHyqQ=="], + + "@babel/preset-modules": ["@babel/preset-modules@0.1.6-no-external-plugins", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA=="], + + "@babel/preset-react": ["@babel/preset-react@7.26.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-validator-option": "^7.25.9", "@babel/plugin-transform-react-display-name": "^7.25.9", "@babel/plugin-transform-react-jsx": "^7.25.9", "@babel/plugin-transform-react-jsx-development": "^7.25.9", "@babel/plugin-transform-react-pure-annotations": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw=="], + + "@babel/preset-typescript": ["@babel/preset-typescript@7.26.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-validator-option": "^7.25.9", "@babel/plugin-syntax-jsx": "^7.25.9", "@babel/plugin-transform-modules-commonjs": "^7.25.9", "@babel/plugin-transform-typescript": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg=="], + + "@babel/register": ["@babel/register@7.25.9", "", { "dependencies": { "clone-deep": "^4.0.1", "find-cache-dir": "^2.0.0", "make-dir": "^2.1.0", "pirates": "^4.0.6", "source-map-support": "^0.5.16" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8D43jXtGsYmEeDvm4MWHYUpWf8iiXgWYx3fW7E7Wb7Oe6FWqJPl5K6TuFW0dOwNZzEE5rjlaSJYH9JjrUKJszA=="], + + "@babel/runtime": ["@babel/runtime@7.26.7", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ=="], + + "@babel/runtime-corejs2": ["@babel/runtime-corejs2@7.26.7", "", { "dependencies": { "core-js": "^2.6.12", "regenerator-runtime": "^0.14.0" } }, "sha512-C7fo97gUfsUP54j6GcQ+rJXyW6vgRRqF7J1ZxXesWcQtSRyzH1+eYrqFGzmU2JSUGFV0hQA2zLY/Z8AMrEx0qg=="], + + "@babel/runtime-corejs3": ["@babel/runtime-corejs3@7.26.7", "", { "dependencies": { "core-js-pure": "^3.30.2", "regenerator-runtime": "^0.14.0" } }, "sha512-55gRV8vGrCIYZnaQHQrD92Lo/hYE3Sj5tmbuf0hhHR7sj2CWhEhHU89hbq+UVDXvFG1zUVXJhUkEq1eAfqXtFw=="], + + "@babel/template": ["@babel/template@7.25.9", "", { "dependencies": { "@babel/code-frame": "^7.25.9", "@babel/parser": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg=="], + + "@babel/traverse": ["@babel/traverse@7.26.7", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", "@babel/template": "^7.25.9", "@babel/types": "^7.26.7", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA=="], + + "@babel/types": ["@babel/types@7.26.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg=="], + + "@base2/pretty-print-object": ["@base2/pretty-print-object@1.0.1", "", {}, "sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA=="], + + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="], + + "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], + + "@cornerstonejs/adapters": ["@cornerstonejs/adapters@2.19.14", "", { "dependencies": { "@babel/runtime-corejs2": "^7.17.8", "buffer": "^6.0.3", "dcmjs": "^0.29.8", "gl-matrix": "^3.4.3", "ndarray": "^1.0.19" }, "peerDependencies": { "@cornerstonejs/core": "^2.19.14", "@cornerstonejs/tools": "^2.19.14" } }, "sha512-tHk8y4c1GGS4kpkJMNEcLVnFL6GfA4JzAUviPnjdTRYHd6GgqoSUt3m3JLAaahYSLPh5lRV9HLU/OYYfkUOuyg=="], + + "@cornerstonejs/calculate-suv": ["@cornerstonejs/calculate-suv@1.1.0", "", {}, "sha512-Q9XraiDJif9aMFArD2iEuDO/HXbcRVCqB7KfaHgDrdTTjgDFovS91Psbdim7crypRSvE6dh/+HKeFNHdvNkA6w=="], + + "@cornerstonejs/codec-charls": ["@cornerstonejs/codec-charls@1.2.3", "", {}, "sha512-qKUe6DN0dnGzhhfZLYhH9UZacMcudjxcaLXCrpxJImT/M/PQvZCT2rllu6VGJbWKJWG+dMVV2zmmleZcdJ7/cA=="], + + "@cornerstonejs/codec-libjpeg-turbo-8bit": ["@cornerstonejs/codec-libjpeg-turbo-8bit@1.2.2", "", {}, "sha512-aAUMK2958YNpOb/7G6e2/aG7hExTiFTASlMt/v90XA0pRHdWiNg5ny4S5SAju0FbIw4zcMnR0qfY+yW3VG2ivg=="], + + "@cornerstonejs/codec-openjpeg": ["@cornerstonejs/codec-openjpeg@1.2.4", "", {}, "sha512-UT2su6xZZnCPSuWf2ldzKa/2+guQ7BGgfBSKqxanggwJHh48gZqIAzekmsLyJHMMK5YDK+ti+fzvVJhBS3Xi/g=="], + + "@cornerstonejs/codec-openjph": ["@cornerstonejs/codec-openjph@2.4.7", "", {}, "sha512-qvP4q4JDib7mi9r7LqKOwqz7YZ8gjtDX4ZCezeYf8+eb7MBXCz5uXAMeVF3yz9Axw4XiIMdB/pqXkm8tqCl13w=="], + + "@cornerstonejs/core": ["@cornerstonejs/core@2.19.14", "", { "dependencies": { "@kitware/vtk.js": "32.9.0", "comlink": "^4.4.1", "gl-matrix": "^3.4.3" } }, "sha512-nVL/rAXFJRfvTC7cHgiSGZiJjRL8vfwhNMnSc+zqx356Bq2FsAPRC9clFlLw1KqvbBGNL/uC+24TD1VtIm1V5w=="], + + "@cornerstonejs/dicom-image-loader": ["@cornerstonejs/dicom-image-loader@2.19.14", "", { "dependencies": { "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.5", "comlink": "^4.4.1", "jpeg-lossless-decoder-js": "^2.1.0", "pako": "^2.0.4", "uuid": "^9.0.0" }, "peerDependencies": { "@cornerstonejs/core": "^2.19.14", "dicom-parser": "^1.8.9" } }, "sha512-5X5Acw2S5D5gH5D30nNGjIeeEFpZkMIZovs043nP72HCTeprNekjalVe/s7n/rOBXb0MfrUvODqJDnYMSBgx6g=="], + + "@cornerstonejs/tools": ["@cornerstonejs/tools@2.19.14", "", { "dependencies": { "@types/offscreencanvas": "2019.7.3", "comlink": "^4.4.1", "lodash.get": "^4.4.2" }, "peerDependencies": { "@cornerstonejs/core": "^2.19.14", "@kitware/vtk.js": "32.9.0", "@types/d3-array": "^3.0.4", "@types/d3-interpolate": "^3.0.1", "d3-array": "^3.2.3", "d3-interpolate": "^3.0.1", "gl-matrix": "^3.4.3" } }, "sha512-uZDhs5TXo9IvMqh+P2na4KiWb5NWCyKreaXN9HWPzv6P1QML9U7tvrfsXdYH3YnCCquddvZeLNwNraV/hWT5jw=="], + + "@csstools/postcss-cascade-layers": ["@csstools/postcss-cascade-layers@1.1.1", "", { "dependencies": { "@csstools/selector-specificity": "^2.0.2", "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA=="], + + "@csstools/postcss-color-function": ["@csstools/postcss-color-function@1.1.1", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^1.1.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw=="], + + "@csstools/postcss-font-format-keywords": ["@csstools/postcss-font-format-keywords@1.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg=="], + + "@csstools/postcss-hwb-function": ["@csstools/postcss-hwb-function@1.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w=="], + + "@csstools/postcss-ic-unit": ["@csstools/postcss-ic-unit@1.0.1", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^1.1.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw=="], + + "@csstools/postcss-is-pseudo-class": ["@csstools/postcss-is-pseudo-class@2.0.7", "", { "dependencies": { "@csstools/selector-specificity": "^2.0.0", "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA=="], + + "@csstools/postcss-nested-calc": ["@csstools/postcss-nested-calc@1.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ=="], + + "@csstools/postcss-normalize-display-values": ["@csstools/postcss-normalize-display-values@1.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw=="], + + "@csstools/postcss-oklab-function": ["@csstools/postcss-oklab-function@1.1.1", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^1.1.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA=="], + + "@csstools/postcss-progressive-custom-properties": ["@csstools/postcss-progressive-custom-properties@1.3.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.3" } }, "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA=="], + + "@csstools/postcss-stepped-value-functions": ["@csstools/postcss-stepped-value-functions@1.0.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ=="], + + "@csstools/postcss-text-decoration-shorthand": ["@csstools/postcss-text-decoration-shorthand@1.0.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw=="], + + "@csstools/postcss-trigonometric-functions": ["@csstools/postcss-trigonometric-functions@1.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og=="], + + "@csstools/postcss-unset-value": ["@csstools/postcss-unset-value@1.0.2", "", { "peerDependencies": { "postcss": "^8.2" } }, "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g=="], + + "@csstools/selector-specificity": ["@csstools/selector-specificity@2.2.0", "", { "peerDependencies": { "postcss-selector-parser": "^6.0.10" } }, "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw=="], + + "@cypress/request": ["@cypress/request@3.0.7", "", { "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", "caseless": "~0.12.0", "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~4.0.0", "http-signature": "~1.4.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", "qs": "6.13.1", "safe-buffer": "^5.1.2", "tough-cookie": "^5.0.0", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" } }, "sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg=="], + + "@cypress/xvfb": ["@cypress/xvfb@1.2.4", "", { "dependencies": { "debug": "^3.1.0", "lodash.once": "^4.1.1" } }, "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q=="], + + "@discoveryjs/json-ext": ["@discoveryjs/json-ext@0.5.7", "", {}, "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="], + + "@emotion/babel-plugin": ["@emotion/babel-plugin@11.13.5", "", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ=="], + + "@emotion/cache": ["@emotion/cache@11.14.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA=="], + + "@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], + + "@emotion/is-prop-valid": ["@emotion/is-prop-valid@0.8.8", "", { "dependencies": { "@emotion/memoize": "0.7.4" } }, "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA=="], + + "@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="], + + "@emotion/react": ["@emotion/react@11.14.0", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA=="], + + "@emotion/serialize": ["@emotion/serialize@1.3.3", "", { "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA=="], + + "@emotion/sheet": ["@emotion/sheet@1.4.0", "", {}, "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="], + + "@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="], + + "@emotion/use-insertion-effect-with-fallbacks": ["@emotion/use-insertion-effect-with-fallbacks@1.2.0", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg=="], + + "@emotion/utils": ["@emotion/utils@1.4.2", "", {}, "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="], + + "@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="], + + "@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], + + "@externals/devDependencies": ["@externals/devDependencies@workspace:addOns/externals/devDependencies"], + + "@externals/dicom-microscopy-viewer": ["@externals/dicom-microscopy-viewer@workspace:addOns/externals/dicom-microscopy-viewer"], + + "@fal-works/esbuild-plugin-global-externals": ["@fal-works/esbuild-plugin-global-externals@2.1.2", "", {}, "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ=="], + + "@floating-ui/core": ["@floating-ui/core@1.6.9", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.6.13", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.2", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="], + + "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="], + + "@hapi/hoek": ["@hapi/hoek@9.3.0", "", {}, "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="], + + "@hapi/topo": ["@hapi/topo@5.1.0", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg=="], + + "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="], + + "@hutson/parse-repository-url": ["@hutson/parse-repository-url@3.0.2", "", {}, "sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q=="], + + "@icons/material": ["@icons/material@0.2.4", "", { "peerDependencies": { "react": "*" } }, "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw=="], + + "@icr/polyseg-wasm": ["@icr/polyseg-wasm@0.4.0", "", {}, "sha512-3sZmiwG8I0NaqPle0L7+V/ZexiR7IjIUFkUsaOoFI9rNuBGyyMMmxAxnCmqcDFtBDk9h+JEYJf6e3NnqlHi/HQ=="], + + "@ipld/car": ["@ipld/car@5.4.0", "", { "dependencies": { "@ipld/dag-cbor": "^9.0.7", "cborg": "^4.0.5", "multiformats": "^13.0.0", "varint": "^6.0.0" } }, "sha512-FiGxOhTUh3fn/kkA+YvNYQjA/T8T5DcKG0NZwAi3aXrizN1qm99HzdYTccEwcX/rUCtI8wTUCKDNPBLUb7pBIQ=="], + + "@ipld/dag-cbor": ["@ipld/dag-cbor@9.2.2", "", { "dependencies": { "cborg": "^4.0.0", "multiformats": "^13.1.0" } }, "sha512-uIEOuruCqKTP50OBWwgz4Js2+LhiBQaxc57cnP71f45b1mHEAo1OCR1Zn/TbvSW/mV1x+JqhacIktkKyaYqhCw=="], + + "@ipld/dag-json": ["@ipld/dag-json@10.2.3", "", { "dependencies": { "cborg": "^4.0.0", "multiformats": "^13.1.0" } }, "sha512-itacv1j1hvYgLox2B42Msn70QLzcr0MEo5yGIENuw2SM/lQzq9bmBiMky+kDsIrsqqblKTXcHBZnnmK7D4a6ZQ=="], + + "@ipld/dag-pb": ["@ipld/dag-pb@4.1.3", "", { "dependencies": { "multiformats": "^13.1.0" } }, "sha512-ueULCaaSCcD+dQga6nKiRr+RSeVgdiYiEPKVUu5iQMNYDN+9osd0KpR3UDd9uQQ+6RWuv9L34SchfEwj7YIbOA=="], + + "@ipld/unixfs": ["@ipld/unixfs@3.0.0", "", { "dependencies": { "@ipld/dag-pb": "^4.0.0", "@multiformats/murmur3": "^2.1.3", "@perma/map": "^1.0.2", "actor": "^2.3.1", "multiformats": "^13.0.1", "protobufjs": "^7.1.2", "rabin-rs": "^2.1.0" } }, "sha512-Tj3/BPOlnemcZQ2ETIZAO8hqAs9KNzWyX5J9+JCL9jDwvYwjxeYjqJ3v+9DusNvTBmJhZnGVP6ijUHrsuOLp+g=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="], + + "@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="], + + "@itk-wasm/dam": ["@itk-wasm/dam@1.1.1", "", { "dependencies": { "axios": "^1.4.0", "commander": "^10.0.1", "decompress": "^4.2.1", "files-from-path": "^1.0.0", "ipfs-car": "^1.0.0", "tar": "^6.1.13" }, "bin": { "dam": "cli.js" } }, "sha512-7+9L3lrLMKF4y6B6qjs8GqfbpxT0waOJUM14NdMNEA6M+BoBS8fdHREhQHo2s7QMA5O7I+Jv7m+dyqlisGnbdQ=="], + + "@itk-wasm/morphological-contour-interpolation": ["@itk-wasm/morphological-contour-interpolation@1.1.0", "", { "dependencies": { "itk-wasm": "1.0.0-b.173" } }, "sha512-n6JIyDcSCCjlpfCW8mnTTzwPTE8U1QT87hNmyAknxdpGR4dfAzIutuKNrwgvr9UiKEBcit0X3HNx9dkzDwcIcw=="], + + "@jest/console": ["@jest/console@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "slash": "^3.0.0" } }, "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg=="], + + "@jest/core": ["@jest/core@29.7.0", "", { "dependencies": { "@jest/console": "^29.7.0", "@jest/reporters": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-changed-files": "^29.7.0", "jest-config": "^29.7.0", "jest-haste-map": "^29.7.0", "jest-message-util": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.7.0", "jest-resolve-dependencies": "^29.7.0", "jest-runner": "^29.7.0", "jest-runtime": "^29.7.0", "jest-snapshot": "^29.7.0", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "jest-watcher": "^29.7.0", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"] }, "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg=="], + + "@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], + + "@jest/expect": ["@jest/expect@29.7.0", "", { "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" } }, "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ=="], + + "@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="], + + "@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="], + + "@jest/globals": ["@jest/globals@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", "@jest/types": "^29.6.3", "jest-mock": "^29.7.0" } }, "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ=="], + + "@jest/reporters": ["@jest/reporters@29.7.0", "", { "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", "exit": "^0.1.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "jest-worker": "^29.7.0", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", "v8-to-istanbul": "^9.0.1" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"] }, "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg=="], + + "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "@jest/source-map": ["@jest/source-map@29.6.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", "graceful-fs": "^4.2.9" } }, "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw=="], + + "@jest/test-result": ["@jest/test-result@29.7.0", "", { "dependencies": { "@jest/console": "^29.7.0", "@jest/types": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" } }, "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA=="], + + "@jest/test-sequencer": ["@jest/test-sequencer@29.7.0", "", { "dependencies": { "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "slash": "^3.0.0" } }, "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw=="], + + "@jest/transform": ["@jest/transform@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", "write-file-atomic": "^4.0.2" } }, "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw=="], + + "@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], + + "@jridgewell/source-map": ["@jridgewell/source-map@0.3.6", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + + "@juggle/resize-observer": ["@juggle/resize-observer@3.4.0", "", {}, "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="], + + "@kitware/vtk.js": ["@kitware/vtk.js@32.1.1", "", { "dependencies": { "@babel/runtime": "7.22.11", "@types/webxr": "^0.5.5", "commander": "9.2.0", "d3-scale": "4.0.2", "fast-deep-equal": "^3.1.3", "fflate": "0.7.3", "gl-matrix": "3.4.3", "globalthis": "1.0.3", "seedrandom": "3.0.5", "shader-loader": "1.3.1", "shelljs": "0.8.5", "spark-md5": "3.0.2", "stream-browserify": "3.0.0", "webworker-promise": "0.5.0", "worker-loader": "3.0.8", "xmlbuilder2": "3.0.2" }, "peerDependencies": { "@babel/preset-env": "^7.17.10", "autoprefixer": "^10.4.7", "wslink": ">=1.1.0 || ^2.0.0" }, "bin": { "xml2json": "Utilities/XMLConverter/xml2json-cli.js", "vtkDataConverter": "Utilities/DataGenerator/convert-cli.js" } }, "sha512-rrh+PvmjpMDCOsgr7B8ZVxnF75TXjUJOVleMO5QJFh7DP+31927l9DJA4WRiJU7mbpYuZGt8/vAojO5fCizUkA=="], + + "@lerna/child-process": ["@lerna/child-process@7.4.2", "", { "dependencies": { "chalk": "^4.1.0", "execa": "^5.0.0", "strong-log-transformer": "^2.1.0" } }, "sha512-je+kkrfcvPcwL5Tg8JRENRqlbzjdlZXyaR88UcnCdNW0AJ1jX9IfHRys1X7AwSroU2ug8ESNC+suoBw1vX833Q=="], + + "@lerna/create": ["@lerna/create@7.4.2", "", { "dependencies": { "@lerna/child-process": "7.4.2", "@npmcli/run-script": "6.0.2", "@nx/devkit": ">=16.5.1 < 17", "@octokit/plugin-enterprise-rest": "6.0.1", "@octokit/rest": "19.0.11", "byte-size": "8.1.1", "chalk": "4.1.0", "clone-deep": "4.0.1", "cmd-shim": "6.0.1", "columnify": "1.6.0", "conventional-changelog-core": "5.0.1", "conventional-recommended-bump": "7.0.1", "cosmiconfig": "^8.2.0", "dedent": "0.7.0", "execa": "5.0.0", "fs-extra": "^11.1.1", "get-stream": "6.0.0", "git-url-parse": "13.1.0", "glob-parent": "5.1.2", "globby": "11.1.0", "graceful-fs": "4.2.11", "has-unicode": "2.0.1", "ini": "^1.3.8", "init-package-json": "5.0.0", "inquirer": "^8.2.4", "is-ci": "3.0.1", "is-stream": "2.0.0", "js-yaml": "4.1.0", "libnpmpublish": "7.3.0", "load-json-file": "6.2.0", "lodash": "^4.17.21", "make-dir": "4.0.0", "minimatch": "3.0.5", "multimatch": "5.0.0", "node-fetch": "2.6.7", "npm-package-arg": "8.1.1", "npm-packlist": "5.1.1", "npm-registry-fetch": "^14.0.5", "npmlog": "^6.0.2", "nx": ">=16.5.1 < 17", "p-map": "4.0.0", "p-map-series": "2.1.0", "p-queue": "6.6.2", "p-reduce": "^2.1.0", "pacote": "^15.2.0", "pify": "5.0.0", "read-cmd-shim": "4.0.0", "read-package-json": "6.0.4", "resolve-from": "5.0.0", "rimraf": "^4.4.1", "semver": "^7.3.4", "signal-exit": "3.0.7", "slash": "^3.0.0", "ssri": "^9.0.1", "strong-log-transformer": "2.1.0", "tar": "6.1.11", "temp-dir": "1.0.0", "upath": "2.0.1", "uuid": "^9.0.0", "validate-npm-package-license": "^3.0.4", "validate-npm-package-name": "5.0.0", "write-file-atomic": "5.0.1", "write-pkg": "4.0.0", "yargs": "16.2.0", "yargs-parser": "20.2.4" } }, "sha512-1wplFbQ52K8E/unnqB0Tq39Z4e+NEoNrpovEnl6GpsTUrC6WDp8+w0Le2uCBV0hXyemxChduCkLz4/y1H1wTeg=="], + + "@mapbox/jsonlint-lines-primitives": ["@mapbox/jsonlint-lines-primitives@2.0.2", "", {}, "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ=="], + + "@mapbox/mapbox-gl-style-spec": ["@mapbox/mapbox-gl-style-spec@13.28.0", "", { "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/point-geometry": "^0.1.0", "@mapbox/unitbezier": "^0.0.0", "csscolorparser": "~1.0.2", "json-stringify-pretty-compact": "^2.0.0", "minimist": "^1.2.6", "rw": "^1.3.3", "sort-object": "^0.3.2" }, "bin": { "gl-style-format": "bin/gl-style-format.js", "gl-style-migrate": "bin/gl-style-migrate.js", "gl-style-validate": "bin/gl-style-validate.js", "gl-style-composite": "bin/gl-style-composite.js" } }, "sha512-B8xM7Fp1nh5kejfIl4SWeY0gtIeewbuRencqO3cJDrCHZpaPg7uY+V8abuR+esMeuOjRl5cLhVTP40v+1ywxbg=="], + + "@mapbox/point-geometry": ["@mapbox/point-geometry@0.1.0", "", {}, "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ=="], + + "@mapbox/unitbezier": ["@mapbox/unitbezier@0.0.0", "", {}, "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA=="], + + "@mdx-js/react": ["@mdx-js/react@2.3.0", "", { "dependencies": { "@types/mdx": "^2.0.0", "@types/react": ">=16" }, "peerDependencies": { "react": ">=16" } }, "sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g=="], + + "@microsoft/tsdoc": ["@microsoft/tsdoc@0.14.2", "", {}, "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug=="], + + "@microsoft/tsdoc-config": ["@microsoft/tsdoc-config@0.16.2", "", { "dependencies": { "@microsoft/tsdoc": "0.14.2", "ajv": "~6.12.6", "jju": "~1.4.0", "resolve": "~1.19.0" } }, "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw=="], + + "@module-federation/error-codes": ["@module-federation/error-codes@0.8.4", "", {}, "sha512-55LYmrDdKb4jt+qr8qE8U3al62ZANp3FhfVaNPOaAmdTh0jHdD8M3yf5HKFlr5xVkVO4eV/F/J2NCfpbh+pEXQ=="], + + "@module-federation/runtime": ["@module-federation/runtime@0.8.4", "", { "dependencies": { "@module-federation/error-codes": "0.8.4", "@module-federation/sdk": "0.8.4" } }, "sha512-yZeZ7z2Rx4gv/0E97oLTF3V6N25vglmwXGgoeju/W2YjsFvWzVtCDI7zRRb0mJhU6+jmSM8jP1DeQGbea/AiZQ=="], + + "@module-federation/runtime-tools": ["@module-federation/runtime-tools@0.8.4", "", { "dependencies": { "@module-federation/runtime": "0.8.4", "@module-federation/webpack-bundler-runtime": "0.8.4" } }, "sha512-fjVOsItJ1u5YY6E9FnS56UDwZgqEQUrWFnouRiPtK123LUuqUI9FH4redZoKWlE1PB0ir1Z3tnqy8eFYzPO38Q=="], + + "@module-federation/sdk": ["@module-federation/sdk@0.8.4", "", { "dependencies": { "isomorphic-rslog": "0.0.6" } }, "sha512-waABomIjg/5m1rPDBWYG4KUhS5r7OUUY7S+avpaVIY/tkPWB3ibRDKy2dNLLAMaLKq0u+B1qIdEp4NIWkqhqpg=="], + + "@module-federation/webpack-bundler-runtime": ["@module-federation/webpack-bundler-runtime@0.8.4", "", { "dependencies": { "@module-federation/runtime": "0.8.4", "@module-federation/sdk": "0.8.4" } }, "sha512-HggROJhvHPUX7uqBD/XlajGygMNM1DG0+4OAkk8MBQe4a18QzrRNzZt6XQbRTSG4OaEoyRWhQHvYD3Yps405tQ=="], + + "@msgpack/msgpack": ["@msgpack/msgpack@2.8.0", "", {}, "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ=="], + + "@multiformats/blake2": ["@multiformats/blake2@1.0.13", "", { "dependencies": { "blakejs": "^1.1.1", "multiformats": "^9.5.4" } }, "sha512-T1Kzya0wjj85CaVeRSpJ858EnSvW1pw94GSitxYf84VsNdv5XYbJ6QG8y26Ft1bVALzrUCmqkQrR53QHSyu6RA=="], + + "@multiformats/murmur3": ["@multiformats/murmur3@2.1.8", "", { "dependencies": { "multiformats": "^13.0.0", "murmurhash3js-revisited": "^3.0.0" } }, "sha512-6vId1C46ra3R1sbJUOFCZnsUIveR9oF20yhPmAFxPm0JfrX3/ZRCgP3YDrBzlGoEppOXnA9czHeYc0T9mB6hbA=="], + + "@multiformats/sha3": ["@multiformats/sha3@2.0.17", "", { "dependencies": { "js-sha3": "^0.8.0", "multiformats": "^9.5.4" } }, "sha512-7ik6pk178qLO2cpNucgf48UnAOBMkq/2H92DP4SprZOJqM9zqbVaKS7XyYW6UvhRsDJ3wi921fYv1ihTtQHLtA=="], + + "@ndelangen/get-tarball": ["@ndelangen/get-tarball@3.0.9", "", { "dependencies": { "gunzip-maybe": "^1.4.2", "pump": "^3.0.0", "tar-fs": "^2.1.1" } }, "sha512-9JKTEik4vq+yGosHYhZ1tiH/3WpUS0Nh0kej4Agndhox8pAdWhEx5knFVRcb/ya9knCRCs1rPxNrSXTDdfVqpA=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@npmcli/fs": ["@npmcli/fs@3.1.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg=="], + + "@npmcli/git": ["@npmcli/git@4.1.0", "", { "dependencies": { "@npmcli/promise-spawn": "^6.0.0", "lru-cache": "^7.4.4", "npm-pick-manifest": "^8.0.0", "proc-log": "^3.0.0", "promise-inflight": "^1.0.1", "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^3.0.0" } }, "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ=="], + + "@npmcli/installed-package-contents": ["@npmcli/installed-package-contents@2.1.0", "", { "dependencies": { "npm-bundled": "^3.0.0", "npm-normalize-package-bin": "^3.0.0" }, "bin": { "installed-package-contents": "bin/index.js" } }, "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w=="], + + "@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="], + + "@npmcli/node-gyp": ["@npmcli/node-gyp@3.0.0", "", {}, "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA=="], + + "@npmcli/promise-spawn": ["@npmcli/promise-spawn@6.0.2", "", { "dependencies": { "which": "^3.0.0" } }, "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg=="], + + "@npmcli/run-script": ["@npmcli/run-script@6.0.2", "", { "dependencies": { "@npmcli/node-gyp": "^3.0.0", "@npmcli/promise-spawn": "^6.0.0", "node-gyp": "^9.0.0", "read-package-json-fast": "^3.0.0", "which": "^3.0.0" } }, "sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA=="], + + "@nrwl/devkit": ["@nrwl/devkit@16.10.0", "", { "dependencies": { "@nx/devkit": "16.10.0" } }, "sha512-fRloARtsDQoQgQ7HKEy0RJiusg/HSygnmg4gX/0n/Z+SUS+4KoZzvHjXc6T5ZdEiSjvLypJ+HBM8dQzIcVACPQ=="], + + "@nrwl/tao": ["@nrwl/tao@16.10.0", "", { "dependencies": { "nx": "16.10.0", "tslib": "^2.3.0" }, "bin": { "tao": "index.js" } }, "sha512-QNAanpINbr+Pod6e1xNgFbzK1x5wmZl+jMocgiEFXZ67KHvmbD6MAQQr0MMz+GPhIu7EE4QCTLTyCEMlAG+K5Q=="], + + "@nx/devkit": ["@nx/devkit@16.10.0", "", { "dependencies": { "@nrwl/devkit": "16.10.0", "ejs": "^3.1.7", "enquirer": "~2.3.6", "ignore": "^5.0.4", "semver": "7.5.3", "tmp": "~0.2.1", "tslib": "^2.3.0" }, "peerDependencies": { "nx": ">= 15 <= 17" } }, "sha512-IvKQqRJFDDiaj33SPfGd3ckNHhHi6ceEoqCbAP4UuMXOPPVOX6H0KVk+9tknkPb48B7jWIw6/AgOeWkBxPRO5w=="], + + "@nx/nx-darwin-arm64": ["@nx/nx-darwin-arm64@16.10.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YF+MIpeuwFkyvM5OwgY/rTNRpgVAI/YiR0yTYCZR+X3AAvP775IVlusNgQ3oedTBRUzyRnI4Tknj1WniENFsvQ=="], + + "@nx/nx-darwin-x64": ["@nx/nx-darwin-x64@16.10.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ypi6YxwXgb0kg2ixKXE3pwf5myVNUgWf1CsV5OzVccCM8NzheMO51KDXTDmEpXdzUsfT0AkO1sk5GZeCjhVONg=="], + + "@nx/nx-freebsd-x64": ["@nx/nx-freebsd-x64@16.10.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UeEYFDmdbbDkTQamqvtU8ibgu5jQLgFF1ruNb/U4Ywvwutw2d4ruOMl2e0u9hiNja9NFFAnDbvzrDcMo7jYqYw=="], + + "@nx/nx-linux-arm-gnueabihf": ["@nx/nx-linux-arm-gnueabihf@16.10.0", "", { "os": "linux", "cpu": "arm" }, "sha512-WV3XUC2DB6/+bz1sx+d1Ai9q2Cdr+kTZRN50SOkfmZUQyEBaF6DRYpx/a4ahhxH3ktpNfyY8Maa9OEYxGCBkQA=="], + + "@nx/nx-linux-arm64-gnu": ["@nx/nx-linux-arm64-gnu@16.10.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-aWIkOUw995V3ItfpAi5FuxQ+1e9EWLS1cjWM1jmeuo+5WtaKToJn5itgQOkvSlPz+HSLgM3VfXMvOFALNk125g=="], + + "@nx/nx-linux-arm64-musl": ["@nx/nx-linux-arm64-musl@16.10.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uO6Gg+irqpVcCKMcEPIQcTFZ+tDI02AZkqkP7koQAjniLEappd8DnUBSQdcn53T086pHpdc264X/ZEpXFfrKWQ=="], + + "@nx/nx-linux-x64-gnu": ["@nx/nx-linux-x64-gnu@16.10.0", "", { "os": "linux", "cpu": "x64" }, "sha512-134PW/u/arNFAQKpqMJniC7irbChMPz+W+qtyKPAUXE0XFKPa7c1GtlI/wK2dvP9qJDZ6bKf0KtA0U/m2HMUOA=="], + + "@nx/nx-linux-x64-musl": ["@nx/nx-linux-x64-musl@16.10.0", "", { "os": "linux", "cpu": "x64" }, "sha512-q8sINYLdIJxK/iUx9vRk5jWAWb/2O0PAbOJFwv4qkxBv4rLoN7y+otgCZ5v0xfx/zztFgk/oNY4lg5xYjIso2Q=="], + + "@nx/nx-win32-arm64-msvc": ["@nx/nx-win32-arm64-msvc@16.10.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-moJkL9kcqxUdJSRpG7dET3UeLIciwrfP08mzBQ12ewo8K8FzxU8ZUsTIVVdNrwt01CXOdXoweGfdQLjJ4qTURA=="], + + "@nx/nx-win32-x64-msvc": ["@nx/nx-win32-x64-msvc@16.10.0", "", { "os": "win32", "cpu": "x64" }, "sha512-5iV2NKZnzxJwZZ4DM5JVbRG/nkhAbzEskKaLBB82PmYGKzaDHuMHP1lcPoD/rtYMlowZgNA/RQndfKvPBPwmXA=="], + + "@octokit/auth-token": ["@octokit/auth-token@3.0.4", "", {}, "sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ=="], + + "@octokit/core": ["@octokit/core@4.2.4", "", { "dependencies": { "@octokit/auth-token": "^3.0.0", "@octokit/graphql": "^5.0.0", "@octokit/request": "^6.0.0", "@octokit/request-error": "^3.0.0", "@octokit/types": "^9.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" } }, "sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ=="], + + "@octokit/endpoint": ["@octokit/endpoint@7.0.6", "", { "dependencies": { "@octokit/types": "^9.0.0", "is-plain-object": "^5.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg=="], + + "@octokit/graphql": ["@octokit/graphql@5.0.6", "", { "dependencies": { "@octokit/request": "^6.0.0", "@octokit/types": "^9.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw=="], + + "@octokit/openapi-types": ["@octokit/openapi-types@18.1.1", "", {}, "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw=="], + + "@octokit/plugin-enterprise-rest": ["@octokit/plugin-enterprise-rest@6.0.1", "", {}, "sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw=="], + + "@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@6.1.2", "", { "dependencies": { "@octokit/tsconfig": "^1.0.2", "@octokit/types": "^9.2.3" }, "peerDependencies": { "@octokit/core": ">=4" } }, "sha512-qhrmtQeHU/IivxucOV1bbI/xZyC/iOBhclokv7Sut5vnejAIAEXVcGQeRpQlU39E0WwK9lNvJHphHri/DB6lbQ=="], + + "@octokit/plugin-request-log": ["@octokit/plugin-request-log@1.0.4", "", { "peerDependencies": { "@octokit/core": ">=3" } }, "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA=="], + + "@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@7.2.3", "", { "dependencies": { "@octokit/types": "^10.0.0" }, "peerDependencies": { "@octokit/core": ">=3" } }, "sha512-I5Gml6kTAkzVlN7KCtjOM+Ruwe/rQppp0QU372K1GP7kNOYEKe8Xn5BW4sE62JAHdwpq95OQK/qGNyKQMUzVgA=="], + + "@octokit/request": ["@octokit/request@6.2.8", "", { "dependencies": { "@octokit/endpoint": "^7.0.0", "@octokit/request-error": "^3.0.0", "@octokit/types": "^9.0.0", "is-plain-object": "^5.0.0", "node-fetch": "^2.6.7", "universal-user-agent": "^6.0.0" } }, "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw=="], + + "@octokit/request-error": ["@octokit/request-error@3.0.3", "", { "dependencies": { "@octokit/types": "^9.0.0", "deprecation": "^2.0.0", "once": "^1.4.0" } }, "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ=="], + + "@octokit/rest": ["@octokit/rest@19.0.11", "", { "dependencies": { "@octokit/core": "^4.2.1", "@octokit/plugin-paginate-rest": "^6.1.2", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-rest-endpoint-methods": "^7.1.2" } }, "sha512-m2a9VhaP5/tUw8FwfnW2ICXlXpLPIqxtg3XcAiGMLj/Xhw3RSBfZ8le/466ktO1Gcjr8oXudGnHhxV1TXJgFxw=="], + + "@octokit/tsconfig": ["@octokit/tsconfig@1.0.2", "", {}, "sha512-I0vDR0rdtP8p2lGMzvsJzbhdOWy405HcGovrspJ8RRibHnyRgggUSNO5AIox5LmqiwmatHKYsvj6VGFHkqS7lA=="], + + "@octokit/types": ["@octokit/types@9.3.2", "", { "dependencies": { "@octokit/openapi-types": "^18.0.0" } }, "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA=="], + + "@ohif/app": ["@ohif/app@workspace:platform/app"], + + "@ohif/cli": ["@ohif/cli@workspace:platform/cli"], + + "@ohif/core": ["@ohif/core@workspace:platform/core"], + + "@ohif/extension-cornerstone": ["@ohif/extension-cornerstone@workspace:extensions/cornerstone"], + + "@ohif/extension-cornerstone-dicom-pmap": ["@ohif/extension-cornerstone-dicom-pmap@workspace:extensions/cornerstone-dicom-pmap"], + + "@ohif/extension-cornerstone-dicom-rt": ["@ohif/extension-cornerstone-dicom-rt@workspace:extensions/cornerstone-dicom-rt"], + + "@ohif/extension-cornerstone-dicom-seg": ["@ohif/extension-cornerstone-dicom-seg@workspace:extensions/cornerstone-dicom-seg"], + + "@ohif/extension-cornerstone-dicom-sr": ["@ohif/extension-cornerstone-dicom-sr@workspace:extensions/cornerstone-dicom-sr"], + + "@ohif/extension-cornerstone-dynamic-volume": ["@ohif/extension-cornerstone-dynamic-volume@workspace:extensions/cornerstone-dynamic-volume"], + + "@ohif/extension-default": ["@ohif/extension-default@workspace:extensions/default"], + + "@ohif/extension-dicom-microscopy": ["@ohif/extension-dicom-microscopy@workspace:extensions/dicom-microscopy"], + + "@ohif/extension-dicom-pdf": ["@ohif/extension-dicom-pdf@workspace:extensions/dicom-pdf"], + + "@ohif/extension-dicom-video": ["@ohif/extension-dicom-video@workspace:extensions/dicom-video"], + + "@ohif/extension-measurement-tracking": ["@ohif/extension-measurement-tracking@workspace:extensions/measurement-tracking"], + + "@ohif/extension-test": ["@ohif/extension-test@workspace:extensions/test-extension"], + + "@ohif/extension-tmtv": ["@ohif/extension-tmtv@workspace:extensions/tmtv"], + + "@ohif/i18n": ["@ohif/i18n@workspace:platform/i18n"], + + "@ohif/mode-basic-dev-mode": ["@ohif/mode-basic-dev-mode@workspace:modes/basic-dev-mode"], + + "@ohif/mode-longitudinal": ["@ohif/mode-longitudinal@workspace:modes/longitudinal"], + + "@ohif/mode-microscopy": ["@ohif/mode-microscopy@workspace:modes/microscopy"], + + "@ohif/mode-preclinical-4d": ["@ohif/mode-preclinical-4d@workspace:modes/preclinical-4d"], + + "@ohif/mode-segmentation": ["@ohif/mode-segmentation@workspace:modes/segmentation"], + + "@ohif/mode-test": ["@ohif/mode-test@workspace:modes/basic-test-mode"], + + "@ohif/mode-tmtv": ["@ohif/mode-tmtv@workspace:modes/tmtv"], + + "@ohif/ui": ["@ohif/ui@workspace:platform/ui"], + + "@ohif/ui-next": ["@ohif/ui-next@workspace:platform/ui-next"], + + "@oozcitak/dom": ["@oozcitak/dom@1.15.10", "", { "dependencies": { "@oozcitak/infra": "1.0.8", "@oozcitak/url": "1.0.4", "@oozcitak/util": "8.3.8" } }, "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ=="], + + "@oozcitak/infra": ["@oozcitak/infra@1.0.8", "", { "dependencies": { "@oozcitak/util": "8.3.8" } }, "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg=="], + + "@oozcitak/url": ["@oozcitak/url@1.0.4", "", { "dependencies": { "@oozcitak/infra": "1.0.8", "@oozcitak/util": "8.3.8" } }, "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw=="], + + "@oozcitak/util": ["@oozcitak/util@8.3.8", "", {}, "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ=="], + + "@parcel/watcher": ["@parcel/watcher@2.0.4", "", { "dependencies": { "node-addon-api": "^3.2.1", "node-gyp-build": "^4.3.0" } }, "sha512-cTDi+FUDBIUOBKEtj+nhiJ71AZVlkAsQFuGQTun5tV9mwQBQgZvhCzG+URPQc8myeN32yRVZEfVAPCs1RW+Jvg=="], + + "@percy/cypress": ["@percy/cypress@3.1.3", "", { "dependencies": { "@percy/sdk-utils": "^1.3.1" }, "peerDependencies": { "cypress": ">=3" } }, "sha512-IboiK03dCvG+7Dy9rmuAuq7J+DAqZ0MaZxGF8u7VhVp9EeiXk3frKfTXH9EGH2vwalC1/r6yV54/y2IfdN1V2w=="], + + "@percy/sdk-utils": ["@percy/sdk-utils@1.30.7", "", {}, "sha512-HVQSg0MgY4Ziv0mtbeelz4aRBKoEQnKaKtWl7Nf6FzSELAdUXNz4BNRBAJWOt8O6M5MRXbk6/7jSFJStGsg5Zw=="], + + "@perma/map": ["@perma/map@1.0.3", "", { "dependencies": { "@multiformats/murmur3": "^2.1.0", "murmurhash3js-revisited": "^3.0.0" } }, "sha512-Bf5njk0fnJGTFE2ETntq0N1oJ6YdCPIpTDn3R3KYZJQdeYSOCNL7mBrFlGnbqav8YQhJA/p81pvHINX9vAtHkQ=="], + + "@petamoriken/float16": ["@petamoriken/float16@3.9.1", "", {}, "sha512-j+ejhYwY6PeB+v1kn7lZFACUIG97u90WxMuGosILFsl9d4Ovi0sjk0GlPfoEcx+FzvXZDAfioD+NGnnPamXgMA=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@pkgr/core": ["@pkgr/core@0.1.1", "", {}, "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA=="], + + "@playwright/test": ["@playwright/test@1.50.0", "", { "dependencies": { "playwright": "1.50.0" }, "bin": { "playwright": "cli.js" } }, "sha512-ZGNXbt+d65EGjBORQHuYKj+XhCewlwpnSd/EDuLPZGSiEWmgOJB5RmMCCYGy5aMfTs9wx61RivfDKi8H/hcMvw=="], + + "@pmmmwh/react-refresh-webpack-plugin": ["@pmmmwh/react-refresh-webpack-plugin@0.5.15", "", { "dependencies": { "ansi-html": "^0.0.9", "core-js-pure": "^3.23.3", "error-stack-parser": "^2.0.6", "html-entities": "^2.1.0", "loader-utils": "^2.0.4", "schema-utils": "^4.2.0", "source-map": "^0.7.3" }, "peerDependencies": { "@types/webpack": "4.x || 5.x", "react-refresh": ">=0.10.0 <1.0.0", "sockjs-client": "^1.4.0", "type-fest": ">=0.17.0 <5.0.0", "webpack": ">=4.43.0 <6.0.0", "webpack-dev-server": "3.x || 4.x || 5.x", "webpack-hot-middleware": "2.x", "webpack-plugin-serve": "0.x || 1.x" }, "optionalPeers": ["@types/webpack", "sockjs-client", "type-fest", "webpack-dev-server", "webpack-hot-middleware", "webpack-plugin-serve"] }, "sha512-LFWllMA55pzB9D34w/wXUCf8+c+IYKuJDgxiZ3qMhl64KRMBHYM1I3VdGaD2BV5FNPV2/S2596bppxHbv2ZydQ=="], + + "@polka/url": ["@polka/url@1.0.0-next.28", "", {}, "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + + "@radix-ui/number": ["@radix-ui/number@1.1.0", "", {}, "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="], + + "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collapsible": "1.1.2", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w=="], + + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw=="], + + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q=="], + + "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-menu": "2.1.5", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-MY5PFCwo/ICaaQtpQBQ0g19AyjzI0mhz+a2GUWA2pJf4XFkvglAdcgDV2Iqm+lLbXn8hb+6rbLgcmRtc6ImPvg=="], + + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-portal": "1.1.3", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-LaO3e5h/NOEL4OfXjxD43k9Dx+vn+8n+PCFt6uhX/BADFflllyv3WJG6rgvvSVBxpTch938Qq/LGc2MMxipXPw=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA=="], + + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-menu": "2.1.5", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-50ZmEFL1kOuLalPKHrLWvPFMons2fGx9TqQCWlPwDVpbAnaUJ1g4XNcKqFNMQymYU0kKWR4MDDi+9vUQBGFgcQ=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA=="], + + "@radix-ui/react-icons": ["@radix-ui/react-icons@1.3.2", "", { "peerDependencies": { "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" } }, "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw=="], + + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.1", "@radix-ui/react-portal": "1.1.3", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-roving-focus": "1.1.1", "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-use-callback-ref": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-uH+3w5heoMJtqVCgYOtYVMECk1TOrkUn0OG0p5MqXC0W2ppcuVeESbou8PTHoqAjbdTEK19AGXBWcEtR5WpEQg=="], + + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.1", "@radix-ui/react-portal": "1.1.3", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YXkTAftOIW2Bt3qKH8vYr6n9gCkVrvyvfiTObVjoHVTHnNj26rmvO87IKa3VgtgCjb8FAQ6qOjNViwl+9iIzlg=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.1", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw=="], + + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.2", "", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g=="], + + "@radix-ui/react-select": ["@radix-ui/react-select@2.1.5", "", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.1", "@radix-ui/react-portal": "1.1.3", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-eVV7N8jBXAXnyrc+PsOF89O9AfVgGnbLxUtBb0clJ8y8ENMWLARGMI/1/SBRLz7u4HqxLgN71BJ17eono3wcjA=="], + + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-RRiNRSrD8iUiXriq/Y5n4/3iE8HzqgLHsusUSg5jVpU2+3tqcUFPJXHDymwEypunc2sWxDUS3UC+rkZRlHedsw=="], + + "@radix-ui/react-slider": ["@radix-ui/react-slider@1.2.2", "", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sNlU06ii1/ZcbHf8I9En54ZPW0Vil/yPVg4vQMcFNjrIx51jsHbFl1HYHQvCIWJSr1q0ZmA+iIs/ZTv8h7HHSA=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g=="], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-roving-focus": "1.1.1", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ=="], + + "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-i77tcgObYr743IonC1hrsnnPmszDRn8p+EGUsUt+5a/JFn28fxaM88Py6V2mc8J5kELMWishI0rLnuGLFD/nnQ=="], + + "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-roving-focus": "1.1.1", "@radix-ui/react-toggle": "1.1.1", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-OgDLZEA30Ylyz8YSXvnGqIHtERqnUt1KUYTKdw/y8u7Ci6zGiJfXc02jahmcSNK3YcErqioj/9flWC9S1ihfwg=="], + + "@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-roving-focus": "1.1.1", "@radix-ui/react-separator": "1.1.1", "@radix-ui/react-toggle-group": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-r7T80WOCHc2n3KRzFCbHWGVzkfVTCzDofGU4gqa5ZuIzgnVaLogGsdyifFJXWQDp0lAr5hrf+X9uqQdE0pa6Ww=="], + + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.1", "@radix-ui/react-portal": "1.1.3", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ss0s80BC0+g0+Zc53MvilcnTYSOi4mSuFWBPYPuTOFGjx+pUU+ZrmamMNwS56t8MTFlniA5ocjd4jYm/CdhbOg=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], + + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], + + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.0", "", { "dependencies": { "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.0", "", {}, "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="], + + "@react-dnd/asap": ["@react-dnd/asap@4.0.1", "", {}, "sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg=="], + + "@react-dnd/invariant": ["@react-dnd/invariant@2.0.0", "", {}, "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw=="], + + "@react-dnd/shallowequal": ["@react-dnd/shallowequal@2.0.0", "", {}, "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg=="], + + "@remix-run/router": ["@remix-run/router@1.22.0", "", {}, "sha512-MBOl8MeOzpK0HQQQshKB7pABXbmyHizdTpqnrIseTbsv0nAepwC2ENZa1aaBExNQcpLoXmWthhak8SABLzvGPw=="], + + "@rollup/plugin-babel": ["@rollup/plugin-babel@5.3.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.10.4", "@rollup/pluginutils": "^3.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0", "@types/babel__core": "^7.1.9", "rollup": "^1.20.0||^2.0.0" }, "optionalPeers": ["@types/babel__core"] }, "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q=="], + + "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@11.2.1", "", { "dependencies": { "@rollup/pluginutils": "^3.1.0", "@types/resolve": "1.17.1", "builtin-modules": "^3.1.0", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.19.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg=="], + + "@rollup/plugin-replace": ["@rollup/plugin-replace@2.4.2", "", { "dependencies": { "@rollup/pluginutils": "^3.1.0", "magic-string": "^0.25.7" }, "peerDependencies": { "rollup": "^1.20.0 || ^2.0.0" } }, "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg=="], + + "@rollup/pluginutils": ["@rollup/pluginutils@3.1.0", "", { "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", "picomatch": "^2.2.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.13.0", "", { "os": "linux", "cpu": "x64" }, "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA=="], + + "@rsbuild/core": ["@rsbuild/core@1.2.3", "", { "dependencies": { "@rspack/core": "1.2.2", "@rspack/lite-tapable": "~1.0.1", "@swc/helpers": "^0.5.15", "core-js": "~3.40.0" }, "bin": { "rsbuild": "bin/rsbuild.js" } }, "sha512-lUCt8gQe9E2PI3srcEJ1Na3GQYmsYuvAqK0f/k00HM0pEjrbOFC9Xq2kR85UoXHFqlTCIw/fLLDe91PKRCbKAw=="], + + "@rsbuild/plugin-node-polyfill": ["@rsbuild/plugin-node-polyfill@1.2.0", "", { "dependencies": { "assert": "^2.1.0", "browserify-zlib": "^0.2.0", "buffer": "^5.7.1", "console-browserify": "^1.2.0", "constants-browserify": "^1.0.0", "crypto-browserify": "^3.12.0", "domain-browser": "^5.7.0", "events": "^3.3.0", "https-browserify": "^1.0.0", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", "process": "^0.11.10", "punycode": "^2.3.1", "querystring-es3": "^0.2.1", "readable-stream": "^4.5.2", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", "string_decoder": "^1.3.0", "timers-browserify": "^2.0.12", "tty-browserify": "^0.0.1", "url": "^0.11.4", "util": "^0.12.5", "vm-browserify": "^1.1.2" }, "peerDependencies": { "@rsbuild/core": "1.x || ^1.0.1-beta.0" }, "optionalPeers": ["@rsbuild/core"] }, "sha512-mYctpK5Jn2yxTOxQ4rOJ0iFBJNW7sADFtKsLp9dL7MjToMhKiyIs4Mc65piI7B+YOBshdyMqCk3LPjJ+CtSRXQ=="], + + "@rsbuild/plugin-react": ["@rsbuild/plugin-react@1.1.0", "", { "dependencies": { "@rspack/plugin-react-refresh": "~1.0.0", "react-refresh": "^0.16.0" }, "peerDependencies": { "@rsbuild/core": "1.x" } }, "sha512-uqdRoV2V91G1XIA14dAmxqYTlTDVf0ktpE7TgwG29oQ2j+DerF1kh29WPHK9HvGE34JTfaBrsme2Zmb6bGD0cw=="], + + "@rspack/binding": ["@rspack/binding@1.2.2", "", { "optionalDependencies": { "@rspack/binding-darwin-arm64": "1.2.2", "@rspack/binding-darwin-x64": "1.2.2", "@rspack/binding-linux-arm64-gnu": "1.2.2", "@rspack/binding-linux-arm64-musl": "1.2.2", "@rspack/binding-linux-x64-gnu": "1.2.2", "@rspack/binding-linux-x64-musl": "1.2.2", "@rspack/binding-win32-arm64-msvc": "1.2.2", "@rspack/binding-win32-ia32-msvc": "1.2.2", "@rspack/binding-win32-x64-msvc": "1.2.2" } }, "sha512-GCZwpGFYlLTdJ2soPLwjw9z4LSZ+GdpbHNfBt3Cm/f/bAF8n6mZc7dHUqN893RFh7MPU17HNEL3fMw7XR+6pHg=="], + + "@rspack/binding-darwin-arm64": ["@rspack/binding-darwin-arm64@1.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-h23F8zEkXWhwMeScm0ZnN78Zh7hCDalxIWsm7bBS0eKadnlegUDwwCF8WE+8NjWr7bRzv0p3QBWlS5ufkcL4eA=="], + + "@rspack/binding-darwin-x64": ["@rspack/binding-darwin-x64@1.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-vG5s7FkEvwrGLfksyDRHwKAHUkhZt1zHZZXJQn4gZKjTBonje8ezdc7IFlDiWpC4S+oBYp73nDWkUzkGRbSdcQ=="], + + "@rspack/binding-linux-arm64-gnu": ["@rspack/binding-linux-arm64-gnu@1.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-VykY/kiYOzO8E1nYzfJ9+gQEHxb5B6lt5wa8M6xFi5B6jEGU+OsaGskmAZB9/GFImeFDHxDPvhUalI4R9p8O2Q=="], + + "@rspack/binding-linux-arm64-musl": ["@rspack/binding-linux-arm64-musl@1.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z5vAC4wGfXi8XXZ6hs8Q06TYjr3zHf819HB4DI5i4C1eQTeKdZSyoFD0NHFG23bP4NWJffp8KhmoObcy9jBT5Q=="], + + "@rspack/binding-linux-x64-gnu": ["@rspack/binding-linux-x64-gnu@1.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-o3pDaL+cH5EeRbDE9gZcdZpBgp5iXvYZBBhe8vZQllYgI4zN5MJEuleV7WplG3UwTXlgZg3Kht4RORSOPn96vg=="], + + "@rspack/binding-linux-x64-musl": ["@rspack/binding-linux-x64-musl@1.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-RE3e0xe4DdchHssttKzryDwjLkbrNk/4H59TkkWeGYJcLw41tmcOZVFQUOwKLUvXWVyif/vjvV/w1SMlqB4wQg=="], + + "@rspack/binding-win32-arm64-msvc": ["@rspack/binding-win32-arm64-msvc@1.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-R+PKBYn6uzTaDdVqTHvjqiJPBr5ZHg1wg5UmFDLNH9OklzVFyQh1JInSdJRb7lzfzTRz6bEkkwUFBPQK/CGScw=="], + + "@rspack/binding-win32-ia32-msvc": ["@rspack/binding-win32-ia32-msvc@1.2.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-dBqz3sRAGZ2f31FgzKLDvIRfq2haRP3X3XVCT0PsiMcvt7QJng+26aYYMy2THatd/nM8IwExYeitHWeiMBoruw=="], + + "@rspack/binding-win32-x64-msvc": ["@rspack/binding-win32-x64-msvc@1.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-eeAvaN831KG553cMSHkVldyk6YQn4ujgRHov6r1wtREq7CD3/ka9LMkJUepCN85K7XtwYT0N4KpFIQyf5GTGoA=="], + + "@rspack/core": ["@rspack/core@1.2.2", "", { "dependencies": { "@module-federation/runtime-tools": "0.8.4", "@rspack/binding": "1.2.2", "@rspack/lite-tapable": "1.0.1", "caniuse-lite": "^1.0.30001616" }, "peerDependencies": { "@rspack/tracing": "^1.x", "@swc/helpers": ">=0.5.1" }, "optionalPeers": ["@rspack/tracing", "@swc/helpers"] }, "sha512-EeHAmY65Uj62hSbUKesbrcWGE7jfUI887RD03G++Gj8jS4WPHEu1TFODXNOXg6pa7zyIvs2BK0Bm16Kwz8AEaQ=="], + + "@rspack/lite-tapable": ["@rspack/lite-tapable@1.0.1", "", {}, "sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w=="], + + "@rspack/plugin-react-refresh": ["@rspack/plugin-react-refresh@1.0.1", "", { "dependencies": { "error-stack-parser": "^2.1.4", "html-entities": "^2.5.2" }, "peerDependencies": { "react-refresh": ">=0.10.0 <1.0.0" }, "optionalPeers": ["react-refresh"] }, "sha512-KSBc3bsr3mrAPViv7w9MpE9KEWm6q87EyRXyHlRfJ9PpQ56NbX9KZ7AXo7jPeECb0q5sfpM2PSEf+syBiMgLSw=="], + + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + + "@samverschueren/stream-to-observable": ["@samverschueren/stream-to-observable@0.3.1", "", { "dependencies": { "any-observable": "^0.3.0" } }, "sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ=="], + + "@scarf/scarf": ["@scarf/scarf@1.4.0", "", {}, "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="], + + "@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="], + + "@sideway/formula": ["@sideway/formula@3.0.1", "", {}, "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="], + + "@sideway/pinpoint": ["@sideway/pinpoint@2.0.0", "", {}, "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="], + + "@sigstore/bundle": ["@sigstore/bundle@1.1.0", "", { "dependencies": { "@sigstore/protobuf-specs": "^0.2.0" } }, "sha512-PFutXEy0SmQxYI4texPw3dd2KewuNqv7OuK1ZFtY2fM754yhvG2KdgwIhRnoEE2uHdtdGNQ8s0lb94dW9sELog=="], + + "@sigstore/protobuf-specs": ["@sigstore/protobuf-specs@0.2.1", "", {}, "sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A=="], + + "@sigstore/sign": ["@sigstore/sign@1.0.0", "", { "dependencies": { "@sigstore/bundle": "^1.1.0", "@sigstore/protobuf-specs": "^0.2.0", "make-fetch-happen": "^11.0.1" } }, "sha512-INxFVNQteLtcfGmcoldzV6Je0sbbfh9I16DM4yJPw3j5+TFP8X6uIiA18mvpEa9yyeycAKgPmOA3X9hVdVTPUA=="], + + "@sigstore/tuf": ["@sigstore/tuf@1.0.3", "", { "dependencies": { "@sigstore/protobuf-specs": "^0.2.0", "tuf-js": "^1.1.7" } }, "sha512-2bRovzs0nJZFlCN3rXirE4gwxCn97JNjMmwpecqlbgV9WcxX7WRuIrgzx/X7Ib7MYRbyUTpBYE0s2x6AmZXnlg=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + + "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], + + "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], + + "@storybook/addon-actions": ["@storybook/addon-actions@7.6.20", "", { "dependencies": { "@storybook/core-events": "7.6.20", "@storybook/global": "^5.0.0", "@types/uuid": "^9.0.1", "dequal": "^2.0.2", "polished": "^4.2.2", "uuid": "^9.0.0" } }, "sha512-c/GkEQ2U9BC/Ew/IMdh+zvsh4N6y6n7Zsn2GIhJgcu9YEAa5aF2a9/pNgEGBMOABH959XE8DAOMERw/5qiLR8g=="], + + "@storybook/addon-backgrounds": ["@storybook/addon-backgrounds@7.6.20", "", { "dependencies": { "@storybook/global": "^5.0.0", "memoizerific": "^1.11.3", "ts-dedent": "^2.0.0" } }, "sha512-a7ukoaXT42vpKsMxkseIeO3GqL0Zst2IxpCTq5dSlXiADrcemSF/8/oNpNW9C4L6F1Zdt+WDtECXslEm017FvQ=="], + + "@storybook/addon-controls": ["@storybook/addon-controls@7.6.20", "", { "dependencies": { "@storybook/blocks": "7.6.20", "lodash": "^4.17.21", "ts-dedent": "^2.0.0" } }, "sha512-06ZT5Ce1sZW52B0s6XuokwjkKO9GqHlTUHvuflvd8wifxKlCmRvNUxjBvwh+ccGJ49ZS73LbMSLFgtmBEkCxbg=="], + + "@storybook/addon-docs": ["@storybook/addon-docs@7.6.20", "", { "dependencies": { "@jest/transform": "^29.3.1", "@mdx-js/react": "^2.1.5", "@storybook/blocks": "7.6.20", "@storybook/client-logger": "7.6.20", "@storybook/components": "7.6.20", "@storybook/csf-plugin": "7.6.20", "@storybook/csf-tools": "7.6.20", "@storybook/global": "^5.0.0", "@storybook/mdx2-csf": "^1.0.0", "@storybook/node-logger": "7.6.20", "@storybook/postinstall": "7.6.20", "@storybook/preview-api": "7.6.20", "@storybook/react-dom-shim": "7.6.20", "@storybook/theming": "7.6.20", "@storybook/types": "7.6.20", "fs-extra": "^11.1.0", "remark-external-links": "^8.0.0", "remark-slug": "^6.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-XNfYRhbxH5JP7B9Lh4W06PtMefNXkfpV39Gaoih5HuqngV3eoSL4RikZYOMkvxRGQ738xc6axySU3+JKcP1OZg=="], + + "@storybook/addon-essentials": ["@storybook/addon-essentials@7.6.20", "", { "dependencies": { "@storybook/addon-actions": "7.6.20", "@storybook/addon-backgrounds": "7.6.20", "@storybook/addon-controls": "7.6.20", "@storybook/addon-docs": "7.6.20", "@storybook/addon-highlight": "7.6.20", "@storybook/addon-measure": "7.6.20", "@storybook/addon-outline": "7.6.20", "@storybook/addon-toolbars": "7.6.20", "@storybook/addon-viewport": "7.6.20", "@storybook/core-common": "7.6.20", "@storybook/manager-api": "7.6.20", "@storybook/node-logger": "7.6.20", "@storybook/preview-api": "7.6.20", "ts-dedent": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-hCupSOiJDeOxJKZSgH0x5Mb2Xqii6mps21g5hpxac1XjhQtmGflShxi/xOHhK3sNqrbgTSbScfpUP3hUlZO/2Q=="], + + "@storybook/addon-highlight": ["@storybook/addon-highlight@7.6.20", "", { "dependencies": { "@storybook/global": "^5.0.0" } }, "sha512-7/x7xFdFyqCki5Dm3uBePldUs9l98/WxJ7rTHQuYqlX7kASwyN5iXPzuhmMRUhlMm/6G6xXtLabIpzwf1sFurA=="], + + "@storybook/addon-links": ["@storybook/addon-links@7.6.20", "", { "dependencies": { "@storybook/csf": "^0.1.2", "@storybook/global": "^5.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" }, "optionalPeers": ["react"] }, "sha512-iomSnBD90CA4MinesYiJkFX2kb3P1Psd/a1Y0ghlFEsHD4uMId9iT6sx2s16DYMja0SlPkrbWYnGukqaCjZpRw=="], + + "@storybook/addon-measure": ["@storybook/addon-measure@7.6.20", "", { "dependencies": { "@storybook/global": "^5.0.0", "tiny-invariant": "^1.3.1" } }, "sha512-i2Iq08bGfI7gZbG6Lb8uF/L287tnaGUR+2KFEmdBjH6+kgjWLiwfpanoPQpy4drm23ar0gUjX+L3Ri03VI5/Xg=="], + + "@storybook/addon-outline": ["@storybook/addon-outline@7.6.20", "", { "dependencies": { "@storybook/global": "^5.0.0", "ts-dedent": "^2.0.0" } }, "sha512-TdsIQZf/TcDsGoZ1XpO+9nBc4OKqcMIzY4SrI8Wj9dzyFLQ37s08gnZr9POci8AEv62NTUOVavsxcafllkzqDQ=="], + + "@storybook/addon-toolbars": ["@storybook/addon-toolbars@7.6.20", "", {}, "sha512-5Btg4i8ffWTDHsU72cqxC8nIv9N3E3ObJAc6k0llrmPBG/ybh3jxmRfs8fNm44LlEXaZ5qrK/petsXX3UbpIFg=="], + + "@storybook/addon-viewport": ["@storybook/addon-viewport@7.6.20", "", { "dependencies": { "memoizerific": "^1.11.3" } }, "sha512-i8mIw8BjLWAVHEQsOTE6UPuEGQvJDpsu1XZnOCkpfTfPMz73m+3td/PmLG7mMT2wPnLu9IZncKLCKTAZRbt/YQ=="], + + "@storybook/blocks": ["@storybook/blocks@7.6.20", "", { "dependencies": { "@storybook/channels": "7.6.20", "@storybook/client-logger": "7.6.20", "@storybook/components": "7.6.20", "@storybook/core-events": "7.6.20", "@storybook/csf": "^0.1.2", "@storybook/docs-tools": "7.6.20", "@storybook/global": "^5.0.0", "@storybook/manager-api": "7.6.20", "@storybook/preview-api": "7.6.20", "@storybook/theming": "7.6.20", "@storybook/types": "7.6.20", "@types/lodash": "^4.14.167", "color-convert": "^2.0.1", "dequal": "^2.0.2", "lodash": "^4.17.21", "markdown-to-jsx": "^7.1.8", "memoizerific": "^1.11.3", "polished": "^4.2.2", "react-colorful": "^5.1.2", "telejson": "^7.2.0", "tocbot": "^4.20.1", "ts-dedent": "^2.0.0", "util-deprecate": "^1.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-xADKGEOJWkG0UD5jbY4mBXRlmj2C+CIupDL0/hpzvLvwobxBMFPKZIkcZIMvGvVnI/Ui+tJxQxLSuJ5QsPthUw=="], + + "@storybook/builder-manager": ["@storybook/builder-manager@7.6.20", "", { "dependencies": { "@fal-works/esbuild-plugin-global-externals": "^2.1.2", "@storybook/core-common": "7.6.20", "@storybook/manager": "7.6.20", "@storybook/node-logger": "7.6.20", "@types/ejs": "^3.1.1", "@types/find-cache-dir": "^3.2.1", "@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.10", "browser-assert": "^1.2.1", "ejs": "^3.1.8", "esbuild": "^0.18.0", "esbuild-plugin-alias": "^0.2.1", "express": "^4.17.3", "find-cache-dir": "^3.0.0", "fs-extra": "^11.1.0", "process": "^0.11.10", "util": "^0.12.4" } }, "sha512-e2GzpjLaw6CM/XSmc4qJRzBF8GOoOyotyu3JrSPTYOt4RD8kjUsK4QlismQM1DQRu8i39aIexxmRbiJyD74xzQ=="], + + "@storybook/builder-webpack5": ["@storybook/builder-webpack5@7.6.20", "", { "dependencies": { "@babel/core": "^7.23.2", "@storybook/channels": "7.6.20", "@storybook/client-logger": "7.6.20", "@storybook/core-common": "7.6.20", "@storybook/core-events": "7.6.20", "@storybook/core-webpack": "7.6.20", "@storybook/node-logger": "7.6.20", "@storybook/preview": "7.6.20", "@storybook/preview-api": "7.6.20", "@swc/core": "^1.3.82", "@types/node": "^18.0.0", "@types/semver": "^7.3.4", "babel-loader": "^9.0.0", "browser-assert": "^1.2.1", "case-sensitive-paths-webpack-plugin": "^2.4.0", "cjs-module-lexer": "^1.2.3", "constants-browserify": "^1.0.0", "css-loader": "^6.7.1", "es-module-lexer": "^1.4.1", "express": "^4.17.3", "fork-ts-checker-webpack-plugin": "^8.0.0", "fs-extra": "^11.1.0", "html-webpack-plugin": "^5.5.0", "magic-string": "^0.30.5", "path-browserify": "^1.0.1", "process": "^0.11.10", "semver": "^7.3.7", "style-loader": "^3.3.1", "swc-loader": "^0.2.3", "terser-webpack-plugin": "^5.3.1", "ts-dedent": "^2.0.0", "url": "^0.11.0", "util": "^0.12.4", "util-deprecate": "^1.0.2", "webpack": "5", "webpack-dev-middleware": "^6.1.1", "webpack-hot-middleware": "^2.25.1", "webpack-virtual-modules": "^0.5.0" } }, "sha512-kUcMZHVo/jybwsje03MFN1ZucdjyH6QB+jlw9dzHrAhM6N1IItwHzhlixvxmseA5OB7jk1b0WcCN8tfD2qByFA=="], + + "@storybook/channels": ["@storybook/channels@7.6.20", "", { "dependencies": { "@storybook/client-logger": "7.6.20", "@storybook/core-events": "7.6.20", "@storybook/global": "^5.0.0", "qs": "^6.10.0", "telejson": "^7.2.0", "tiny-invariant": "^1.3.1" } }, "sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A=="], + + "@storybook/cli": ["@storybook/cli@7.6.20", "", { "dependencies": { "@babel/core": "^7.23.2", "@babel/preset-env": "^7.23.2", "@babel/types": "^7.23.0", "@ndelangen/get-tarball": "^3.0.7", "@storybook/codemod": "7.6.20", "@storybook/core-common": "7.6.20", "@storybook/core-events": "7.6.20", "@storybook/core-server": "7.6.20", "@storybook/csf-tools": "7.6.20", "@storybook/node-logger": "7.6.20", "@storybook/telemetry": "7.6.20", "@storybook/types": "7.6.20", "@types/semver": "^7.3.4", "@yarnpkg/fslib": "2.10.3", "@yarnpkg/libzip": "2.3.0", "chalk": "^4.1.0", "commander": "^6.2.1", "cross-spawn": "^7.0.3", "detect-indent": "^6.1.0", "envinfo": "^7.7.3", "execa": "^5.0.0", "express": "^4.17.3", "find-up": "^5.0.0", "fs-extra": "^11.1.0", "get-npm-tarball-url": "^2.0.3", "get-port": "^5.1.1", "giget": "^1.0.0", "globby": "^11.0.2", "jscodeshift": "^0.15.1", "leven": "^3.1.0", "ora": "^5.4.1", "prettier": "^2.8.0", "prompts": "^2.4.0", "puppeteer-core": "^2.1.1", "read-pkg-up": "^7.0.1", "semver": "^7.3.7", "strip-json-comments": "^3.0.1", "tempy": "^1.0.1", "ts-dedent": "^2.0.0", "util-deprecate": "^1.0.2" }, "bin": { "sb": "./bin/index.js", "getstorybook": "./bin/index.js" } }, "sha512-ZlP+BJyqg7HlnXf7ypjG2CKMI/KVOn03jFIiClItE/jQfgR6kRFgtjRU7uajh427HHfjv9DRiur8nBzuO7vapA=="], + + "@storybook/client-logger": ["@storybook/client-logger@7.6.20", "", { "dependencies": { "@storybook/global": "^5.0.0" } }, "sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ=="], + + "@storybook/codemod": ["@storybook/codemod@7.6.20", "", { "dependencies": { "@babel/core": "^7.23.2", "@babel/preset-env": "^7.23.2", "@babel/types": "^7.23.0", "@storybook/csf": "^0.1.2", "@storybook/csf-tools": "7.6.20", "@storybook/node-logger": "7.6.20", "@storybook/types": "7.6.20", "@types/cross-spawn": "^6.0.2", "cross-spawn": "^7.0.3", "globby": "^11.0.2", "jscodeshift": "^0.15.1", "lodash": "^4.17.21", "prettier": "^2.8.0", "recast": "^0.23.1" } }, "sha512-8vmSsksO4XukNw0TmqylPmk7PxnfNfE21YsxFa7mnEBmEKQcZCQsNil4ZgWfG0IzdhTfhglAN4r++Ew0WE+PYA=="], + + "@storybook/components": ["@storybook/components@7.6.20", "", { "dependencies": { "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-toolbar": "^1.0.4", "@storybook/client-logger": "7.6.20", "@storybook/csf": "^0.1.2", "@storybook/global": "^5.0.0", "@storybook/theming": "7.6.20", "@storybook/types": "7.6.20", "memoizerific": "^1.11.3", "use-resize-observer": "^9.1.0", "util-deprecate": "^1.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-0d8u4m558R+W5V+rseF/+e9JnMciADLXTpsILrG+TBhwECk0MctIWW18bkqkujdCm8kDZr5U2iM/5kS1Noy7Ug=="], + + "@storybook/core-client": ["@storybook/core-client@7.6.20", "", { "dependencies": { "@storybook/client-logger": "7.6.20", "@storybook/preview-api": "7.6.20" } }, "sha512-upQuQQinLmlOPKcT8yqXNtwIucZ4E4qegYZXH5HXRWoLAL6GQtW7sUVSIuFogdki8OXRncr/dz8OA+5yQyYS4w=="], + + "@storybook/core-common": ["@storybook/core-common@7.6.20", "", { "dependencies": { "@storybook/core-events": "7.6.20", "@storybook/node-logger": "7.6.20", "@storybook/types": "7.6.20", "@types/find-cache-dir": "^3.2.1", "@types/node": "^18.0.0", "@types/node-fetch": "^2.6.4", "@types/pretty-hrtime": "^1.0.0", "chalk": "^4.1.0", "esbuild": "^0.18.0", "esbuild-register": "^3.5.0", "file-system-cache": "2.3.0", "find-cache-dir": "^3.0.0", "find-up": "^5.0.0", "fs-extra": "^11.1.0", "glob": "^10.0.0", "handlebars": "^4.7.7", "lazy-universal-dotenv": "^4.0.0", "node-fetch": "^2.0.0", "picomatch": "^2.3.0", "pkg-dir": "^5.0.0", "pretty-hrtime": "^1.0.3", "resolve-from": "^5.0.0", "ts-dedent": "^2.0.0" } }, "sha512-8H1zPWPjcmeD4HbDm4FDD0WLsfAKGVr566IZ4hG+h3iWVW57II9JW9MLBtiR2LPSd8u7o0kw64lwRGmtCO1qAw=="], + + "@storybook/core-events": ["@storybook/core-events@7.6.20", "", { "dependencies": { "ts-dedent": "^2.0.0" } }, "sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ=="], + + "@storybook/core-server": ["@storybook/core-server@7.6.20", "", { "dependencies": { "@aw-web-design/x-default-browser": "1.4.126", "@discoveryjs/json-ext": "^0.5.3", "@storybook/builder-manager": "7.6.20", "@storybook/channels": "7.6.20", "@storybook/core-common": "7.6.20", "@storybook/core-events": "7.6.20", "@storybook/csf": "^0.1.2", "@storybook/csf-tools": "7.6.20", "@storybook/docs-mdx": "^0.1.0", "@storybook/global": "^5.0.0", "@storybook/manager": "7.6.20", "@storybook/node-logger": "7.6.20", "@storybook/preview-api": "7.6.20", "@storybook/telemetry": "7.6.20", "@storybook/types": "7.6.20", "@types/detect-port": "^1.3.0", "@types/node": "^18.0.0", "@types/pretty-hrtime": "^1.0.0", "@types/semver": "^7.3.4", "better-opn": "^3.0.2", "chalk": "^4.1.0", "cli-table3": "^0.6.1", "compression": "^1.7.4", "detect-port": "^1.3.0", "express": "^4.17.3", "fs-extra": "^11.1.0", "globby": "^11.0.2", "lodash": "^4.17.21", "open": "^8.4.0", "pretty-hrtime": "^1.0.3", "prompts": "^2.4.0", "read-pkg-up": "^7.0.1", "semver": "^7.3.7", "telejson": "^7.2.0", "tiny-invariant": "^1.3.1", "ts-dedent": "^2.0.0", "util": "^0.12.4", "util-deprecate": "^1.0.2", "watchpack": "^2.2.0", "ws": "^8.2.3" } }, "sha512-qC5BdbqqwMLTdCwMKZ1Hbc3+3AaxHYWLiJaXL9e8s8nJw89xV8c8l30QpbJOGvcDmsgY6UTtXYaJ96OsTr7MrA=="], + + "@storybook/core-webpack": ["@storybook/core-webpack@7.6.20", "", { "dependencies": { "@storybook/core-common": "7.6.20", "@storybook/node-logger": "7.6.20", "@storybook/types": "7.6.20", "@types/node": "^18.0.0", "ts-dedent": "^2.0.0" } }, "sha512-pGYhKQhMYQ76HPL336L5n7eiJGk1sjWFkA+xRRRmQ9q6VUlqtEPuRHjKBQwrrTb1nA33BQX58Be06OtlbsFkjg=="], + + "@storybook/csf": ["@storybook/csf@0.1.13", "", { "dependencies": { "type-fest": "^2.19.0" } }, "sha512-7xOOwCLGB3ebM87eemep89MYRFTko+D8qE7EdAAq74lgdqRR5cOUtYWJLjO2dLtP94nqoOdHJo6MdLLKzg412Q=="], + + "@storybook/csf-plugin": ["@storybook/csf-plugin@7.6.20", "", { "dependencies": { "@storybook/csf-tools": "7.6.20", "unplugin": "^1.3.1" } }, "sha512-dzBzq0dN+8WLDp6NxYS4G7BCe8+vDeDRBRjHmM0xb0uJ6xgQViL8SDplYVSGnk3bXE/1WmtvyRzQyTffBnaj9Q=="], + + "@storybook/csf-tools": ["@storybook/csf-tools@7.6.20", "", { "dependencies": { "@babel/generator": "^7.23.0", "@babel/parser": "^7.23.0", "@babel/traverse": "^7.23.2", "@babel/types": "^7.23.0", "@storybook/csf": "^0.1.2", "@storybook/types": "7.6.20", "fs-extra": "^11.1.0", "recast": "^0.23.1", "ts-dedent": "^2.0.0" } }, "sha512-rwcwzCsAYh/m/WYcxBiEtLpIW5OH1ingxNdF/rK9mtGWhJxXRDV8acPkFrF8rtFWIVKoOCXu5USJYmc3f2gdYQ=="], + + "@storybook/docs-mdx": ["@storybook/docs-mdx@0.1.0", "", {}, "sha512-JDaBR9lwVY4eSH5W8EGHrhODjygPd6QImRbwjAuJNEnY0Vw4ie3bPkeGfnacB3OBW6u/agqPv2aRlR46JcAQLg=="], + + "@storybook/docs-tools": ["@storybook/docs-tools@7.6.20", "", { "dependencies": { "@storybook/core-common": "7.6.20", "@storybook/preview-api": "7.6.20", "@storybook/types": "7.6.20", "@types/doctrine": "^0.0.3", "assert": "^2.1.0", "doctrine": "^3.0.0", "lodash": "^4.17.21" } }, "sha512-Bw2CcCKQ5xGLQgtexQsI1EGT6y5epoFzOINi0FSTGJ9Wm738nRp5LH3dLk1GZLlywIXcYwOEThb2pM+pZeRQxQ=="], + + "@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="], + + "@storybook/manager": ["@storybook/manager@7.6.20", "", {}, "sha512-0Cf6WN0t7yEG2DR29tN5j+i7H/TH5EfPppg9h9/KiQSoFHk+6KLoy2p5do94acFU+Ro4+zzxvdCGbcYGKuArpg=="], + + "@storybook/manager-api": ["@storybook/manager-api@7.6.20", "", { "dependencies": { "@storybook/channels": "7.6.20", "@storybook/client-logger": "7.6.20", "@storybook/core-events": "7.6.20", "@storybook/csf": "^0.1.2", "@storybook/global": "^5.0.0", "@storybook/router": "7.6.20", "@storybook/theming": "7.6.20", "@storybook/types": "7.6.20", "dequal": "^2.0.2", "lodash": "^4.17.21", "memoizerific": "^1.11.3", "store2": "^2.14.2", "telejson": "^7.2.0", "ts-dedent": "^2.0.0" } }, "sha512-gOB3m8hO3gBs9cBoN57T7jU0wNKDh+hi06gLcyd2awARQlAlywnLnr3s1WH5knih6Aq+OpvGBRVKkGLOkaouCQ=="], + + "@storybook/mdx2-csf": ["@storybook/mdx2-csf@1.1.0", "", {}, "sha512-TXJJd5RAKakWx4BtpwvSNdgTDkKM6RkXU8GK34S/LhidQ5Pjz3wcnqb0TxEkfhK/ztbP8nKHqXFwLfa2CYkvQw=="], + + "@storybook/node-logger": ["@storybook/node-logger@7.6.20", "", {}, "sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw=="], + + "@storybook/postinstall": ["@storybook/postinstall@7.6.20", "", {}, "sha512-AN4WPeNma2xC2/K/wP3I/GMbBUyeSGD3+86ZFFJFO1QmE/Zea6E+1aVlTd1iKHQUcNkZ9bZTrqkhPGVYx10pIw=="], + + "@storybook/preset-react-webpack": ["@storybook/preset-react-webpack@7.6.20", "", { "dependencies": { "@babel/preset-flow": "^7.22.15", "@babel/preset-react": "^7.22.15", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", "@storybook/core-webpack": "7.6.20", "@storybook/docs-tools": "7.6.20", "@storybook/node-logger": "7.6.20", "@storybook/react": "7.6.20", "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0", "@types/node": "^18.0.0", "@types/semver": "^7.3.4", "babel-plugin-add-react-displayname": "^0.0.5", "fs-extra": "^11.1.0", "magic-string": "^0.30.5", "react-docgen": "^7.0.0", "react-refresh": "^0.14.0", "semver": "^7.3.7", "webpack": "5" }, "peerDependencies": { "@babel/core": "^7.22.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" }, "optionalPeers": ["@babel/core"] }, "sha512-z5/NF+HI9zN/ONocNyxQwewaG5G/1ChCeWfi5m5E1mwKQxxJbFUgE8oiAFhe90A1R7lAEsGFKd8WxdefY2JvEg=="], + + "@storybook/preview": ["@storybook/preview@7.6.20", "", {}, "sha512-cxYlZ5uKbCYMHoFpgleZqqGWEnqHrk5m5fT8bYSsDsdQ+X5wPcwI/V+v8dxYAdQcMphZVIlTjo6Dno9WG8qmVA=="], + + "@storybook/preview-api": ["@storybook/preview-api@7.6.20", "", { "dependencies": { "@storybook/channels": "7.6.20", "@storybook/client-logger": "7.6.20", "@storybook/core-events": "7.6.20", "@storybook/csf": "^0.1.2", "@storybook/global": "^5.0.0", "@storybook/types": "7.6.20", "@types/qs": "^6.9.5", "dequal": "^2.0.2", "lodash": "^4.17.21", "memoizerific": "^1.11.3", "qs": "^6.10.0", "synchronous-promise": "^2.0.15", "ts-dedent": "^2.0.0", "util-deprecate": "^1.0.2" } }, "sha512-3ic2m9LDZEPwZk02wIhNc3n3rNvbi7VDKn52hDXfAxnL5EYm7yDICAkaWcVaTfblru2zn0EDJt7ROpthscTW5w=="], + + "@storybook/react": ["@storybook/react@7.6.20", "", { "dependencies": { "@storybook/client-logger": "7.6.20", "@storybook/core-client": "7.6.20", "@storybook/docs-tools": "7.6.20", "@storybook/global": "^5.0.0", "@storybook/preview-api": "7.6.20", "@storybook/react-dom-shim": "7.6.20", "@storybook/types": "7.6.20", "@types/escodegen": "^0.0.6", "@types/estree": "^0.0.51", "@types/node": "^18.0.0", "acorn": "^7.4.1", "acorn-jsx": "^5.3.1", "acorn-walk": "^7.2.0", "escodegen": "^2.1.0", "html-tags": "^3.1.0", "lodash": "^4.17.21", "prop-types": "^15.7.2", "react-element-to-jsx-string": "^15.0.0", "ts-dedent": "^2.0.0", "type-fest": "~2.19", "util-deprecate": "^1.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-i5tKNgUbTNwlqBWGwPveDhh9ktlS0wGtd97A1ZgKZc3vckLizunlAFc7PRC1O/CMq5PTyxbuUb4RvRD2jWKwDA=="], + + "@storybook/react-docgen-typescript-plugin": ["@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0", "", { "dependencies": { "debug": "^4.1.1", "endent": "^2.0.1", "find-cache-dir": "^3.3.1", "flat-cache": "^3.0.4", "micromatch": "^4.0.2", "react-docgen-typescript": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "typescript": ">= 4.x", "webpack": ">= 4" } }, "sha512-KUqXC3oa9JuQ0kZJLBhVdS4lOneKTOopnNBK4tUAgoxWQ3u/IjzdueZjFr7gyBrXMoU6duutk3RQR9u8ZpYJ4Q=="], + + "@storybook/react-dom-shim": ["@storybook/react-dom-shim@7.6.20", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-SRvPDr9VWcS24ByQOVmbfZ655y5LvjXRlsF1I6Pr9YZybLfYbu3L5IicfEHT4A8lMdghzgbPFVQaJez46DTrkg=="], + + "@storybook/react-webpack5": ["@storybook/react-webpack5@7.6.20", "", { "dependencies": { "@storybook/builder-webpack5": "7.6.20", "@storybook/preset-react-webpack": "7.6.20", "@storybook/react": "7.6.20", "@types/node": "^18.0.0" }, "peerDependencies": { "@babel/core": "^7.22.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", "typescript": "*" }, "optionalPeers": ["@babel/core", "typescript"] }, "sha512-xaLtadKczfUdpyPMk/e49qGnRpjMDtTwFq4RqkS7q+Z+EO72kTCUPGtK3jJXyv70pp/qbzM5OfjFLjXjMezvYw=="], + + "@storybook/router": ["@storybook/router@7.6.20", "", { "dependencies": { "@storybook/client-logger": "7.6.20", "memoizerific": "^1.11.3", "qs": "^6.10.0" } }, "sha512-mCzsWe6GrH47Xb1++foL98Zdek7uM5GhaSlrI7blWVohGa0qIUYbfJngqR4ZsrXmJeeEvqowobh+jlxg3IJh+w=="], + + "@storybook/source-loader": ["@storybook/source-loader@7.6.20", "", { "dependencies": { "@storybook/csf": "^0.1.2", "@storybook/types": "7.6.20", "estraverse": "^5.2.0", "lodash": "^4.17.21", "prettier": "^2.8.0" } }, "sha512-AlOX5w95tajmZEsEcbYCtwtpKYW0xLV2hRzk2MZfc6XUXC9/n2jwhz1X4EKilwKGCWFUj/HAYbNc2XChS9juvg=="], + + "@storybook/telemetry": ["@storybook/telemetry@7.6.20", "", { "dependencies": { "@storybook/client-logger": "7.6.20", "@storybook/core-common": "7.6.20", "@storybook/csf-tools": "7.6.20", "chalk": "^4.1.0", "detect-package-manager": "^2.0.1", "fetch-retry": "^5.0.2", "fs-extra": "^11.1.0", "read-pkg-up": "^7.0.1" } }, "sha512-dmAOCWmOscYN6aMbhCMmszQjoycg7tUPRVy2kTaWg6qX10wtMrvEtBV29W4eMvqdsoRj5kcvoNbzRdYcWBUOHQ=="], + + "@storybook/theming": ["@storybook/theming@7.6.20", "", { "dependencies": { "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", "@storybook/client-logger": "7.6.20", "@storybook/global": "^5.0.0", "memoizerific": "^1.11.3" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-iT1pXHkSkd35JsCte6Qbanmprx5flkqtSHC6Gi6Umqoxlg9IjiLPmpHbaIXzoC06DSW93hPj5Zbi1lPlTvRC7Q=="], + + "@storybook/types": ["@storybook/types@7.6.20", "", { "dependencies": { "@storybook/channels": "7.6.20", "@types/babel__core": "^7.0.0", "@types/express": "^4.7.0", "file-system-cache": "2.3.0" } }, "sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q=="], + + "@surma/rollup-plugin-off-main-thread": ["@surma/rollup-plugin-off-main-thread@2.2.3", "", { "dependencies": { "ejs": "^3.1.6", "json5": "^2.2.0", "magic-string": "^0.25.0", "string.prototype.matchall": "^4.0.6" } }, "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ=="], + + "@svgr/babel-plugin-add-jsx-attribute": ["@svgr/babel-plugin-add-jsx-attribute@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g=="], + + "@svgr/babel-plugin-remove-jsx-attribute": ["@svgr/babel-plugin-remove-jsx-attribute@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA=="], + + "@svgr/babel-plugin-remove-jsx-empty-expression": ["@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA=="], + + "@svgr/babel-plugin-replace-jsx-attribute-value": ["@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ=="], + + "@svgr/babel-plugin-svg-dynamic-title": ["@svgr/babel-plugin-svg-dynamic-title@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og=="], + + "@svgr/babel-plugin-svg-em-dimensions": ["@svgr/babel-plugin-svg-em-dimensions@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g=="], + + "@svgr/babel-plugin-transform-react-native-svg": ["@svgr/babel-plugin-transform-react-native-svg@8.1.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q=="], + + "@svgr/babel-plugin-transform-svg-component": ["@svgr/babel-plugin-transform-svg-component@8.0.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw=="], + + "@svgr/babel-preset": ["@svgr/babel-preset@8.1.0", "", { "dependencies": { "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", "@svgr/babel-plugin-transform-svg-component": "8.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug=="], + + "@svgr/core": ["@svgr/core@8.1.0", "", { "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", "camelcase": "^6.2.0", "cosmiconfig": "^8.1.3", "snake-case": "^3.0.4" } }, "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA=="], + + "@svgr/hast-util-to-babel-ast": ["@svgr/hast-util-to-babel-ast@8.0.0", "", { "dependencies": { "@babel/types": "^7.21.3", "entities": "^4.4.0" } }, "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q=="], + + "@svgr/plugin-jsx": ["@svgr/plugin-jsx@8.1.0", "", { "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", "@svgr/hast-util-to-babel-ast": "8.0.0", "svg-parser": "^2.0.4" }, "peerDependencies": { "@svgr/core": "*" } }, "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA=="], + + "@svgr/plugin-svgo": ["@svgr/plugin-svgo@8.1.0", "", { "dependencies": { "cosmiconfig": "^8.1.3", "deepmerge": "^4.3.1", "svgo": "^3.0.2" }, "peerDependencies": { "@svgr/core": "*" } }, "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA=="], + + "@svgr/webpack": ["@svgr/webpack@8.1.0", "", { "dependencies": { "@babel/core": "^7.21.3", "@babel/plugin-transform-react-constant-elements": "^7.21.3", "@babel/preset-env": "^7.20.2", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.21.0", "@svgr/core": "8.1.0", "@svgr/plugin-jsx": "8.1.0", "@svgr/plugin-svgo": "8.1.0" } }, "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA=="], + + "@swc/core": ["@swc/core@1.10.12", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.17" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.10.12", "@swc/core-darwin-x64": "1.10.12", "@swc/core-linux-arm-gnueabihf": "1.10.12", "@swc/core-linux-arm64-gnu": "1.10.12", "@swc/core-linux-arm64-musl": "1.10.12", "@swc/core-linux-x64-gnu": "1.10.12", "@swc/core-linux-x64-musl": "1.10.12", "@swc/core-win32-arm64-msvc": "1.10.12", "@swc/core-win32-ia32-msvc": "1.10.12", "@swc/core-win32-x64-msvc": "1.10.12" }, "peerDependencies": { "@swc/helpers": "*" }, "optionalPeers": ["@swc/helpers"] }, "sha512-+iUL0PYpPm6N9AdV1wvafakvCqFegQus1aoEDxgFsv3/uNVNIyRaupf/v/Zkp5hbep2EzhtoJR0aiJIzDbXWHg=="], + + "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.10.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pOANQegUTAriW7jq3SSMZGM5l89yLVMs48R0F2UG6UZsH04SiViCnDctOGlA/Sa++25C+rL9MGMYM1jDLylBbg=="], + + "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.10.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-m4kbpIDDsN1FrwfNQMU+FTrss356xsXvatLbearwR+V0lqOkjLBP0VmRvQfHEg+uy13VPyrT9gj4HLoztlci7w=="], + + "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.10.12", "", { "os": "linux", "cpu": "arm" }, "sha512-OY9LcupgqEu8zVK+rJPes6LDJJwPDmwaShU96beTaxX2K6VrXbpwm5WbPS/8FfQTsmpnuA7dCcMPUKhNgmzTrQ=="], + + "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.10.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-nJD587rO0N4y4VZszz3xzVr7JIiCzSMhEMWnPjuh+xmPxDBz0Qccpr8xCr1cSxpl1uY7ERkqAGlKr6CwoV5kVg=="], + + "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.10.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-oqhSmV+XauSf0C//MoQnVErNUB/5OzmSiUzuazyLsD5pwqKNN+leC3JtRQ/QVzaCpr65jv9bKexT9+I2Tt3xDw=="], + + "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.10.12", "", { "os": "linux", "cpu": "x64" }, "sha512-XldSIHyjD7m1Gh+/8rxV3Ok711ENLI420CU2EGEqSe3VSGZ7pHJvJn9ZFbYpWhsLxPqBYMFjp3Qw+J6OXCPXCA=="], + + "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.10.12", "", { "os": "linux", "cpu": "x64" }, "sha512-wvPXzJxzPgTqhyp1UskOx1hRTtdWxlyFD1cGWOxgLsMik0V9xKRgqKnMPv16Nk7L9xl6quQ6DuUHj9ID7L3oVw=="], + + "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.10.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-TUYzWuu1O7uyIcRfxdm6Wh1u+gNnrW5M1DUgDOGZLsyQzgc2Zjwfh2llLhuAIilvCVg5QiGbJlpibRYJ/8QGsg=="], + + "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.10.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-4Qrw+0Xt+Fe2rz4OJ/dEPMeUf/rtuFWWAj/e0vL7J5laUHirzxawLRE5DCJLQTarOiYR6mWnmadt9o3EKzV6Xg=="], + + "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.10.12", "", { "os": "win32", "cpu": "x64" }, "sha512-YiloZXLW7rUxJpALwHXaGjVaAEn+ChoblG7/3esque+Y7QCyheoBUJp2DVM1EeVA43jBfZ8tvYF0liWd9Tpz1A=="], + + "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], + + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + + "@swc/types": ["@swc/types@0.1.17", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ=="], + + "@testing-library/dom": ["@testing-library/dom@8.20.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.1.3", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" } }, "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g=="], + + "@testing-library/react": ["@testing-library/react@13.4.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@testing-library/dom": "^8.5.0", "@types/react-dom": "^18.0.0" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw=="], + + "@thewtex/zstddec": ["@thewtex/zstddec@0.2.1", "", {}, "sha512-1yTu7m/qU1nsJy4mCZAB3GAhczsClhw+WIXK0oe598eHcvefH16WLOYN4Uko7K2/Ttz9KEBvvT7WFrZD41ShgA=="], + + "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], + + "@trysound/sax": ["@trysound/sax@0.2.0", "", {}, "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="], + + "@tufjs/canonical-json": ["@tufjs/canonical-json@1.0.0", "", {}, "sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ=="], + + "@tufjs/models": ["@tufjs/models@1.0.4", "", { "dependencies": { "@tufjs/canonical-json": "1.0.0", "minimatch": "^9.0.0" } }, "sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A=="], + + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.6.8", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="], + + "@types/body-parser": ["@types/body-parser@1.19.5", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg=="], + + "@types/bonjour": ["@types/bonjour@3.5.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ=="], + + "@types/child-process-promise": ["@types/child-process-promise@2.2.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-g0pOHijr6Trug43D2bV0PLSIsSHa/xHEES2HeX5BAlduq1vW0nZcq27Zeud5lgmNB+kPYYVqiMap32EHGTco/w=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/connect-history-api-fallback": ["@types/connect-history-api-fallback@1.5.4", "", { "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" } }, "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw=="], + + "@types/cross-spawn": ["@types/cross-spawn@6.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA=="], + + "@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + + "@types/detect-port": ["@types/detect-port@1.3.5", "", {}, "sha512-Rf3/lB9WkDfIL9eEKaSYKc+1L/rNVYBjThk22JTqQw0YozXarX8YljFAz+HCoC6h4B4KwCMsBPZHaFezwT4BNA=="], + + "@types/doctrine": ["@types/doctrine@0.0.3", "", {}, "sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA=="], + + "@types/ejs": ["@types/ejs@3.1.5", "", {}, "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg=="], + + "@types/emscripten": ["@types/emscripten@1.40.0", "", {}, "sha512-MD2JJ25S4tnjnhjWyalMS6K6p0h+zQV6+Ylm+aGbiS8tSn/aHLSGNzBgduj6FB4zH0ax2GRMGYi/8G1uOxhXWA=="], + + "@types/escodegen": ["@types/escodegen@0.0.6", "", {}, "sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig=="], + + "@types/eslint": ["@types/eslint@7.29.0", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng=="], + + "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], + + "@types/execa": ["@types/execa@0.9.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-mgfd93RhzjYBUHHV532turHC2j4l/qxsF/PbfDmprHDEUHmNZGlDn1CEsulGK3AfsPdhkWzZQT/S/k0UGhLGsA=="], + + "@types/express": ["@types/express@5.0.0", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA=="], + + "@types/find-cache-dir": ["@types/find-cache-dir@3.2.1", "", {}, "sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw=="], + + "@types/glob": ["@types/glob@7.2.0", "", { "dependencies": { "@types/minimatch": "*", "@types/node": "*" } }, "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA=="], + + "@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="], + + "@types/html-minifier-terser": ["@types/html-minifier-terser@6.1.0", "", {}, "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg=="], + + "@types/http-errors": ["@types/http-errors@2.0.4", "", {}, "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="], + + "@types/http-proxy": ["@types/http-proxy@1.17.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ=="], + + "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], + + "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="], + + "@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="], + + "@types/jest": ["@types/jest@27.5.2", "", { "dependencies": { "jest-matcher-utils": "^27.0.0", "pretty-format": "^27.0.0" } }, "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA=="], + + "@types/jsdom": ["@types/jsdom@20.0.1", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0" } }, "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + + "@types/lodash": ["@types/lodash@4.17.15", "", {}, "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw=="], + + "@types/mdast": ["@types/mdast@3.0.15", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ=="], + + "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + + "@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="], + + "@types/minimatch": ["@types/minimatch@3.0.5", "", {}, "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="], + + "@types/minimist": ["@types/minimist@1.2.5", "", {}, "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/node": ["@types/node@20.17.16", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-vOTpLduLkZXePLxHiHsBLp98mHGnl8RptV4YAO3HfKO5UHjDvySGbxKtpYfy8Sx5+WKcgc45qNreJJRVM3L6mw=="], + + "@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="], + + "@types/node-forge": ["@types/node-forge@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ=="], + + "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], + + "@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="], + + "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], + + "@types/pretty-hrtime": ["@types/pretty-hrtime@1.0.3", "", {}, "sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA=="], + + "@types/prop-types": ["@types/prop-types@15.7.14", "", {}, "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="], + + "@types/qs": ["@types/qs@6.9.18", "", {}, "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/react": ["@types/react@18.3.18", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ=="], + + "@types/react-dom": ["@types/react-dom@18.3.5", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q=="], + + "@types/react-transition-group": ["@types/react-transition-group@4.4.12", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="], + + "@types/resolve": ["@types/resolve@1.17.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw=="], + + "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], + + "@types/semver": ["@types/semver@7.5.8", "", {}, "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ=="], + + "@types/send": ["@types/send@0.17.4", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA=="], + + "@types/serve-index": ["@types/serve-index@1.9.4", "", { "dependencies": { "@types/express": "*" } }, "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug=="], + + "@types/serve-static": ["@types/serve-static@1.15.7", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw=="], + + "@types/sinonjs__fake-timers": ["@types/sinonjs__fake-timers@8.1.1", "", {}, "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g=="], + + "@types/sizzle": ["@types/sizzle@2.3.9", "", {}, "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w=="], + + "@types/sockjs": ["@types/sockjs@0.3.36", "", { "dependencies": { "@types/node": "*" } }, "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q=="], + + "@types/source-list-map": ["@types/source-list-map@0.1.6", "", {}, "sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g=="], + + "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], + + "@types/tapable": ["@types/tapable@1.0.12", "", {}, "sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q=="], + + "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "@types/uglify-js": ["@types/uglify-js@3.17.5", "", { "dependencies": { "source-map": "^0.6.1" } }, "sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ=="], + + "@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "@types/uuid": ["@types/uuid@9.0.8", "", {}, "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="], + + "@types/webpack": ["@types/webpack@4.41.40", "", { "dependencies": { "@types/node": "*", "@types/tapable": "^1", "@types/uglify-js": "*", "@types/webpack-sources": "*", "anymatch": "^3.0.0", "source-map": "^0.6.0" } }, "sha512-u6kMFSBM9HcoTpUXnL6mt2HSzftqb3JgYV6oxIgL2dl6sX6aCa5k6SOkzv5DuZjBTPUE/dJltKtwwuqrkZHpfw=="], + + "@types/webpack-sources": ["@types/webpack-sources@3.2.3", "", { "dependencies": { "@types/node": "*", "@types/source-list-map": "*", "source-map": "^0.7.3" } }, "sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw=="], + + "@types/webxr": ["@types/webxr@0.5.21", "", {}, "sha512-geZIAtLzjGmgY2JUi6VxXdCrTb99A7yP49lxLr2Nm/uIK0PkkxcEi4OGhoGDO4pxCf3JwGz2GiJL2Ej4K2bKaA=="], + + "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], + + "@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="], + + "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], + + "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@6.21.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.5.1", "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/type-utils": "6.21.0", "@typescript-eslint/utils": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", "natural-compare": "^1.4.0", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@6.21.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", "@typescript-eslint/typescript-estree": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0" } }, "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@6.21.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "6.21.0", "@typescript-eslint/utils": "6.21.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@6.21.0", "", {}, "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" } }, "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@6.21.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", "@typescript-eslint/typescript-estree": "6.21.0", "semver": "^7.5.4" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" } }, "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + + "@web3-storage/car-block-validator": ["@web3-storage/car-block-validator@1.2.0", "", { "dependencies": { "@multiformats/blake2": "^1.0.13", "@multiformats/murmur3": "^1.1.3", "@multiformats/sha3": "^2.0.15", "multiformats": "9.9.0", "uint8arrays": "^3.1.1" } }, "sha512-KKQ/M5WtpH/JlkX+bQYKzdG4azmSF495T7vpewje2xh7MBh1d94/BLblxCcLM/larWvXDxOkbAyTTdlECAAuUw=="], + + "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="], + + "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="], + + "@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="], + + "@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="], + + "@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="], + + "@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="], + + "@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="], + + "@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="], + + "@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="], + + "@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="], + + "@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="], + + "@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="], + + "@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="], + + "@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="], + + "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="], + + "@webpack-cli/configtest": ["@webpack-cli/configtest@1.2.0", "", { "peerDependencies": { "webpack": "4.x.x || 5.x.x", "webpack-cli": "4.x.x" } }, "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg=="], + + "@webpack-cli/info": ["@webpack-cli/info@1.5.0", "", { "dependencies": { "envinfo": "^7.7.3" }, "peerDependencies": { "webpack-cli": "4.x.x" } }, "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ=="], + + "@webpack-cli/serve": ["@webpack-cli/serve@1.7.0", "", { "peerDependencies": { "webpack-cli": "4.x.x" } }, "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q=="], + + "@xstate/react": ["@xstate/react@3.2.2", "", { "dependencies": { "use-isomorphic-layout-effect": "^1.1.2", "use-sync-external-store": "^1.0.0" }, "peerDependencies": { "@xstate/fsm": "^2.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "xstate": "^4.37.2" }, "optionalPeers": ["@xstate/fsm", "xstate"] }, "sha512-feghXWLedyq8JeL13yda3XnHPZKwYDN5HPBLykpLeuNpr9178tQd2/3d0NrH6gSd0sG5mLuLeuD+ck830fgzLQ=="], + + "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="], + + "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="], + + "@yarnpkg/esbuild-plugin-pnp": ["@yarnpkg/esbuild-plugin-pnp@3.0.0-rc.15", "", { "dependencies": { "tslib": "^2.4.0" }, "peerDependencies": { "esbuild": ">=0.10.0" } }, "sha512-kYzDJO5CA9sy+on/s2aIW0411AklfCi8Ck/4QDivOqsMKpStZA2SsR+X27VTggGwpStWaLrjJcDcdDMowtG8MA=="], + + "@yarnpkg/fslib": ["@yarnpkg/fslib@2.10.3", "", { "dependencies": { "@yarnpkg/libzip": "^2.3.0", "tslib": "^1.13.0" } }, "sha512-41H+Ga78xT9sHvWLlFOZLIhtU6mTGZ20pZ29EiZa97vnxdohJD2AF42rCoAoWfqUz486xY6fhjMH+DYEM9r14A=="], + + "@yarnpkg/libzip": ["@yarnpkg/libzip@2.3.0", "", { "dependencies": { "@types/emscripten": "^1.39.6", "tslib": "^1.13.0" } }, "sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg=="], + + "@yarnpkg/lockfile": ["@yarnpkg/lockfile@1.1.0", "", {}, "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="], + + "@yarnpkg/parsers": ["@yarnpkg/parsers@3.0.0-rc.46", "", { "dependencies": { "js-yaml": "^3.10.0", "tslib": "^2.4.0" } }, "sha512-aiATs7pSutzda/rq8fnuPwTglyVwjM22bNnK2ZgjrpAjQHSSl3lztd2f9evst1W/qnC58DRz7T7QndUDumAR4Q=="], + + "@zeit/schemas": ["@zeit/schemas@2.36.0", "", {}, "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg=="], + + "@zkochan/js-yaml": ["@zkochan/js-yaml@0.0.6", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-nzvgl3VfhcELQ8LyVrYOru+UtAy1nrygk2+AGbTm8a5YcO6o8lSjAT+pfg3vJWxIoZKOUhrK6UU7xW/+00kQrg=="], + + "JSONStream": ["JSONStream@1.3.5", "", { "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" }, "bin": { "JSONStream": "./bin.js" } }, "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ=="], + + "abab": ["abab@2.0.6", "", {}, "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA=="], + + "abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="], + + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], + + "acorn-globals": ["acorn-globals@7.0.1", "", { "dependencies": { "acorn": "^8.1.0", "acorn-walk": "^8.0.2" } }, "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q=="], + + "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "acorn-node": ["acorn-node@1.8.2", "", { "dependencies": { "acorn": "^7.0.0", "acorn-walk": "^7.0.0", "xtend": "^4.0.2" } }, "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A=="], + + "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + + "actor": ["actor@2.3.1", "", {}, "sha512-ST/3wnvcP2tKDXnum7nLCLXm+/rsf8vPocXH2Fre6D8FQwNkGDd4JEitBlXj007VQJfiGYRQvXqwOBZVi+JtRg=="], + + "add-stream": ["add-stream@1.0.0", "", {}, "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ=="], + + "address": ["address@1.2.2", "", {}, "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA=="], + + "adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="], + + "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + + "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], + + "airbnb-prop-types": ["airbnb-prop-types@2.16.0", "", { "dependencies": { "array.prototype.find": "^2.1.1", "function.prototype.name": "^1.1.2", "is-regex": "^1.1.0", "object-is": "^1.1.2", "object.assign": "^4.1.0", "object.entries": "^1.1.2", "prop-types": "^15.7.2", "prop-types-exact": "^1.2.0", "react-is": "^16.13.1" }, "peerDependencies": { "react": "^0.14 || ^15.0.0 || ^16.0.0-alpha" } }, "sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ajv-errors": ["ajv-errors@1.0.1", "", { "peerDependencies": { "ajv": ">=5.0.0" } }, "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ=="], + + "ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + + "ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], + + "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], + + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + + "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "ansi-html": ["ansi-html@0.0.9", "", { "bin": { "ansi-html": "bin/ansi-html" } }, "sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg=="], + + "ansi-html-community": ["ansi-html-community@0.0.8", "", { "bin": { "ansi-html": "bin/ansi-html" } }, "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "any-observable": ["any-observable@0.3.0", "", {}, "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "app-root-dir": ["app-root-dir@1.0.2", "", {}, "sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g=="], + + "aproba": ["aproba@2.0.0", "", {}, "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="], + + "arch": ["arch@2.2.0", "", {}, "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ=="], + + "are-we-there-yet": ["are-we-there-yet@3.0.1", "", { "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" } }, "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-hidden": ["aria-hidden@1.2.4", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + + "array-differ": ["array-differ@3.0.0", "", {}, "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg=="], + + "array-flatten": ["array-flatten@2.1.2", "", {}, "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ=="], + + "array-ify": ["array-ify@1.0.0", "", {}, "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng=="], + + "array-includes": ["array-includes@3.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "array-uniq": ["array-uniq@1.0.3", "", {}, "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q=="], + + "array.prototype.find": ["array.prototype.find@2.2.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-fO/ORdOELvjbbeIfZfzrXFMhYHGofRGqd+am9zm3tZ4GlJINj/pA2eITyfd65Vg6+ZbHd/Cys7stpoRSWtQFdA=="], + + "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], + + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "arrify": ["arrify@2.0.1", "", {}, "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug=="], + + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], + + "asn1.js": ["asn1.js@4.10.1", "", { "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw=="], + + "assert": ["assert@2.1.0", "", { "dependencies": { "call-bind": "^1.0.2", "is-nan": "^1.3.2", "object-is": "^1.1.5", "object.assign": "^4.1.4", "util": "^0.12.5" } }, "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw=="], + + "assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="], + + "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], + + "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], + + "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], + + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + + "async-limiter": ["async-limiter@1.0.1", "", {}, "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], + + "attr-accept": ["attr-accept@2.2.5", "", {}, "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ=="], + + "autoprefixer": ["autoprefixer@10.4.20", "", { "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "aws-sign2": ["aws-sign2@0.7.0", "", {}, "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA=="], + + "aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="], + + "axe-core": ["axe-core@4.10.2", "", {}, "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w=="], + + "axios": ["axios@0.28.1", "", { "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-iUcGA5a7p0mVb4Gm/sy+FSECNkPFT4y7wt6OM/CDpO/OnNCvSs3PoMG8ibrC9jRoGYU0gUK5pXVC4NPXq6lHRQ=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "babel-core": ["babel-core@7.0.0-bridge.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg=="], + + "babel-eslint": ["babel-eslint@9.0.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "@babel/parser": "^7.0.0", "@babel/traverse": "^7.0.0", "@babel/types": "^7.0.0", "eslint-scope": "3.7.1", "eslint-visitor-keys": "^1.0.0" } }, "sha512-itv1MwE3TMbY0QtNfeL7wzak1mV47Uy+n6HtSOO4Xd7rvmO+tsGQSgyOEEgo6Y2vHZKZphaoelNeSVj4vkLA1g=="], + + "babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="], + + "babel-loader": ["babel-loader@8.4.1", "", { "dependencies": { "find-cache-dir": "^3.3.1", "loader-utils": "^2.0.4", "make-dir": "^3.1.0", "schema-utils": "^2.6.5" }, "peerDependencies": { "@babel/core": "^7.0.0", "webpack": ">=2" } }, "sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA=="], + + "babel-plugin-add-react-displayname": ["babel-plugin-add-react-displayname@0.0.5", "", {}, "sha512-LY3+Y0XVDYcShHHorshrDbt4KFWL4bSeniCtl4SYZbask+Syngk1uMPCeN9+nSiZo6zX5s0RTq/J9Pnaaf/KHw=="], + + "babel-plugin-istanbul": ["babel-plugin-istanbul@6.1.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA=="], + + "babel-plugin-jest-hoist": ["babel-plugin-jest-hoist@29.6.3", "", { "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" } }, "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg=="], + + "babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="], + + "babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="], + + "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.12", "", { "dependencies": { "@babel/compat-data": "^7.22.6", "@babel/helper-define-polyfill-provider": "^0.6.3", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og=="], + + "babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.10.6", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.2", "core-js-compat": "^3.38.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA=="], + + "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.3", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.3" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q=="], + + "babel-preset-current-node-syntax": ["babel-preset-current-node-syntax@1.1.0", "", { "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw=="], + + "babel-preset-jest": ["babel-preset-jest@29.6.3", "", { "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA=="], + + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "batch": ["batch@0.6.1", "", {}, "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw=="], + + "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], + + "before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="], + + "better-opn": ["better-opn@3.0.2", "", { "dependencies": { "open": "^8.0.4" } }, "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ=="], + + "big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="], + + "big.js": ["big.js@5.2.2", "", {}, "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "blakejs": ["blakejs@1.2.1", "", {}, "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ=="], + + "blob-util": ["blob-util@2.0.2", "", {}, "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ=="], + + "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], + + "bn.js": ["bn.js@5.2.1", "", {}, "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ=="], + + "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], + + "bonjour": ["bonjour@3.5.0", "", { "dependencies": { "array-flatten": "^2.1.0", "deep-equal": "^1.0.1", "dns-equal": "^1.0.0", "dns-txt": "^2.0.2", "multicast-dns": "^6.0.1", "multicast-dns-service-types": "^1.1.0" } }, "sha512-RaVTblr+OnEli0r/ud8InrU7D+G0y6aJhlxaLa6Pwty4+xoxboF1BsUI45tujvRpbj9dQVoglChqonGAsjEBYg=="], + + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + + "boxen": ["boxen@7.0.0", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^7.0.0", "chalk": "^5.0.1", "cli-boxes": "^3.0.0", "string-width": "^5.1.2", "type-fest": "^2.13.0", "widest-line": "^4.0.1", "wrap-ansi": "^8.0.1" } }, "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg=="], + + "bplist-parser": ["bplist-parser@0.2.0", "", { "dependencies": { "big-integer": "^1.6.44" } }, "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw=="], + + "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "brcast": ["brcast@2.0.2", "", {}, "sha512-Tfn5JSE7hrUlFcOoaLzVvkbgIemIorMIyoMr3TgvszWW7jFt2C9PdeMLtysYD9RU0MmU17b69+XJG1eRY2OBRg=="], + + "brorand": ["brorand@1.1.0", "", {}, "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="], + + "browser-assert": ["browser-assert@1.2.1", "", {}, "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ=="], + + "browser-detect": ["browser-detect@0.2.28", "", { "dependencies": { "core-js": "^2.5.7" } }, "sha512-KeWGHqYQmHDkCFG2dIiX/2wFUgqevbw/rd6wNi9N6rZbaSJFtG5kel0HtprRwCGp8sqpQP79LzDJXf/WCx4WAw=="], + + "browserify-aes": ["browserify-aes@1.2.0", "", { "dependencies": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.3", "inherits": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA=="], + + "browserify-cipher": ["browserify-cipher@1.0.1", "", { "dependencies": { "browserify-aes": "^1.0.4", "browserify-des": "^1.0.0", "evp_bytestokey": "^1.0.0" } }, "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w=="], + + "browserify-des": ["browserify-des@1.0.2", "", { "dependencies": { "cipher-base": "^1.0.1", "des.js": "^1.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A=="], + + "browserify-rsa": ["browserify-rsa@4.1.1", "", { "dependencies": { "bn.js": "^5.2.1", "randombytes": "^2.1.0", "safe-buffer": "^5.2.1" } }, "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ=="], + + "browserify-sign": ["browserify-sign@4.2.3", "", { "dependencies": { "bn.js": "^5.2.1", "browserify-rsa": "^4.1.0", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "elliptic": "^6.5.5", "hash-base": "~3.0", "inherits": "^2.0.4", "parse-asn1": "^5.1.7", "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1" } }, "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw=="], + + "browserify-zlib": ["browserify-zlib@0.2.0", "", { "dependencies": { "pako": "~1.0.5" } }, "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA=="], + + "browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="], + + "bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "buffer-alloc": ["buffer-alloc@1.2.0", "", { "dependencies": { "buffer-alloc-unsafe": "^1.1.0", "buffer-fill": "^1.0.0" } }, "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow=="], + + "buffer-alloc-unsafe": ["buffer-alloc-unsafe@1.1.0", "", {}, "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="], + + "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + + "buffer-fill": ["buffer-fill@1.0.0", "", {}, "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "buffer-indexof": ["buffer-indexof@1.1.1", "", {}, "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g=="], + + "buffer-xor": ["buffer-xor@1.0.3", "", {}, "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ=="], + + "builtin-modules": ["builtin-modules@3.3.0", "", {}, "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw=="], + + "builtin-status-codes": ["builtin-status-codes@3.0.0", "", {}, "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ=="], + + "builtins": ["builtins@5.1.0", "", { "dependencies": { "semver": "^7.0.0" } }, "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg=="], + + "byte-size": ["byte-size@8.1.1", "", {}, "sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg=="], + + "bytes": ["bytes@3.0.0", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="], + + "cacache": ["cacache@17.1.4", "", { "dependencies": { "@npmcli/fs": "^3.1.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^7.7.1", "minipass": "^7.0.3", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^4.0.0", "ssri": "^10.0.0", "tar": "^6.1.11", "unique-filename": "^3.0.0" } }, "sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A=="], + + "cachedir": ["cachedir@2.4.0", "", {}, "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ=="], + + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g=="], + + "call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "caller-callsite": ["caller-callsite@2.0.0", "", { "dependencies": { "callsites": "^2.0.0" } }, "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ=="], + + "caller-path": ["caller-path@2.0.0", "", { "dependencies": { "caller-callsite": "^2.0.0" } }, "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "camel-case": ["camel-case@4.1.2", "", { "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw=="], + + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], + + "camelcase-keys": ["camelcase-keys@6.2.2", "", { "dependencies": { "camelcase": "^5.3.1", "map-obj": "^4.0.0", "quick-lru": "^4.0.1" } }, "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg=="], + + "caniuse-api": ["caniuse-api@3.0.0", "", { "dependencies": { "browserslist": "^4.0.0", "caniuse-lite": "^1.0.0", "lodash.memoize": "^4.1.2", "lodash.uniq": "^4.5.0" } }, "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001696", "", {}, "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ=="], + + "case-sensitive-paths-webpack-plugin": ["case-sensitive-paths-webpack-plugin@2.4.0", "", {}, "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw=="], + + "caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="], + + "cborg": ["cborg@4.2.8", "", { "bin": { "cborg": "lib/bin.js" } }, "sha512-z9M+TZCWQbf89Gl8ulpYThM9fqmkjBDdMiq+wS72OAK2zqDaXNquoAWFDrAKHQAukVtPspmadB9chuFC0ut7ew=="], + + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + + "chalk-template": ["chalk-template@0.4.0", "", { "dependencies": { "chalk": "^4.1.2" } }, "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg=="], + + "char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], + + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], + + "check-more-types": ["check-more-types@2.24.0", "", {}, "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA=="], + + "child-process-promise": ["child-process-promise@2.2.1", "", { "dependencies": { "cross-spawn": "^4.0.2", "node-version": "^1.0.0", "promise-polyfill": "^6.0.1" } }, "sha512-Fi4aNdqBsr0mv+jgWxcZ/7rAIC2mgihrptyVI4foh/rrjY/3BNjfP9+oaiFx/fzim+1ZyCNBae0DlyfQhSugog=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + + "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="], + + "ci-info": ["ci-info@4.1.0", "", {}, "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A=="], + + "cipher-base": ["cipher-base@1.0.6", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1" } }, "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw=="], + + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + + "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="], + + "clean-css": ["clean-css@5.3.3", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="], + + "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], + + "clean-webpack-plugin": ["clean-webpack-plugin@3.0.0", "", { "dependencies": { "@types/webpack": "^4.4.31", "del": "^4.1.1" }, "peerDependencies": { "webpack": "*" } }, "sha512-MciirUH5r+cYLGCOL5JX/ZLzOZbVr1ot3Fw+KcvbhUb6PM+yycqd9ZhIlcigQ5gl+XhppNmw3bEFuaaMNyLj3A=="], + + "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], + + "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], + + "cli-spinners": ["cli-spinners@2.6.1", "", {}, "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g=="], + + "cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="], + + "cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="], + + "cli-width": ["cli-width@3.0.0", "", {}, "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw=="], + + "clipboardy": ["clipboardy@3.0.0", "", { "dependencies": { "arch": "^2.2.0", "execa": "^5.1.1", "is-wsl": "^2.2.0" } }, "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + + "clone-deep": ["clone-deep@4.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", "shallow-clone": "^3.0.0" } }, "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "cmd-shim": ["cmd-shim@6.0.1", "", {}, "sha512-S9iI9y0nKR4hwEQsVWpyxld/6kRfGepGfzff83FcaiEBpmvlbA2nnGe7Cylgrx2f/p1P5S5wpRm9oL8z1PbS3Q=="], + + "cmdk": ["cmdk@1.0.4", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.0", "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg=="], + + "co": ["co@4.6.0", "", {}, "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ=="], + + "code-point-at": ["code-point-at@1.1.0", "", {}, "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA=="], + + "collect-v8-coverage": ["collect-v8-coverage@1.0.2", "", {}, "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="], + + "colord": ["colord@2.9.3", "", {}, "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "colormap": ["colormap@2.3.2", "", { "dependencies": { "lerp": "^1.0.3" } }, "sha512-jDOjaoEEmA9AgA11B/jCSAvYE95r3wRoAyTf3LEHGiUVlNHJaL1mRkf5AyLSpQBVGfTEPwGEqCIzL+kgr2WgNA=="], + + "columnify": ["columnify@1.6.0", "", { "dependencies": { "strip-ansi": "^6.0.1", "wcwidth": "^1.0.0" } }, "sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "comlink": ["comlink@4.4.2", "", {}, "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g=="], + + "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + + "common-path-prefix": ["common-path-prefix@3.0.0", "", {}, "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w=="], + + "common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="], + + "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], + + "compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="], + + "complex.js": ["complex.js@2.4.2", "", {}, "sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g=="], + + "compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="], + + "compression": ["compression@1.7.4", "", { "dependencies": { "accepts": "~1.3.5", "bytes": "3.0.0", "compressible": "~2.0.16", "debug": "2.6.9", "on-headers": "~1.0.2", "safe-buffer": "5.1.2", "vary": "~1.1.2" } }, "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="], + + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "confusing-browser-globals": ["confusing-browser-globals@1.0.11", "", {}, "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA=="], + + "connect-history-api-fallback": ["connect-history-api-fallback@1.6.0", "", {}, "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg=="], + + "consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="], + + "console-browserify": ["console-browserify@1.2.0", "", {}, "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA=="], + + "console-control-strings": ["console-control-strings@1.1.0", "", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="], + + "consolidated-events": ["consolidated-events@2.0.2", "", {}, "sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ=="], + + "constants-browserify": ["constants-browserify@1.0.0", "", {}, "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ=="], + + "content-disposition": ["content-disposition@0.5.2", "", {}, "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "conventional-changelog-angular": ["conventional-changelog-angular@7.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ=="], + + "conventional-changelog-core": ["conventional-changelog-core@5.0.1", "", { "dependencies": { "add-stream": "^1.0.0", "conventional-changelog-writer": "^6.0.0", "conventional-commits-parser": "^4.0.0", "dateformat": "^3.0.3", "get-pkg-repo": "^4.2.1", "git-raw-commits": "^3.0.0", "git-remote-origin-url": "^2.0.0", "git-semver-tags": "^5.0.0", "normalize-package-data": "^3.0.3", "read-pkg": "^3.0.0", "read-pkg-up": "^3.0.0" } }, "sha512-Rvi5pH+LvgsqGwZPZ3Cq/tz4ty7mjijhr3qR4m9IBXNbxGGYgTVVO+duXzz9aArmHxFtwZ+LRkrNIMDQzgoY4A=="], + + "conventional-changelog-preset-loader": ["conventional-changelog-preset-loader@3.0.0", "", {}, "sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA=="], + + "conventional-changelog-writer": ["conventional-changelog-writer@6.0.1", "", { "dependencies": { "conventional-commits-filter": "^3.0.0", "dateformat": "^3.0.3", "handlebars": "^4.7.7", "json-stringify-safe": "^5.0.1", "meow": "^8.1.2", "semver": "^7.0.0", "split": "^1.0.1" }, "bin": { "conventional-changelog-writer": "cli.js" } }, "sha512-359t9aHorPw+U+nHzUXHS5ZnPBOizRxfQsWT5ZDHBfvfxQOAik+yfuhKXG66CN5LEWPpMNnIMHUTCKeYNprvHQ=="], + + "conventional-commits-filter": ["conventional-commits-filter@3.0.0", "", { "dependencies": { "lodash.ismatch": "^4.4.0", "modify-values": "^1.0.1" } }, "sha512-1ymej8b5LouPx9Ox0Dw/qAO2dVdfpRFq28e5Y0jJEU8ZrLdy0vOSkkIInwmxErFGhg6SALro60ZrwYFVTUDo4Q=="], + + "conventional-commits-parser": ["conventional-commits-parser@4.0.0", "", { "dependencies": { "JSONStream": "^1.3.5", "is-text-path": "^1.0.1", "meow": "^8.1.2", "split2": "^3.2.2" }, "bin": { "conventional-commits-parser": "cli.js" } }, "sha512-WRv5j1FsVM5FISJkoYMR6tPk07fkKT0UodruX4je86V4owk451yjXAKzKAPOs9l7y59E2viHUS9eQ+dfUA9NSg=="], + + "conventional-recommended-bump": ["conventional-recommended-bump@7.0.1", "", { "dependencies": { "concat-stream": "^2.0.0", "conventional-changelog-preset-loader": "^3.0.0", "conventional-commits-filter": "^3.0.0", "conventional-commits-parser": "^4.0.0", "git-raw-commits": "^3.0.0", "git-semver-tags": "^5.0.0", "meow": "^8.1.2" }, "bin": { "conventional-recommended-bump": "cli.js" } }, "sha512-Ft79FF4SlOFvX4PkwFDRnaNiIVX7YbmqGU0RwccUaiGvgp3S0a8ipR2/Qxk31vclDNM+GSdJOVs2KrsUCjblVA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], + + "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], + + "copy-webpack-plugin": ["copy-webpack-plugin@9.1.0", "", { "dependencies": { "fast-glob": "^3.2.7", "glob-parent": "^6.0.1", "globby": "^11.0.3", "normalize-path": "^3.0.0", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-rxnR7PaGigJzhqETHGmAcxKnLZSR5u1Y3/bcIv/1FnqXedcL/E2ewK7ZCNrArJKCiSv8yVXhTqetJh8inDvfsA=="], + + "core-js": ["core-js@3.40.0", "", {}, "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ=="], + + "core-js-compat": ["core-js-compat@3.40.0", "", { "dependencies": { "browserslist": "^4.24.3" } }, "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ=="], + + "core-js-pure": ["core-js-pure@3.40.0", "", {}, "sha512-AtDzVIgRrmRKQai62yuSIN5vNiQjcJakJb4fbhVw3ehxx7Lohphvw9SGNWKhLFqSxC4ilD0g/L1huAYFQU3Q6A=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "cornerstone-math": ["cornerstone-math@0.1.10", "", {}, "sha512-23XSAyP7t70ANvhFyqwvva+zFd1bQ2d5GL7tg9qKE932WmImjA2Y9tiy5n0iTtnf51W/78Png8Lia2o4dCdJaQ=="], + + "cosmiconfig": ["cosmiconfig@5.2.1", "", { "dependencies": { "import-fresh": "^2.0.0", "is-directory": "^0.3.1", "js-yaml": "^3.13.1", "parse-json": "^4.0.0" } }, "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA=="], + + "create-ecdh": ["create-ecdh@4.0.4", "", { "dependencies": { "bn.js": "^4.1.0", "elliptic": "^6.5.3" } }, "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A=="], + + "create-hash": ["create-hash@1.2.0", "", { "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", "md5.js": "^1.3.4", "ripemd160": "^2.0.1", "sha.js": "^2.4.0" } }, "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg=="], + + "create-hmac": ["create-hmac@1.1.7", "", { "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", "inherits": "^2.0.1", "ripemd160": "^2.0.0", "safe-buffer": "^5.0.1", "sha.js": "^2.4.8" } }, "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg=="], + + "create-jest": ["create-jest@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-config": "^29.7.0", "jest-util": "^29.7.0", "prompts": "^2.0.1" }, "bin": { "create-jest": "bin/create-jest.js" } }, "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q=="], + + "cross-env": ["cross-env@5.2.1", "", { "dependencies": { "cross-spawn": "^6.0.5" }, "bin": { "cross-env": "dist/bin/cross-env.js", "cross-env-shell": "dist/bin/cross-env-shell.js" } }, "sha512-1yHhtcfAd1r4nwQgknowuUNfIT9E8dOMMspC36g45dN+iD1blloi7xp8X/xAIDnjHWyt1uQ8PHk2fkNaym7soQ=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "crypto-browserify": ["crypto-browserify@3.12.1", "", { "dependencies": { "browserify-cipher": "^1.0.1", "browserify-sign": "^4.2.3", "create-ecdh": "^4.0.4", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "diffie-hellman": "^5.0.3", "hash-base": "~3.0.4", "inherits": "^2.0.4", "pbkdf2": "^3.1.2", "public-encrypt": "^4.0.3", "randombytes": "^2.1.0", "randomfill": "^1.0.4" } }, "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ=="], + + "crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="], + + "crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="], + + "css-blank-pseudo": ["css-blank-pseudo@3.0.3", "", { "dependencies": { "postcss-selector-parser": "^6.0.9" }, "peerDependencies": { "postcss": "^8.4" }, "bin": { "css-blank-pseudo": "dist/cli.cjs" } }, "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ=="], + + "css-declaration-sorter": ["css-declaration-sorter@6.4.1", "", { "peerDependencies": { "postcss": "^8.0.9" } }, "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g=="], + + "css-has-pseudo": ["css-has-pseudo@3.0.4", "", { "dependencies": { "postcss-selector-parser": "^6.0.9" }, "peerDependencies": { "postcss": "^8.4" }, "bin": { "css-has-pseudo": "dist/cli.cjs" } }, "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw=="], + + "css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="], + + "css-loader": ["css-loader@6.11.0", "", { "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.4.33", "postcss-modules-extract-imports": "^3.1.0", "postcss-modules-local-by-default": "^4.0.5", "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", "semver": "^7.5.4" }, "peerDependencies": { "@rspack/core": "0.x || 1.x", "webpack": "^5.0.0" }, "optionalPeers": ["@rspack/core", "webpack"] }, "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g=="], + + "css-prefers-color-scheme": ["css-prefers-color-scheme@6.0.3", "", { "peerDependencies": { "postcss": "^8.4" }, "bin": { "css-prefers-color-scheme": "dist/cli.cjs" } }, "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA=="], + + "css-select": ["css-select@5.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg=="], + + "css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="], + + "css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="], + + "csscolorparser": ["csscolorparser@1.0.3", "", {}, "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w=="], + + "cssdb": ["cssdb@7.11.2", "", {}, "sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "cssfontparser": ["cssfontparser@1.2.1", "", {}, "sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg=="], + + "cssnano": ["cssnano@5.1.15", "", { "dependencies": { "cssnano-preset-default": "^5.2.14", "lilconfig": "^2.0.3", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw=="], + + "cssnano-preset-default": ["cssnano-preset-default@5.2.14", "", { "dependencies": { "css-declaration-sorter": "^6.3.1", "cssnano-utils": "^3.1.0", "postcss-calc": "^8.2.3", "postcss-colormin": "^5.3.1", "postcss-convert-values": "^5.1.3", "postcss-discard-comments": "^5.1.2", "postcss-discard-duplicates": "^5.1.0", "postcss-discard-empty": "^5.1.1", "postcss-discard-overridden": "^5.1.0", "postcss-merge-longhand": "^5.1.7", "postcss-merge-rules": "^5.1.4", "postcss-minify-font-values": "^5.1.0", "postcss-minify-gradients": "^5.1.1", "postcss-minify-params": "^5.1.4", "postcss-minify-selectors": "^5.2.1", "postcss-normalize-charset": "^5.1.0", "postcss-normalize-display-values": "^5.1.0", "postcss-normalize-positions": "^5.1.1", "postcss-normalize-repeat-style": "^5.1.1", "postcss-normalize-string": "^5.1.0", "postcss-normalize-timing-functions": "^5.1.0", "postcss-normalize-unicode": "^5.1.1", "postcss-normalize-url": "^5.1.0", "postcss-normalize-whitespace": "^5.1.1", "postcss-ordered-values": "^5.1.3", "postcss-reduce-initial": "^5.1.2", "postcss-reduce-transforms": "^5.1.0", "postcss-svgo": "^5.1.0", "postcss-unique-selectors": "^5.1.1" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A=="], + + "cssnano-utils": ["cssnano-utils@3.1.0", "", { "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA=="], + + "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], + + "cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="], + + "cssstyle": ["cssstyle@2.3.0", "", { "dependencies": { "cssom": "~0.3.6" } }, "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "cypress": ["cypress@14.0.1", "", { "dependencies": { "@cypress/request": "^3.0.6", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", "blob-util": "^2.0.2", "bluebird": "^3.7.2", "buffer": "^5.7.1", "cachedir": "^2.3.0", "chalk": "^4.1.0", "check-more-types": "^2.24.0", "ci-info": "^4.0.0", "cli-cursor": "^3.1.0", "cli-table3": "~0.6.1", "commander": "^6.2.1", "common-tags": "^1.8.0", "dayjs": "^1.10.4", "debug": "^4.3.4", "enquirer": "^2.3.6", "eventemitter2": "6.4.7", "execa": "4.1.0", "executable": "^4.1.1", "extract-zip": "2.0.1", "figures": "^3.2.0", "fs-extra": "^9.1.0", "getos": "^3.2.1", "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", "listr2": "^3.8.3", "lodash": "^4.17.21", "log-symbols": "^4.0.0", "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", "semver": "^7.5.3", "supports-color": "^8.1.1", "tmp": "~0.2.3", "tree-kill": "1.2.2", "untildify": "^4.0.0", "yauzl": "^2.10.0" }, "bin": { "cypress": "bin/cypress" } }, "sha512-gBAvKZE3f6eBaW1v8OtrwAFP90rjNZjjOO40M2KvOvmwVXk96Ps5Yjyck1EzGkXmNCaC/8kXFOY/1KD/wsaWpQ=="], + + "cypress-file-upload": ["cypress-file-upload@5.0.8", "", { "peerDependencies": { "cypress": ">3.0.0" } }, "sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g=="], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + + "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], + + "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], + + "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + + "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], + + "dargs": ["dargs@7.0.0", "", {}, "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg=="], + + "dashdash": ["dashdash@1.14.1", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + + "data-urls": ["data-urls@3.0.2", "", { "dependencies": { "abab": "^2.0.6", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^11.0.0" } }, "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ=="], + + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + + "date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], + + "dateformat": ["dateformat@3.0.3", "", {}, "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q=="], + + "dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="], + + "dcmjs": ["dcmjs@0.38.0", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevel": "^1.8.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-fsVASGco1aDaq9g683/masbhc7jeGRT1breGsMuiCgCWtOGe7oJFeDDzPcmLTxM1fFt6daHPCk0FnQZTxpr8Ew=="], + + "debounce": ["debounce@1.2.1", "", {}, "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="], + + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "debug-log": ["debug-log@1.0.1", "", {}, "sha512-gV/pe1YIaKNgLYnd1g9VNW80tcb7oV5qvNUxG7NM8rbDpnl6RGunzlAtlGSb0wEs3nesu2vHNiX9TSsZ+Y+RjA=="], + + "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + + "decamelize-keys": ["decamelize-keys@1.1.1", "", { "dependencies": { "decamelize": "^1.1.0", "map-obj": "^1.0.0" } }, "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg=="], + + "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], + + "decode-named-character-reference": ["decode-named-character-reference@1.0.2", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg=="], + + "decode-uri-component": ["decode-uri-component@0.2.2", "", {}, "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="], + + "decompress": ["decompress@4.2.1", "", { "dependencies": { "decompress-tar": "^4.0.0", "decompress-tarbz2": "^4.0.0", "decompress-targz": "^4.0.0", "decompress-unzip": "^4.0.1", "graceful-fs": "^4.1.10", "make-dir": "^1.0.0", "pify": "^2.3.0", "strip-dirs": "^2.0.0" } }, "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ=="], + + "decompress-tar": ["decompress-tar@4.1.1", "", { "dependencies": { "file-type": "^5.2.0", "is-stream": "^1.1.0", "tar-stream": "^1.5.2" } }, "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ=="], + + "decompress-tarbz2": ["decompress-tarbz2@4.1.1", "", { "dependencies": { "decompress-tar": "^4.1.0", "file-type": "^6.1.0", "is-stream": "^1.1.0", "seek-bzip": "^1.0.5", "unbzip2-stream": "^1.0.9" } }, "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A=="], + + "decompress-targz": ["decompress-targz@4.1.1", "", { "dependencies": { "decompress-tar": "^4.1.1", "file-type": "^5.2.0", "is-stream": "^1.1.0" } }, "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w=="], + + "decompress-unzip": ["decompress-unzip@4.0.1", "", { "dependencies": { "file-type": "^3.8.0", "get-stream": "^2.2.0", "pify": "^2.3.0", "yauzl": "^2.4.2" } }, "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw=="], + + "dedent": ["dedent@0.7.0", "", {}, "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA=="], + + "deep-equal": ["deep-equal@1.1.2", "", { "dependencies": { "is-arguments": "^1.1.1", "is-date-object": "^1.0.5", "is-regex": "^1.1.4", "object-is": "^1.1.5", "object-keys": "^1.1.1", "regexp.prototype.flags": "^1.5.1" } }, "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg=="], + + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "deepmerge": ["deepmerge@1.5.2", "", {}, "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ=="], + + "deepmerge-ts": ["deepmerge-ts@5.1.0", "", {}, "sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw=="], + + "default-browser-id": ["default-browser-id@3.0.0", "", { "dependencies": { "bplist-parser": "^0.2.0", "untildify": "^4.0.0" } }, "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA=="], + + "default-gateway": ["default-gateway@6.0.3", "", { "dependencies": { "execa": "^5.0.0" } }, "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg=="], + + "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "defined": ["defined@1.0.1", "", {}, "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q=="], + + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + + "deglob": ["deglob@3.1.0", "", { "dependencies": { "find-root": "^1.0.0", "glob": "^7.0.5", "ignore": "^5.0.0", "pkg-config": "^1.1.0", "run-parallel": "^1.1.2", "uniq": "^1.0.1" } }, "sha512-al10l5QAYaM/PeuXkAr1Y9AQz0LCtWsnJG23pIgh44hDxHFOj36l6qvhfjnIWBYwZOqM1fXUFV9tkjL7JPdGvw=="], + + "del": ["del@4.1.1", "", { "dependencies": { "@types/glob": "^7.1.1", "globby": "^6.1.0", "is-path-cwd": "^2.0.0", "is-path-in-cwd": "^2.0.0", "p-map": "^2.0.0", "pify": "^4.0.1", "rimraf": "^2.6.3" } }, "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "delegates": ["delegates@1.0.0", "", {}, "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "des.js": ["des.js@1.1.0", "", { "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg=="], + + "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + + "detect-gpu": ["detect-gpu@4.0.50", "", { "dependencies": { "webgl-constants": "^1.1.1" } }, "sha512-T67HE5+ONONN8rPXCBJPupyCg2QT8+l2NUUMuPxAppsMJBDPG/Jg0URLs6GyDzLm2niUE+oncIHSuy3VinoPeQ=="], + + "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], + + "detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="], + + "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "detect-package-manager": ["detect-package-manager@2.0.1", "", { "dependencies": { "execa": "^5.1.1" } }, "sha512-j/lJHyoLlWi6G1LDdLgvUtz60Zo5GEj+sVYtTVXnYLDPuzgC3llMxonXym9zIwhhUII8vjdw0LXxavpLqTbl1A=="], + + "detect-port": ["detect-port@1.6.1", "", { "dependencies": { "address": "^1.0.1", "debug": "4" }, "bin": { "detect": "bin/detect-port.js", "detect-port": "bin/detect-port.js" } }, "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q=="], + + "detective": ["detective@5.2.1", "", { "dependencies": { "acorn-node": "^1.8.2", "defined": "^1.0.0", "minimist": "^1.2.6" }, "bin": { "detective": "bin/detective.js" } }, "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw=="], + + "dicom-microscopy-viewer": ["dicom-microscopy-viewer@0.46.1", "", { "dependencies": { "@cornerstonejs/codec-charls": "^1.2.3", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.5", "colormap": "^2.3", "dcmjs": "^0.29.8", "dicomicc": "^0.1", "dicomweb-client": "^0.8.4", "image-type": "^4.1", "mathjs": "^11.2", "ol": "^7.1", "uuid": "^9.0" } }, "sha512-QLozX/iM6ZA0TxheHQnTNiLg+RbSVlxYKMNG9qdqV5oNbEDOf+z4/8mDqnAQ8wjlQXE2MbUDX8JRa0LmO9mDTg=="], + + "dicom-parser": ["dicom-parser@1.8.21", "", {}, "sha512-lYCweHQDsC8UFpXErPlg86Px2A8bay0HiUY+wzoG3xv5GzgqVHU3lziwSc/Gzn7VV7y2KeP072SzCviuOoU02w=="], + + "dicomicc": ["dicomicc@0.1.0", "", {}, "sha512-kZejPGjLQ9NsgovSyVsiAuCpq6LofNR9Erc8Tt/vQAYGYCoQnTyWDlg5D0TJJQATKul7cSr9k/q0TF8G9qdDkQ=="], + + "dicomweb-client": ["dicomweb-client@0.10.4", "", {}, "sha512-TEt26c0JI37IGmSqoj1k1/Y/ZIyq33/ysVaUwE0/Haosn2IBM55NEIPkT+AnhFss2nFAMVtKKWKWLox4luthVw=="], + + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], + + "diff": ["diff@5.2.0", "", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="], + + "diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="], + + "diffie-hellman": ["diffie-hellman@5.0.3", "", { "dependencies": { "bn.js": "^4.1.0", "miller-rabin": "^4.0.0", "randombytes": "^2.0.0" } }, "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "direction": ["direction@1.0.4", "", { "bin": { "direction": "cli.js" } }, "sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ=="], + + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + + "dnd-core": ["dnd-core@14.0.0", "", { "dependencies": { "@react-dnd/asap": "^4.0.0", "@react-dnd/invariant": "^2.0.0", "redux": "^4.0.5" } }, "sha512-wTDYKyjSqWuYw3ZG0GJ7k+UIfzxTNoZLjDrut37PbcPGNfwhlKYlPUqjAKUjOOv80izshUiqusaKgJPItXSevA=="], + + "dns-equal": ["dns-equal@1.0.0", "", {}, "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg=="], + + "dns-packet": ["dns-packet@1.3.4", "", { "dependencies": { "ip": "^1.1.0", "safe-buffer": "^5.0.1" } }, "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA=="], + + "dns-txt": ["dns-txt@2.0.2", "", { "dependencies": { "buffer-indexof": "^1.0.0" } }, "sha512-Ix5PrWjphuSoUXV/Zv5gaFHjnaJtb02F2+Si3Ht9dyJ87+Z/lMmy+dpNHtTGraNK958ndXq2i+GLkWsWHcKaBQ=="], + + "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], + + "document.contains": ["document.contains@1.0.2", "", { "dependencies": { "define-properties": "^1.1.3" } }, "sha512-YcvYFs15mX8m3AO1QNQy3BlIpSMfNRj3Ujk2BEJxsZG+HZf7/hZ6jr7mDpXrF8q+ff95Vef5yjhiZxm8CGJr6Q=="], + + "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + + "dom-converter": ["dom-converter@0.2.0", "", { "dependencies": { "utila": "~0.4" } }, "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA=="], + + "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], + + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "dom7": ["dom7@4.0.6", "", { "dependencies": { "ssr-window": "^4.0.0" } }, "sha512-emjdpPLhpNubapLFdjNL9tP06Sr+GZkrIHEXLWvOGsytACUrkbeIdjO5g77m00BrHTznnlcNqgmn7pCN192TBA=="], + + "domain-browser": ["domain-browser@5.7.0", "", {}, "sha512-edTFu0M/7wO1pXY6GDxVNVW086uqwWYIHP98txhcPyV995X21JIH2DtYp33sQJOupYoXKe9RwTw2Ya2vWaquTQ=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domexception": ["domexception@4.0.0", "", { "dependencies": { "webidl-conversions": "^7.0.0" } }, "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + + "dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="], + + "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], + + "dotenv": ["dotenv@8.6.0", "", {}, "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g=="], + + "dotenv-defaults": ["dotenv-defaults@1.1.1", "", { "dependencies": { "dotenv": "^6.2.0" } }, "sha512-6fPRo9o/3MxKvmRZBD3oNFdxODdhJtIy1zcJeUSCs6HCy4tarUpd+G67UTU9tF6OWXeSPqsm4fPAB+2eY9Rt9Q=="], + + "dotenv-expand": ["dotenv-expand@10.0.0", "", {}, "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A=="], + + "dotenv-webpack": ["dotenv-webpack@1.8.0", "", { "dependencies": { "dotenv-defaults": "^1.0.2" }, "peerDependencies": { "webpack": "^1 || ^2 || ^3 || ^4" } }, "sha512-o8pq6NLBehtrqA8Jv8jFQNtG9nhRtVqmoD4yWbgUyoU3+9WBlPe+c2EAiaJok9RB28QvrWvdWLZGeTT5aATDMg=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="], + + "duplexify": ["duplexify@3.7.1", "", { "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g=="], + + "earcut": ["earcut@2.2.4", "", {}, "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "ecc-jsbn": ["ecc-jsbn@0.1.2", "", { "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" } }, "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.90", "", {}, "sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug=="], + + "elegant-spinner": ["elegant-spinner@1.0.1", "", {}, "sha512-B+ZM+RXvRqQaAmkMlO/oSe5nMUOaUnyfGYCEHoR8wrXsZR2mA0XVibsxV1bvTwxdRWah1PkQqso2EzhILGHtEQ=="], + + "elliptic": ["elliptic@6.6.1", "", { "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.1", "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g=="], + + "emittery": ["emittery@0.13.1", "", {}, "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ=="], + + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "emojis-list": ["emojis-list@3.0.0", "", {}, "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], + + "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], + + "endent": ["endent@2.1.0", "", { "dependencies": { "dedent": "^0.7.0", "fast-json-parse": "^1.0.3", "objectorarray": "^1.0.5" } }, "sha512-r8VyPX7XL8U01Xgnb1CjZ3XV+z90cXIJ9JPE/R9SEC9vpw2P6CfsRPJmp20DppC5N7ZAMCmjYkJIa744Iyg96w=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ=="], + + "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "envinfo": ["envinfo@7.8.1", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw=="], + + "enzyme-shallow-equal": ["enzyme-shallow-equal@1.0.7", "", { "dependencies": { "hasown": "^2.0.0", "object-is": "^1.1.5" } }, "sha512-/um0GFqUXnpM9SvKtje+9Tjoz3f1fpBC3eXRFrNs8kpYn69JljciYP7KZTqM/YQbUY9KUjvKB4jo/q+L6WGGvg=="], + + "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], + + "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="], + + "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], + + "es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-get-iterator": ["es-get-iterator@1.1.3", "", { "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", "has-symbols": "^1.0.3", "is-arguments": "^1.1.1", "is-map": "^2.0.2", "is-set": "^2.0.2", "is-string": "^1.0.7", "isarray": "^2.0.5", "stop-iteration-iterator": "^1.0.0" } }, "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw=="], + + "es-iterator-helpers": ["es-iterator-helpers@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.6", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.4", "safe-array-concat": "^1.1.3" } }, "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w=="], + + "es-module-lexer": ["es-module-lexer@1.6.0", "", {}, "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-shim-unscopables": ["es-shim-unscopables@1.0.2", "", { "dependencies": { "hasown": "^2.0.0" } }, "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw=="], + + "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + + "esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + + "esbuild-plugin-alias": ["esbuild-plugin-alias@0.2.1", "", {}, "sha512-jyfL/pwPqaFXyKnj8lP8iLk6Z0m099uXR45aSN8Av1XD4vhvQutxxPzgA2bTcAwQpa1zCXDcWOlhFgyP3GKqhQ=="], + + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "escape-latex": ["escape-latex@1.2.0", "", {}, "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], + + "eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="], + + "eslint-config-prettier": ["eslint-config-prettier@7.2.0", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg=="], + + "eslint-config-react-app": ["eslint-config-react-app@6.0.0", "", { "dependencies": { "confusing-browser-globals": "^1.0.10" }, "peerDependencies": { "@typescript-eslint/eslint-plugin": "^4.0.0", "@typescript-eslint/parser": "^4.0.0", "babel-eslint": "^10.0.0", "eslint": "^7.5.0", "eslint-plugin-flowtype": "^5.2.0", "eslint-plugin-import": "^2.22.0", "eslint-plugin-jest": "^24.0.0", "eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-react": "^7.20.3", "eslint-plugin-react-hooks": "^4.0.8", "eslint-plugin-testing-library": "^3.9.0" }, "optionalPeers": ["eslint-plugin-jest", "eslint-plugin-testing-library"] }, "sha512-bpoAAC+YRfzq0dsTk+6v9aHm/uqnDwayNAXleMypGl6CpxI9oXXscVHo4fk3eJPIn+rsbtNetB4r/ZIidFIE8A=="], + + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="], + + "eslint-loader": ["eslint-loader@2.2.1", "", { "dependencies": { "loader-fs-cache": "^1.0.0", "loader-utils": "^1.0.2", "object-assign": "^4.0.1", "object-hash": "^1.1.4", "rimraf": "^2.6.1" }, "peerDependencies": { "eslint": ">=1.6.0 <7.0.0", "webpack": ">=2.0.0 <5.0.0" } }, "sha512-RLgV9hoCVsMLvOxCuNjdqOrUqIj9oJg8hF44vzJaYqsAHuY9G2YAeN3joQ9nxP0p5Th9iFSIpKo+SD8KISxXRg=="], + + "eslint-module-utils": ["eslint-module-utils@2.12.0", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg=="], + + "eslint-plugin-cypress": ["eslint-plugin-cypress@2.15.2", "", { "dependencies": { "globals": "^13.20.0" }, "peerDependencies": { "eslint": ">= 3.2.1" } }, "sha512-CtcFEQTDKyftpI22FVGpx8bkpKyYXBlNge6zSo0pl5/qJvBAnzaD76Vu2AsP16d6mTj478Ldn2mhgrWV+Xr0vQ=="], + + "eslint-plugin-es": ["eslint-plugin-es@3.0.1", "", { "dependencies": { "eslint-utils": "^2.0.0", "regexpp": "^3.0.0" }, "peerDependencies": { "eslint": ">=4.19.1" } }, "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ=="], + + "eslint-plugin-flowtype": ["eslint-plugin-flowtype@7.0.0", "", { "dependencies": { "lodash": "^4.17.21", "string-natural-compare": "^3.0.1" }, "peerDependencies": { "eslint": "^7.32.0" } }, "sha512-kW3eipG2Vth6e0apYqmFs05IHhFklJJNokYNiNEO5AIjm7H29oTDybYNB2bMULUYcf7iX7Wf3GdRhfBORKcT1g=="], + + "eslint-plugin-import": ["eslint-plugin-import@2.31.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.0", "hasown": "^2.0.2", "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.0", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A=="], + + "eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="], + + "eslint-plugin-node": ["eslint-plugin-node@11.1.0", "", { "dependencies": { "eslint-plugin-es": "^3.0.0", "eslint-utils": "^2.0.0", "ignore": "^5.1.1", "minimatch": "^3.0.4", "resolve": "^1.10.1", "semver": "^6.1.0" }, "peerDependencies": { "eslint": ">=5.16.0" } }, "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g=="], + + "eslint-plugin-prettier": ["eslint-plugin-prettier@5.2.3", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.9.1" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": "*", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw=="], + + "eslint-plugin-promise": ["eslint-plugin-promise@5.2.0", "", { "peerDependencies": { "eslint": "^7.0.0" } }, "sha512-SftLb1pUG01QYq2A/hGAWfDRXqYD82zE7j7TopDOyNdU+7SvvoXREls/+PRTY17vUXzXnZA/zfnyKgRH6x4JJw=="], + + "eslint-plugin-react": ["eslint-plugin-react@7.37.4", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.8", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@4.6.2", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ=="], + + "eslint-plugin-tsdoc": ["eslint-plugin-tsdoc@0.2.17", "", { "dependencies": { "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "0.16.2" } }, "sha512-xRmVi7Zx44lOBuYqG8vzTXuL6IdGOeF9nHX17bjJ8+VE6fsxpdGem0/SBTmAwgYMKYB1WBkqRJVQ+n8GK041pA=="], + + "eslint-scope": ["eslint-scope@3.7.1", "", { "dependencies": { "esrecurse": "^4.1.0", "estraverse": "^4.1.1" } }, "sha512-ivpbtpUgg9SJS4TLjK7KdcDhqc/E3CGItsvQbBNLkNGUeMhd5qnJcryba/brESS+dg3vrLqPuc/UcS7jRJdN5A=="], + + "eslint-utils": ["eslint-utils@2.1.0", "", { "dependencies": { "eslint-visitor-keys": "^1.1.0" } }, "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@1.3.0", "", {}, "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ=="], + + "eslint-webpack-plugin": ["eslint-webpack-plugin@2.7.0", "", { "dependencies": { "@types/eslint": "^7.29.0", "arrify": "^2.0.1", "jest-worker": "^27.5.1", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "schema-utils": "^3.1.1" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0", "webpack": "^4.0.0 || ^5.0.0" } }, "sha512-bNaVVUvU4srexGhVcayn/F4pJAz19CWBkKoMx7aSQ4wtTbZQCnG5O9LHCE42mM+JSKOUp7n6vd5CIwzj7lOVGA=="], + + "espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "estree-walker": ["estree-walker@1.0.1", "", {}, "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "event-stream": ["event-stream@3.3.4", "", { "dependencies": { "duplexer": "~0.1.1", "from": "~0", "map-stream": "~0.1.0", "pause-stream": "0.0.11", "split": "0.3", "stream-combiner": "~0.0.4", "through": "~2.3.1" } }, "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g=="], + + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + + "eventemitter2": ["eventemitter2@6.4.7", "", {}, "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg=="], + + "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "evp_bytestokey": ["evp_bytestokey@1.0.3", "", { "dependencies": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" } }, "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA=="], + + "execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], + + "executable": ["executable@4.1.1", "", { "dependencies": { "pify": "^2.2.0" } }, "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg=="], + + "exenv": ["exenv@1.2.2", "", {}, "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw=="], + + "exit": ["exit@0.1.2", "", {}, "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ=="], + + "expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], + + "exponential-backoff": ["exponential-backoff@3.1.1", "", {}, "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw=="], + + "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="], + + "extract-css-chunks-webpack-plugin": ["extract-css-chunks-webpack-plugin@4.10.0", "", { "dependencies": { "loader-utils": "^2.0.4", "normalize-url": "1.9.1", "schema-utils": "^1.0.0", "webpack-sources": "^1.1.0" }, "peerDependencies": { "webpack": "^4.4.0 || ^5.0.0" } }, "sha512-D/wb/Tbexq8XMBl4uhthto25WBaHI9P8vucDdzwPtLTyVi4Rdw/aiRLSL2rHaF6jZfPAjThWXepFU9PXsdtIbA=="], + + "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], + + "extsprintf": ["extsprintf@1.3.0", "", {}, "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-parse": ["fast-json-parse@1.0.3", "", {}, "sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="], + + "fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="], + + "faye-websocket": ["faye-websocket@0.11.4", "", { "dependencies": { "websocket-driver": ">=0.5.1" } }, "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g=="], + + "fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="], + + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], + + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "fetch-retry": ["fetch-retry@5.0.6", "", {}, "sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ=="], + + "fflate": ["fflate@0.7.3", "", {}, "sha512-0Zz1jOzJWERhyhsimS54VTqOteCNwRtIlh8isdL0AXLo0g7xNTfTL7oWrkmCnPhZGocKIkWHBistBrrpoNH3aw=="], + + "figures": ["figures@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg=="], + + "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], + + "file-loader": ["file-loader@6.2.0", "", { "dependencies": { "loader-utils": "^2.0.0", "schema-utils": "^3.0.0" }, "peerDependencies": { "webpack": "^4.0.0 || ^5.0.0" } }, "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw=="], + + "file-selector": ["file-selector@0.1.19", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-kCWw3+Aai8Uox+5tHCNgMFaUdgidxvMnLWO6fM5sZ0hA2wlHP5/DHGF0ECe84BiB95qdJbKNEJhWKVDvMN+JDQ=="], + + "file-system-cache": ["file-system-cache@2.3.0", "", { "dependencies": { "fs-extra": "11.1.1", "ramda": "0.29.0" } }, "sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ=="], + + "file-type": ["file-type@10.11.0", "", {}, "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw=="], + + "filelist": ["filelist@1.0.4", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="], + + "files-from-path": ["files-from-path@1.1.1", "", { "dependencies": { "graceful-fs": "^4.2.10" } }, "sha512-M2JDH/0gHqIsdwTnp8IBMWEYUUiHe9ei0ZMTXLxqKFcGxJF4Ki+nicw2k8HP5KGEzPLTyJ81XwLmP8l8rKa6qg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "filter-obj": ["filter-obj@1.1.0", "", {}, "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="], + + "finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], + + "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="], + + "find-cache-dir": ["find-cache-dir@3.3.2", "", { "dependencies": { "commondir": "^1.0.1", "make-dir": "^3.0.2", "pkg-dir": "^4.1.0" } }, "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig=="], + + "find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat": ["flat@5.0.2", "", { "bin": { "flat": "cli.js" } }, "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="], + + "flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="], + + "flatted": ["flatted@3.3.2", "", {}, "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA=="], + + "flow-parser": ["flow-parser@0.259.1", "", {}, "sha512-xiXLmMH2Z7OmdE9Q+MjljUMr/rbemFqZIRxaeZieVScG4HzQrKKhNcCYZbWTGpoN7ZPi7z8ClQbeVPq6t5AszQ=="], + + "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], + + "for-each": ["for-each@0.3.4", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw=="], + + "foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="], + + "forever-agent": ["forever-agent@0.6.1", "", {}, "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="], + + "fork-ts-checker-webpack-plugin": ["fork-ts-checker-webpack-plugin@8.0.0", "", { "dependencies": { "@babel/code-frame": "^7.16.7", "chalk": "^4.1.2", "chokidar": "^3.5.3", "cosmiconfig": "^7.0.1", "deepmerge": "^4.2.2", "fs-extra": "^10.0.0", "memfs": "^3.4.1", "minimatch": "^3.0.4", "node-abort-controller": "^3.0.1", "schema-utils": "^3.1.1", "semver": "^7.3.5", "tapable": "^2.2.1" }, "peerDependencies": { "typescript": ">3.6.0", "webpack": "^5.11.0" } }, "sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg=="], + + "form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], + + "framer-motion": ["framer-motion@6.2.4", "", { "dependencies": { "framesync": "6.0.1", "hey-listen": "^1.0.8", "popmotion": "11.0.3", "style-value-types": "5.0.0", "tslib": "^2.1.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": ">=16.8 || ^17.0.0", "react-dom": ">=16.8 || ^17.0.0" } }, "sha512-1UfnSG4c4CefKft6QMYGx8AWt3TtaFoR/Ax4dkuDDD5BDDeIuUm7gesmJrF8GzxeX/i6fMm8+MEdPngUyPVdLA=="], + + "framesync": ["framesync@6.0.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA=="], + + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "from": ["from@0.1.7", "", {}, "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g=="], + + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + + "fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="], + + "fs-monkey": ["fs-monkey@1.0.6", "", {}, "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "gauge": ["gauge@4.0.4", "", { "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.3", "console-control-strings": "^1.1.0", "has-unicode": "^2.0.1", "signal-exit": "^3.0.7", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wide-align": "^1.1.5" } }, "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "geotiff": ["geotiff@2.1.3", "", { "dependencies": { "@petamoriken/float16": "^3.4.7", "lerc": "^3.0.0", "pako": "^2.0.4", "parse-headers": "^2.0.2", "quick-lru": "^6.1.1", "web-worker": "^1.2.0", "xml-utils": "^1.0.2", "zstddec": "^0.1.0" } }, "sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "get-npm-tarball-url": ["get-npm-tarball-url@2.1.0", "", {}, "sha512-ro+DiMu5DXgRBabqXupW38h7WPZ9+Ad8UjwhvsmmN8w1sU7ab0nzAXvVZ4kqYg57OrqomRtJvepX5/xvFKNtjA=="], + + "get-own-enumerable-property-symbols": ["get-own-enumerable-property-symbols@3.0.2", "", {}, "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g=="], + + "get-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="], + + "get-pkg-repo": ["get-pkg-repo@4.2.1", "", { "dependencies": { "@hutson/parse-repository-url": "^3.0.0", "hosted-git-info": "^4.0.0", "through2": "^2.0.0", "yargs": "^16.2.0" }, "bin": { "get-pkg-repo": "src/cli.js" } }, "sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA=="], + + "get-port": ["get-port@5.1.1", "", {}, "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stdin": ["get-stdin@7.0.0", "", {}, "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ=="], + + "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], + + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "getos": ["getos@3.2.1", "", { "dependencies": { "async": "^3.2.0" } }, "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q=="], + + "getpass": ["getpass@0.1.7", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng=="], + + "giget": ["giget@1.2.4", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.5.1", "ohash": "^1.1.4", "pathe": "^2.0.2", "tar": "^6.2.1" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-Wv+daGyispVoA31TrWAVR+aAdP7roubTPEM/8JzRnqXhLbdJH0T9eQyXVFF8fjk3WKTsctII6QcyxILYgNp2DA=="], + + "git-raw-commits": ["git-raw-commits@3.0.0", "", { "dependencies": { "dargs": "^7.0.0", "meow": "^8.1.2", "split2": "^3.2.2" }, "bin": { "git-raw-commits": "cli.js" } }, "sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw=="], + + "git-remote-origin-url": ["git-remote-origin-url@2.0.0", "", { "dependencies": { "gitconfiglocal": "^1.0.0", "pify": "^2.3.0" } }, "sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw=="], + + "git-semver-tags": ["git-semver-tags@5.0.1", "", { "dependencies": { "meow": "^8.1.2", "semver": "^7.0.0" }, "bin": { "git-semver-tags": "cli.js" } }, "sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA=="], + + "git-up": ["git-up@7.0.0", "", { "dependencies": { "is-ssh": "^1.4.0", "parse-url": "^8.1.0" } }, "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ=="], + + "git-url-parse": ["git-url-parse@13.1.0", "", { "dependencies": { "git-up": "^7.0.0" } }, "sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA=="], + + "gitconfiglocal": ["gitconfiglocal@1.0.0", "", { "dependencies": { "ini": "^1.3.2" } }, "sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ=="], + + "github-slugger": ["github-slugger@1.5.0", "", {}, "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw=="], + + "gitignore": ["gitignore@0.7.0", "", { "bin": { "gitignore": "bin/gitignore.js" } }, "sha512-6iE891OyeYQYVvdoWI/hcxDWJ0sOngSpIRadxLoYbsnZdqWIUsEfx+IrOpW3d/zWBA/eKYvs6ZZx6ogz2wEGoQ=="], + + "gl-matrix": ["gl-matrix@3.4.3", "", {}, "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="], + + "glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], + + "global-cache": ["global-cache@1.2.1", "", { "dependencies": { "define-properties": "^1.1.2", "is-symbol": "^1.0.1" } }, "sha512-EOeUaup5DgWKlCMhA9YFqNRIlZwoxt731jCh47WBV9fQqHgXhr3Fa55hfgIUqilIcPsfdNKN7LHjrNY+Km40KA=="], + + "global-dirs": ["global-dirs@3.0.1", "", { "dependencies": { "ini": "2.0.0" } }, "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA=="], + + "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], + + "globalthis": ["globalthis@1.0.3", "", { "dependencies": { "define-properties": "^1.1.3" } }, "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "gunzip-maybe": ["gunzip-maybe@1.4.2", "", { "dependencies": { "browserify-zlib": "^0.1.4", "is-deflate": "^1.0.0", "is-gzip": "^1.0.0", "peek-stream": "^1.1.0", "pumpify": "^1.3.3", "through2": "^2.0.3" }, "bin": { "gunzip-maybe": "bin.js" } }, "sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw=="], + + "gzip-size": ["gzip-size@6.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="], + + "hammerjs": ["hammerjs@2.0.8", "", {}, "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ=="], + + "hamt-sharding": ["hamt-sharding@3.0.6", "", { "dependencies": { "sparse-array": "^1.3.1", "uint8arrays": "^5.0.1" } }, "sha512-nZeamxfymIWLpVcAN0CRrb7uVq3hCOGj9IcL6NMA6VVCVWqj+h9Jo/SmaWuS92AEDf1thmHsM5D5c70hM3j2Tg=="], + + "handle-thing": ["handle-thing@2.0.1", "", {}, "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg=="], + + "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], + + "hard-rejection": ["hard-rejection@2.1.0", "", {}, "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA=="], + + "harmony-reflect": ["harmony-reflect@1.6.2", "", {}, "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g=="], + + "has-ansi": ["has-ansi@2.0.0", "", { "dependencies": { "ansi-regex": "^2.0.0" } }, "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg=="], + + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "has-unicode": ["has-unicode@2.0.1", "", {}, "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="], + + "hash-base": ["hash-base@3.0.5", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1" } }, "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg=="], + + "hash.js": ["hash.js@1.1.7", "", { "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" } }, "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + + "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="], + + "history": ["history@5.3.0", "", { "dependencies": { "@babel/runtime": "^7.7.6" } }, "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ=="], + + "hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg=="], + + "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], + + "hosted-git-info": ["hosted-git-info@3.0.8", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw=="], + + "hpack.js": ["hpack.js@2.1.6", "", { "dependencies": { "inherits": "^2.0.1", "obuf": "^1.0.0", "readable-stream": "^2.0.1", "wbuf": "^1.1.0" } }, "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@3.0.0", "", { "dependencies": { "whatwg-encoding": "^2.0.0" } }, "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA=="], + + "html-entities": ["html-entities@2.5.2", "", {}, "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA=="], + + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + + "html-minifier-terser": ["html-minifier-terser@6.1.0", "", { "dependencies": { "camel-case": "^4.1.2", "clean-css": "^5.2.2", "commander": "^8.3.0", "he": "^1.2.0", "param-case": "^3.0.4", "relateurl": "^0.2.7", "terser": "^5.10.0" }, "bin": { "html-minifier-terser": "cli.js" } }, "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw=="], + + "html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="], + + "html-tags": ["html-tags@3.3.1", "", {}, "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ=="], + + "html-webpack-plugin": ["html-webpack-plugin@5.6.3", "", { "dependencies": { "@types/html-minifier-terser": "^6.0.0", "html-minifier-terser": "^6.0.2", "lodash": "^4.17.21", "pretty-error": "^4.0.0", "tapable": "^2.0.0" }, "peerDependencies": { "@rspack/core": "0.x || 1.x", "webpack": "^5.20.0" }, "optionalPeers": ["@rspack/core", "webpack"] }, "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg=="], + + "html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="], + + "htmlparser2": ["htmlparser2@6.1.0", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.0.0", "domutils": "^2.5.2", "entities": "^2.0.0" } }, "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A=="], + + "http-cache-semantics": ["http-cache-semantics@4.1.1", "", {}, "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="], + + "http-deceiver": ["http-deceiver@1.2.7", "", {}, "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw=="], + + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + + "http-parser-js": ["http-parser-js@0.5.9", "", {}, "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw=="], + + "http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ=="], + + "http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], + + "http-proxy-middleware": ["http-proxy-middleware@2.0.7", "", { "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", "is-glob": "^4.0.1", "is-plain-obj": "^3.0.0", "micromatch": "^4.0.2" }, "peerDependencies": { "@types/express": "^4.17.13" }, "optionalPeers": ["@types/express"] }, "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA=="], + + "http-signature": ["http-signature@1.4.0", "", { "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^2.0.2", "sshpk": "^1.18.0" } }, "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg=="], + + "https-browserify": ["https-browserify@1.0.0", "", {}, "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg=="], + + "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + + "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], + + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + + "husky": ["husky@3.1.0", "", { "dependencies": { "chalk": "^2.4.2", "ci-info": "^2.0.0", "cosmiconfig": "^5.2.1", "execa": "^1.0.0", "get-stdin": "^7.0.0", "opencollective-postinstall": "^2.0.2", "pkg-dir": "^4.2.0", "please-upgrade-node": "^3.2.0", "read-pkg": "^5.2.0", "run-node": "^1.0.0", "slash": "^3.0.0" }, "bin": { "husky-run": "./run.js", "husky-upgrade": "./lib/upgrader/bin.js" } }, "sha512-FJkPoHHB+6s4a+jwPqBudBDvYZsoQW5/HBuMSehC8qDiCe50kpcxeqFoDSlow+9I6wg47YxBoT3WxaURlrDIIQ=="], + + "i18next": ["i18next@17.3.1", "", { "dependencies": { "@babel/runtime": "^7.3.1" } }, "sha512-4nY+yaENaoZKmpbiDXPzucVHCN3hN9Z9Zk7LyQXVOKVIpnYOJ3L/yxHJlBPtJDq3PGgjFwA0QBFm/26Z0iDT5A=="], + + "i18next-browser-languagedetector": ["i18next-browser-languagedetector@3.1.1", "", {}, "sha512-JBgFWijjI1t6as4WgGvDdX4GLJPZwC/SMHzLQQ3ef7XaJsEkomlXFqXifKvOVJg09Hj2BVWe6strDdIF4J/0ng=="], + + "i18next-locize-backend": ["i18next-locize-backend@2.2.2", "", { "dependencies": { "@babel/runtime": "^7.7.4" } }, "sha512-YUHyrCr/8V85fh4WKE8rvzcQdS47lRbHOrDu69bBfMSiycCW26wYwcw2UQjtMY8GcoKgDYbYGbSfzunpnLATvQ=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "icss-utils": ["icss-utils@5.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA=="], + + "idb": ["idb@7.1.1", "", {}, "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="], + + "identity-obj-proxy": ["identity-obj-proxy@3.0.0", "", { "dependencies": { "harmony-reflect": "^1.4.6" } }, "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "ignore-walk": ["ignore-walk@5.0.1", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-yemi4pMf51WKT7khInJqAvsIGzoqYXblnsz0ql8tM+yi1EKYTY1evX4NAbJrLL/Aanr2HyZeluqU+Oi7MGHokw=="], + + "image-type": ["image-type@4.1.0", "", { "dependencies": { "file-type": "^10.10.0" } }, "sha512-CFJMJ8QK8lJvRlTCEgarL4ro6hfDQKif2HjSvYCdQZESaIPV4v9imrf7BQHK+sQeTeNeMpWciR9hyC/g8ybXEg=="], + + "immutability-helper": ["immutability-helper@3.1.1", "", {}, "sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ=="], + + "import-fresh": ["import-fresh@3.3.0", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="], + + "import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "indent-string": ["indent-string@3.2.0", "", {}, "sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ=="], + + "infer-owner": ["infer-owner@1.0.4", "", {}, "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "init-package-json": ["init-package-json@5.0.0", "", { "dependencies": { "npm-package-arg": "^10.0.0", "promzard": "^1.0.0", "read": "^2.0.0", "read-package-json": "^6.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4", "validate-npm-package-name": "^5.0.0" } }, "sha512-kBhlSheBfYmq3e0L1ii+VKe3zBTLL5lDCDWR+f9dLmEGSB3MqLlMlsolubSsyI88Bg6EA+BIMlomAnQ1SwgQBw=="], + + "inquirer": ["inquirer@8.2.6", "", { "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", "wrap-ansi": "^6.0.1" } }, "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg=="], + + "interface-blockstore": ["interface-blockstore@5.3.1", "", { "dependencies": { "interface-store": "^6.0.0", "multiformats": "^13.2.3" } }, "sha512-nhgrQnz6yUQEqxTFLhlOBurQOy5lWlwCpgFmZ3GTObTVTQS9RZjK/JTozY6ty9uz2lZs7VFJSqwjWAltorJ4Vw=="], + + "interface-store": ["interface-store@6.0.2", "", {}, "sha512-KSFCXtBlNoG0hzwNa0RmhHtrdhzexp+S+UY2s0rWTBJyfdEIgn6i6Zl9otVqrcFYbYrneBT7hbmHQ8gE0C3umA=="], + + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + + "interpret": ["interpret@2.2.0", "", {}, "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw=="], + + "iota-array": ["iota-array@1.0.0", "", {}, "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA=="], + + "ip": ["ip@1.1.9", "", {}, "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ=="], + + "ip-address": ["ip-address@9.0.5", "", { "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" } }, "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g=="], + + "ipaddr.js": ["ipaddr.js@2.2.0", "", {}, "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA=="], + + "ipfs-car": ["ipfs-car@1.2.0", "", { "dependencies": { "@ipld/car": "^5.1.0", "@ipld/dag-cbor": "^9.0.0", "@ipld/dag-json": "^10.0.1", "@ipld/dag-pb": "^4.0.2", "@ipld/unixfs": "^3.0.0", "@web3-storage/car-block-validator": "^1.0.1", "files-from-path": "^1.0.0", "ipfs-unixfs-exporter": "^13.0.1", "multiformats": "^13.0.1", "sade": "^1.8.1", "varint": "^6.0.0" }, "bin": { "๐Ÿš˜": "bin.js", "ipfs-car": "bin.js" } }, "sha512-A++1UesxqwfNv14NmFxr4MHi+vD9rR6SWr87MU9o0315Mzqys48pEefL8rlCAA9cw2qKYeT/ZPYVtqIMAr6U1Q=="], + + "ipfs-unixfs": ["ipfs-unixfs@11.2.0", "", { "dependencies": { "protons-runtime": "^5.5.0", "uint8arraylist": "^2.4.8" } }, "sha512-J8FN1qM5nfrDo8sQKQwfj0+brTg1uBfZK2vY9hxci33lcl3BFrsELS9+1+4q/8tO1ASKfxZO8W3Pi2O4sVX2Lg=="], + + "ipfs-unixfs-exporter": ["ipfs-unixfs-exporter@13.6.1", "", { "dependencies": { "@ipld/dag-cbor": "^9.2.1", "@ipld/dag-json": "^10.2.2", "@ipld/dag-pb": "^4.1.2", "@multiformats/murmur3": "^2.1.8", "hamt-sharding": "^3.0.6", "interface-blockstore": "^5.3.0", "ipfs-unixfs": "^11.0.0", "it-filter": "^3.1.1", "it-last": "^3.0.6", "it-map": "^3.1.1", "it-parallel": "^3.0.8", "it-pipe": "^3.0.1", "it-pushable": "^3.2.3", "multiformats": "^13.2.3", "p-queue": "^8.0.1", "progress-events": "^1.0.1" } }, "sha512-pYPI4oBTWao2//sFzAL0pURyojn79q/u5BuK6L5/nVbVUQVw6DcVP5uB1ySdWlTM2H+0Zlhp9+OL9aJBRIICpg=="], + + "is-absolute-url": ["is-absolute-url@3.0.3", "", {}, "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q=="], + + "is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="], + + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-boolean-object": ["is-boolean-object@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng=="], + + "is-buffer": ["is-buffer@1.1.6", "", {}, "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-ci": ["is-ci@3.0.1", "", { "dependencies": { "ci-info": "^3.2.0" }, "bin": { "is-ci": "bin.js" } }, "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + + "is-deflate": ["is-deflate@1.0.0", "", {}, "sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ=="], + + "is-directory": ["is-directory@0.3.1", "", {}, "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw=="], + + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-generator-fn": ["is-generator-fn@2.1.0", "", {}, "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ=="], + + "is-generator-function": ["is-generator-function@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-gzip": ["is-gzip@1.0.0", "", {}, "sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ=="], + + "is-installed-globally": ["is-installed-globally@0.4.0", "", { "dependencies": { "global-dirs": "^3.0.0", "is-path-inside": "^3.0.2" } }, "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ=="], + + "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], + + "is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="], + + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], + + "is-nan": ["is-nan@1.3.2", "", { "dependencies": { "call-bind": "^1.0.0", "define-properties": "^1.1.3" } }, "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w=="], + + "is-natural-number": ["is-natural-number@4.0.1", "", {}, "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "is-obj": ["is-obj@1.0.1", "", {}, "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg=="], + + "is-observable": ["is-observable@1.1.0", "", { "dependencies": { "symbol-observable": "^1.1.0" } }, "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA=="], + + "is-path-cwd": ["is-path-cwd@2.2.0", "", {}, "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ=="], + + "is-path-in-cwd": ["is-path-in-cwd@2.1.0", "", { "dependencies": { "is-path-inside": "^2.1.0" } }, "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ=="], + + "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], + + "is-plain-obj": ["is-plain-obj@3.0.0", "", {}, "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA=="], + + "is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="], + + "is-port-reachable": ["is-port-reachable@4.0.0", "", {}, "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + + "is-promise": ["is-promise@2.2.2", "", {}, "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-regexp": ["is-regexp@1.0.0", "", {}, "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA=="], + + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-ssh": ["is-ssh@1.4.0", "", { "dependencies": { "protocols": "^2.0.1" } }, "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ=="], + + "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-text-path": ["is-text-path@1.0.1", "", { "dependencies": { "text-extensions": "^1.0.0" } }, "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w=="], + + "is-touch-device": ["is-touch-device@1.0.1", "", {}, "sha512-LAYzo9kMT1b2p19L/1ATGt2XcSilnzNlyvq6c0pbPRVisLbAPpLqr53tIJS00kvrTkj0HtR8U7+u8X0yR8lPSw=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="], + + "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], + + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2" } }, "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q=="], + + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="], + + "isomorphic-base64": ["isomorphic-base64@1.0.2", "", {}, "sha512-pQFyLwShVPA1Qr0dE1ZPguJkbOsFGDfSq6Wzz6XaO33v74X6/iQjgYPozwkeKGQxOI1/H3Fz7+ROtnV1veyKEg=="], + + "isomorphic-rslog": ["isomorphic-rslog@0.0.6", "", {}, "sha512-HM0q6XqQ93psDlqvuViNs/Ea3hAyGDkIdVAHlrEocjjAwGrs1fZ+EdQjS9eUPacnYB7Y8SoDdSY3H8p3ce205A=="], + + "isstream": ["isstream@0.1.2", "", {}, "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="], + + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-instrument": ["istanbul-lib-instrument@6.0.3", "", { "dependencies": { "@babel/core": "^7.23.9", "@babel/parser": "^7.23.9", "@istanbuljs/schema": "^0.1.3", "istanbul-lib-coverage": "^3.2.0", "semver": "^7.5.4" } }, "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-lib-source-maps": ["istanbul-lib-source-maps@4.0.1", "", { "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", "source-map": "^0.6.1" } }, "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw=="], + + "istanbul-reports": ["istanbul-reports@3.1.7", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g=="], + + "it-filter": ["it-filter@3.1.1", "", { "dependencies": { "it-peekable": "^3.0.0" } }, "sha512-TOXmVuaSkxlLp2hXKoMTra0WMZMKVFxE3vSsbIA+PbADNCBAHhjJ/lM31vBOUTddHMO34Ku++vU8T9PLlBxQtg=="], + + "it-last": ["it-last@3.0.6", "", {}, "sha512-M4/get95O85u2vWvWQinF8SJUc/RPC5bWTveBTYXvlP2q5TF9Y+QhT3nz+CRCyS2YEc66VJkyl/da6WrJ0wKhw=="], + + "it-map": ["it-map@3.1.1", "", { "dependencies": { "it-peekable": "^3.0.0" } }, "sha512-9bCSwKD1yN1wCOgJ9UOl+46NQtdatosPWzxxUk2NdTLwRPXLh+L7iwCC9QKsbgM60RQxT/nH8bKMqm3H/o8IHQ=="], + + "it-merge": ["it-merge@3.0.5", "", { "dependencies": { "it-pushable": "^3.2.3" } }, "sha512-2l7+mPf85pyRF5pqi0dKcA54E5Jm/2FyY5GsOaN51Ta0ipC7YZ3szuAsH8wOoB6eKY4XsU4k2X+mzPmFBMayEA=="], + + "it-parallel": ["it-parallel@3.0.8", "", { "dependencies": { "p-defer": "^4.0.1" } }, "sha512-URLhs6eG4Hdr4OdvgBBPDzOjBeSSmI+Kqex2rv/aAyYClME26RYHirLVhZsZP5M+ZP6M34iRlXk8Wlqtezuqpg=="], + + "it-peekable": ["it-peekable@3.0.5", "", {}, "sha512-JWQOGMt6rKiPcY30zUVMR4g6YxkpueTwHVE7CMs/aGqCf4OydM6w+7ZM3PvmO1e0TocjuR4aL8xyZWR46cTqCQ=="], + + "it-pipe": ["it-pipe@3.0.1", "", { "dependencies": { "it-merge": "^3.0.0", "it-pushable": "^3.1.2", "it-stream-types": "^2.0.1" } }, "sha512-sIoNrQl1qSRg2seYSBH/3QxWhJFn9PKYvOf/bHdtCBF0bnghey44VyASsWzn5dAx0DCDDABq1hZIuzKmtBZmKA=="], + + "it-pushable": ["it-pushable@3.2.3", "", { "dependencies": { "p-defer": "^4.0.0" } }, "sha512-gzYnXYK8Y5t5b/BnJUr7glfQLO4U5vyb05gPx/TyTw+4Bv1zM9gFk4YsOrnulWefMewlphCjKkakFvj1y99Tcg=="], + + "it-stream-types": ["it-stream-types@2.0.2", "", {}, "sha512-Rz/DEZ6Byn/r9+/SBCuJhpPATDF9D+dz5pbgSUyBsCDtza6wtNATrz/jz1gDyNanC3XdLboriHnOC925bZRBww=="], + + "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], + + "itk-wasm": ["itk-wasm@1.0.0-b.173", "", { "dependencies": { "@itk-wasm/dam": "^1.1.1", "@thewtex/zstddec": "^0.2.0", "@types/emscripten": "^1.39.10", "axios": "^1.6.2", "chalk": "^5.3.0", "comlink": "^4.4.1", "commander": "^11.1.0", "fs-extra": "^11.2.0", "glob": "^8.1.0", "markdown-table": "^3.0.3", "mime-types": "^2.1.35", "wasm-feature-detect": "^1.6.1" }, "bin": { "itk-wasm": "src/itk-wasm-cli.js" } }, "sha512-SV2lfZ1mClWuSK/noaZgGj9jhroY4MZu19ci9pIucuyhoGdXrVSmWlPH/JYMDi9RP3BogmQwe9wfFc3X1dcEPg=="], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "jake": ["jake@10.9.2", "", { "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", "filelist": "^1.0.4", "minimatch": "^3.1.2" }, "bin": { "jake": "bin/cli.js" } }, "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA=="], + + "javascript-natural-sort": ["javascript-natural-sort@0.7.1", "", {}, "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="], + + "jest": ["jest@29.7.0", "", { "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", "import-local": "^3.0.2", "jest-cli": "^29.7.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": { "jest": "bin/jest.js" } }, "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw=="], + + "jest-canvas-mock": ["jest-canvas-mock@2.5.2", "", { "dependencies": { "cssfontparser": "^1.2.1", "moo-color": "^1.0.2" } }, "sha512-vgnpPupjOL6+L5oJXzxTxFrlGEIbHdZqFU+LFNdtLxZ3lRDCl17FlTMM7IatoRQkrcyOTMlDinjUguqmQ6bR2A=="], + + "jest-changed-files": ["jest-changed-files@29.7.0", "", { "dependencies": { "execa": "^5.0.0", "jest-util": "^29.7.0", "p-limit": "^3.1.0" } }, "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w=="], + + "jest-circus": ["jest-circus@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^1.0.0", "is-generator-fn": "^2.0.0", "jest-each": "^29.7.0", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-runtime": "^29.7.0", "jest-snapshot": "^29.7.0", "jest-util": "^29.7.0", "p-limit": "^3.1.0", "pretty-format": "^29.7.0", "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw=="], + + "jest-cli": ["jest-cli@29.7.0", "", { "dependencies": { "@jest/core": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "chalk": "^4.0.0", "create-jest": "^29.7.0", "exit": "^0.1.2", "import-local": "^3.0.2", "jest-config": "^29.7.0", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "yargs": "^17.3.1" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": { "jest": "bin/jest.js" } }, "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg=="], + + "jest-config": ["jest-config@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@jest/test-sequencer": "^29.7.0", "@jest/types": "^29.6.3", "babel-jest": "^29.7.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "jest-circus": "^29.7.0", "jest-environment-node": "^29.7.0", "jest-get-type": "^29.6.3", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.7.0", "jest-runner": "^29.7.0", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "peerDependencies": { "@types/node": "*", "ts-node": ">=9.0.0" }, "optionalPeers": ["@types/node", "ts-node"] }, "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ=="], + + "jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="], + + "jest-docblock": ["jest-docblock@29.7.0", "", { "dependencies": { "detect-newline": "^3.0.0" } }, "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g=="], + + "jest-each": ["jest-each@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "jest-util": "^29.7.0", "pretty-format": "^29.7.0" } }, "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ=="], + + "jest-environment-jsdom": ["jest-environment-jsdom@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/jsdom": "^20.0.0", "@types/node": "*", "jest-mock": "^29.7.0", "jest-util": "^29.7.0", "jsdom": "^20.0.0" }, "peerDependencies": { "canvas": "^2.5.0" }, "optionalPeers": ["canvas"] }, "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA=="], + + "jest-environment-node": ["jest-environment-node@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw=="], + + "jest-get-type": ["jest-get-type@27.5.1", "", {}, "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw=="], + + "jest-haste-map": ["jest-haste-map@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.6.3", "jest-util": "^29.7.0", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA=="], + + "jest-junit": ["jest-junit@6.4.0", "", { "dependencies": { "jest-validate": "^24.0.0", "mkdirp": "^0.5.1", "strip-ansi": "^4.0.0", "xml": "^1.0.1" } }, "sha512-GXEZA5WBeUich94BARoEUccJumhCgCerg7mXDFLxWwI2P7wL3Z7sGWk+53x343YdBLjiMR9aD/gYMVKO+0pE4Q=="], + + "jest-leak-detector": ["jest-leak-detector@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw=="], + + "jest-matcher-utils": ["jest-matcher-utils@27.5.1", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^27.5.1", "jest-get-type": "^27.5.1", "pretty-format": "^27.5.1" } }, "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw=="], + + "jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], + + "jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="], + + "jest-pnp-resolver": ["jest-pnp-resolver@1.2.3", "", { "peerDependencies": { "jest-resolve": "*" }, "optionalPeers": ["jest-resolve"] }, "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w=="], + + "jest-regex-util": ["jest-regex-util@29.6.3", "", {}, "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg=="], + + "jest-resolve": ["jest-resolve@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-pnp-resolver": "^1.2.2", "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" } }, "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA=="], + + "jest-resolve-dependencies": ["jest-resolve-dependencies@29.7.0", "", { "dependencies": { "jest-regex-util": "^29.6.3", "jest-snapshot": "^29.7.0" } }, "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA=="], + + "jest-runner": ["jest-runner@29.7.0", "", { "dependencies": { "@jest/console": "^29.7.0", "@jest/environment": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", "jest-docblock": "^29.7.0", "jest-environment-node": "^29.7.0", "jest-haste-map": "^29.7.0", "jest-leak-detector": "^29.7.0", "jest-message-util": "^29.7.0", "jest-resolve": "^29.7.0", "jest-runtime": "^29.7.0", "jest-util": "^29.7.0", "jest-watcher": "^29.7.0", "jest-worker": "^29.7.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" } }, "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ=="], + + "jest-runtime": ["jest-runtime@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/globals": "^29.7.0", "@jest/source-map": "^29.6.3", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "jest-haste-map": "^29.7.0", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.7.0", "jest-snapshot": "^29.7.0", "jest-util": "^29.7.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" } }, "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ=="], + + "jest-snapshot": ["jest-snapshot@29.7.0", "", { "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/types": "^7.3.3", "@jest/expect-utils": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^29.7.0", "graceful-fs": "^4.2.9", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0", "natural-compare": "^1.4.0", "pretty-format": "^29.7.0", "semver": "^7.5.3" } }, "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw=="], + + "jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], + + "jest-validate": ["jest-validate@24.9.0", "", { "dependencies": { "@jest/types": "^24.9.0", "camelcase": "^5.3.1", "chalk": "^2.0.1", "jest-get-type": "^24.9.0", "leven": "^3.1.0", "pretty-format": "^24.9.0" } }, "sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ=="], + + "jest-watcher": ["jest-watcher@29.7.0", "", { "dependencies": { "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", "jest-util": "^29.7.0", "string-length": "^4.0.1" } }, "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g=="], + + "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], + + "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "jju": ["jju@1.4.0", "", {}, "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA=="], + + "joi": ["joi@17.13.3", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA=="], + + "jpeg-lossless-decoder-js": ["jpeg-lossless-decoder-js@2.1.2", "", { "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "4.13.0" } }, "sha512-fYf/plymnuKwVw+s8gJ8O/BPw8y8OLaN11MBvEcT05kuy3m45o7BEC4J0JbxMNUYyO+MdK/I/jq0q1gkVYZm2Q=="], + + "js-sha3": ["js-sha3@0.8.0", "", {}, "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "jsbn": ["jsbn@0.1.1", "", {}, "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="], + + "jscodeshift": ["jscodeshift@0.15.2", "", { "dependencies": { "@babel/core": "^7.23.0", "@babel/parser": "^7.23.0", "@babel/plugin-transform-class-properties": "^7.22.5", "@babel/plugin-transform-modules-commonjs": "^7.23.0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", "@babel/plugin-transform-optional-chaining": "^7.23.0", "@babel/plugin-transform-private-methods": "^7.22.5", "@babel/preset-flow": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@babel/register": "^7.22.15", "babel-core": "^7.0.0-bridge.0", "chalk": "^4.1.2", "flow-parser": "0.*", "graceful-fs": "^4.2.4", "micromatch": "^4.0.4", "neo-async": "^2.5.0", "node-dir": "^0.1.17", "recast": "^0.23.3", "temp": "^0.8.4", "write-file-atomic": "^2.3.0" }, "peerDependencies": { "@babel/preset-env": "^7.1.6" }, "optionalPeers": ["@babel/preset-env"], "bin": { "jscodeshift": "bin/jscodeshift.js" } }, "sha512-FquR7Okgmc4Sd0aEDwqho3rEiKR3BdvuG9jfdHjLJ6JQoWSMpavug3AoIfnfWhxFlf+5pzQh8qjqz0DWFrNQzA=="], + + "jsdom": ["jsdom@20.0.3", "", { "dependencies": { "abab": "^2.0.6", "acorn": "^8.8.1", "acorn-globals": "^7.0.0", "cssom": "^0.5.0", "cssstyle": "^2.3.0", "data-urls": "^3.0.2", "decimal.js": "^10.4.2", "domexception": "^4.0.0", "escodegen": "^2.0.0", "form-data": "^4.0.0", "html-encoding-sniffer": "^3.0.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.1", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.2", "parse5": "^7.1.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^4.1.2", "w3c-xmlserializer": "^4.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^2.0.0", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^11.0.0", "ws": "^8.11.0", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "canvas": "^2.5.0" }, "optionalPeers": ["canvas"] }, "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-better-errors": ["json-parse-better-errors@1.0.2", "", {}, "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json-stringify-pretty-compact": ["json-stringify-pretty-compact@2.0.0", "", {}, "sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ=="], + + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonc-parser": ["jsonc-parser@3.2.0", "", {}, "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w=="], + + "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], + + "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], + + "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], + + "jsprim": ["jsprim@2.0.2", "", { "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", "json-schema": "0.4.0", "verror": "1.10.0" } }, "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ=="], + + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + + "jwt-decode": ["jwt-decode@4.0.0", "", {}, "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], + + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + + "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], + + "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="], + + "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="], + + "last-call-webpack-plugin": ["last-call-webpack-plugin@3.0.0", "", { "dependencies": { "lodash": "^4.17.5", "webpack-sources": "^1.1.0" } }, "sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w=="], + + "lazy-ass": ["lazy-ass@1.6.0", "", {}, "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw=="], + + "lazy-universal-dotenv": ["lazy-universal-dotenv@4.0.0", "", { "dependencies": { "app-root-dir": "^1.0.2", "dotenv": "^16.0.0", "dotenv-expand": "^10.0.0" } }, "sha512-aXpZJRnTkpK6gQ/z4nk+ZBLd/Qdp118cvPruLSIQzQNRhKwEcdXCOzXuF55VDqIiuAaY3UGZ10DJtvZzDcvsxg=="], + + "lerc": ["lerc@3.0.0", "", {}, "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww=="], + + "lerna": ["lerna@7.4.2", "", { "dependencies": { "@lerna/child-process": "7.4.2", "@lerna/create": "7.4.2", "@npmcli/run-script": "6.0.2", "@nx/devkit": ">=16.5.1 < 17", "@octokit/plugin-enterprise-rest": "6.0.1", "@octokit/rest": "19.0.11", "byte-size": "8.1.1", "chalk": "4.1.0", "clone-deep": "4.0.1", "cmd-shim": "6.0.1", "columnify": "1.6.0", "conventional-changelog-angular": "7.0.0", "conventional-changelog-core": "5.0.1", "conventional-recommended-bump": "7.0.1", "cosmiconfig": "^8.2.0", "dedent": "0.7.0", "envinfo": "7.8.1", "execa": "5.0.0", "fs-extra": "^11.1.1", "get-port": "5.1.1", "get-stream": "6.0.0", "git-url-parse": "13.1.0", "glob-parent": "5.1.2", "globby": "11.1.0", "graceful-fs": "4.2.11", "has-unicode": "2.0.1", "import-local": "3.1.0", "ini": "^1.3.8", "init-package-json": "5.0.0", "inquirer": "^8.2.4", "is-ci": "3.0.1", "is-stream": "2.0.0", "jest-diff": ">=29.4.3 < 30", "js-yaml": "4.1.0", "libnpmaccess": "7.0.2", "libnpmpublish": "7.3.0", "load-json-file": "6.2.0", "lodash": "^4.17.21", "make-dir": "4.0.0", "minimatch": "3.0.5", "multimatch": "5.0.0", "node-fetch": "2.6.7", "npm-package-arg": "8.1.1", "npm-packlist": "5.1.1", "npm-registry-fetch": "^14.0.5", "npmlog": "^6.0.2", "nx": ">=16.5.1 < 17", "p-map": "4.0.0", "p-map-series": "2.1.0", "p-pipe": "3.1.0", "p-queue": "6.6.2", "p-reduce": "2.1.0", "p-waterfall": "2.1.1", "pacote": "^15.2.0", "pify": "5.0.0", "read-cmd-shim": "4.0.0", "read-package-json": "6.0.4", "resolve-from": "5.0.0", "rimraf": "^4.4.1", "semver": "^7.3.8", "signal-exit": "3.0.7", "slash": "3.0.0", "ssri": "^9.0.1", "strong-log-transformer": "2.1.0", "tar": "6.1.11", "temp-dir": "1.0.0", "typescript": ">=3 < 6", "upath": "2.0.1", "uuid": "^9.0.0", "validate-npm-package-license": "3.0.4", "validate-npm-package-name": "5.0.0", "write-file-atomic": "5.0.1", "write-pkg": "4.0.0", "yargs": "16.2.0", "yargs-parser": "20.2.4" }, "bin": { "lerna": "dist/cli.js" } }, "sha512-gxavfzHfJ4JL30OvMunmlm4Anw7d7Tq6tdVHzUukLdS9nWnxCN/QB21qR+VJYp5tcyXogHKbdUEGh6qmeyzxSA=="], + + "lerp": ["lerp@1.0.3", "", {}, "sha512-70Rh4rCkJDvwWiTsyZ1HmJGvnyfFah4m6iTux29XmasRiZPDBpT9Cfa4ai73+uLZxnlKruUS62jj2lb11wURiA=="], + + "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "libnpmaccess": ["libnpmaccess@7.0.2", "", { "dependencies": { "npm-package-arg": "^10.1.0", "npm-registry-fetch": "^14.0.3" } }, "sha512-vHBVMw1JFMTgEk15zRsJuSAg7QtGGHpUSEfnbcRL1/gTBag9iEfJbyjpDmdJmwMhvpoLoNBtdAUCdGnaP32hhw=="], + + "libnpmpublish": ["libnpmpublish@7.3.0", "", { "dependencies": { "ci-info": "^3.6.1", "normalize-package-data": "^5.0.0", "npm-package-arg": "^10.1.0", "npm-registry-fetch": "^14.0.3", "proc-log": "^3.0.0", "semver": "^7.3.7", "sigstore": "^1.4.0", "ssri": "^10.0.1" } }, "sha512-fHUxw5VJhZCNSls0KLNEG0mCD2PN1i14gH5elGOgiVnU3VgTcRahagYP2LKI1m0tFCJ+XrAm0zVYyF5RCbXzcg=="], + + "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], + + "lines-and-columns": ["lines-and-columns@2.0.4", "", {}, "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A=="], + + "lint-staged": ["lint-staged@9.5.0", "", { "dependencies": { "chalk": "^2.4.2", "commander": "^2.20.0", "cosmiconfig": "^5.2.1", "debug": "^4.1.1", "dedent": "^0.7.0", "del": "^5.0.0", "execa": "^2.0.3", "listr": "^0.14.3", "log-symbols": "^3.0.0", "micromatch": "^4.0.2", "normalize-path": "^3.0.0", "please-upgrade-node": "^3.1.1", "string-argv": "^0.3.0", "stringify-object": "^3.3.0" }, "bin": { "lint-staged": "./bin/lint-staged" } }, "sha512-nawMob9cb/G1J98nb8v3VC/E8rcX1rryUYXVZ69aT9kde6YWX+uvNOEHY5yf2gcWcTJGiD0kqXmCnS3oD75GIA=="], + + "listr": ["listr@0.14.3", "", { "dependencies": { "@samverschueren/stream-to-observable": "^0.3.0", "is-observable": "^1.1.0", "is-promise": "^2.1.0", "is-stream": "^1.1.0", "listr-silent-renderer": "^1.1.1", "listr-update-renderer": "^0.5.0", "listr-verbose-renderer": "^0.5.0", "p-map": "^2.0.0", "rxjs": "^6.3.3" } }, "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA=="], + + "listr-silent-renderer": ["listr-silent-renderer@1.1.1", "", {}, "sha512-L26cIFm7/oZeSNVhWB6faeorXhMg4HNlb/dS/7jHhr708jxlXrtrBWo4YUxZQkc6dGoxEAe6J/D3juTRBUzjtA=="], + + "listr-update-renderer": ["listr-update-renderer@0.5.0", "", { "dependencies": { "chalk": "^1.1.3", "cli-truncate": "^0.2.1", "elegant-spinner": "^1.0.1", "figures": "^1.7.0", "indent-string": "^3.0.0", "log-symbols": "^1.0.2", "log-update": "^2.3.0", "strip-ansi": "^3.0.1" }, "peerDependencies": { "listr": "^0.14.2" } }, "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA=="], + + "listr-verbose-renderer": ["listr-verbose-renderer@0.5.0", "", { "dependencies": { "chalk": "^2.4.1", "cli-cursor": "^2.1.0", "date-fns": "^1.27.2", "figures": "^2.0.0" } }, "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw=="], + + "listr2": ["listr2@3.14.0", "", { "dependencies": { "cli-truncate": "^2.1.0", "colorette": "^2.0.16", "log-update": "^4.0.0", "p-map": "^4.0.0", "rfdc": "^1.3.0", "rxjs": "^7.5.1", "through": "^2.3.8", "wrap-ansi": "^7.0.0" }, "peerDependencies": { "enquirer": ">= 2.3.0 < 3" }, "optionalPeers": ["enquirer"] }, "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g=="], + + "load-json-file": ["load-json-file@6.2.0", "", { "dependencies": { "graceful-fs": "^4.1.15", "parse-json": "^5.0.0", "strip-bom": "^4.0.0", "type-fest": "^0.6.0" } }, "sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ=="], + + "loader-fs-cache": ["loader-fs-cache@1.0.3", "", { "dependencies": { "find-cache-dir": "^0.1.1", "mkdirp": "^0.5.1" } }, "sha512-ldcgZpjNJj71n+2Mf6yetz+c9bM4xpKtNds4LbqXzU/PTdeAX0g3ytnU1AJMEcTk2Lex4Smpe3Q/eCTsvUBxbA=="], + + "loader-runner": ["loader-runner@4.3.0", "", {}, "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg=="], + + "loader-utils": ["loader-utils@2.0.4", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^2.1.2" } }, "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "locize-editor": ["locize-editor@2.2.2", "", { "dependencies": { "@babel/runtime": "^7.4.5" } }, "sha512-geVXaA89vhfGTqzysA/EeLWRJEWBtdL/OoksiWmBFX2rUCmVrRN0Yb2sQhTvM84ZmQzxpGWTJvf1gzV3IpSGEg=="], + + "locize-lastused": ["locize-lastused@1.1.1", "", {}, "sha512-zq310En1BWRRjWaYdTScUwCWUcvLJuOUqFpN5LBQoCccWg9CQB5gDg/D+b8YjR2GkofRGy08C+4LGpqcGqfkHA=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], + + "lodash.clonedeep": ["lodash.clonedeep@4.5.0", "", {}, "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="], + + "lodash.compact": ["lodash.compact@3.0.1", "", {}, "sha512-2ozeiPi+5eBXW1CLtzjk8XQFhQOEMwwfxblqeq6EGyTxZJ1bPATqilY0e6g2SLQpP4KuMeuioBhEnWz5Pr7ICQ=="], + + "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], + + "lodash.flatten": ["lodash.flatten@4.4.0", "", {}, "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="], + + "lodash.get": ["lodash.get@4.4.2", "", {}, "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="], + + "lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="], + + "lodash.ismatch": ["lodash.ismatch@4.4.0", "", {}, "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g=="], + + "lodash.memoize": ["lodash.memoize@4.1.2", "", {}, "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + + "lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="], + + "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="], + + "lodash.uniqby": ["lodash.uniqby@4.7.0", "", {}, "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww=="], + + "lodash.zip": ["lodash.zip@4.2.0", "", {}, "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg=="], + + "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + + "log-update": ["log-update@4.0.0", "", { "dependencies": { "ansi-escapes": "^4.3.0", "cli-cursor": "^3.1.0", "slice-ansi": "^4.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg=="], + + "loglevel": ["loglevel@1.9.2", "", {}, "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg=="], + + "loglevelnext": ["loglevelnext@3.0.1", "", {}, "sha512-JpjaJhIN1reaSb26SIxDGtE0uc67gPl19OMVHrr+Ggt6b/Vy60jmCtKgQBrygAH0bhRA2nkxgDvM+8QvR8r0YA=="], + + "long": ["long@5.2.4", "", {}, "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg=="], + + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "lucide-react": ["lucide-react@0.379.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, "sha512-KcdeVPqmhRldldAAgptb8FjIunM2x2Zy26ZBh1RsEUcdLIvsEmbcw7KpzFYUy5BbpGeWhPu9Z9J5YXfStiXwhg=="], + + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + + "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], + + "make-dir": ["make-dir@3.1.0", "", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="], + + "make-fetch-happen": ["make-fetch-happen@11.1.1", "", { "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^17.0.0", "http-cache-semantics": "^4.1.1", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", "minipass": "^5.0.0", "minipass-fetch": "^3.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "promise-retry": "^2.0.1", "socks-proxy-agent": "^7.0.0", "ssri": "^10.0.0" } }, "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w=="], + + "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="], + + "map-obj": ["map-obj@4.3.0", "", {}, "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ=="], + + "map-or-similar": ["map-or-similar@1.5.0", "", {}, "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg=="], + + "map-stream": ["map-stream@0.1.0", "", {}, "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g=="], + + "mapbox-to-css-font": ["mapbox-to-css-font@2.4.5", "", {}, "sha512-VJ6nB8emkO9VODI0Fk+TQ/0zKBTqmf/Pkt8Xv0kHstoc0iXRajA00DAid4Kc3K5xeFIOoiZrVxijEzj0GLVO2w=="], + + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + + "markdown-to-jsx": ["markdown-to-jsx@7.7.3", "", { "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-o35IhJDFP6Fv60zPy+hbvZSQMmgvSGdK5j8NRZ7FeZMY+Bgqw+dSg7SC1ZEzC26++CiOUCqkbq96/c3j/FfTEQ=="], + + "material-colors": ["material-colors@1.2.6", "", {}, "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mathjs": ["mathjs@12.4.3", "", { "dependencies": { "@babel/runtime": "^7.24.4", "complex.js": "^2.1.1", "decimal.js": "^10.4.3", "escape-latex": "^1.2.0", "fraction.js": "4.3.4", "javascript-natural-sort": "^0.7.1", "seedrandom": "^3.0.5", "tiny-emitter": "^2.1.0", "typed-function": "^4.1.1" }, "bin": { "mathjs": "bin/cli.js" } }, "sha512-oHdGPDbp7gO873xxG90RLq36IuicuKvbpr/bBG5g9c8Obm/VsKVrK9uoRZZHUodohzlnmCEqfDzbR3LH6m+aAQ=="], + + "md5.js": ["md5.js@1.3.5", "", { "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg=="], + + "mdast-util-definitions": ["mdast-util-definitions@4.0.0", "", { "dependencies": { "unist-util-visit": "^2.0.0" } }, "sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ=="], + + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@2.2.2", "", { "dependencies": { "@types/mdast": "^3.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.0.0" } }, "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@1.3.1", "", { "dependencies": { "@types/mdast": "^3.0.0", "@types/unist": "^2.0.0", "decode-named-character-reference": "^1.0.0", "mdast-util-to-string": "^3.1.0", "micromark": "^3.0.0", "micromark-util-decode-numeric-character-reference": "^1.0.0", "micromark-util-decode-string": "^1.0.0", "micromark-util-normalize-identifier": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0", "unist-util-stringify-position": "^3.0.0", "uvu": "^0.5.0" } }, "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww=="], + + "mdast-util-gfm": ["mdast-util-gfm@2.0.2", "", { "dependencies": { "mdast-util-from-markdown": "^1.0.0", "mdast-util-gfm-autolink-literal": "^1.0.0", "mdast-util-gfm-footnote": "^1.0.0", "mdast-util-gfm-strikethrough": "^1.0.0", "mdast-util-gfm-table": "^1.0.0", "mdast-util-gfm-task-list-item": "^1.0.0", "mdast-util-to-markdown": "^1.0.0" } }, "sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@1.0.3", "", { "dependencies": { "@types/mdast": "^3.0.0", "ccount": "^2.0.0", "mdast-util-find-and-replace": "^2.0.0", "micromark-util-character": "^1.0.0" } }, "sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@1.0.2", "", { "dependencies": { "@types/mdast": "^3.0.0", "mdast-util-to-markdown": "^1.3.0", "micromark-util-normalize-identifier": "^1.0.0" } }, "sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@1.0.3", "", { "dependencies": { "@types/mdast": "^3.0.0", "mdast-util-to-markdown": "^1.3.0" } }, "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@1.0.7", "", { "dependencies": { "@types/mdast": "^3.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^1.0.0", "mdast-util-to-markdown": "^1.3.0" } }, "sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@1.0.2", "", { "dependencies": { "@types/mdast": "^3.0.0", "mdast-util-to-markdown": "^1.3.0" } }, "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@3.0.1", "", { "dependencies": { "@types/mdast": "^3.0.0", "unist-util-is": "^5.0.0" } }, "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@1.5.0", "", { "dependencies": { "@types/mdast": "^3.0.0", "@types/unist": "^2.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^3.0.0", "mdast-util-to-string": "^3.0.0", "micromark-util-decode-string": "^1.0.0", "unist-util-visit": "^4.0.0", "zwitch": "^2.0.0" } }, "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A=="], + + "mdast-util-to-string": ["mdast-util-to-string@1.1.0", "", {}, "sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A=="], + + "mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="], + + "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "memfs": ["memfs@3.6.0", "", { "dependencies": { "fs-monkey": "^1.0.4" } }, "sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ=="], + + "memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="], + + "memoizerific": ["memoizerific@1.11.3", "", { "dependencies": { "map-or-similar": "^1.5.0" } }, "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog=="], + + "meow": ["meow@8.1.2", "", { "dependencies": { "@types/minimist": "^1.2.0", "camelcase-keys": "^6.2.2", "decamelize-keys": "^1.1.0", "hard-rejection": "^2.1.0", "minimist-options": "4.1.0", "normalize-package-data": "^3.0.0", "read-pkg-up": "^7.0.1", "redent": "^3.0.0", "trim-newlines": "^3.0.0", "type-fest": "^0.18.0", "yargs-parser": "^20.2.3" } }, "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q=="], + + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + + "micromark": ["micromark@3.2.0", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "micromark-core-commonmark": "^1.0.1", "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-chunked": "^1.0.0", "micromark-util-combine-extensions": "^1.0.0", "micromark-util-decode-numeric-character-reference": "^1.0.0", "micromark-util-encode": "^1.0.0", "micromark-util-normalize-identifier": "^1.0.0", "micromark-util-resolve-all": "^1.0.0", "micromark-util-sanitize-uri": "^1.0.0", "micromark-util-subtokenize": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.1", "uvu": "^0.5.0" } }, "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@1.1.0", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-factory-destination": "^1.0.0", "micromark-factory-label": "^1.0.0", "micromark-factory-space": "^1.0.0", "micromark-factory-title": "^1.0.0", "micromark-factory-whitespace": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-chunked": "^1.0.0", "micromark-util-classify-character": "^1.0.0", "micromark-util-html-tag-name": "^1.0.0", "micromark-util-normalize-identifier": "^1.0.0", "micromark-util-resolve-all": "^1.0.0", "micromark-util-subtokenize": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.1", "uvu": "^0.5.0" } }, "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@2.0.3", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^1.0.0", "micromark-extension-gfm-footnote": "^1.0.0", "micromark-extension-gfm-strikethrough": "^1.0.0", "micromark-extension-gfm-table": "^1.0.0", "micromark-extension-gfm-tagfilter": "^1.0.0", "micromark-extension-gfm-task-list-item": "^1.0.0", "micromark-util-combine-extensions": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@1.0.5", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-sanitize-uri": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@1.1.2", "", { "dependencies": { "micromark-core-commonmark": "^1.0.0", "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-normalize-identifier": "^1.0.0", "micromark-util-sanitize-uri": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0", "uvu": "^0.5.0" } }, "sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@1.0.7", "", { "dependencies": { "micromark-util-chunked": "^1.0.0", "micromark-util-classify-character": "^1.0.0", "micromark-util-resolve-all": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0", "uvu": "^0.5.0" } }, "sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@1.0.7", "", { "dependencies": { "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0", "uvu": "^0.5.0" } }, "sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@1.0.2", "", { "dependencies": { "micromark-util-types": "^1.0.0" } }, "sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@1.0.5", "", { "dependencies": { "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0", "uvu": "^0.5.0" } }, "sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ=="], + + "micromark-factory-destination": ["micromark-factory-destination@1.1.0", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg=="], + + "micromark-factory-label": ["micromark-factory-label@1.1.0", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0", "uvu": "^0.5.0" } }, "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w=="], + + "micromark-factory-space": ["micromark-factory-space@1.1.0", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ=="], + + "micromark-factory-title": ["micromark-factory-title@1.1.0", "", { "dependencies": { "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@1.1.0", "", { "dependencies": { "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ=="], + + "micromark-util-character": ["micromark-util-character@1.2.0", "", { "dependencies": { "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg=="], + + "micromark-util-chunked": ["micromark-util-chunked@1.1.0", "", { "dependencies": { "micromark-util-symbol": "^1.0.0" } }, "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@1.1.0", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@1.1.0", "", { "dependencies": { "micromark-util-chunked": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@1.1.0", "", { "dependencies": { "micromark-util-symbol": "^1.0.0" } }, "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@1.1.0", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-decode-numeric-character-reference": "^1.0.0", "micromark-util-symbol": "^1.0.0" } }, "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ=="], + + "micromark-util-encode": ["micromark-util-encode@1.1.0", "", {}, "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@1.2.0", "", {}, "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@1.1.0", "", { "dependencies": { "micromark-util-symbol": "^1.0.0" } }, "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@1.1.0", "", { "dependencies": { "micromark-util-types": "^1.0.0" } }, "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@1.2.0", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-encode": "^1.0.0", "micromark-util-symbol": "^1.0.0" } }, "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@1.1.0", "", { "dependencies": { "micromark-util-chunked": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0", "uvu": "^0.5.0" } }, "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A=="], + + "micromark-util-symbol": ["micromark-util-symbol@1.1.0", "", {}, "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag=="], + + "micromark-util-types": ["micromark-util-types@1.1.0", "", {}, "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "miller-rabin": ["miller-rabin@4.0.1", "", { "dependencies": { "bn.js": "^4.0.0", "brorand": "^1.0.1" }, "bin": { "miller-rabin": "bin/miller-rabin" } }, "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA=="], + + "mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + + "mini-css-extract-plugin": ["mini-css-extract-plugin@2.9.2", "", { "dependencies": { "schema-utils": "^4.0.0", "tapable": "^2.2.1" }, "peerDependencies": { "webpack": "^5.0.0" } }, "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w=="], + + "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], + + "minimalistic-crypto-utils": ["minimalistic-crypto-utils@1.0.1", "", {}, "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minimist-options": ["minimist-options@4.1.0", "", { "dependencies": { "arrify": "^1.0.1", "is-plain-obj": "^1.1.0", "kind-of": "^6.0.3" } }, "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A=="], + + "minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], + + "minipass-collect": ["minipass-collect@1.0.2", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA=="], + + "minipass-fetch": ["minipass-fetch@3.0.5", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^2.1.2" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg=="], + + "minipass-flush": ["minipass-flush@1.0.5", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw=="], + + "minipass-json-stream": ["minipass-json-stream@1.0.2", "", { "dependencies": { "jsonparse": "^1.3.1", "minipass": "^3.0.0" } }, "sha512-myxeeTm57lYs8pH2nxPzmEEg8DGIgW+9mv6D4JZD2pa81I/OBjeU7PtICXV6c9eRGTA5JMDsuIPUZRCyBMYNhg=="], + + "minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="], + + "minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="], + + "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + + "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + + "mlly": ["mlly@1.7.4", "", { "dependencies": { "acorn": "^8.14.0", "pathe": "^2.0.1", "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw=="], + + "modify-values": ["modify-values@1.0.1", "", {}, "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw=="], + + "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], + + "moo-color": ["moo-color@1.0.3", "", { "dependencies": { "color-name": "^1.1.4" } }, "sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ=="], + + "mousetrap": ["mousetrap@1.6.5", "", {}, "sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "mrmime": ["mrmime@2.0.0", "", {}, "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "multicast-dns": ["multicast-dns@6.2.3", "", { "dependencies": { "dns-packet": "^1.3.1", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g=="], + + "multicast-dns-service-types": ["multicast-dns-service-types@1.1.0", "", {}, "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ=="], + + "multiformats": ["multiformats@13.3.1", "", {}, "sha512-QxowxTNwJ3r5RMctoGA5p13w5RbRT2QDkoM+yFlqfLiioBp78nhDjnRLvmSBI9+KAqN4VdgOVWM9c0CHd86m3g=="], + + "multimatch": ["multimatch@5.0.0", "", { "dependencies": { "@types/minimatch": "^3.0.3", "array-differ": "^3.0.0", "array-union": "^2.1.0", "arrify": "^2.0.1", "minimatch": "^3.0.4" } }, "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA=="], + + "murmurhash3js-revisited": ["murmurhash3js-revisited@3.0.0", "", {}, "sha512-/sF3ee6zvScXMb1XFJ8gDsSnY+X8PbOyjIuBhtgis10W2Jx4ZjIhikUCIF9c4gpJxVnQIsPAFrSwTCuAjicP6g=="], + + "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], + + "mute-stream": ["mute-stream@0.0.8", "", {}, "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="], + + "nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "ncp": ["ncp@2.0.0", "", { "bin": { "ncp": "./bin/ncp" } }, "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA=="], + + "ndarray": ["ndarray@1.0.19", "", { "dependencies": { "iota-array": "^1.0.0", "is-buffer": "^1.0.2" } }, "sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ=="], + + "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], + + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + + "next-themes": ["next-themes@0.3.0", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18", "react-dom": "^16.8 || ^17 || ^18" } }, "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w=="], + + "nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="], + + "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], + + "node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="], + + "node-addon-api": ["node-addon-api@3.2.1", "", {}, "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="], + + "node-dir": ["node-dir@0.1.17", "", { "dependencies": { "minimatch": "^3.0.2" } }, "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + + "node-fetch-native": ["node-fetch-native@1.6.6", "", {}, "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ=="], + + "node-forge": ["node-forge@1.3.1", "", {}, "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="], + + "node-gyp": ["node-gyp@9.4.1", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^7.1.4", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.0.3", "nopt": "^6.0.0", "npmlog": "^6.0.0", "rimraf": "^3.0.2", "semver": "^7.3.5", "tar": "^6.1.2", "which": "^2.0.2" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ=="], + + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + + "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], + + "node-machine-id": ["node-machine-id@1.1.12", "", {}, "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ=="], + + "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], + + "node-version": ["node-version@1.2.0", "", {}, "sha512-ma6oU4Sk0qOoKEAymVoTvk8EdXEobdS7m/mAGhDJ8Rouugho48crHBORAmy5BoOcv8wraPM6xumapQp5hl4iIQ=="], + + "nopt": ["nopt@6.0.0", "", { "dependencies": { "abbrev": "^1.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g=="], + + "normalize-package-data": ["normalize-package-data@2.5.0", "", { "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" } }, "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="], + + "normalize-url": ["normalize-url@1.9.1", "", { "dependencies": { "object-assign": "^4.0.1", "prepend-http": "^1.0.0", "query-string": "^4.1.0", "sort-keys": "^1.0.0" } }, "sha512-A48My/mtCklowHBlI8Fq2jFWK4tX4lJ5E6ytFsSOq1fzpvT0SQSgKhSg7lN5c2uYFOrUAOQp6zhhJnpp1eMloQ=="], + + "npm-bundled": ["npm-bundled@1.1.2", "", { "dependencies": { "npm-normalize-package-bin": "^1.0.1" } }, "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ=="], + + "npm-install-checks": ["npm-install-checks@6.3.0", "", { "dependencies": { "semver": "^7.1.1" } }, "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw=="], + + "npm-normalize-package-bin": ["npm-normalize-package-bin@1.0.1", "", {}, "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA=="], + + "npm-package-arg": ["npm-package-arg@8.1.1", "", { "dependencies": { "hosted-git-info": "^3.0.6", "semver": "^7.0.0", "validate-npm-package-name": "^3.0.0" } }, "sha512-CsP95FhWQDwNqiYS+Q0mZ7FAEDytDZAkNxQqea6IaAFJTAY9Lhhqyl0irU/6PMc7BGfUmnsbHcqxJD7XuVM/rg=="], + + "npm-packlist": ["npm-packlist@5.1.1", "", { "dependencies": { "glob": "^8.0.1", "ignore-walk": "^5.0.1", "npm-bundled": "^1.1.2", "npm-normalize-package-bin": "^1.0.1" }, "bin": { "npm-packlist": "bin/index.js" } }, "sha512-UfpSvQ5YKwctmodvPPkK6Fwk603aoVsf8AEbmVKAEECrfvL8SSe1A2YIwrJ6xmTHAITKPwwZsWo7WwEbNk0kxw=="], + + "npm-pick-manifest": ["npm-pick-manifest@8.0.2", "", { "dependencies": { "npm-install-checks": "^6.0.0", "npm-normalize-package-bin": "^3.0.0", "npm-package-arg": "^10.0.0", "semver": "^7.3.5" } }, "sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg=="], + + "npm-registry-fetch": ["npm-registry-fetch@14.0.5", "", { "dependencies": { "make-fetch-happen": "^11.0.0", "minipass": "^5.0.0", "minipass-fetch": "^3.0.0", "minipass-json-stream": "^1.0.1", "minizlib": "^2.1.2", "npm-package-arg": "^10.0.0", "proc-log": "^3.0.0" } }, "sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA=="], + + "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], + + "npmlog": ["npmlog@6.0.2", "", { "dependencies": { "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", "gauge": "^4.0.3", "set-blocking": "^2.0.0" } }, "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg=="], + + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + + "number-is-nan": ["number-is-nan@1.0.1", "", {}, "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ=="], + + "nwsapi": ["nwsapi@2.2.16", "", {}, "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ=="], + + "nx": ["nx@16.10.0", "", { "dependencies": { "@nrwl/tao": "16.10.0", "@parcel/watcher": "2.0.4", "@yarnpkg/lockfile": "^1.1.0", "@yarnpkg/parsers": "3.0.0-rc.46", "@zkochan/js-yaml": "0.0.6", "axios": "^1.0.0", "chalk": "^4.1.0", "cli-cursor": "3.1.0", "cli-spinners": "2.6.1", "cliui": "^8.0.1", "dotenv": "~16.3.1", "dotenv-expand": "~10.0.0", "enquirer": "~2.3.6", "figures": "3.2.0", "flat": "^5.0.2", "fs-extra": "^11.1.0", "glob": "7.1.4", "ignore": "^5.0.4", "jest-diff": "^29.4.1", "js-yaml": "4.1.0", "jsonc-parser": "3.2.0", "lines-and-columns": "~2.0.3", "minimatch": "3.0.5", "node-machine-id": "1.1.12", "npm-run-path": "^4.0.1", "open": "^8.4.0", "semver": "7.5.3", "string-width": "^4.2.3", "strong-log-transformer": "^2.1.0", "tar-stream": "~2.2.0", "tmp": "~0.2.1", "tsconfig-paths": "^4.1.2", "tslib": "^2.3.0", "v8-compile-cache": "2.3.0", "yargs": "^17.6.2", "yargs-parser": "21.1.1" }, "optionalDependencies": { "@nx/nx-darwin-arm64": "16.10.0", "@nx/nx-darwin-x64": "16.10.0", "@nx/nx-freebsd-x64": "16.10.0", "@nx/nx-linux-arm-gnueabihf": "16.10.0", "@nx/nx-linux-arm64-gnu": "16.10.0", "@nx/nx-linux-arm64-musl": "16.10.0", "@nx/nx-linux-x64-gnu": "16.10.0", "@nx/nx-linux-x64-musl": "16.10.0", "@nx/nx-win32-arm64-msvc": "16.10.0", "@nx/nx-win32-x64-msvc": "16.10.0" }, "peerDependencies": { "@swc-node/register": "^1.6.7", "@swc/core": "^1.3.85" }, "optionalPeers": ["@swc-node/register", "@swc/core"], "bin": { "nx": "bin/nx.js" } }, "sha512-gZl4iCC0Hx0Qe1VWmO4Bkeul2nttuXdPpfnlcDKSACGu3ZIo+uySqwOF8yBAxSTIf8xe2JRhgzJN1aFkuezEBg=="], + + "nypm": ["nypm@0.5.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "pathe": "^2.0.2", "pkg-types": "^1.3.1", "tinyexec": "^0.3.2", "ufo": "^1.5.4" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-AHzvnyUJYSrrphPhRWWZNcoZfArGNp3Vrc4pm/ZurO74tYNTgAPrEyBQEKy+qioqmWlPXwvMZCG2wOaHlPG0Pw=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@2.1.1", "", {}, "sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ=="], + + "object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "object-is": ["object-is@1.1.6", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" } }, "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.entries": ["object.entries@1.1.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ=="], + + "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], + + "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + + "objectorarray": ["objectorarray@1.0.5", "", {}, "sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg=="], + + "obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="], + + "ohash": ["ohash@1.1.4", "", {}, "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g=="], + + "oidc-client": ["oidc-client@1.11.5", "", { "dependencies": { "acorn": "^7.4.1", "base64-js": "^1.5.1", "core-js": "^3.8.3", "crypto-js": "^4.0.0", "serialize-javascript": "^4.0.0" } }, "sha512-LcKrKC8Av0m/KD/4EFmo9Sg8fSQ+WFJWBrmtWd+tZkNn3WT/sQG3REmPANE9tzzhbjW6VkTNy4xhAXCfPApAOg=="], + + "oidc-client-ts": ["oidc-client-ts@3.1.0", "", { "dependencies": { "jwt-decode": "^4.0.0" } }, "sha512-IDopEXjiwjkmJLYZo6BTlvwOtnlSniWZkKZoXforC/oLZHC9wkIxd25Kwtmo5yKFMMVcsp3JY6bhcNJqdYk8+g=="], + + "ol": ["ol@7.5.2", "", { "dependencies": { "earcut": "^2.2.3", "geotiff": "^2.0.7", "ol-mapbox-style": "^10.1.0", "pbf": "3.2.1", "rbush": "^3.0.1" } }, "sha512-HJbb3CxXrksM6ct367LsP3N+uh+iBBMdP3DeGGipdV9YAYTP0vTJzqGnoqQ6C2IW4qf8krw9yuyQbc9fjOIaOQ=="], + + "ol-mapbox-style": ["ol-mapbox-style@10.7.0", "", { "dependencies": { "@mapbox/mapbox-gl-style-spec": "^13.23.1", "mapbox-to-css-font": "^2.4.1", "ol": "^7.3.0" } }, "sha512-S/UdYBuOjrotcR95Iq9AejGYbifKeZE85D9VtH11ryJLQPTZXZSW1J5bIXcr4AlAH6tyjPPHTK34AdkwB32Myw=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "on-headers": ["on-headers@1.0.2", "", {}, "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], + + "open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + + "opencollective-postinstall": ["opencollective-postinstall@2.0.3", "", { "bin": { "opencollective-postinstall": "index.js" } }, "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q=="], + + "opener": ["opener@1.5.2", "", { "bin": { "opener": "bin/opener-bin.js" } }, "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A=="], + + "optimize-css-assets-webpack-plugin": ["optimize-css-assets-webpack-plugin@6.0.1", "", { "dependencies": { "cssnano": "^5.0.2", "last-call-webpack-plugin": "^3.0.0", "postcss": "^8.2.1" }, "peerDependencies": { "webpack": "^4.0.0" } }, "sha512-BshV2UZPfggZLdUfN3zFBbG4sl/DynUI+YCB6fRRDWaqO2OiWN8GPcp4Y0/fEV6B3k9Hzyk3czve3V/8B/SzKQ=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], + + "os-browserify": ["os-browserify@0.3.0", "", {}, "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A=="], + + "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], + + "ospath": ["ospath@1.2.2", "", {}, "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA=="], + + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + + "p-defer": ["p-defer@4.0.1", "", {}, "sha512-Mr5KC5efvAK5VUptYEIopP1bakB85k2IWXaRC0rsh1uwn1L6M0LVml8OIQ4Gudg4oyZakf7FmeRLkMMtZW1i5A=="], + + "p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="], + + "p-map-series": ["p-map-series@2.1.0", "", {}, "sha512-RpYIIK1zXSNEOdwxcfe7FdvGcs7+y5n8rifMhMNWvaxRNMPINJHF5GDeuVxWqnfrcHPSCnp7Oo5yNXHId9Av2Q=="], + + "p-pipe": ["p-pipe@3.1.0", "", {}, "sha512-08pj8ATpzMR0Y80x50yJHn37NF6vjrqHutASaX5LiH5npS9XPvrUmscd9MF5R4fuYRHOxQR1FfMIlF7AzwoPqw=="], + + "p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="], + + "p-reduce": ["p-reduce@2.1.0", "", {}, "sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw=="], + + "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], + + "p-timeout": ["p-timeout@3.2.0", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "p-waterfall": ["p-waterfall@2.1.1", "", { "dependencies": { "p-reduce": "^2.0.0" } }, "sha512-RRTnDb2TBG/epPRI2yYXsimO0v3BXC8Yd3ogr1545IaqKK17VGhbWVeGGN+XfCm/08OK8635nH31c8bATkHuSw=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "pacote": ["pacote@15.2.0", "", { "dependencies": { "@npmcli/git": "^4.0.0", "@npmcli/installed-package-contents": "^2.0.1", "@npmcli/promise-spawn": "^6.0.1", "@npmcli/run-script": "^6.0.0", "cacache": "^17.0.0", "fs-minipass": "^3.0.0", "minipass": "^5.0.0", "npm-package-arg": "^10.0.0", "npm-packlist": "^7.0.0", "npm-pick-manifest": "^8.0.0", "npm-registry-fetch": "^14.0.0", "proc-log": "^3.0.0", "promise-retry": "^2.0.1", "read-package-json": "^6.0.0", "read-package-json-fast": "^3.0.0", "sigstore": "^1.3.0", "ssri": "^10.0.0", "tar": "^6.1.11" }, "bin": { "pacote": "lib/bin.js" } }, "sha512-rJVZeIwHTUta23sIZgEIM62WYwbmGbThdbnkt81ravBplQv+HjyroqnLRNH2+sLJHcGZmLRmhPwACqhfTcOmnA=="], + + "pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="], + + "param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-asn1": ["parse-asn1@5.1.7", "", { "dependencies": { "asn1.js": "^4.10.1", "browserify-aes": "^1.2.0", "evp_bytestokey": "^1.0.3", "hash-base": "~3.0", "pbkdf2": "^3.1.2", "safe-buffer": "^5.2.1" } }, "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg=="], + + "parse-headers": ["parse-headers@2.0.5", "", {}, "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA=="], + + "parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="], + + "parse-path": ["parse-path@7.0.0", "", { "dependencies": { "protocols": "^2.0.0" } }, "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog=="], + + "parse-url": ["parse-url@8.1.0", "", { "dependencies": { "parse-path": "^7.0.0" } }, "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w=="], + + "parse5": ["parse5@7.2.1", "", { "dependencies": { "entities": "^4.5.0" } }, "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="], + + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-is-inside": ["path-is-inside@1.0.2", "", {}, "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "pathe": ["pathe@2.0.2", "", {}, "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w=="], + + "pause-stream": ["pause-stream@0.0.11", "", { "dependencies": { "through": "~2.3" } }, "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A=="], + + "pbf": ["pbf@3.2.1", "", { "dependencies": { "ieee754": "^1.1.12", "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ=="], + + "pbkdf2": ["pbkdf2@3.1.2", "", { "dependencies": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", "ripemd160": "^2.0.1", "safe-buffer": "^5.0.1", "sha.js": "^2.4.8" } }, "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA=="], + + "peek-stream": ["peek-stream@1.1.3", "", { "dependencies": { "buffer-from": "^1.0.0", "duplexify": "^3.5.0", "through2": "^2.0.3" } }, "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA=="], + + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], + + "performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pify": ["pify@5.0.0", "", {}, "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA=="], + + "pinkie": ["pinkie@2.0.4", "", {}, "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg=="], + + "pinkie-promise": ["pinkie-promise@2.0.1", "", { "dependencies": { "pinkie": "^2.0.0" } }, "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw=="], + + "pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="], + + "pkg-config": ["pkg-config@1.1.1", "", { "dependencies": { "debug-log": "^1.0.0", "find-root": "^1.0.0", "xtend": "^4.0.1" } }, "sha512-ft/WI9YK6FuTuw4Ql+QUaNXtm/ASQNqDUUsZEgFZKyFpW6amyP8Gx01xrRs8KdiNbbqXfYxkOXplpq1euWbOjw=="], + + "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], + + "pkg-install": ["pkg-install@1.0.0", "", { "dependencies": { "@types/execa": "^0.9.0", "@types/node": "^11.9.4", "execa": "^1.0.0" } }, "sha512-UGI8bfhrDb1KN01RZ7Bq08GRQc8rmVjxQ2up0g4mUHPCYDTK1FzQ0PMmLOBCHg3yaIijZ2U3Fn9ofLa4N392Ug=="], + + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], + + "playwright": ["playwright@1.50.0", "", { "dependencies": { "playwright-core": "1.50.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-+GinGfGTrd2IfX1TA4N2gNmeIksSb+IAe589ZH+FlmpV3MYTx6+buChGIuDLQwrGNCw2lWibqV50fU510N7S+w=="], + + "playwright-core": ["playwright-core@1.50.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-CXkSSlr4JaZs2tZHI40DsZUN/NIwgaUPsyLuOAaIZp2CyF2sN5MM5NJsyB188lFSSozFxQ5fPT4qM+f0tH/6wQ=="], + + "please-upgrade-node": ["please-upgrade-node@3.2.0", "", { "dependencies": { "semver-compare": "^1.0.0" } }, "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg=="], + + "polished": ["polished@4.3.1", "", { "dependencies": { "@babel/runtime": "^7.17.8" } }, "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA=="], + + "popmotion": ["popmotion@11.0.3", "", { "dependencies": { "framesync": "6.0.1", "hey-listen": "^1.0.8", "style-value-types": "5.0.0", "tslib": "^2.1.0" } }, "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA=="], + + "portfinder": ["portfinder@1.0.32", "", { "dependencies": { "async": "^2.6.4", "debug": "^3.2.7", "mkdirp": "^0.5.6" } }, "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.0.0", "", {}, "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q=="], + + "postcss": ["postcss@8.5.1", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ=="], + + "postcss-attribute-case-insensitive": ["postcss-attribute-case-insensitive@5.0.2", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ=="], + + "postcss-calc": ["postcss-calc@8.2.4", "", { "dependencies": { "postcss-selector-parser": "^6.0.9", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.2" } }, "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q=="], + + "postcss-clamp": ["postcss-clamp@4.1.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.6" } }, "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow=="], + + "postcss-color-functional-notation": ["postcss-color-functional-notation@4.2.4", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg=="], + + "postcss-color-hex-alpha": ["postcss-color-hex-alpha@8.0.4", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ=="], + + "postcss-color-rebeccapurple": ["postcss-color-rebeccapurple@7.1.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg=="], + + "postcss-colormin": ["postcss-colormin@5.3.1", "", { "dependencies": { "browserslist": "^4.21.4", "caniuse-api": "^3.0.0", "colord": "^2.9.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ=="], + + "postcss-convert-values": ["postcss-convert-values@5.1.3", "", { "dependencies": { "browserslist": "^4.21.4", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA=="], + + "postcss-custom-media": ["postcss-custom-media@8.0.2", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.3" } }, "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg=="], + + "postcss-custom-properties": ["postcss-custom-properties@12.1.11", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ=="], + + "postcss-custom-selectors": ["postcss-custom-selectors@6.0.3", "", { "dependencies": { "postcss-selector-parser": "^6.0.4" }, "peerDependencies": { "postcss": "^8.3" } }, "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg=="], + + "postcss-dir-pseudo-class": ["postcss-dir-pseudo-class@6.0.5", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA=="], + + "postcss-discard-comments": ["postcss-discard-comments@5.1.2", "", { "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ=="], + + "postcss-discard-duplicates": ["postcss-discard-duplicates@5.1.0", "", { "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw=="], + + "postcss-discard-empty": ["postcss-discard-empty@5.1.1", "", { "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A=="], + + "postcss-discard-overridden": ["postcss-discard-overridden@5.1.0", "", { "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw=="], + + "postcss-double-position-gradients": ["postcss-double-position-gradients@3.1.2", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^1.1.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ=="], + + "postcss-env-function": ["postcss-env-function@4.0.6", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA=="], + + "postcss-focus-visible": ["postcss-focus-visible@6.0.4", "", { "dependencies": { "postcss-selector-parser": "^6.0.9" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw=="], + + "postcss-focus-within": ["postcss-focus-within@5.0.4", "", { "dependencies": { "postcss-selector-parser": "^6.0.9" }, "peerDependencies": { "postcss": "^8.4" } }, "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ=="], + + "postcss-font-variant": ["postcss-font-variant@5.0.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA=="], + + "postcss-gap-properties": ["postcss-gap-properties@3.0.5", "", { "peerDependencies": { "postcss": "^8.2" } }, "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg=="], + + "postcss-image-set-function": ["postcss-image-set-function@4.0.7", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw=="], + + "postcss-import": ["postcss-import@14.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw=="], + + "postcss-initial": ["postcss-initial@4.0.1", "", { "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ=="], + + "postcss-js": ["postcss-js@4.0.1", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw=="], + + "postcss-lab-function": ["postcss-lab-function@4.2.1", "", { "dependencies": { "@csstools/postcss-progressive-custom-properties": "^1.1.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w=="], + + "postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="], + + "postcss-loader": ["postcss-loader@6.2.1", "", { "dependencies": { "cosmiconfig": "^7.0.0", "klona": "^2.0.5", "semver": "^7.3.5" }, "peerDependencies": { "postcss": "^7.0.0 || ^8.0.1", "webpack": "^5.0.0" } }, "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q=="], + + "postcss-logical": ["postcss-logical@5.0.4", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g=="], + + "postcss-media-minmax": ["postcss-media-minmax@5.0.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ=="], + + "postcss-merge-longhand": ["postcss-merge-longhand@5.1.7", "", { "dependencies": { "postcss-value-parser": "^4.2.0", "stylehacks": "^5.1.1" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ=="], + + "postcss-merge-rules": ["postcss-merge-rules@5.1.4", "", { "dependencies": { "browserslist": "^4.21.4", "caniuse-api": "^3.0.0", "cssnano-utils": "^3.1.0", "postcss-selector-parser": "^6.0.5" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g=="], + + "postcss-minify-font-values": ["postcss-minify-font-values@5.1.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA=="], + + "postcss-minify-gradients": ["postcss-minify-gradients@5.1.1", "", { "dependencies": { "colord": "^2.9.1", "cssnano-utils": "^3.1.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw=="], + + "postcss-minify-params": ["postcss-minify-params@5.1.4", "", { "dependencies": { "browserslist": "^4.21.4", "cssnano-utils": "^3.1.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw=="], + + "postcss-minify-selectors": ["postcss-minify-selectors@5.2.1", "", { "dependencies": { "postcss-selector-parser": "^6.0.5" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg=="], + + "postcss-modules-extract-imports": ["postcss-modules-extract-imports@3.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q=="], + + "postcss-modules-local-by-default": ["postcss-modules-local-by-default@4.2.0", "", { "dependencies": { "icss-utils": "^5.0.0", "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.1.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw=="], + + "postcss-modules-scope": ["postcss-modules-scope@3.2.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA=="], + + "postcss-modules-values": ["postcss-modules-values@4.0.0", "", { "dependencies": { "icss-utils": "^5.0.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ=="], + + "postcss-nested": ["postcss-nested@6.0.0", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w=="], + + "postcss-nesting": ["postcss-nesting@10.2.0", "", { "dependencies": { "@csstools/selector-specificity": "^2.0.0", "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA=="], + + "postcss-normalize-charset": ["postcss-normalize-charset@5.1.0", "", { "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg=="], + + "postcss-normalize-display-values": ["postcss-normalize-display-values@5.1.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA=="], + + "postcss-normalize-positions": ["postcss-normalize-positions@5.1.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg=="], + + "postcss-normalize-repeat-style": ["postcss-normalize-repeat-style@5.1.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g=="], + + "postcss-normalize-string": ["postcss-normalize-string@5.1.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w=="], + + "postcss-normalize-timing-functions": ["postcss-normalize-timing-functions@5.1.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg=="], + + "postcss-normalize-unicode": ["postcss-normalize-unicode@5.1.1", "", { "dependencies": { "browserslist": "^4.21.4", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA=="], + + "postcss-normalize-url": ["postcss-normalize-url@5.1.0", "", { "dependencies": { "normalize-url": "^6.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew=="], + + "postcss-normalize-whitespace": ["postcss-normalize-whitespace@5.1.1", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA=="], + + "postcss-opacity-percentage": ["postcss-opacity-percentage@1.1.3", "", { "peerDependencies": { "postcss": "^8.2" } }, "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A=="], + + "postcss-ordered-values": ["postcss-ordered-values@5.1.3", "", { "dependencies": { "cssnano-utils": "^3.1.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ=="], + + "postcss-overflow-shorthand": ["postcss-overflow-shorthand@3.0.4", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A=="], + + "postcss-page-break": ["postcss-page-break@3.0.4", "", { "peerDependencies": { "postcss": "^8" } }, "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ=="], + + "postcss-place": ["postcss-place@7.0.5", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g=="], + + "postcss-preset-env": ["postcss-preset-env@7.8.3", "", { "dependencies": { "@csstools/postcss-cascade-layers": "^1.1.1", "@csstools/postcss-color-function": "^1.1.1", "@csstools/postcss-font-format-keywords": "^1.0.1", "@csstools/postcss-hwb-function": "^1.0.2", "@csstools/postcss-ic-unit": "^1.0.1", "@csstools/postcss-is-pseudo-class": "^2.0.7", "@csstools/postcss-nested-calc": "^1.0.0", "@csstools/postcss-normalize-display-values": "^1.0.1", "@csstools/postcss-oklab-function": "^1.1.1", "@csstools/postcss-progressive-custom-properties": "^1.3.0", "@csstools/postcss-stepped-value-functions": "^1.0.1", "@csstools/postcss-text-decoration-shorthand": "^1.0.0", "@csstools/postcss-trigonometric-functions": "^1.0.2", "@csstools/postcss-unset-value": "^1.0.2", "autoprefixer": "^10.4.13", "browserslist": "^4.21.4", "css-blank-pseudo": "^3.0.3", "css-has-pseudo": "^3.0.4", "css-prefers-color-scheme": "^6.0.3", "cssdb": "^7.1.0", "postcss-attribute-case-insensitive": "^5.0.2", "postcss-clamp": "^4.1.0", "postcss-color-functional-notation": "^4.2.4", "postcss-color-hex-alpha": "^8.0.4", "postcss-color-rebeccapurple": "^7.1.1", "postcss-custom-media": "^8.0.2", "postcss-custom-properties": "^12.1.10", "postcss-custom-selectors": "^6.0.3", "postcss-dir-pseudo-class": "^6.0.5", "postcss-double-position-gradients": "^3.1.2", "postcss-env-function": "^4.0.6", "postcss-focus-visible": "^6.0.4", "postcss-focus-within": "^5.0.4", "postcss-font-variant": "^5.0.0", "postcss-gap-properties": "^3.0.5", "postcss-image-set-function": "^4.0.7", "postcss-initial": "^4.0.1", "postcss-lab-function": "^4.2.1", "postcss-logical": "^5.0.4", "postcss-media-minmax": "^5.0.0", "postcss-nesting": "^10.2.0", "postcss-opacity-percentage": "^1.1.2", "postcss-overflow-shorthand": "^3.0.4", "postcss-page-break": "^3.0.4", "postcss-place": "^7.0.5", "postcss-pseudo-class-any-link": "^7.1.6", "postcss-replace-overflow-wrap": "^4.0.0", "postcss-selector-not": "^6.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag=="], + + "postcss-pseudo-class-any-link": ["postcss-pseudo-class-any-link@7.1.6", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w=="], + + "postcss-reduce-initial": ["postcss-reduce-initial@5.1.2", "", { "dependencies": { "browserslist": "^4.21.4", "caniuse-api": "^3.0.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg=="], + + "postcss-reduce-transforms": ["postcss-reduce-transforms@5.1.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ=="], + + "postcss-replace-overflow-wrap": ["postcss-replace-overflow-wrap@4.0.0", "", { "peerDependencies": { "postcss": "^8.0.3" } }, "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw=="], + + "postcss-selector-not": ["postcss-selector-not@6.0.1", "", { "dependencies": { "postcss-selector-parser": "^6.0.10" }, "peerDependencies": { "postcss": "^8.2" } }, "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "postcss-svgo": ["postcss-svgo@5.1.0", "", { "dependencies": { "postcss-value-parser": "^4.2.0", "svgo": "^2.7.0" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA=="], + + "postcss-unique-selectors": ["postcss-unique-selectors@5.1.1", "", { "dependencies": { "postcss-selector-parser": "^6.0.5" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prepend-http": ["prepend-http@1.0.4", "", {}, "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg=="], + + "prettier": ["prettier@3.4.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="], + + "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], + + "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.9", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig-melody": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig-melody", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-r0i3uhaZAXYP0At5xGfJH876W3HHGHDp+LCRUJrs57PBeQ6mYHMwr25KH8NPX44F2yGTvdnH7OqCshlQx183Eg=="], + + "pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], + + "pretty-error": ["pretty-error@4.0.0", "", { "dependencies": { "lodash": "^4.17.20", "renderkid": "^3.0.0" } }, "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw=="], + + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + + "pretty-hrtime": ["pretty-hrtime@1.0.3", "", {}, "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A=="], + + "proc-log": ["proc-log@3.0.0", "", {}, "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A=="], + + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + + "progress-events": ["progress-events@1.0.1", "", {}, "sha512-MOzLIwhpt64KIVN64h1MwdKWiyKFNc/S6BoYKPIVUHFg0/eIEyBulhWCgn678v/4c0ri3FdGuzXymNCv02MUIw=="], + + "promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="], + + "promise-polyfill": ["promise-polyfill@6.1.0", "", {}, "sha512-g0LWaH0gFsxovsU7R5LrrhHhWAWiHRnh1GPrhXnPgYsDkIqjRYUYSZEsej/wtleDrz5xVSIDbeKfidztp2XHFQ=="], + + "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + + "promzard": ["promzard@1.0.2", "", { "dependencies": { "read": "^3.0.1" } }, "sha512-2FPputGL+mP3jJ3UZg/Dl9YOkovB7DX0oOr+ck5QbZ5MtORtds8k/BZdn+02peDLI8/YWbmzx34k5fA+fHvCVQ=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "prop-types-exact": ["prop-types-exact@1.2.7", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "hasown": "^2.0.2", "isarray": "^2.0.5", "object.assign": "^4.1.7", "own-keys": "^1.0.0" } }, "sha512-A4RaV6mg3jocQqBYmqi2ojJ2VnV4AKTEHhl3xHsud08/u87gcVJc8DUOtgnPegoOCQv/shUqEk4eZGYibjnHzQ=="], + + "protobufjs": ["protobufjs@7.4.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw=="], + + "protocol-buffers-schema": ["protocol-buffers-schema@3.6.0", "", {}, "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="], + + "protocols": ["protocols@2.0.1", "", {}, "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q=="], + + "protons-runtime": ["protons-runtime@5.5.0", "", { "dependencies": { "uint8-varint": "^2.0.2", "uint8arraylist": "^2.4.3", "uint8arrays": "^5.0.1" } }, "sha512-EsALjF9QsrEk6gbCx3lmfHxVN0ah7nG3cY7GySD4xf4g8cr7g543zB88Foh897Sr1RQJ9yDCUsoT1i1H/cVUFA=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "proxy-from-env": ["proxy-from-env@1.0.0", "", {}, "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A=="], + + "ps-tree": ["ps-tree@1.2.0", "", { "dependencies": { "event-stream": "=3.3.4" }, "bin": { "ps-tree": "./bin/ps-tree.js" } }, "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA=="], + + "pseudomap": ["pseudomap@1.0.2", "", {}, "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ=="], + + "psl": ["psl@1.15.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w=="], + + "public-encrypt": ["public-encrypt@4.0.3", "", { "dependencies": { "bn.js": "^4.1.0", "browserify-rsa": "^4.0.0", "create-hash": "^1.1.0", "parse-asn1": "^5.0.0", "randombytes": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q=="], + + "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], + + "pumpify": ["pumpify@1.5.1", "", { "dependencies": { "duplexify": "^3.6.0", "inherits": "^2.0.3", "pump": "^2.0.0" } }, "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "puppeteer-core": ["puppeteer-core@2.1.1", "", { "dependencies": { "@types/mime-types": "^2.1.0", "debug": "^4.1.0", "extract-zip": "^1.6.6", "https-proxy-agent": "^4.0.0", "mime": "^2.0.3", "mime-types": "^2.1.25", "progress": "^2.0.1", "proxy-from-env": "^1.0.0", "rimraf": "^2.6.1", "ws": "^6.1.0" } }, "sha512-n13AWriBMPYxnpbb6bnaY5YoY6rGj8vPLrz6CZF3o0qJNEwlcfJVxBzYZ0NJsQ21UbdJoijPCDrM++SUVEz7+w=="], + + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + + "qs": ["qs@6.13.1", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg=="], + + "query-string": ["query-string@6.14.1", "", { "dependencies": { "decode-uri-component": "^0.2.0", "filter-obj": "^1.1.0", "split-on-first": "^1.0.0", "strict-uri-encode": "^2.0.0" } }, "sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw=="], + + "querystring-es3": ["querystring-es3@0.2.1", "", {}, "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA=="], + + "querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + + "quickselect": ["quickselect@2.0.0", "", {}, "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw=="], + + "rabin-rs": ["rabin-rs@2.1.0", "", {}, "sha512-5y72gAXPzIBsAMHcpxZP8eMDuDT98qMP1BqSDHRbHkJJXEgWIN1lA47LxUqzsK6jknOJtgfkQr9v+7qMlFDm6g=="], + + "raf": ["raf@3.4.1", "", { "dependencies": { "performance-now": "^2.1.0" } }, "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA=="], + + "ramda": ["ramda@0.29.0", "", {}, "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA=="], + + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], + + "randomfill": ["randomfill@1.0.4", "", { "dependencies": { "randombytes": "^2.0.5", "safe-buffer": "^5.1.0" } }, "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw=="], + + "range-parser": ["range-parser@1.2.0", "", {}, "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A=="], + + "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], + + "rbush": ["rbush@3.0.1", "", { "dependencies": { "quickselect": "^2.0.0" } }, "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w=="], + + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + + "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], + + "react-color": ["react-color@2.19.3", "", { "dependencies": { "@icons/material": "^0.2.4", "lodash": "^4.17.15", "lodash-es": "^4.17.15", "material-colors": "^1.2.1", "prop-types": "^15.5.10", "reactcss": "^1.2.0", "tinycolor2": "^1.4.1" }, "peerDependencies": { "react": "*" } }, "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA=="], + + "react-colorful": ["react-colorful@5.6.1", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw=="], + + "react-dates": ["react-dates@21.8.0", "", { "dependencies": { "airbnb-prop-types": "^2.15.0", "consolidated-events": "^1.1.1 || ^2.0.0", "enzyme-shallow-equal": "^1.0.0", "is-touch-device": "^1.0.1", "lodash": "^4.1.1", "object.assign": "^4.1.0", "object.values": "^1.1.0", "prop-types": "^15.7.2", "raf": "^3.4.1", "react-moment-proptypes": "^1.6.0", "react-outside-click-handler": "^1.2.4", "react-portal": "^4.2.0", "react-with-direction": "^1.3.1", "react-with-styles": "^4.1.0", "react-with-styles-interface-css": "^6.0.0" }, "peerDependencies": { "@babel/runtime": "^7.0.0", "moment": "^2.18.1", "react": "^0.14 || ^15.5.4 || ^16.1.1", "react-dom": "^0.14 || ^15.5.4 || ^16.1.1" } }, "sha512-PPriGqi30CtzZmoHiGdhlA++YPYPYGCZrhydYmXXQ6RAvAsaONcPtYgXRTLozIOrsQ5mSo40+DiA5eOFHnZ6xw=="], + + "react-day-picker": ["react-day-picker@8.10.1", "", { "peerDependencies": { "date-fns": "^2.28.0 || ^3.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA=="], + + "react-dnd": ["react-dnd@14.0.2", "", { "dependencies": { "@react-dnd/invariant": "^2.0.0", "@react-dnd/shallowequal": "^2.0.0", "dnd-core": "14.0.0", "fast-deep-equal": "^3.1.3", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { "@types/hoist-non-react-statics": ">= 3.3.1", "@types/node": ">= 12", "@types/react": ">= 16", "react": ">= 16.14" }, "optionalPeers": ["@types/hoist-non-react-statics", "@types/node", "@types/react"] }, "sha512-JoEL78sBCg8SzjOKMlkR70GWaPORudhWuTNqJ56lb2P8Vq0eM2+er3ZrMGiSDhOmzaRPuA9SNBz46nHCrjn11A=="], + + "react-dnd-html5-backend": ["react-dnd-html5-backend@14.0.0", "", { "dependencies": { "dnd-core": "14.0.0" } }, "sha512-2wAQqRFC1hbRGmk6+dKhOXsyQQOn3cN8PSZyOUeOun9J8t3tjZ7PS2+aFu7CVu2ujMDwTJR3VTwZh8pj2kCv7g=="], + + "react-docgen": ["react-docgen@7.1.1", "", { "dependencies": { "@babel/core": "^7.18.9", "@babel/traverse": "^7.18.9", "@babel/types": "^7.18.9", "@types/babel__core": "^7.18.0", "@types/babel__traverse": "^7.18.0", "@types/doctrine": "^0.0.9", "@types/resolve": "^1.20.2", "doctrine": "^3.0.0", "resolve": "^1.22.1", "strip-indent": "^4.0.0" } }, "sha512-hlSJDQ2synMPKFZOsKo9Hi8WWZTC7POR8EmWvTSjow+VDgKzkmjQvFm2fk0tmRw+f0vTOIYKlarR0iL4996pdg=="], + + "react-docgen-typescript": ["react-docgen-typescript@2.2.2", "", { "peerDependencies": { "typescript": ">= 4.3.x" } }, "sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg=="], + + "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + + "react-draggable": ["react-draggable@4.4.6", "", { "dependencies": { "clsx": "^1.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.3.0", "react-dom": ">= 16.3.0" } }, "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw=="], + + "react-dropzone": ["react-dropzone@10.2.2", "", { "dependencies": { "attr-accept": "^2.0.0", "file-selector": "^0.1.12", "prop-types": "^15.7.2" }, "peerDependencies": { "react": ">= 16.8" } }, "sha512-U5EKckXVt6IrEyhMMsgmHQiWTGLudhajPPG77KFSvgsMqNEHSyGpqWvOMc5+DhEah/vH4E1n+J5weBNLd5VtyA=="], + + "react-element-to-jsx-string": ["react-element-to-jsx-string@15.0.0", "", { "dependencies": { "@base2/pretty-print-object": "1.0.1", "is-plain-object": "5.0.0", "react-is": "18.1.0" }, "peerDependencies": { "react": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0", "react-dom": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0" } }, "sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ=="], + + "react-error-boundary": ["react-error-boundary@3.1.4", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "react": ">=16.13.1" } }, "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA=="], + + "react-i18next": ["react-i18next@12.3.1", "", { "dependencies": { "@babel/runtime": "^7.20.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 19.0.0", "react": ">= 16.8.0" } }, "sha512-5v8E2XjZDFzK7K87eSwC7AJcAkcLt5xYZ4+yTPDAW1i7C93oOY1dnr4BaQM7un4Hm+GmghuiPvevWwlca5PwDA=="], + + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "react-lifecycles-compat": ["react-lifecycles-compat@3.0.4", "", {}, "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="], + + "react-modal": ["react-modal@3.11.2", "", { "dependencies": { "exenv": "^1.2.0", "prop-types": "^15.5.10", "react-lifecycles-compat": "^3.0.0", "warning": "^4.0.3" }, "peerDependencies": { "react": "^0.14.0 || ^15.0.0 || ^16", "react-dom": "^0.14.0 || ^15.0.0 || ^16" } }, "sha512-o8gvvCOFaG1T7W6JUvsYjRjMVToLZgLIsi5kdhFIQCtHxDkA47LznX62j+l6YQkpXDbvQegsDyxe/+JJsFQN7w=="], + + "react-moment-proptypes": ["react-moment-proptypes@1.8.1", "", { "dependencies": { "moment": ">=1.6.0" } }, "sha512-Er940DxWoObfIqPrZNfwXKugjxMIuk1LAuEzn23gytzV6hKS/sw108wibi9QubfMN4h+nrlje8eUCSbQRJo2fQ=="], + + "react-outside-click-handler": ["react-outside-click-handler@1.3.0", "", { "dependencies": { "airbnb-prop-types": "^2.15.0", "consolidated-events": "^1.1.1 || ^2.0.0", "document.contains": "^1.0.1", "object.values": "^1.1.0", "prop-types": "^15.7.2" }, "peerDependencies": { "react": "^0.14 || >=15", "react-dom": "^0.14 || >=15" } }, "sha512-Te/7zFU0oHpAnctl//pP3hEAeobfeHMyygHB8MnjP6sX5OR8KHT1G3jmLsV3U9RnIYo+Yn+peJYWu+D5tUS8qQ=="], + + "react-portal": ["react-portal@4.3.0", "", { "dependencies": { "prop-types": "^15.5.8" }, "peerDependencies": { "react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0", "react-dom": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0" } }, "sha512-qs/2uKq1ifB3J1+K8ExfgUvCDZqlqCkfOEhqTELEDTfosloKiuzOzc7hl7IQ/7nohiFZD41BUYU0boAsIsGYHw=="], + + "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], + + "react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-resizable-panels": ["react-resizable-panels@2.1.7", "", { "peerDependencies": { "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA=="], + + "react-resize-detector": ["react-resize-detector@10.0.1", "", { "dependencies": { "lodash": "^4.17.21" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-CR2EdP83ycGlWkhhrd6+hhZVhPJO4xnzClFCTBXlODVTHOgiDJQu77sBt67J7P3gfU4ec/kOuf2c5EcyTUNLXQ=="], + + "react-router": ["react-router@6.29.0", "", { "dependencies": { "@remix-run/router": "1.22.0" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-DXZJoE0q+KyeVw75Ck6GkPxFak63C4fGqZGNijnWgzB/HzSP1ZfTlBj5COaGWwhrMQ/R8bXiq5Ooy4KG+ReyjQ=="], + + "react-router-dom": ["react-router-dom@6.29.0", "", { "dependencies": { "@remix-run/router": "1.22.0", "react-router": "6.29.0" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-pkEbJPATRJ2iotK+wUwHfy0xs2T59YPEN8BQxVCPeBZvK7kfPESRc/nyxzdcxR17hXgUPYx2whMwl+eo9cUdnQ=="], + + "react-select": ["react-select@5.7.4", "", { "dependencies": { "@babel/runtime": "^7.12.0", "@emotion/cache": "^11.4.0", "@emotion/react": "^11.8.1", "@floating-ui/dom": "^1.0.1", "@types/react-transition-group": "^4.4.0", "memoize-one": "^6.0.0", "prop-types": "^15.6.0", "react-transition-group": "^4.3.0", "use-isomorphic-layout-effect": "^1.1.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-NhuE56X+p9QDFh4BgeygHFIvJJszO1i1KSkg/JPcIJrbovyRtI+GuOEa4XzFCEpZRAEoEI8u/cAHK+jG/PgUzQ=="], + + "react-shallow-renderer": ["react-shallow-renderer@16.15.0", "", { "dependencies": { "object-assign": "^4.1.1", "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA=="], + + "react-shepherd": ["react-shepherd@6.1.1", "", { "dependencies": { "shepherd.js": "13.0.3" }, "peerDependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.0.0" } }, "sha512-lylVKsH8w9gV7674RznDhl4uPrTXLYuc2E0+gYJPrz4FymHrhUpDqYvYvqESPODigRK+TFFpTZAUdAZzwzPvRg=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "react-test-renderer": ["react-test-renderer@18.3.1", "", { "dependencies": { "react-is": "^18.3.1", "react-shallow-renderer": "^16.15.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-KkAgygexHUkQqtvvx/otwxtuFu5cVjfzTCtjXLH9boS19/Nbtg84zS7wIQn39G8IlrhThBpQsMKkq5ZHZIYFXA=="], + + "react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="], + + "react-window": ["react-window@1.8.11", "", { "dependencies": { "@babel/runtime": "^7.0.0", "memoize-one": ">=3.1.1 <6" }, "peerDependencies": { "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ=="], + + "react-with-direction": ["react-with-direction@1.4.0", "", { "dependencies": { "airbnb-prop-types": "^2.16.0", "brcast": "^2.0.2", "deepmerge": "^1.5.2", "direction": "^1.0.4", "hoist-non-react-statics": "^3.3.2", "object.assign": "^4.1.2", "object.values": "^1.1.5", "prop-types": "^15.7.2" }, "peerDependencies": { "react": "^0.14 || ^15 || ^16", "react-dom": "^0.14 || ^15 || ^16" } }, "sha512-ybHNPiAmaJpoWwugwqry9Hd1Irl2hnNXlo/2SXQBwbLn/jGMauMS2y9jw+ydyX5V9ICryCqObNSthNt5R94xpg=="], + + "react-with-styles": ["react-with-styles@4.2.0", "", { "dependencies": { "airbnb-prop-types": "^2.14.0", "hoist-non-react-statics": "^3.2.1", "object.assign": "^4.1.0", "prop-types": "^15.7.2", "react-with-direction": "^1.3.1" }, "peerDependencies": { "@babel/runtime": "^7.0.0", "react": ">=0.14" } }, "sha512-tZCTY27KriRNhwHIbg1NkSdTTOSfXDg6Z7s+Q37mtz0Ym7Sc7IOr3PzVt4qJhJMW6Nkvfi3g34FuhtiGAJCBQA=="], + + "react-with-styles-interface-css": ["react-with-styles-interface-css@6.0.0", "", { "dependencies": { "array.prototype.flat": "^1.2.1", "global-cache": "^1.2.1" }, "peerDependencies": { "@babel/runtime": "^7.0.0", "react-with-styles": "^3.0.0 || ^4.0.0" } }, "sha512-6khSG1Trf4L/uXOge/ZAlBnq2O2PEXlQEqAhCRbvzaQU4sksIkdwpCPEl6d+DtP3+IdhyffTWuHDO9lhe1iYvA=="], + + "reactcss": ["reactcss@1.2.3", "", { "dependencies": { "lodash": "^4.0.1" } }, "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A=="], + + "read": ["read@2.1.0", "", { "dependencies": { "mute-stream": "~1.0.0" } }, "sha512-bvxi1QLJHcaywCAEsAk4DG3nVoqiY2Csps3qzWalhj5hFqRn1d/OixkFXtLO1PrgHUcAP0FNaSY/5GYNfENFFQ=="], + + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], + + "read-cmd-shim": ["read-cmd-shim@4.0.0", "", {}, "sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q=="], + + "read-package-json": ["read-package-json@6.0.4", "", { "dependencies": { "glob": "^10.2.2", "json-parse-even-better-errors": "^3.0.0", "normalize-package-data": "^5.0.0", "npm-normalize-package-bin": "^3.0.0" } }, "sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw=="], + + "read-package-json-fast": ["read-package-json-fast@3.0.2", "", { "dependencies": { "json-parse-even-better-errors": "^3.0.0", "npm-normalize-package-bin": "^3.0.0" } }, "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw=="], + + "read-pkg": ["read-pkg@5.2.0", "", { "dependencies": { "@types/normalize-package-data": "^2.4.0", "normalize-package-data": "^2.5.0", "parse-json": "^5.0.0", "type-fest": "^0.6.0" } }, "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg=="], + + "read-pkg-up": ["read-pkg-up@7.0.1", "", { "dependencies": { "find-up": "^4.1.0", "read-pkg": "^5.2.0", "type-fest": "^0.8.1" } }, "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg=="], + + "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "recast": ["recast@0.23.9", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q=="], + + "rechoir": ["rechoir@0.7.1", "", { "dependencies": { "resolve": "^1.9.0" } }, "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg=="], + + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + + "redux": ["redux@4.2.1", "", { "dependencies": { "@babel/runtime": "^7.9.2" } }, "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w=="], + + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], + + "regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.0", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA=="], + + "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], + + "regenerator-transform": ["regenerator-transform@0.15.2", "", { "dependencies": { "@babel/runtime": "^7.8.4" } }, "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "regexpp": ["regexpp@3.2.0", "", {}, "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg=="], + + "regexpu-core": ["regexpu-core@6.2.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.0", "regjsgen": "^0.8.0", "regjsparser": "^0.12.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" } }, "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA=="], + + "registry-auth-token": ["registry-auth-token@3.3.2", "", { "dependencies": { "rc": "^1.1.6", "safe-buffer": "^5.0.1" } }, "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ=="], + + "registry-url": ["registry-url@6.0.1", "", { "dependencies": { "rc": "1.2.8" } }, "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q=="], + + "regjsgen": ["regjsgen@0.8.0", "", {}, "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q=="], + + "regjsparser": ["regjsparser@0.12.0", "", { "dependencies": { "jsesc": "~3.0.2" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ=="], + + "relateurl": ["relateurl@0.2.7", "", {}, "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog=="], + + "remark-external-links": ["remark-external-links@8.0.0", "", { "dependencies": { "extend": "^3.0.0", "is-absolute-url": "^3.0.0", "mdast-util-definitions": "^4.0.0", "space-separated-tokens": "^1.0.0", "unist-util-visit": "^2.0.0" } }, "sha512-5vPSX0kHoSsqtdftSHhIYofVINC8qmp0nctkeU9YoJwV3YfiBRiI6cbFRJ0oI/1F9xS+bopXG0m2KS8VFscuKA=="], + + "remark-gfm": ["remark-gfm@3.0.1", "", { "dependencies": { "@types/mdast": "^3.0.0", "mdast-util-gfm": "^2.0.0", "micromark-extension-gfm": "^2.0.0", "unified": "^10.0.0" } }, "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig=="], + + "remark-slug": ["remark-slug@6.1.0", "", { "dependencies": { "github-slugger": "^1.0.0", "mdast-util-to-string": "^1.0.0", "unist-util-visit": "^2.0.0" } }, "sha512-oGCxDF9deA8phWvxFuyr3oSJsdyUAxMFbA0mZ7Y1Sas+emILtO+e5WutF9564gDsEN4IXaQXm5pFo6MLH+YmwQ=="], + + "renderkid": ["renderkid@3.0.0", "", { "dependencies": { "css-select": "^4.1.3", "dom-converter": "^0.2.0", "htmlparser2": "^6.1.0", "lodash": "^4.17.21", "strip-ansi": "^6.0.1" } }, "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg=="], + + "request-progress": ["request-progress@3.0.0", "", { "dependencies": { "throttleit": "^1.0.0" } }, "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], + + "reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="], + + "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + + "resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "resolve-protobuf-schema": ["resolve-protobuf-schema@2.1.0", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="], + + "resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="], + + "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], + + "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + + "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "rimraf": ["rimraf@4.4.1", "", { "dependencies": { "glob": "^9.2.0" }, "bin": { "rimraf": "dist/cjs/src/bin.js" } }, "sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og=="], + + "ripemd160": ["ripemd160@2.0.2", "", { "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" } }, "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA=="], + + "rollup": ["rollup@2.79.2", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ=="], + + "rollup-plugin-terser": ["rollup-plugin-terser@7.0.2", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "jest-worker": "^26.2.1", "serialize-javascript": "^4.0.0", "terser": "^5.0.0" }, "peerDependencies": { "rollup": "^2.0.0" } }, "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ=="], + + "run-async": ["run-async@2.4.1", "", {}, "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ=="], + + "run-node": ["run-node@1.0.0", "", { "bin": { "run-node": "run-node" } }, "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], + + "rxjs": ["rxjs@7.8.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg=="], + + "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + + "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "sax": ["sax@1.2.4", "", {}, "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="], + + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "schema-utils": ["schema-utils@4.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g=="], + + "seedrandom": ["seedrandom@3.0.5", "", {}, "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="], + + "seek-bzip": ["seek-bzip@1.0.6", "", { "dependencies": { "commander": "^2.8.1" }, "bin": { "seek-bunzip": "bin/seek-bunzip", "seek-table": "bin/seek-bzip-table" } }, "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ=="], + + "select-hose": ["select-hose@2.0.0", "", {}, "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg=="], + + "selfsigned": ["selfsigned@2.4.1", "", { "dependencies": { "@types/node-forge": "^1.3.0", "node-forge": "^1" } }, "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q=="], + + "semver": ["semver@7.7.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ=="], + + "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], + + "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], + + "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], + + "serve": ["serve@14.2.4", "", { "dependencies": { "@zeit/schemas": "2.36.0", "ajv": "8.12.0", "arg": "5.0.2", "boxen": "7.0.0", "chalk": "5.0.1", "chalk-template": "0.4.0", "clipboardy": "3.0.0", "compression": "1.7.4", "is-port-reachable": "4.0.0", "serve-handler": "6.1.6", "update-check": "1.5.4" }, "bin": { "serve": "build/main.js" } }, "sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ=="], + + "serve-handler": ["serve-handler@6.1.6", "", { "dependencies": { "bytes": "3.0.0", "content-disposition": "0.5.2", "mime-types": "2.1.18", "minimatch": "3.1.2", "path-is-inside": "1.0.2", "path-to-regexp": "3.3.0", "range-parser": "1.2.0" } }, "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ=="], + + "serve-index": ["serve-index@1.9.1", "", { "dependencies": { "accepts": "~1.3.4", "batch": "0.6.1", "debug": "2.6.9", "escape-html": "~1.0.3", "http-errors": "~1.6.2", "mime-types": "~2.1.17", "parseurl": "~1.3.2" } }, "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw=="], + + "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], + + "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + + "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "sha.js": ["sha.js@2.4.11", "", { "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" }, "bin": { "sha.js": "./bin.js" } }, "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ=="], + + "shader-loader": ["shader-loader@1.3.1", "", { "dependencies": { "loader-utils": "^1.1.0" } }, "sha512-dt8F9K0x4rjmaFyHh7rNDfpt4LUiR64zhNIEwp2WbE99B3z4ALuvvmhftkElg93dUD6sTmv/aXa/z9SJiEddcA=="], + + "shallow-clone": ["shallow-clone@3.0.1", "", { "dependencies": { "kind-of": "^6.0.2" } }, "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "shelljs": ["shelljs@0.8.5", "", { "dependencies": { "glob": "^7.0.0", "interpret": "^1.0.0", "rechoir": "^0.6.2" }, "bin": { "shjs": "bin/shjs" } }, "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow=="], + + "shepherd.js": ["shepherd.js@13.0.3", "", { "dependencies": { "@floating-ui/dom": "^1.6.5", "@scarf/scarf": "^1.3.0", "deepmerge-ts": "^5.1.0" } }, "sha512-1lQtQUNQYi+8k9BAmbUZh7D2QxFfkxiWKU0XFTbzYaIrCkB4nR0DLQuarH5G7Ym6L8wfbadxP3hJhZ2HzVktaA=="], + + "shx": ["shx@0.3.4", "", { "dependencies": { "minimist": "^1.2.3", "shelljs": "^0.8.5" }, "bin": { "shx": "lib/cli.js" } }, "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "sigstore": ["sigstore@1.9.0", "", { "dependencies": { "@sigstore/bundle": "^1.1.0", "@sigstore/protobuf-specs": "^0.2.0", "@sigstore/sign": "^1.0.0", "@sigstore/tuf": "^1.0.3", "make-fetch-happen": "^11.0.1" }, "bin": { "sigstore": "bin/sigstore.js" } }, "sha512-0Zjz0oe37d08VeOtBIuB6cRriqXse2e8w+7yIy2XSXjshRKxbc2KkhXjL229jXSxEm7UbcjS76wcJDGQddVI9A=="], + + "sirv": ["sirv@2.0.4", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="], + + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + + "snake-case": ["snake-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg=="], + + "sockjs": ["sockjs@0.3.24", "", { "dependencies": { "faye-websocket": "^0.11.3", "uuid": "^8.3.2", "websocket-driver": "^0.7.4" } }, "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ=="], + + "socks": ["socks@2.8.3", "", { "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" } }, "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw=="], + + "socks-proxy-agent": ["socks-proxy-agent@7.0.0", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww=="], + + "sonner": ["sonner@1.7.3", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-KXLWQfyR6AHpYZuQk8eO8fCbZSJY3JOpgsu/tbGc++jgPjj8JsR1ZpO8vFhqR/OxvWMQCSAmnSShY0gr4FPqHg=="], + + "sort-asc": ["sort-asc@0.1.0", "", {}, "sha512-jBgdDd+rQ+HkZF2/OHCmace5dvpos/aWQpcxuyRs9QUbPRnkEJmYVo81PIGpjIdpOcsnJ4rGjStfDHsbn+UVyw=="], + + "sort-desc": ["sort-desc@0.1.1", "", {}, "sha512-jfZacW5SKOP97BF5rX5kQfJmRVZP5/adDUTY8fCSPvNcXDVpUEe2pr/iKGlcyZzchRJZrswnp68fgk3qBXgkJw=="], + + "sort-keys": ["sort-keys@1.1.2", "", { "dependencies": { "is-plain-obj": "^1.0.0" } }, "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg=="], + + "sort-object": ["sort-object@0.3.2", "", { "dependencies": { "sort-asc": "^0.1.0", "sort-desc": "^0.1.1" } }, "sha512-aAQiEdqFTTdsvUFxXm3umdo04J7MRljoVGbBlkH7BgNsMvVNAJyGj7C/wV1A8wHWAJj/YikeZbfuCKqhggNWGA=="], + + "source-list-map": ["source-list-map@2.0.1", "", {}, "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw=="], + + "source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "source-map-loader": ["source-map-loader@4.0.2", "", { "dependencies": { "iconv-lite": "^0.6.3", "source-map-js": "^1.0.2" }, "peerDependencies": { "webpack": "^5.72.1" } }, "sha512-oYwAqCuL0OZhBoSgmdrLa7mv9MjommVMiQIWgcztf+eS4+8BfcUee6nenFnDhKOhzAVnk5gpZdfnz1iiBv+5sg=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "sourcemap-codec": ["sourcemap-codec@1.4.8", "", {}, "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="], + + "space-separated-tokens": ["space-separated-tokens@1.1.5", "", {}, "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA=="], + + "spark-md5": ["spark-md5@3.0.2", "", {}, "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw=="], + + "sparse-array": ["sparse-array@1.3.2", "", {}, "sha512-ZT711fePGn3+kQyLuv1fpd3rNSkNF8vd5Kv2D+qnOANeyKs3fx6bUMGWRPvgTTcYV64QMqZKZwcuaQSP3AZ0tg=="], + + "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], + + "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], + + "spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], + + "spdx-license-ids": ["spdx-license-ids@3.0.21", "", {}, "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg=="], + + "spdx-license-list": ["spdx-license-list@6.9.0", "", {}, "sha512-L2jl5vc2j6jxWcNCvcVj/BW9A8yGIG02Dw+IUw0ZxDM70f7Ylf5Hq39appV1BI9yxyWQRpq2TQ1qaXvf+yjkqA=="], + + "spdy": ["spdy@4.0.2", "", { "dependencies": { "debug": "^4.1.0", "handle-thing": "^2.0.0", "http-deceiver": "^1.2.7", "select-hose": "^2.0.0", "spdy-transport": "^3.0.0" } }, "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA=="], + + "spdy-transport": ["spdy-transport@3.0.0", "", { "dependencies": { "debug": "^4.1.0", "detect-node": "^2.0.4", "hpack.js": "^2.1.6", "obuf": "^1.1.2", "readable-stream": "^3.0.6", "wbuf": "^1.7.3" } }, "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw=="], + + "split": ["split@1.0.1", "", { "dependencies": { "through": "2" } }, "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg=="], + + "split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="], + + "split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="], + + "ssr-window": ["ssr-window@4.0.2", "", {}, "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ=="], + + "ssri": ["ssri@9.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q=="], + + "stable": ["stable@0.1.8", "", {}, "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w=="], + + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + + "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], + + "start-server-and-test": ["start-server-and-test@1.15.5", "", { "dependencies": { "arg": "^5.0.2", "bluebird": "3.7.2", "check-more-types": "2.24.0", "debug": "4.3.4", "execa": "5.1.1", "lazy-ass": "1.6.0", "ps-tree": "1.2.0", "wait-on": "7.0.1" }, "bin": { "start-test": "src/bin/start.js", "server-test": "src/bin/start.js", "start-server-and-test": "src/bin/start.js" } }, "sha512-o3EmkX0++GV+qsvIJ/OKWm3w91fD8uS/bPQVPrh/7loaxkpXSuAIHdnmN/P/regQK9eNAK76aBJcHt+OSTk+nA=="], + + "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + + "store2": ["store2@2.14.4", "", {}, "sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw=="], + + "storybook": ["storybook@7.6.20", "", { "dependencies": { "@storybook/cli": "7.6.20" }, "bin": { "sb": "./index.js", "storybook": "./index.js" } }, "sha512-Wt04pPTO71pwmRmsgkyZhNo4Bvdb/1pBAMsIFb9nQLykEdzzpXjvingxFFvdOG4nIowzwgxD+CLlyRqVJqnATw=="], + + "stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="], + + "stream-combiner": ["stream-combiner@0.0.4", "", { "dependencies": { "duplexer": "~0.1.1" } }, "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw=="], + + "stream-http": ["stream-http@3.2.0", "", { "dependencies": { "builtin-status-codes": "^3.0.0", "inherits": "^2.0.4", "readable-stream": "^3.6.0", "xtend": "^4.0.2" } }, "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A=="], + + "stream-shift": ["stream-shift@1.0.3", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="], + + "strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="], + + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + + "string-length": ["string-length@4.0.2", "", { "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" } }, "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ=="], + + "string-natural-compare": ["string-natural-compare@3.0.1", "", {}, "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="], + + "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], + + "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "stringify-object": ["stringify-object@3.3.0", "", { "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", "is-regexp": "^1.0.0" } }, "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-comments": ["strip-comments@2.0.1", "", {}, "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw=="], + + "strip-dirs": ["strip-dirs@2.1.0", "", { "dependencies": { "is-natural-number": "^4.0.1" } }, "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g=="], + + "strip-eof": ["strip-eof@1.0.0", "", {}, "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q=="], + + "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + + "strip-indent": ["strip-indent@4.0.0", "", { "dependencies": { "min-indent": "^1.0.1" } }, "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "strong-log-transformer": ["strong-log-transformer@2.1.0", "", { "dependencies": { "duplexer": "^0.1.1", "minimist": "^1.2.0", "through": "^2.3.4" }, "bin": { "sl-log-transformer": "bin/sl-log-transformer.js" } }, "sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA=="], + + "style-loader": ["style-loader@1.3.0", "", { "dependencies": { "loader-utils": "^2.0.0", "schema-utils": "^2.7.0" }, "peerDependencies": { "webpack": "^4.0.0 || ^5.0.0" } }, "sha512-V7TCORko8rs9rIqkSrlMfkqA63DfoGBBJmK1kKGCcSi+BWb4cqz0SRsnp4l6rU5iwOEd0/2ePv68SV22VXon4Q=="], + + "style-value-types": ["style-value-types@5.0.0", "", { "dependencies": { "hey-listen": "^1.0.8", "tslib": "^2.1.0" } }, "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA=="], + + "stylehacks": ["stylehacks@5.1.1", "", { "dependencies": { "browserslist": "^4.21.4", "postcss-selector-parser": "^6.0.4" }, "peerDependencies": { "postcss": "^8.2.15" } }, "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw=="], + + "stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], + + "stylus": ["stylus@0.59.0", "", { "dependencies": { "@adobe/css-tools": "^4.0.1", "debug": "^4.3.2", "glob": "^7.1.6", "sax": "~1.2.4", "source-map": "^0.7.3" }, "bin": { "stylus": "bin/stylus" } }, "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg=="], + + "stylus-loader": ["stylus-loader@7.1.3", "", { "dependencies": { "fast-glob": "^3.2.12", "normalize-path": "^3.0.0" }, "peerDependencies": { "stylus": ">=0.52.4", "webpack": "^5.0.0" } }, "sha512-TY0SKwiY7D2kMd3UxaWKSf3xHF0FFN/FAfsSqfrhxRT/koXTwffq2cgEWDkLQz7VojMu7qEEHt5TlMjkPx9UDw=="], + + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "svg-parser": ["svg-parser@2.0.4", "", {}, "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ=="], + + "svgo": ["svgo@3.3.2", "", { "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0" }, "bin": "./bin/svgo" }, "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw=="], + + "swc-loader": ["swc-loader@0.2.6", "", { "dependencies": { "@swc/counter": "^0.1.3" }, "peerDependencies": { "@swc/core": "^1.2.147", "webpack": ">=2" } }, "sha512-9Zi9UP2YmDpgmQVbyOPJClY0dwf58JDyDMQ7uRc4krmc72twNI2fvlBWHLqVekBpPc7h5NJkGVT1zNDxFrqhvg=="], + + "swiper": ["swiper@8.4.7", "", { "dependencies": { "dom7": "^4.0.4", "ssr-window": "^4.0.2" } }, "sha512-VwO/KU3i9IV2Sf+W2NqyzwWob4yX9Qdedq6vBtS0rFqJ6Fa5iLUJwxQkuD4I38w0WDJwmFl8ojkdcRFPHWD+2g=="], + + "symbol-observable": ["symbol-observable@1.2.0", "", {}, "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "synchronous-promise": ["synchronous-promise@2.0.17", "", {}, "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g=="], + + "synckit": ["synckit@0.9.2", "", { "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw=="], + + "tailwind-merge": ["tailwind-merge@2.6.0", "", {}, "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="], + + "tailwindcss": ["tailwindcss@3.2.4", "", { "dependencies": { "arg": "^5.0.2", "chokidar": "^3.5.3", "color-name": "^1.1.4", "detective": "^5.2.1", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "lilconfig": "^2.0.6", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.18", "postcss-import": "^14.1.0", "postcss-js": "^4.0.0", "postcss-load-config": "^3.1.4", "postcss-nested": "6.0.0", "postcss-selector-parser": "^6.0.10", "postcss-value-parser": "^4.2.0", "quick-lru": "^5.1.1", "resolve": "^1.22.1" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ=="], + + "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], + + "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], + + "tar": ["tar@6.1.11", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^3.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA=="], + + "tar-fs": ["tar-fs@2.1.2", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + + "telejson": ["telejson@7.2.0", "", { "dependencies": { "memoizerific": "^1.11.3" } }, "sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ=="], + + "temp": ["temp@0.8.4", "", { "dependencies": { "rimraf": "~2.6.2" } }, "sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg=="], + + "temp-dir": ["temp-dir@1.0.0", "", {}, "sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ=="], + + "tempy": ["tempy@1.0.1", "", { "dependencies": { "del": "^6.0.0", "is-stream": "^2.0.0", "temp-dir": "^2.0.0", "type-fest": "^0.16.0", "unique-string": "^2.0.0" } }, "sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w=="], + + "terser": ["terser@5.37.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA=="], + + "terser-webpack-plugin": ["terser-webpack-plugin@5.3.11", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ=="], + + "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], + + "text-extensions": ["text-extensions@1.9.0", "", {}, "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ=="], + + "text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="], + + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], + + "throttleit": ["throttleit@1.0.1", "", {}, "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ=="], + + "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], + + "through2": ["through2@2.0.5", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="], + + "thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="], + + "timers-browserify": ["timers-browserify@2.0.12", "", { "dependencies": { "setimmediate": "^1.0.4" } }, "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ=="], + + "tiny-emitter": ["tiny-emitter@2.1.0", "", {}, "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tldts": ["tldts@6.1.75", "", { "dependencies": { "tldts-core": "^6.1.75" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-+lFzEXhpl7JXgWYaXcB6DqTYXbUArvrWAE/5ioq/X3CdWLbDjpPP4XTrQBmEJ91y3xbe4Fkw7Lxv4P3GWeJaNg=="], + + "tldts-core": ["tldts-core@6.1.75", "", {}, "sha512-AOvV5YYIAFFBfransBzSTyztkc3IMfz5Eq3YluaRiEu55nn43Fzaufx70UqEKYr8BoLCach4q8g/bg6e5+/aFw=="], + + "tmp": ["tmp@0.2.3", "", {}, "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w=="], + + "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], + + "to-buffer": ["to-buffer@1.1.1", "", {}, "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "tocbot": ["tocbot@4.33.0", "", {}, "sha512-mwTJXunwTyic/22uyLeBGXeX/c6jLcfrF8ONVJmwSg5tieW6kYGSfSSJQEOQXStbutv7WS7QdZ+gk4vQIQ23nA=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "tough-cookie": ["tough-cookie@5.1.0", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-rvZUv+7MoBYTiDmFPBrhL7Ujx9Sk+q9wwm22x8c8T5IJaR+Wsyc7TNxbVxo84kZoRJZZMazowFLqpankBEQrGg=="], + + "tr46": ["tr46@3.0.0", "", { "dependencies": { "punycode": "^2.1.1" } }, "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA=="], + + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + + "trim-newlines": ["trim-newlines@5.0.0", "", {}, "sha512-kstfs+hgwmdsOadN3KgA+C68wPJwnZq4DN6WMDCvZapDWEF34W2TyPKN2v2+BJnZgIz5QOfxFeldLyYvdgRAwg=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + + "ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="], + + "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], + + "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tty-browserify": ["tty-browserify@0.0.1", "", {}, "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw=="], + + "tuf-js": ["tuf-js@1.1.7", "", { "dependencies": { "@tufjs/models": "1.0.4", "debug": "^4.3.4", "make-fetch-happen": "^11.1.1" } }, "sha512-i3P9Kgw3ytjELUfpuKVDNBJvk4u5bXL6gskv572mcevPbSKCV3zt3djhmlEQ65yERjIbOSncy7U4cQJaB1CBCg=="], + + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + + "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], + + "type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], + + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + + "typed-function": ["typed-function@4.2.1", "", {}, "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA=="], + + "typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="], + + "typescript": ["typescript@5.5.4", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q=="], + + "ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="], + + "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], + + "uint8-varint": ["uint8-varint@2.0.4", "", { "dependencies": { "uint8arraylist": "^2.0.0", "uint8arrays": "^5.0.0" } }, "sha512-FwpTa7ZGA/f/EssWAb5/YV6pHgVF1fViKdW8cWaEarjB8t7NyofSWBdOTyFPaGuUG4gx3v1O3PQ8etsiOs3lcw=="], + + "uint8arraylist": ["uint8arraylist@2.4.8", "", { "dependencies": { "uint8arrays": "^5.0.1" } }, "sha512-vc1PlGOzglLF0eae1M8mLRTBivsvrGsdmJ5RbK3e+QRvRLOZfZhQROTwH/OfyF3+ZVUg9/8hE8bmKP2CvP9quQ=="], + + "uint8arrays": ["uint8arrays@3.1.1", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg=="], + + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + + "unbzip2-stream": ["unbzip2-stream@1.4.3", "", { "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg=="], + + "undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + + "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], + + "unicode-match-property-ecmascript": ["unicode-match-property-ecmascript@2.0.0", "", { "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" } }, "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q=="], + + "unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.0", "", {}, "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg=="], + + "unicode-property-aliases-ecmascript": ["unicode-property-aliases-ecmascript@2.1.0", "", {}, "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w=="], + + "unified": ["unified@10.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "bail": "^2.0.0", "extend": "^3.0.0", "is-buffer": "^2.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^5.0.0" } }, "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q=="], + + "uniq": ["uniq@1.0.1", "", {}, "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA=="], + + "unique-filename": ["unique-filename@3.0.0", "", { "dependencies": { "unique-slug": "^4.0.0" } }, "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g=="], + + "unique-slug": ["unique-slug@4.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ=="], + + "unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="], + + "unist-util-is": ["unist-util-is@4.1.0", "", {}, "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@3.0.3", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg=="], + + "unist-util-visit": ["unist-util-visit@2.0.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^4.0.0", "unist-util-visit-parents": "^3.0.0" } }, "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@3.1.1", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^4.0.0" } }, "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg=="], + + "universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "unplugin": ["unplugin@1.16.1", "", { "dependencies": { "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" } }, "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w=="], + + "untildify": ["untildify@4.0.0", "", {}, "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw=="], + + "unused-webpack-plugin": ["unused-webpack-plugin@2.4.0", "", { "dependencies": { "chalk": "^2.1.0", "deglob": "^3.1.0" } }, "sha512-v/9lL+ICYVJodolusinh7j+Lj51Quj6erA5YiBl5W0L19BAZ29H+88l9GCdWl3bZEb6BowGX2Ig8CMvxKzqhwQ=="], + + "upath": ["upath@2.0.1", "", {}, "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg=="], + + "update-check": ["update-check@1.5.4", "", { "dependencies": { "registry-auth-token": "3.3.2", "registry-url": "3.1.0" } }, "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "url": ["url@0.11.4", "", { "dependencies": { "punycode": "^1.4.1", "qs": "^6.12.3" } }, "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg=="], + + "url-loader": ["url-loader@4.1.1", "", { "dependencies": { "loader-utils": "^2.0.0", "mime-types": "^2.1.27", "schema-utils": "^3.0.0" }, "peerDependencies": { "file-loader": "*", "webpack": "^4.0.0 || ^5.0.0" }, "optionalPeers": ["file-loader"] }, "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA=="], + + "url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-isomorphic-layout-effect": ["use-isomorphic-layout-effect@1.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w=="], + + "use-resize-observer": ["use-resize-observer@9.1.0", "", { "dependencies": { "@juggle/resize-observer": "^3.3.1" }, "peerDependencies": { "react": "16.8.0 - 18", "react-dom": "16.8.0 - 18" } }, "sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "use-sync-external-store": ["use-sync-external-store@1.2.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw=="], + + "utif": ["utif@3.1.0", "", { "dependencies": { "pako": "^1.0.5" } }, "sha512-WEo4D/xOvFW53K5f5QTaTbbiORcm2/pCL9P6qmJnup+17eYfKaEhDeX9PeQkuyEoIxlbGklDuGl8xwuXYMrrXQ=="], + + "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "utila": ["utila@0.4.0", "", {}, "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA=="], + + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + + "utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="], + + "uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + + "uvu": ["uvu@0.5.6", "", { "dependencies": { "dequal": "^2.0.0", "diff": "^5.0.0", "kleur": "^4.0.3", "sade": "^1.7.3" }, "bin": { "uvu": "bin.js" } }, "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA=="], + + "v8-compile-cache": ["v8-compile-cache@2.3.0", "", {}, "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA=="], + + "v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="], + + "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], + + "validate-npm-package-name": ["validate-npm-package-name@5.0.0", "", { "dependencies": { "builtins": "^5.0.0" } }, "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ=="], + + "validate.js": ["validate.js@0.12.0", "", {}, "sha512-/x2RJSvbqEyxKj0RPN4xaRquK+EggjeVXiDDEyrJzsJogjtiZ9ov7lj/svVb4DM5Q5braQF4cooAryQbUwOxlA=="], + + "varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "verror": ["verror@1.10.0", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw=="], + + "vfile": ["vfile@5.3.7", "", { "dependencies": { "@types/unist": "^2.0.0", "is-buffer": "^2.0.0", "unist-util-stringify-position": "^3.0.0", "vfile-message": "^3.0.0" } }, "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g=="], + + "vfile-message": ["vfile-message@3.1.4", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^3.0.0" } }, "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw=="], + + "vm-browserify": ["vm-browserify@1.1.2", "", {}, "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="], + + "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@4.0.0", "", { "dependencies": { "xml-name-validator": "^4.0.0" } }, "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw=="], + + "wait-on": ["wait-on@7.0.1", "", { "dependencies": { "axios": "^0.27.2", "joi": "^17.7.0", "lodash": "^4.17.21", "minimist": "^1.2.7", "rxjs": "^7.8.0" }, "bin": { "wait-on": "bin/wait-on" } }, "sha512-9AnJE9qTjRQOlTZIldAaf/da2eW0eSRSgcqq85mXQja/DW3MriHxkpODDSUEg+Gri/rKEcXUZHe+cevvYItaog=="], + + "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], + + "warning": ["warning@4.0.3", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w=="], + + "wasm-feature-detect": ["wasm-feature-detect@1.8.0", "", {}, "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ=="], + + "watchpack": ["watchpack@2.4.2", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw=="], + + "wbuf": ["wbuf@1.7.3", "", { "dependencies": { "minimalistic-assert": "^1.0.0" } }, "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA=="], + + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], + + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + + "web-worker": ["web-worker@1.3.0", "", {}, "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA=="], + + "webgl-constants": ["webgl-constants@1.1.1", "", {}, "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="], + + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "webpack": ["webpack@5.94.0", "", { "dependencies": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.10", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg=="], + + "webpack-bundle-analyzer": ["webpack-bundle-analyzer@4.10.2", "", { "dependencies": { "@discoveryjs/json-ext": "0.5.7", "acorn": "^8.0.4", "acorn-walk": "^8.0.0", "commander": "^7.2.0", "debounce": "^1.2.1", "escape-string-regexp": "^4.0.0", "gzip-size": "^6.0.0", "html-escaper": "^2.0.2", "opener": "^1.5.2", "picocolors": "^1.0.0", "sirv": "^2.0.3", "ws": "^7.3.1" }, "bin": { "webpack-bundle-analyzer": "lib/bin/analyzer.js" } }, "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw=="], + + "webpack-cli": ["webpack-cli@4.10.0", "", { "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^1.2.0", "@webpack-cli/info": "^1.5.0", "@webpack-cli/serve": "^1.7.0", "colorette": "^2.0.14", "commander": "^7.0.0", "cross-spawn": "^7.0.3", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", "interpret": "^2.2.0", "rechoir": "^0.7.0", "webpack-merge": "^5.7.3" }, "peerDependencies": { "webpack": "4.x.x || 5.x.x" }, "bin": { "webpack-cli": "bin/cli.js" } }, "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w=="], + + "webpack-dev-middleware": ["webpack-dev-middleware@5.3.4", "", { "dependencies": { "colorette": "^2.0.10", "memfs": "^3.4.3", "mime-types": "^2.1.31", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" }, "peerDependencies": { "webpack": "^4.0.0 || ^5.0.0" } }, "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q=="], + + "webpack-dev-server": ["webpack-dev-server@4.7.3", "", { "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", "@types/serve-index": "^1.9.1", "@types/sockjs": "^0.3.33", "@types/ws": "^8.2.2", "ansi-html-community": "^0.0.8", "bonjour": "^3.5.0", "chokidar": "^3.5.2", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^1.6.0", "default-gateway": "^6.0.3", "del": "^6.0.0", "express": "^4.17.1", "graceful-fs": "^4.2.6", "html-entities": "^2.3.2", "http-proxy-middleware": "^2.0.0", "ipaddr.js": "^2.0.1", "open": "^8.0.9", "p-retry": "^4.5.0", "portfinder": "^1.0.28", "schema-utils": "^4.0.0", "selfsigned": "^2.0.0", "serve-index": "^1.9.1", "sockjs": "^0.3.21", "spdy": "^4.0.2", "strip-ansi": "^7.0.0", "webpack-dev-middleware": "^5.3.0", "ws": "^8.1.0" }, "peerDependencies": { "webpack": "^4.37.0 || ^5.0.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" } }, "sha512-mlxq2AsIw2ag016nixkzUkdyOE8ST2GTy34uKSABp1c4nhjZvH90D5ZRR+UOLSsG4Z3TFahAi72a3ymRtfRm+Q=="], + + "webpack-hot-middleware": ["webpack-hot-middleware@2.26.1", "", { "dependencies": { "ansi-html-community": "0.0.8", "html-entities": "^2.1.0", "strip-ansi": "^6.0.0" } }, "sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A=="], + + "webpack-merge": ["webpack-merge@5.10.0", "", { "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", "wildcard": "^2.0.0" } }, "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA=="], + + "webpack-sources": ["webpack-sources@1.4.3", "", { "dependencies": { "source-list-map": "^2.0.0", "source-map": "~0.6.1" } }, "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ=="], + + "webpack-virtual-modules": ["webpack-virtual-modules@0.5.0", "", {}, "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw=="], + + "websocket-driver": ["websocket-driver@0.7.4", "", { "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } }, "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg=="], + + "websocket-extensions": ["websocket-extensions@0.1.4", "", {}, "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg=="], + + "webworker-promise": ["webworker-promise@0.5.0", "", {}, "sha512-14iR79jHAV7ozwvbfif+3wCaApT3I1g8Lo0rJZrwAu6wxZGx/08Y8KXz6as6ZLNUEEufeiEBBYrqyDBClXOsEw=="], + + "whatwg-encoding": ["whatwg-encoding@2.0.0", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg=="], + + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], + + "whatwg-url": ["whatwg-url@11.0.0", "", { "dependencies": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" } }, "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.18", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA=="], + + "wide-align": ["wide-align@1.1.5", "", { "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="], + + "widest-line": ["widest-line@4.0.1", "", { "dependencies": { "string-width": "^5.0.1" } }, "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig=="], + + "wildcard": ["wildcard@2.0.1", "", {}, "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], + + "workbox-background-sync": ["workbox-background-sync@6.6.1", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "6.6.1" } }, "sha512-trJd3ovpWCvzu4sW0E8rV3FUyIcC0W8G+AZ+VcqzzA890AsWZlUGOTSxIMmIHVusUw/FDq1HFWfy/kC/WTRqSg=="], + + "workbox-broadcast-update": ["workbox-broadcast-update@6.6.1", "", { "dependencies": { "workbox-core": "6.6.1" } }, "sha512-fBhffRdaANdeQ1V8s692R9l/gzvjjRtydBOvR6WCSB0BNE2BacA29Z4r9/RHd9KaXCPl6JTdI9q0bR25YKP8TQ=="], + + "workbox-build": ["workbox-build@6.6.1", "", { "dependencies": { "@apideck/better-ajv-errors": "^0.3.1", "@babel/core": "^7.11.1", "@babel/preset-env": "^7.11.0", "@babel/runtime": "^7.11.2", "@rollup/plugin-babel": "^5.2.0", "@rollup/plugin-node-resolve": "^11.2.1", "@rollup/plugin-replace": "^2.4.1", "@surma/rollup-plugin-off-main-thread": "^2.2.3", "ajv": "^8.6.0", "common-tags": "^1.8.0", "fast-json-stable-stringify": "^2.1.0", "fs-extra": "^9.0.1", "glob": "^7.1.6", "lodash": "^4.17.20", "pretty-bytes": "^5.3.0", "rollup": "^2.43.1", "rollup-plugin-terser": "^7.0.0", "source-map": "^0.8.0-beta.0", "stringify-object": "^3.3.0", "strip-comments": "^2.0.1", "tempy": "^0.6.0", "upath": "^1.2.0", "workbox-background-sync": "6.6.1", "workbox-broadcast-update": "6.6.1", "workbox-cacheable-response": "6.6.1", "workbox-core": "6.6.1", "workbox-expiration": "6.6.1", "workbox-google-analytics": "6.6.1", "workbox-navigation-preload": "6.6.1", "workbox-precaching": "6.6.1", "workbox-range-requests": "6.6.1", "workbox-recipes": "6.6.1", "workbox-routing": "6.6.1", "workbox-strategies": "6.6.1", "workbox-streams": "6.6.1", "workbox-sw": "6.6.1", "workbox-window": "6.6.1" } }, "sha512-INPgDx6aRycAugUixbKgiEQBWD0MPZqU5r0jyr24CehvNuLPSXp/wGOpdRJmts656lNiXwqV7dC2nzyrzWEDnw=="], + + "workbox-cacheable-response": ["workbox-cacheable-response@6.6.1", "", { "dependencies": { "workbox-core": "6.6.1" } }, "sha512-85LY4veT2CnTCDxaVG7ft3NKaFbH6i4urZXgLiU4AiwvKqS2ChL6/eILiGRYXfZ6gAwDnh5RkuDbr/GMS4KSag=="], + + "workbox-core": ["workbox-core@6.6.1", "", {}, "sha512-ZrGBXjjaJLqzVothoE12qTbVnOAjFrHDXpZe7coCb6q65qI/59rDLwuFMO4PcZ7jcbxY+0+NhUVztzR/CbjEFw=="], + + "workbox-expiration": ["workbox-expiration@6.6.1", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "6.6.1" } }, "sha512-qFiNeeINndiOxaCrd2DeL1Xh1RFug3JonzjxUHc5WkvkD2u5abY3gZL1xSUNt3vZKsFFGGORItSjVTVnWAZO4A=="], + + "workbox-google-analytics": ["workbox-google-analytics@6.6.1", "", { "dependencies": { "workbox-background-sync": "6.6.1", "workbox-core": "6.6.1", "workbox-routing": "6.6.1", "workbox-strategies": "6.6.1" } }, "sha512-1TjSvbFSLmkpqLcBsF7FuGqqeDsf+uAXO/pjiINQKg3b1GN0nBngnxLcXDYo1n/XxK4N7RaRrpRlkwjY/3ocuA=="], + + "workbox-navigation-preload": ["workbox-navigation-preload@6.6.1", "", { "dependencies": { "workbox-core": "6.6.1" } }, "sha512-DQCZowCecO+wRoIxJI2V6bXWK6/53ff+hEXLGlQL4Rp9ZaPDLrgV/32nxwWIP7QpWDkVEtllTAK5h6cnhxNxDA=="], + + "workbox-precaching": ["workbox-precaching@6.6.1", "", { "dependencies": { "workbox-core": "6.6.1", "workbox-routing": "6.6.1", "workbox-strategies": "6.6.1" } }, "sha512-K4znSJ7IKxCnCYEdhNkMr7X1kNh8cz+mFgx9v5jFdz1MfI84pq8C2zG+oAoeE5kFrUf7YkT5x4uLWBNg0DVZ5A=="], + + "workbox-range-requests": ["workbox-range-requests@6.6.1", "", { "dependencies": { "workbox-core": "6.6.1" } }, "sha512-4BDzk28govqzg2ZpX0IFkthdRmCKgAKreontYRC5YsAPB2jDtPNxqx3WtTXgHw1NZalXpcH/E4LqUa9+2xbv1g=="], + + "workbox-recipes": ["workbox-recipes@6.6.1", "", { "dependencies": { "workbox-cacheable-response": "6.6.1", "workbox-core": "6.6.1", "workbox-expiration": "6.6.1", "workbox-precaching": "6.6.1", "workbox-routing": "6.6.1", "workbox-strategies": "6.6.1" } }, "sha512-/oy8vCSzromXokDA+X+VgpeZJvtuf8SkQ8KL0xmRivMgJZrjwM3c2tpKTJn6PZA6TsbxGs3Sc7KwMoZVamcV2g=="], + + "workbox-routing": ["workbox-routing@6.6.1", "", { "dependencies": { "workbox-core": "6.6.1" } }, "sha512-j4ohlQvfpVdoR8vDYxTY9rA9VvxTHogkIDwGdJ+rb2VRZQ5vt1CWwUUZBeD/WGFAni12jD1HlMXvJ8JS7aBWTg=="], + + "workbox-strategies": ["workbox-strategies@6.6.1", "", { "dependencies": { "workbox-core": "6.6.1" } }, "sha512-WQLXkRnsk4L81fVPkkgon1rZNxnpdO5LsO+ws7tYBC6QQQFJVI6v98klrJEjFtZwzw/mB/HT5yVp7CcX0O+mrw=="], + + "workbox-streams": ["workbox-streams@6.6.1", "", { "dependencies": { "workbox-core": "6.6.1", "workbox-routing": "6.6.1" } }, "sha512-maKG65FUq9e4BLotSKWSTzeF0sgctQdYyTMq529piEN24Dlu9b6WhrAfRpHdCncRS89Zi2QVpW5V33NX8PgH3Q=="], + + "workbox-sw": ["workbox-sw@6.6.1", "", {}, "sha512-R7whwjvU2abHH/lR6kQTTXLHDFU2izht9kJOvBRYK65FbwutT4VvnUAJIgHvfWZ/fokrOPhfoWYoPCMpSgUKHQ=="], + + "workbox-webpack-plugin": ["workbox-webpack-plugin@6.6.1", "", { "dependencies": { "fast-json-stable-stringify": "^2.1.0", "pretty-bytes": "^5.4.1", "upath": "^1.2.0", "webpack-sources": "^1.4.3", "workbox-build": "6.6.1" }, "peerDependencies": { "webpack": "^4.4.0 || ^5.9.0" } }, "sha512-zpZ+ExFj9NmiI66cFEApyjk7hGsfJ1YMOaLXGXBoZf0v7Iu6hL0ZBe+83mnDq3YYWAfA3fnyFejritjOHkFcrA=="], + + "workbox-window": ["workbox-window@6.6.1", "", { "dependencies": { "@types/trusted-types": "^2.0.2", "workbox-core": "6.6.1" } }, "sha512-wil4nwOY58nTdCvif/KEZjQ2NP8uk3gGeRNy2jPBbzypU4BT4D9L8xiwbmDBpZlSgJd2xsT9FvSNU0gsxV51JQ=="], + + "worker-loader": ["worker-loader@3.0.8", "", { "dependencies": { "loader-utils": "^2.0.0", "schema-utils": "^3.0.0" }, "peerDependencies": { "webpack": "^4.0.0 || ^5.0.0" } }, "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g=="], + + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], + + "write-json-file": ["write-json-file@3.2.0", "", { "dependencies": { "detect-indent": "^5.0.0", "graceful-fs": "^4.1.15", "make-dir": "^2.1.0", "pify": "^4.0.1", "sort-keys": "^2.0.0", "write-file-atomic": "^2.4.2" } }, "sha512-3xZqT7Byc2uORAatYiP3DHUUAVEkNOswEWNs9H5KXiicRTvzYzYqKjYc4G7p+8pltvAw641lVByKVtMpf+4sYQ=="], + + "write-pkg": ["write-pkg@4.0.0", "", { "dependencies": { "sort-keys": "^2.0.0", "type-fest": "^0.4.1", "write-json-file": "^3.2.0" } }, "sha512-v2UQ+50TNf2rNHJ8NyWttfm/EJUBWMJcx6ZTYZr6Qp52uuegWw/lBkCtCbnYZEmPRNL61m+u67dAmGxo+HTULA=="], + + "ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + + "wslink": ["wslink@2.2.2", "", { "dependencies": { "@msgpack/msgpack": "^2.8.0" } }, "sha512-lVmhPxB5pKO2pq280a56bKifMScrMADuz+q0qdZsdb6LKVHLnj1kZwIDrEziO4jPBZaIIKu3shgzpOMg4p7j2Q=="], + + "xml": ["xml@1.0.1", "", {}, "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw=="], + + "xml-name-validator": ["xml-name-validator@4.0.0", "", {}, "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw=="], + + "xml-utils": ["xml-utils@1.10.1", "", {}, "sha512-Dn6vJ1Z9v1tepSjvnCpwk5QqwIPcEFKdgnjqfYOABv1ngSofuAhtlugcUC3ehS1OHdgDWSG6C5mvj+Qm15udTQ=="], + + "xmlbuilder2": ["xmlbuilder2@3.0.2", "", { "dependencies": { "@oozcitak/dom": "1.15.10", "@oozcitak/infra": "1.0.8", "@oozcitak/util": "8.3.8", "@types/node": "*", "js-yaml": "3.14.0" } }, "sha512-h4MUawGY21CTdhV4xm3DG9dgsqyhDkZvVJBx88beqX8wJs3VgyGQgAn5VreHuae6unTQxh115aMK5InCVmOIKw=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "xstate": ["xstate@4.38.3", "", {}, "sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + + "yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], + + "yargs-parser": ["yargs-parser@20.2.4", "", {}, "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA=="], + + "yarn-programmatic": ["yarn-programmatic@0.1.2", "", { "dependencies": { "@types/child-process-promise": "^2.2.1", "child-process-promise": "^2.2.1" } }, "sha512-pnunUzmOpsZR+G8GbqX+FBNaKsu+lXrtcPGC8UOn1tw8NgPVx1GHHlosQCu1dlGNppjb6MF7WzBPONiAaEvp4Q=="], + + "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "zstddec": ["zstddec@0.1.0", "", {}, "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg=="], + + "zustand": ["zustand@4.5.5", "", { "dependencies": { "use-sync-external-store": "1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "@apideck/better-ajv-errors/ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/plugin-transform-classes/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], + + "@babel/plugin-transform-runtime/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/preset-env/@babel/plugin-proposal-private-property-in-object": ["@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w=="], + + "@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/register/find-cache-dir": ["find-cache-dir@2.1.0", "", { "dependencies": { "commondir": "^1.0.1", "make-dir": "^2.0.0", "pkg-dir": "^3.0.0" } }, "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ=="], + + "@babel/register/make-dir": ["make-dir@2.1.0", "", { "dependencies": { "pify": "^4.0.1", "semver": "^5.6.0" } }, "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA=="], + + "@babel/runtime-corejs2/core-js": ["core-js@2.6.12", "", {}, "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ=="], + + "@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], + + "@cornerstonejs/adapters/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "@cornerstonejs/adapters/dcmjs": ["dcmjs@0.29.13", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevelnext": "^3.0.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-Bf9tKzJNWqk4kbV210N5TLEHDqaZvO3S+MH9vezFAU8WKcG4cR6z4/II3TQVqhLI185eNUL+lhfPCVH1Uu2yTA=="], + + "@cornerstonejs/core/@kitware/vtk.js": ["@kitware/vtk.js@32.9.0", "", { "dependencies": { "@babel/runtime": "7.22.11", "@types/webxr": "^0.5.5", "commander": "9.2.0", "d3-scale": "4.0.2", "fast-deep-equal": "^3.1.3", "fflate": "0.7.3", "gl-matrix": "3.4.3", "globalthis": "1.0.3", "seedrandom": "3.0.5", "shader-loader": "1.3.1", "shelljs": "0.8.5", "spark-md5": "3.0.2", "stream-browserify": "3.0.0", "utif": "3.1.0", "webworker-promise": "0.5.0", "worker-loader": "3.0.8", "xmlbuilder2": "3.0.2" }, "peerDependencies": { "@babel/preset-env": "^7.17.10", "autoprefixer": "^10.4.7", "wslink": ">=1.1.0 || ^2.0.0" }, "bin": { "vtkDataConverter": "Utilities/DataGenerator/convert-cli.js", "xml2json": "Utilities/XMLConverter/xml2json-cli.js" } }, "sha512-Tk9O++q6J4Z47DQnYXMnSeTmSRTQeK0Wa3447iGivCt162swOkIFCHBRtiwwHjWbe8tKCS8N/7d+eHCxkK7JtA=="], + + "@cornerstonejs/tools/@kitware/vtk.js": ["@kitware/vtk.js@32.9.0", "", { "dependencies": { "@babel/runtime": "7.22.11", "@types/webxr": "^0.5.5", "commander": "9.2.0", "d3-scale": "4.0.2", "fast-deep-equal": "^3.1.3", "fflate": "0.7.3", "gl-matrix": "3.4.3", "globalthis": "1.0.3", "seedrandom": "3.0.5", "shader-loader": "1.3.1", "shelljs": "0.8.5", "spark-md5": "3.0.2", "stream-browserify": "3.0.0", "utif": "3.1.0", "webworker-promise": "0.5.0", "worker-loader": "3.0.8", "xmlbuilder2": "3.0.2" }, "peerDependencies": { "@babel/preset-env": "^7.17.10", "autoprefixer": "^10.4.7", "wslink": ">=1.1.0 || ^2.0.0" }, "bin": { "vtkDataConverter": "Utilities/DataGenerator/convert-cli.js", "xml2json": "Utilities/XMLConverter/xml2json-cli.js" } }, "sha512-Tk9O++q6J4Z47DQnYXMnSeTmSRTQeK0Wa3447iGivCt162swOkIFCHBRtiwwHjWbe8tKCS8N/7d+eHCxkK7JtA=="], + + "@cypress/request/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "@cypress/xvfb/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + + "@emotion/babel-plugin/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], + + "@emotion/is-prop-valid/@emotion/memoize": ["@emotion/memoize@0.7.4", "", {}, "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@externals/devDependencies/prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.5.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig-melody": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig-melody", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-Puaz+wPUAhFp8Lo9HuciYKM2Y2XExESjeT+9NQoVFXZsPPnc9VYss2SpxdQ6vbatmt8/4+SN0oe0I1cPDABg9Q=="], + + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + + "@istanbuljs/load-nyc-config/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "@istanbuljs/load-nyc-config/js-yaml": ["js-yaml@3.14.0", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A=="], + + "@itk-wasm/dam/axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="], + + "@itk-wasm/dam/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + + "@jest/console/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@jest/core/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@jest/core/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + + "@jest/core/jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="], + + "@jest/core/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "@jest/expect-utils/jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], + + "@jest/reporters/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@jest/reporters/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "@jest/reporters/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], + + "@jest/transform/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@jest/transform/write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="], + + "@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@kitware/vtk.js/@babel/runtime": ["@babel/runtime@7.22.11", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA=="], + + "@lerna/child-process/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@lerna/child-process/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "@lerna/create/chalk": ["chalk@4.1.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A=="], + + "@lerna/create/cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], + + "@lerna/create/execa": ["execa@5.0.0", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ=="], + + "@lerna/create/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + + "@lerna/create/get-stream": ["get-stream@6.0.0", "", {}, "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg=="], + + "@lerna/create/is-stream": ["is-stream@2.0.0", "", {}, "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw=="], + + "@lerna/create/make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + + "@lerna/create/minimatch": ["minimatch@3.0.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw=="], + + "@lerna/create/node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="], + + "@lerna/create/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "@microsoft/tsdoc-config/resolve": ["resolve@1.19.0", "", { "dependencies": { "is-core-module": "^2.1.0", "path-parse": "^1.0.6" } }, "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg=="], + + "@multiformats/blake2/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], + + "@multiformats/sha3/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], + + "@npmcli/git/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "@npmcli/git/which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="], + + "@npmcli/installed-package-contents/npm-bundled": ["npm-bundled@3.0.1", "", { "dependencies": { "npm-normalize-package-bin": "^3.0.0" } }, "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ=="], + + "@npmcli/installed-package-contents/npm-normalize-package-bin": ["npm-normalize-package-bin@3.0.1", "", {}, "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ=="], + + "@npmcli/move-file/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "@npmcli/move-file/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "@npmcli/promise-spawn/which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="], + + "@npmcli/run-script/which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="], + + "@nx/devkit/enquirer": ["enquirer@2.3.6", "", { "dependencies": { "ansi-colors": "^4.1.1" } }, "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg=="], + + "@nx/devkit/semver": ["semver@7.5.3", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ=="], + + "@octokit/endpoint/is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="], + + "@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@10.0.0", "", { "dependencies": { "@octokit/openapi-types": "^18.0.0" } }, "sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg=="], + + "@octokit/request/is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="], + + "@octokit/request/node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="], + + "@ohif/mode-segmentation/@babel/preset-env": ["@babel/preset-env@7.23.2", "", { "dependencies": { "@babel/compat-data": "^7.23.2", "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-option": "^7.22.15", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", "@babel/plugin-syntax-import-assertions": "^7.22.5", "@babel/plugin-syntax-import-attributes": "^7.22.5", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.22.5", "@babel/plugin-transform-async-generator-functions": "^7.23.2", "@babel/plugin-transform-async-to-generator": "^7.22.5", "@babel/plugin-transform-block-scoped-functions": "^7.22.5", "@babel/plugin-transform-block-scoping": "^7.23.0", "@babel/plugin-transform-class-properties": "^7.22.5", "@babel/plugin-transform-class-static-block": "^7.22.11", "@babel/plugin-transform-classes": "^7.22.15", "@babel/plugin-transform-computed-properties": "^7.22.5", "@babel/plugin-transform-destructuring": "^7.23.0", "@babel/plugin-transform-dotall-regex": "^7.22.5", "@babel/plugin-transform-duplicate-keys": "^7.22.5", "@babel/plugin-transform-dynamic-import": "^7.22.11", "@babel/plugin-transform-exponentiation-operator": "^7.22.5", "@babel/plugin-transform-export-namespace-from": "^7.22.11", "@babel/plugin-transform-for-of": "^7.22.15", "@babel/plugin-transform-function-name": "^7.22.5", "@babel/plugin-transform-json-strings": "^7.22.11", "@babel/plugin-transform-literals": "^7.22.5", "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", "@babel/plugin-transform-member-expression-literals": "^7.22.5", "@babel/plugin-transform-modules-amd": "^7.23.0", "@babel/plugin-transform-modules-commonjs": "^7.23.0", "@babel/plugin-transform-modules-systemjs": "^7.23.0", "@babel/plugin-transform-modules-umd": "^7.22.5", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", "@babel/plugin-transform-new-target": "^7.22.5", "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", "@babel/plugin-transform-numeric-separator": "^7.22.11", "@babel/plugin-transform-object-rest-spread": "^7.22.15", "@babel/plugin-transform-object-super": "^7.22.5", "@babel/plugin-transform-optional-catch-binding": "^7.22.11", "@babel/plugin-transform-optional-chaining": "^7.23.0", "@babel/plugin-transform-parameters": "^7.22.15", "@babel/plugin-transform-private-methods": "^7.22.5", "@babel/plugin-transform-private-property-in-object": "^7.22.11", "@babel/plugin-transform-property-literals": "^7.22.5", "@babel/plugin-transform-regenerator": "^7.22.10", "@babel/plugin-transform-reserved-words": "^7.22.5", "@babel/plugin-transform-shorthand-properties": "^7.22.5", "@babel/plugin-transform-spread": "^7.22.5", "@babel/plugin-transform-sticky-regex": "^7.22.5", "@babel/plugin-transform-template-literals": "^7.22.5", "@babel/plugin-transform-typeof-symbol": "^7.22.5", "@babel/plugin-transform-unicode-escapes": "^7.22.10", "@babel/plugin-transform-unicode-property-regex": "^7.22.5", "@babel/plugin-transform-unicode-regex": "^7.22.5", "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", "@babel/preset-modules": "0.1.6-no-external-plugins", "@babel/types": "^7.23.0", "babel-plugin-polyfill-corejs2": "^0.4.6", "babel-plugin-polyfill-corejs3": "^0.8.5", "babel-plugin-polyfill-regenerator": "^0.5.3", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BW3gsuDD+rvHL2VO2SjAUNTBe5YrjsTiDyqamPDWY723na3/yPQ65X5oQkFVJZ0o50/2d+svm1rkPoJeR1KxVQ=="], + + "@ohif/mode-segmentation/babel-eslint": ["babel-eslint@10.1.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "@babel/parser": "^7.7.0", "@babel/traverse": "^7.7.0", "@babel/types": "^7.7.0", "eslint-visitor-keys": "^1.0.0", "resolve": "^1.12.0" }, "peerDependencies": { "eslint": ">= 4.12.1" } }, "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg=="], + + "@ohif/mode-segmentation/clean-webpack-plugin": ["clean-webpack-plugin@4.0.0", "", { "dependencies": { "del": "^4.1.1" }, "peerDependencies": { "webpack": ">=4.0.0 <6.0.0" } }, "sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w=="], + + "@ohif/mode-segmentation/copy-webpack-plugin": ["copy-webpack-plugin@10.2.4", "", { "dependencies": { "fast-glob": "^3.2.7", "glob-parent": "^6.0.1", "globby": "^12.0.2", "normalize-path": "^3.0.0", "schema-utils": "^4.0.0", "serialize-javascript": "^6.0.0" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-xFVltahqlsRcyyJqQbDY6EYTtyQZF9rf+JPjwHObLdPFMEISqkFkr7mFoVOC6BfYS/dNThyoQKvziugm+OnwBg=="], + + "@ohif/mode-segmentation/cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], + + "@ohif/mode-segmentation/dotenv": ["dotenv@14.3.2", "", {}, "sha512-vwEppIphpFdvaMCaHfCEv9IgwcxMljMw2TnAQBB4VWPvzXQLTb82jwmdOKzlEVUL3gNFT4l4TPKO+Bn+sqcrVQ=="], + + "@ohif/ui/babel-loader": ["babel-loader@9.2.1", "", { "dependencies": { "find-cache-dir": "^4.0.0", "schema-utils": "^4.0.0" }, "peerDependencies": { "@babel/core": "^7.12.0", "webpack": ">=5" } }, "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA=="], + + "@ohif/ui/dotenv-webpack": ["dotenv-webpack@8.1.0", "", { "dependencies": { "dotenv-defaults": "^2.0.2" }, "peerDependencies": { "webpack": "^4 || ^5" } }, "sha512-owK1JcsPkIobeqjVrk6h7jPED/W6ZpdFsMPR+5ursB7/SdgDyO+VzAU+szK8C8u3qUhtENyYnj8eyXMR5kkGag=="], + + "@ohif/ui/postcss-loader": ["postcss-loader@7.3.4", "", { "dependencies": { "cosmiconfig": "^8.3.5", "jiti": "^1.20.0", "semver": "^7.5.4" }, "peerDependencies": { "postcss": "^7.0.0 || ^8.0.1", "webpack": "^5.0.0" } }, "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A=="], + + "@rollup/plugin-node-resolve/deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "@rollup/plugin-replace/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="], + + "@rollup/pluginutils/@types/estree": ["@types/estree@0.0.39", "", {}, "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="], + + "@rsbuild/plugin-react/react-refresh": ["react-refresh@0.16.0", "", {}, "sha512-FPvF2XxTSikpJxcr+bHut2H4gJ17+18Uy20D5/F+SKzFap62R3cM5wH6b8WN3LyGSYeQilLEcJcR1fjBSI2S1A=="], + + "@storybook/addon-docs/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + + "@storybook/builder-manager/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + + "@storybook/builder-webpack5/@types/node": ["@types/node@18.19.74", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A=="], + + "@storybook/builder-webpack5/babel-loader": ["babel-loader@9.2.1", "", { "dependencies": { "find-cache-dir": "^4.0.0", "schema-utils": "^4.0.0" }, "peerDependencies": { "@babel/core": "^7.12.0", "webpack": ">=5" } }, "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA=="], + + "@storybook/builder-webpack5/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + + "@storybook/builder-webpack5/style-loader": ["style-loader@3.3.4", "", { "peerDependencies": { "webpack": "^5.0.0" } }, "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w=="], + + "@storybook/builder-webpack5/webpack-dev-middleware": ["webpack-dev-middleware@6.1.3", "", { "dependencies": { "colorette": "^2.0.10", "memfs": "^3.4.12", "mime-types": "^2.1.31", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" }, "peerDependencies": { "webpack": "^5.0.0" }, "optionalPeers": ["webpack"] }, "sha512-A4ChP0Qj8oGociTs6UdlRUGANIGrCDL3y+pmQMc+dSsraXHCatFpmMey4mYELA+juqwUqwQsUgJJISXl1KWmiw=="], + + "@storybook/channels/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + + "@storybook/cli/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@storybook/cli/envinfo": ["envinfo@7.14.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg=="], + + "@storybook/cli/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "@storybook/cli/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + + "@storybook/cli/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + + "@storybook/codemod/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + + "@storybook/components/@radix-ui/react-select": ["@radix-ui/react-select@1.2.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/number": "1.0.1", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-collection": "1.0.3", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-direction": "1.0.1", "@radix-ui/react-dismissable-layer": "1.0.4", "@radix-ui/react-focus-guards": "1.0.1", "@radix-ui/react-focus-scope": "1.0.3", "@radix-ui/react-id": "1.0.1", "@radix-ui/react-popper": "1.1.2", "@radix-ui/react-portal": "1.0.3", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-slot": "1.0.2", "@radix-ui/react-use-callback-ref": "1.0.1", "@radix-ui/react-use-controllable-state": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1", "@radix-ui/react-use-previous": "1.0.1", "@radix-ui/react-visually-hidden": "1.0.3", "aria-hidden": "^1.1.1", "react-remove-scroll": "2.5.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw=="], + + "@storybook/core-common/@types/node": ["@types/node@18.19.74", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A=="], + + "@storybook/core-common/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@storybook/core-common/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + + "@storybook/core-common/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "@storybook/core-common/node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="], + + "@storybook/core-common/pkg-dir": ["pkg-dir@5.0.0", "", { "dependencies": { "find-up": "^5.0.0" } }, "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA=="], + + "@storybook/core-server/@types/node": ["@types/node@18.19.74", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A=="], + + "@storybook/core-server/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@storybook/core-server/compression": ["compression@1.7.5", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.0.2", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q=="], + + "@storybook/core-server/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + + "@storybook/core-server/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + + "@storybook/core-webpack/@types/node": ["@types/node@18.19.74", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A=="], + + "@storybook/csf-tools/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + + "@storybook/preset-react-webpack/@types/node": ["@types/node@18.19.74", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A=="], + + "@storybook/preset-react-webpack/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + + "@storybook/preview-api/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + + "@storybook/react/@types/estree": ["@types/estree@0.0.51", "", {}, "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ=="], + + "@storybook/react/@types/node": ["@types/node@18.19.74", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A=="], + + "@storybook/react/acorn": ["acorn@7.4.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="], + + "@storybook/react/acorn-walk": ["acorn-walk@7.2.0", "", {}, "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA=="], + + "@storybook/react-webpack5/@types/node": ["@types/node@18.19.74", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A=="], + + "@storybook/router/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + + "@storybook/source-loader/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + + "@storybook/telemetry/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@storybook/telemetry/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + + "@storybook/types/@types/express": ["@types/express@4.17.21", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ=="], + + "@surma/rollup-plugin-off-main-thread/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="], + + "@svgr/core/cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], + + "@svgr/plugin-svgo/cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], + + "@svgr/plugin-svgo/deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "@testing-library/dom/aria-query": ["aria-query@5.1.3", "", { "dependencies": { "deep-equal": "^2.0.5" } }, "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ=="], + + "@testing-library/dom/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@tufjs/models/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@types/uglify-js/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "@types/webpack/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="], + + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@web3-storage/car-block-validator/@multiformats/murmur3": ["@multiformats/murmur3@1.1.3", "", { "dependencies": { "multiformats": "^9.5.4", "murmurhash3js-revisited": "^3.0.0" } }, "sha512-wAPLUErGR8g6Lt+bAZn6218k9YQPym+sjszsXL6o4zfxbA22P+gxWZuuD9wDbwL55xrKO5idpcuQUX7/E3oHcw=="], + + "@web3-storage/car-block-validator/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], + + "@webpack-cli/info/envinfo": ["envinfo@7.14.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg=="], + + "@yarnpkg/fslib/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "@yarnpkg/libzip/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "@yarnpkg/parsers/js-yaml": ["js-yaml@3.14.0", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A=="], + + "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "acorn-node/acorn": ["acorn@7.4.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="], + + "acorn-node/acorn-walk": ["acorn-walk@7.2.0", "", {}, "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA=="], + + "aggregate-error/indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "ajv-formats/ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="], + + "ajv-keywords/ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="], + + "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + + "are-we-there-yet/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "asn1.js/bn.js": ["bn.js@4.12.1", "", {}, "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg=="], + + "axios/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "babel-jest/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "babel-loader/schema-utils": ["schema-utils@2.7.1", "", { "dependencies": { "@types/json-schema": "^7.0.5", "ajv": "^6.12.4", "ajv-keywords": "^3.5.2" } }, "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg=="], + + "babel-plugin-istanbul/istanbul-lib-instrument": ["istanbul-lib-instrument@5.2.1", "", { "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" } }, "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg=="], + + "babel-plugin-macros/cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], + + "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "body-parser/bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "body-parser/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + + "boxen/camelcase": ["camelcase@7.0.1", "", {}, "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw=="], + + "boxen/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "boxen/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "browser-detect/core-js": ["core-js@2.6.12", "", {}, "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ=="], + + "browserify-sign/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "browserify-zlib/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + + "cacache/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "cacache/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "cacache/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "cacache/ssri": ["ssri@10.0.6", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ=="], + + "cacache/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + + "caller-callsite/callsites": ["callsites@2.0.0", "", {}, "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ=="], + + "camelcase-keys/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + + "camelcase-keys/quick-lru": ["quick-lru@4.0.1", "", {}, "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g=="], + + "chalk-template/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "child-process-promise/cross-spawn": ["cross-spawn@4.0.2", "", { "dependencies": { "lru-cache": "^4.0.1", "which": "^1.2.9" } }, "sha512-yAXz/pA1tD8Gtg2S98Ekf/sewp3Lcp3YoFKJ4Hkp5h5yLWnKVTDU0kwjKJ8NDCYcfTLfyGkzTikst+jWypT1iA=="], + + "chokidar/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "clipboardy/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "compression/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "concat-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "conventional-changelog-core/normalize-package-data": ["normalize-package-data@3.0.3", "", { "dependencies": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", "semver": "^7.3.4", "validate-npm-package-license": "^3.0.1" } }, "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA=="], + + "conventional-changelog-core/read-pkg": ["read-pkg@3.0.0", "", { "dependencies": { "load-json-file": "^4.0.0", "normalize-package-data": "^2.3.2", "path-type": "^3.0.0" } }, "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA=="], + + "conventional-changelog-core/read-pkg-up": ["read-pkg-up@3.0.0", "", { "dependencies": { "find-up": "^2.0.0", "read-pkg": "^3.0.0" } }, "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw=="], + + "copy-webpack-plugin/schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], + + "cosmiconfig/import-fresh": ["import-fresh@2.0.0", "", { "dependencies": { "caller-path": "^2.0.0", "resolve-from": "^3.0.0" } }, "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg=="], + + "cosmiconfig/js-yaml": ["js-yaml@3.14.0", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A=="], + + "create-ecdh/bn.js": ["bn.js@4.12.1", "", {}, "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg=="], + + "create-jest/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "cross-env/cross-spawn": ["cross-spawn@6.0.6", "", { "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw=="], + + "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], + + "cssstyle/cssom": ["cssom@0.3.8", "", {}, "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="], + + "cypress/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "cypress/execa": ["execa@4.1.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", "human-signals": "^1.1.1", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.0", "onetime": "^5.1.0", "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" } }, "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA=="], + + "decamelize-keys/map-obj": ["map-obj@1.0.1", "", {}, "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg=="], + + "decompress/make-dir": ["make-dir@1.3.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ=="], + + "decompress/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "decompress-tar/file-type": ["file-type@5.2.0", "", {}, "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ=="], + + "decompress-tar/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="], + + "decompress-tar/tar-stream": ["tar-stream@1.6.2", "", { "dependencies": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", "end-of-stream": "^1.0.0", "fs-constants": "^1.0.0", "readable-stream": "^2.3.0", "to-buffer": "^1.1.1", "xtend": "^4.0.0" } }, "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A=="], + + "decompress-tarbz2/file-type": ["file-type@6.2.0", "", {}, "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg=="], + + "decompress-tarbz2/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="], + + "decompress-targz/file-type": ["file-type@5.2.0", "", {}, "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ=="], + + "decompress-targz/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="], + + "decompress-unzip/file-type": ["file-type@3.9.0", "", {}, "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA=="], + + "decompress-unzip/get-stream": ["get-stream@2.3.1", "", { "dependencies": { "object-assign": "^4.0.1", "pinkie-promise": "^2.0.0" } }, "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA=="], + + "decompress-unzip/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "default-gateway/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "deglob/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "del/globby": ["globby@6.1.0", "", { "dependencies": { "array-union": "^1.0.1", "glob": "^7.0.3", "object-assign": "^4.0.1", "pify": "^2.0.0", "pinkie-promise": "^2.0.0" } }, "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw=="], + + "del/p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], + + "del/pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], + + "del/rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], + + "detect-package-manager/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "dicom-microscopy-viewer/dcmjs": ["dcmjs@0.29.13", "", { "dependencies": { "@babel/runtime-corejs3": "^7.22.5", "adm-zip": "^0.5.10", "gl-matrix": "^3.1.0", "lodash.clonedeep": "^4.5.0", "loglevelnext": "^3.0.1", "ndarray": "^1.0.19", "pako": "^2.0.4" } }, "sha512-Bf9tKzJNWqk4kbV210N5TLEHDqaZvO3S+MH9vezFAU8WKcG4cR6z4/II3TQVqhLI185eNUL+lhfPCVH1Uu2yTA=="], + + "dicom-microscopy-viewer/dicomweb-client": ["dicomweb-client@0.8.4", "", {}, "sha512-/6oY3/Fg9JyAlbTWuJOYbVqici3+nlZt43+Z/Y47RNiqLc028JcxNlY28u4VQqksxfB59f1hhNbsqsHyDT4vhw=="], + + "dicom-microscopy-viewer/mathjs": ["mathjs@11.12.0", "", { "dependencies": { "@babel/runtime": "^7.23.2", "complex.js": "^2.1.1", "decimal.js": "^10.4.3", "escape-latex": "^1.2.0", "fraction.js": "4.3.4", "javascript-natural-sort": "^0.7.1", "seedrandom": "^3.0.5", "tiny-emitter": "^2.1.0", "typed-function": "^4.1.1" }, "bin": { "mathjs": "bin/cli.js" } }, "sha512-UGhVw8rS1AyedyI55DGz9q1qZ0p98kyKPyc9vherBkoueLntPfKtPBh14x+V4cdUWK0NZV2TBwqRFlvadscSuw=="], + + "diffie-hellman/bn.js": ["bn.js@4.12.1", "", {}, "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg=="], + + "dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], + + "dotenv-defaults/dotenv": ["dotenv@6.2.0", "", {}, "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w=="], + + "duplexify/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "elliptic/bn.js": ["bn.js@4.12.1", "", {}, "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg=="], + + "es-abstract/globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "es-iterator-helpers/globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "eslint/eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], + + "eslint/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "eslint-config-react-app/babel-eslint": ["babel-eslint@10.1.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "@babel/parser": "^7.7.0", "@babel/traverse": "^7.7.0", "@babel/types": "^7.7.0", "eslint-visitor-keys": "^1.0.0", "resolve": "^1.12.0" }, "peerDependencies": { "eslint": ">= 4.12.1" } }, "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg=="], + + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-loader/loader-utils": ["loader-utils@1.4.2", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^1.0.1" } }, "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg=="], + + "eslint-loader/object-hash": ["object-hash@1.3.1", "", {}, "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA=="], + + "eslint-loader/rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], + + "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + + "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "eslint-plugin-node/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "eslint-plugin-react/doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + + "eslint-plugin-react/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], + + "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + + "eslint-webpack-plugin/schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], + + "espree/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "event-stream/split": ["split@0.3.3", "", { "dependencies": { "through": "2" } }, "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA=="], + + "executable/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "expect/jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], + + "expect/jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], + + "express/array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + + "express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + + "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "express/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + + "express/range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "external-editor/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "external-editor/tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], + + "extract-css-chunks-webpack-plugin/schema-utils": ["schema-utils@1.0.0", "", { "dependencies": { "ajv": "^6.1.0", "ajv-errors": "^1.0.0", "ajv-keywords": "^3.1.0" } }, "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g=="], + + "extract-zip/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + + "figures/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "file-loader/schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], + + "file-system-cache/fs-extra": ["fs-extra@11.1.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ=="], + + "filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "flat-cache/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "fork-ts-checker-webpack-plugin/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "fork-ts-checker-webpack-plugin/cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], + + "fork-ts-checker-webpack-plugin/deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "fork-ts-checker-webpack-plugin/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "fork-ts-checker-webpack-plugin/schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], + + "fs-minipass/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "gauge/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "geotiff/quick-lru": ["quick-lru@6.1.2", "", {}, "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ=="], + + "get-pkg-repo/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], + + "giget/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + + "git-remote-origin-url/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="], + + "global-dirs/ini": ["ini@2.0.0", "", {}, "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA=="], + + "globals/type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], + + "gunzip-maybe/browserify-zlib": ["browserify-zlib@0.1.4", "", { "dependencies": { "pako": "~0.2.0" } }, "sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ=="], + + "hamt-sharding/uint8arrays": ["uint8arrays@5.1.0", "", { "dependencies": { "multiformats": "^13.0.0" } }, "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww=="], + + "handlebars/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "has-ansi/ansi-regex": ["ansi-regex@2.1.1", "", {}, "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA=="], + + "hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "hpack.js/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "htmlparser2/domhandler": ["domhandler@4.3.1", "", { "dependencies": { "domelementtype": "^2.2.0" } }, "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ=="], + + "htmlparser2/domutils": ["domutils@2.8.0", "", { "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", "domhandler": "^4.2.0" } }, "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A=="], + + "htmlparser2/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], + + "husky/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "husky/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], + + "husky/execa": ["execa@1.0.0", "", { "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } }, "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA=="], + + "ignore-walk/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "init-package-json/npm-package-arg": ["npm-package-arg@10.1.0", "", { "dependencies": { "hosted-git-info": "^6.0.0", "proc-log": "^3.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^5.0.0" } }, "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA=="], + + "inquirer/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "ip-address/jsbn": ["jsbn@1.1.0", "", {}, "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="], + + "ip-address/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], + + "ipfs-unixfs-exporter/p-queue": ["p-queue@8.1.0", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw=="], + + "is-ci/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + + "is-path-in-cwd/is-path-inside": ["is-path-inside@2.1.0", "", { "dependencies": { "path-is-inside": "^1.0.2" } }, "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg=="], + + "istanbul-lib-report/make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + + "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "istanbul-lib-source-maps/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "itk-wasm/axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="], + + "itk-wasm/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + + "itk-wasm/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], + + "jake/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-changed-files/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "jest-circus/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-circus/dedent": ["dedent@1.5.3", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ=="], + + "jest-circus/jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], + + "jest-circus/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "jest-cli/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-cli/jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="], + + "jest-cli/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "jest-config/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-config/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + + "jest-config/deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "jest-config/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "jest-config/jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], + + "jest-config/jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="], + + "jest-config/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "jest-config/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "jest-diff/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-diff/jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], + + "jest-diff/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "jest-each/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-each/jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], + + "jest-each/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "jest-haste-map/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "jest-haste-map/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], + + "jest-junit/strip-ansi": ["strip-ansi@4.0.0", "", { "dependencies": { "ansi-regex": "^3.0.0" } }, "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow=="], + + "jest-leak-detector/jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], + + "jest-leak-detector/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "jest-matcher-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-matcher-utils/jest-diff": ["jest-diff@27.5.1", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^27.5.1", "jest-get-type": "^27.5.1", "pretty-format": "^27.5.1" } }, "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw=="], + + "jest-message-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-message-util/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "jest-resolve/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-resolve/jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="], + + "jest-runner/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-runner/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], + + "jest-runner/source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="], + + "jest-runtime/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-runtime/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "jest-runtime/strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], + + "jest-snapshot/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-snapshot/jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], + + "jest-snapshot/jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="], + + "jest-snapshot/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + + "jest-validate/@jest/types": ["@jest/types@24.9.0", "", { "dependencies": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^1.1.1", "@types/yargs": "^13.0.0" } }, "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw=="], + + "jest-validate/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + + "jest-validate/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "jest-validate/jest-get-type": ["jest-get-type@24.9.0", "", {}, "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q=="], + + "jest-validate/pretty-format": ["pretty-format@24.9.0", "", { "dependencies": { "@jest/types": "^24.9.0", "ansi-regex": "^4.0.0", "ansi-styles": "^3.2.0", "react-is": "^16.8.4" } }, "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA=="], + + "jest-watcher/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jscodeshift/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "jscodeshift/write-file-atomic": ["write-file-atomic@2.4.3", "", { "dependencies": { "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", "signal-exit": "^3.0.2" } }, "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ=="], + + "jsdom/tough-cookie": ["tough-cookie@4.1.4", "", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag=="], + + "jsdom/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + + "lazy-universal-dotenv/dotenv": ["dotenv@16.3.2", "", {}, "sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ=="], + + "lerna/chalk": ["chalk@4.1.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A=="], + + "lerna/cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], + + "lerna/execa": ["execa@5.0.0", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ=="], + + "lerna/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + + "lerna/get-stream": ["get-stream@6.0.0", "", {}, "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg=="], + + "lerna/import-local": ["import-local@3.1.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg=="], + + "lerna/is-stream": ["is-stream@2.0.0", "", {}, "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw=="], + + "lerna/make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + + "lerna/minimatch": ["minimatch@3.0.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw=="], + + "lerna/node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="], + + "lerna/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "libnpmaccess/npm-package-arg": ["npm-package-arg@10.1.0", "", { "dependencies": { "hosted-git-info": "^6.0.0", "proc-log": "^3.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^5.0.0" } }, "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA=="], + + "libnpmpublish/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + + "libnpmpublish/normalize-package-data": ["normalize-package-data@5.0.0", "", { "dependencies": { "hosted-git-info": "^6.0.0", "is-core-module": "^2.8.1", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q=="], + + "libnpmpublish/npm-package-arg": ["npm-package-arg@10.1.0", "", { "dependencies": { "hosted-git-info": "^6.0.0", "proc-log": "^3.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^5.0.0" } }, "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA=="], + + "libnpmpublish/ssri": ["ssri@10.0.6", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ=="], + + "lint-staged/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "lint-staged/del": ["del@5.1.0", "", { "dependencies": { "globby": "^10.0.1", "graceful-fs": "^4.2.2", "is-glob": "^4.0.1", "is-path-cwd": "^2.2.0", "is-path-inside": "^3.0.1", "p-map": "^3.0.0", "rimraf": "^3.0.0", "slash": "^3.0.0" } }, "sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA=="], + + "lint-staged/execa": ["execa@2.1.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^3.0.0", "onetime": "^5.1.0", "p-finally": "^2.0.0", "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" } }, "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw=="], + + "lint-staged/log-symbols": ["log-symbols@3.0.0", "", { "dependencies": { "chalk": "^2.4.2" } }, "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ=="], + + "listr/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="], + + "listr/p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], + + "listr/rxjs": ["rxjs@6.6.7", "", { "dependencies": { "tslib": "^1.9.0" } }, "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ=="], + + "listr-update-renderer/chalk": ["chalk@1.1.3", "", { "dependencies": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", "has-ansi": "^2.0.0", "strip-ansi": "^3.0.0", "supports-color": "^2.0.0" } }, "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A=="], + + "listr-update-renderer/cli-truncate": ["cli-truncate@0.2.1", "", { "dependencies": { "slice-ansi": "0.0.4", "string-width": "^1.0.1" } }, "sha512-f4r4yJnbT++qUPI9NR4XLDLq41gQ+uqnPItWG0F5ZkehuNiTTa3EY0S4AqTSUOeJ7/zU41oWPQSNkW5BqPL9bg=="], + + "listr-update-renderer/figures": ["figures@1.7.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5", "object-assign": "^4.1.0" } }, "sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ=="], + + "listr-update-renderer/log-symbols": ["log-symbols@1.0.2", "", { "dependencies": { "chalk": "^1.0.0" } }, "sha512-mmPrW0Fh2fxOzdBbFv4g1m6pR72haFLPJ2G5SJEELf1y+iaQrDG6cWCPjy54RHYbZAt7X+ls690Kw62AdWXBzQ=="], + + "listr-update-renderer/log-update": ["log-update@2.3.0", "", { "dependencies": { "ansi-escapes": "^3.0.0", "cli-cursor": "^2.0.0", "wrap-ansi": "^3.0.1" } }, "sha512-vlP11XfFGyeNQlmEn9tJ66rEW1coA/79m5z6BCkudjbAGE83uhAcGYrBFwfs3AdLiLzGRusRPAbSPK9xZteCmg=="], + + "listr-update-renderer/strip-ansi": ["strip-ansi@3.0.1", "", { "dependencies": { "ansi-regex": "^2.0.0" } }, "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg=="], + + "listr-verbose-renderer/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "listr-verbose-renderer/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], + + "listr-verbose-renderer/date-fns": ["date-fns@1.30.1", "", {}, "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw=="], + + "listr-verbose-renderer/figures": ["figures@2.0.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA=="], + + "listr2/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "load-json-file/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "load-json-file/strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], + + "load-json-file/type-fest": ["type-fest@0.6.0", "", {}, "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg=="], + + "loader-fs-cache/find-cache-dir": ["find-cache-dir@0.1.1", "", { "dependencies": { "commondir": "^1.0.1", "mkdirp": "^0.5.1", "pkg-dir": "^1.0.0" } }, "sha512-Z9XSBoNE7xQiV6MSgPuCfyMokH2K7JdpRkOYE1+mu3d4BFJtx3GW+f6Bo4q8IX6rlf5MYbLBKW0pjl2cWdkm2A=="], + + "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "log-update/slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], + + "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "make-dir/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "make-fetch-happen/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "make-fetch-happen/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "make-fetch-happen/ssri": ["ssri@10.0.6", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ=="], + + "mathjs/fraction.js": ["fraction.js@4.3.4", "", {}, "sha512-pwiTgt0Q7t+GHZA4yaLjObx4vXmmdcS0iSJ19o8d/goUGgItX9UZWKWNnLHehxviD8wU2IWRsnR8cD5+yOJP2Q=="], + + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "mdast-util-find-and-replace/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], + + "mdast-util-find-and-replace/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], + + "mdast-util-from-markdown/mdast-util-to-string": ["mdast-util-to-string@3.2.0", "", { "dependencies": { "@types/mdast": "^3.0.0" } }, "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg=="], + + "mdast-util-phrasing/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], + + "mdast-util-to-markdown/mdast-util-to-string": ["mdast-util-to-string@3.2.0", "", { "dependencies": { "@types/mdast": "^3.0.0" } }, "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg=="], + + "mdast-util-to-markdown/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.1.1" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], + + "meow/normalize-package-data": ["normalize-package-data@3.0.3", "", { "dependencies": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", "semver": "^7.3.4", "validate-npm-package-license": "^3.0.1" } }, "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA=="], + + "meow/type-fest": ["type-fest@0.18.1", "", {}, "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw=="], + + "miller-rabin/bn.js": ["bn.js@4.12.1", "", {}, "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg=="], + + "minimist-options/arrify": ["arrify@1.0.1", "", {}, "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA=="], + + "minimist-options/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], + + "minipass-collect/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-fetch/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-json-stream/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "node-gyp/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "node-gyp/make-fetch-happen": ["make-fetch-happen@10.2.1", "", { "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "promise-retry": "^2.0.1", "socks-proxy-agent": "^7.0.0", "ssri": "^9.0.0" } }, "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w=="], + + "node-gyp/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "node-gyp/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + + "normalize-package-data/hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="], + + "normalize-package-data/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "normalize-url/query-string": ["query-string@4.3.4", "", { "dependencies": { "object-assign": "^4.1.0", "strict-uri-encode": "^1.0.0" } }, "sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q=="], + + "npm-package-arg/validate-npm-package-name": ["validate-npm-package-name@3.0.0", "", { "dependencies": { "builtins": "^1.0.3" } }, "sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw=="], + + "npm-packlist/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], + + "npm-pick-manifest/npm-normalize-package-bin": ["npm-normalize-package-bin@3.0.1", "", {}, "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ=="], + + "npm-pick-manifest/npm-package-arg": ["npm-package-arg@10.1.0", "", { "dependencies": { "hosted-git-info": "^6.0.0", "proc-log": "^3.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^5.0.0" } }, "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA=="], + + "npm-registry-fetch/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "npm-registry-fetch/npm-package-arg": ["npm-package-arg@10.1.0", "", { "dependencies": { "hosted-git-info": "^6.0.0", "proc-log": "^3.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^5.0.0" } }, "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "nx/axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="], + + "nx/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "nx/dotenv": ["dotenv@16.3.2", "", {}, "sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ=="], + + "nx/enquirer": ["enquirer@2.3.6", "", { "dependencies": { "ansi-colors": "^4.1.1" } }, "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg=="], + + "nx/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + + "nx/glob": ["glob@7.1.4", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A=="], + + "nx/minimatch": ["minimatch@3.0.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw=="], + + "nx/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "nx/semver": ["semver@7.5.3", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ=="], + + "nx/tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], + + "nx/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "nx/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "oidc-client/acorn": ["acorn@7.4.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="], + + "oidc-client/serialize-javascript": ["serialize-javascript@4.0.0", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw=="], + + "ora/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "ora/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "pacote/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "pacote/npm-package-arg": ["npm-package-arg@10.1.0", "", { "dependencies": { "hosted-git-info": "^6.0.0", "proc-log": "^3.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^5.0.0" } }, "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA=="], + + "pacote/npm-packlist": ["npm-packlist@7.0.4", "", { "dependencies": { "ignore-walk": "^6.0.0" } }, "sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q=="], + + "pacote/ssri": ["ssri@10.0.6", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ=="], + + "pacote/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + + "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "pkg-install/@types/node": ["@types/node@11.15.54", "", {}, "sha512-1RWYiq+5UfozGsU6MwJyFX6BtktcT10XRjvcAQmskCtMcW3tPske88lM/nHv7BQG1w9KBXI1zPGuu5PnNCX14g=="], + + "pkg-install/execa": ["execa@1.0.0", "", { "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } }, "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA=="], + + "pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + + "portfinder/async": ["async@2.6.4", "", { "dependencies": { "lodash": "^4.17.14" } }, "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA=="], + + "portfinder/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "postcss-loader/cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], + + "postcss-modules-local-by-default/postcss-selector-parser": ["postcss-selector-parser@7.0.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ=="], + + "postcss-modules-scope/postcss-selector-parser": ["postcss-selector-parser@7.0.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ=="], + + "postcss-normalize-url/normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], + + "postcss-svgo/svgo": ["svgo@2.8.0", "", { "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^4.1.3", "css-tree": "^1.1.3", "csso": "^4.2.0", "picocolors": "^1.0.0", "stable": "^0.1.8" }, "bin": { "svgo": "bin/svgo" } }, "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg=="], + + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + + "promise-retry/retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + + "promzard/read": ["read@3.0.1", "", { "dependencies": { "mute-stream": "^1.0.0" } }, "sha512-SLBrDU/Srs/9EoWhU5GdbAoxG1GzpQHo/6qiGItaoLJ1thmYpcNIM1qISEUvyHBzfGlWIyd6p2DNi1oV1VmAuw=="], + + "protons-runtime/uint8arrays": ["uint8arrays@5.1.0", "", { "dependencies": { "multiformats": "^13.0.0" } }, "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww=="], + + "proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "public-encrypt/bn.js": ["bn.js@4.12.1", "", {}, "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg=="], + + "pumpify/pump": ["pump@2.0.1", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA=="], + + "puppeteer-core/extract-zip": ["extract-zip@1.7.0", "", { "dependencies": { "concat-stream": "^1.6.2", "debug": "^2.6.9", "mkdirp": "^0.5.4", "yauzl": "^2.10.0" }, "bin": { "extract-zip": "cli.js" } }, "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA=="], + + "puppeteer-core/https-proxy-agent": ["https-proxy-agent@4.0.0", "", { "dependencies": { "agent-base": "5", "debug": "4" } }, "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg=="], + + "puppeteer-core/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "puppeteer-core/rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], + + "puppeteer-core/ws": ["ws@6.2.3", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA=="], + + "raw-body/bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + + "react-docgen/@types/doctrine": ["@types/doctrine@0.0.9", "", {}, "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA=="], + + "react-docgen/@types/resolve": ["@types/resolve@1.20.6", "", {}, "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ=="], + + "react-draggable/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], + + "react-element-to-jsx-string/is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="], + + "react-element-to-jsx-string/react-is": ["react-is@18.1.0", "", {}, "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg=="], + + "react-select/memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="], + + "react-shallow-renderer/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "react-test-renderer/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "read/mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="], + + "read-cache/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "read-package-json/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "read-package-json/json-parse-even-better-errors": ["json-parse-even-better-errors@3.0.2", "", {}, "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ=="], + + "read-package-json/normalize-package-data": ["normalize-package-data@5.0.0", "", { "dependencies": { "hosted-git-info": "^6.0.0", "is-core-module": "^2.8.1", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q=="], + + "read-package-json/npm-normalize-package-bin": ["npm-normalize-package-bin@3.0.1", "", {}, "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ=="], + + "read-package-json-fast/json-parse-even-better-errors": ["json-parse-even-better-errors@3.0.2", "", {}, "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ=="], + + "read-package-json-fast/npm-normalize-package-bin": ["npm-normalize-package-bin@3.0.1", "", {}, "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ=="], + + "read-pkg/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "read-pkg/type-fest": ["type-fest@0.6.0", "", {}, "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg=="], + + "read-pkg-up/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "read-pkg-up/type-fest": ["type-fest@0.8.1", "", {}, "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="], + + "readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "redent/indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "redent/strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + + "regjsparser/jsesc": ["jsesc@3.0.2", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="], + + "renderkid/css-select": ["css-select@4.3.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", "domhandler": "^4.3.1", "domutils": "^2.8.0", "nth-check": "^2.0.1" } }, "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ=="], + + "restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "rollup-plugin-terser/jest-worker": ["jest-worker@26.6.2", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^7.0.0" } }, "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ=="], + + "rollup-plugin-terser/serialize-javascript": ["serialize-javascript@4.0.0", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw=="], + + "schema-utils/ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="], + + "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], + + "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "send/range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "serve/ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="], + + "serve/chalk": ["chalk@5.0.1", "", {}, "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w=="], + + "serve-handler/mime-types": ["mime-types@2.1.18", "", { "dependencies": { "mime-db": "~1.33.0" } }, "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ=="], + + "serve-index/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "serve-index/http-errors": ["http-errors@1.6.3", "", { "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", "setprototypeof": "1.1.0", "statuses": ">= 1.4.0 < 2" } }, "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A=="], + + "shader-loader/loader-utils": ["loader-utils@1.4.2", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^1.0.1" } }, "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg=="], + + "shelljs/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "shelljs/interpret": ["interpret@1.4.0", "", {}, "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="], + + "shelljs/rechoir": ["rechoir@0.6.2", "", { "dependencies": { "resolve": "^1.1.6" } }, "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw=="], + + "sockjs/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "sort-keys/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], + + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "spdy-transport/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "split2/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + + "start-server-and-test/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="], + + "start-server-and-test/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "stream-browserify/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "stream-http/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "style-loader/schema-utils": ["schema-utils@2.7.1", "", { "dependencies": { "@types/json-schema": "^7.0.5", "ajv": "^6.12.4", "ajv-keywords": "^3.5.2" } }, "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg=="], + + "stylus/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "tailwindcss/object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "tar/fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + + "tar/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "temp/rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="], + + "tempy/del": ["del@6.1.1", "", { "dependencies": { "globby": "^11.0.1", "graceful-fs": "^4.2.4", "is-glob": "^4.0.1", "is-path-cwd": "^2.2.0", "is-path-inside": "^3.0.2", "p-map": "^4.0.0", "rimraf": "^3.0.2", "slash": "^3.0.0" } }, "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg=="], + + "tempy/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "tempy/temp-dir": ["temp-dir@2.0.0", "", {}, "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg=="], + + "tempy/type-fest": ["type-fest@0.16.0", "", {}, "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg=="], + + "test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "through2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "uint8-varint/uint8arrays": ["uint8arrays@5.1.0", "", { "dependencies": { "multiformats": "^13.0.0" } }, "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww=="], + + "uint8arraylist/uint8arrays": ["uint8arrays@5.1.0", "", { "dependencies": { "multiformats": "^13.0.0" } }, "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww=="], + + "uint8arrays/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], + + "unified/is-buffer": ["is-buffer@2.0.5", "", {}, "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="], + + "unified/is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "unplugin/webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + + "unused-webpack-plugin/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "update-check/registry-url": ["registry-url@3.1.0", "", { "dependencies": { "rc": "^1.0.1" } }, "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA=="], + + "url/punycode": ["punycode@1.4.1", "", {}, "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="], + + "url/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + + "url-loader/schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], + + "utif/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + + "uvu/kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + + "vfile/is-buffer": ["is-buffer@2.0.5", "", {}, "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="], + + "wait-on/axios": ["axios@0.27.2", "", { "dependencies": { "follow-redirects": "^1.14.9", "form-data": "^4.0.0" } }, "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ=="], + + "webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + + "webpack/schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], + + "webpack/webpack-sources": ["webpack-sources@3.2.3", "", {}, "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w=="], + + "webpack-dev-middleware/range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "webpack-dev-server/compression": ["compression@1.7.5", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.0.2", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q=="], + + "webpack-dev-server/del": ["del@6.1.1", "", { "dependencies": { "globby": "^11.0.1", "graceful-fs": "^4.2.4", "is-glob": "^4.0.1", "is-path-cwd": "^2.2.0", "is-path-inside": "^3.0.2", "p-map": "^4.0.0", "rimraf": "^3.0.2", "slash": "^3.0.0" } }, "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg=="], + + "webpack-dev-server/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "webpack-dev-server/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + + "webpack-sources/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "widest-line/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "workbox-build/ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="], + + "workbox-build/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "workbox-build/source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="], + + "workbox-build/tempy": ["tempy@0.6.0", "", { "dependencies": { "is-stream": "^2.0.0", "temp-dir": "^2.0.0", "type-fest": "^0.16.0", "unique-string": "^2.0.0" } }, "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw=="], + + "workbox-build/upath": ["upath@1.2.0", "", {}, "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg=="], + + "workbox-webpack-plugin/upath": ["upath@1.2.0", "", {}, "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg=="], + + "worker-loader/schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], + + "write-json-file/detect-indent": ["detect-indent@5.0.0", "", {}, "sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g=="], + + "write-json-file/make-dir": ["make-dir@2.1.0", "", { "dependencies": { "pify": "^4.0.1", "semver": "^5.6.0" } }, "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA=="], + + "write-json-file/pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], + + "write-json-file/sort-keys": ["sort-keys@2.0.0", "", { "dependencies": { "is-plain-obj": "^1.0.0" } }, "sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg=="], + + "write-json-file/write-file-atomic": ["write-file-atomic@2.4.3", "", { "dependencies": { "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", "signal-exit": "^3.0.2" } }, "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ=="], + + "write-pkg/sort-keys": ["sort-keys@2.0.0", "", { "dependencies": { "is-plain-obj": "^1.0.0" } }, "sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg=="], + + "write-pkg/type-fest": ["type-fest@0.4.1", "", {}, "sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw=="], + + "xmlbuilder2/js-yaml": ["js-yaml@3.14.0", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A=="], + + "yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], + + "@apideck/better-ajv-errors/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "@babel/register/find-cache-dir/pkg-dir": ["pkg-dir@3.0.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw=="], + + "@babel/register/make-dir/pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], + + "@babel/register/make-dir/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "@cornerstonejs/core/@kitware/vtk.js/@babel/runtime": ["@babel/runtime@7.22.11", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA=="], + + "@cornerstonejs/tools/@kitware/vtk.js/@babel/runtime": ["@babel/runtime@7.22.11", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA=="], + + "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "@itk-wasm/dam/axios/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "@itk-wasm/dam/tar/fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + + "@itk-wasm/dam/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "@itk-wasm/dam/tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "@jest/console/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@jest/core/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@jest/core/jest-validate/jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], + + "@jest/core/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "@jest/core/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "@jest/reporters/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@jest/transform/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@jest/transform/write-file-atomic/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "@jest/types/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@lerna/child-process/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@lerna/child-process/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "@lerna/child-process/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "@lerna/child-process/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "@lerna/child-process/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "@lerna/child-process/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "@lerna/child-process/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "@lerna/child-process/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "@lerna/create/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@lerna/create/cosmiconfig/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "@lerna/create/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "@lerna/create/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "@lerna/create/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "@lerna/create/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "@lerna/create/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "@lerna/create/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "@lerna/create/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "@npmcli/move-file/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "@nx/devkit/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "@octokit/request/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "@ohif/mode-segmentation/@babel/preset-env/@babel/plugin-proposal-private-property-in-object": ["@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w=="], + + "@ohif/mode-segmentation/@babel/preset-env/babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.8.7", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.4.4", "core-js-compat": "^3.33.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA=="], + + "@ohif/mode-segmentation/@babel/preset-env/babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.5.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.5.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg=="], + + "@ohif/mode-segmentation/@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@ohif/mode-segmentation/copy-webpack-plugin/globby": ["globby@12.2.0", "", { "dependencies": { "array-union": "^3.0.1", "dir-glob": "^3.0.1", "fast-glob": "^3.2.7", "ignore": "^5.1.9", "merge2": "^1.4.1", "slash": "^4.0.0" } }, "sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA=="], + + "@ohif/ui/babel-loader/find-cache-dir": ["find-cache-dir@4.0.0", "", { "dependencies": { "common-path-prefix": "^3.0.0", "pkg-dir": "^7.0.0" } }, "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg=="], + + "@ohif/ui/dotenv-webpack/dotenv-defaults": ["dotenv-defaults@2.0.2", "", { "dependencies": { "dotenv": "^8.2.0" } }, "sha512-iOIzovWfsUHU91L5i8bJce3NYK5JXeAwH50Jh6+ARUdLiiGlYWfGw6UkzsYqaXZH/hjE/eCd/PlfM/qqyK0AMg=="], + + "@ohif/ui/postcss-loader/cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], + + "@storybook/builder-webpack5/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@storybook/builder-webpack5/babel-loader/find-cache-dir": ["find-cache-dir@4.0.0", "", { "dependencies": { "common-path-prefix": "^3.0.0", "pkg-dir": "^7.0.0" } }, "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg=="], + + "@storybook/builder-webpack5/webpack-dev-middleware/range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "@storybook/cli/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@storybook/cli/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "@storybook/cli/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "@storybook/cli/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "@storybook/cli/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "@storybook/cli/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "@storybook/cli/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "@storybook/cli/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/number": ["@radix-ui/number@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/primitive": ["@radix-ui/primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-collection": ["@radix-ui/react-collection@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-slot": "1.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-context": ["@radix-ui/react-context@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-direction": ["@radix-ui/react-direction@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.0.4", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-callback-ref": "1.0.1", "@radix-ui/react-use-escape-keydown": "1.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-callback-ref": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-id": ["@radix-ui/react-id@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-popper": ["@radix-ui/react-popper@1.1.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.0.3", "@radix-ui/react-compose-refs": "1.0.1", "@radix-ui/react-context": "1.0.1", "@radix-ui/react-primitive": "1.0.3", "@radix-ui/react-use-callback-ref": "1.0.1", "@radix-ui/react-use-layout-effect": "1.0.1", "@radix-ui/react-use-rect": "1.0.1", "@radix-ui/react-use-size": "1.0.1", "@radix-ui/rect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-portal": ["@radix-ui/react-portal@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-primitive": ["@radix-ui/react-primitive@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-slot": "1.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-compose-refs": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA=="], + + "@storybook/components/@radix-ui/react-select/react-remove-scroll": ["react-remove-scroll@2.5.5", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.3", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", "use-sidecar": "^1.1.2" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw=="], + + "@storybook/core-common/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@storybook/core-common/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@storybook/core-common/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@storybook/core-common/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "@storybook/core-common/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "@storybook/core-server/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@storybook/core-server/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@storybook/core-server/compression/bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "@storybook/core-server/compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "@storybook/core-webpack/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@storybook/preset-react-webpack/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@storybook/react-webpack5/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@storybook/react/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@storybook/telemetry/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@storybook/types/@types/express/@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="], + + "@svgr/core/cosmiconfig/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "@svgr/plugin-svgo/cosmiconfig/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "@testing-library/dom/aria-query/deep-equal": ["deep-equal@2.2.3", "", { "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.5", "es-get-iterator": "^1.1.3", "get-intrinsic": "^1.2.2", "is-arguments": "^1.1.1", "is-array-buffer": "^3.0.2", "is-date-object": "^1.0.5", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "isarray": "^2.0.5", "object-is": "^1.1.5", "object-keys": "^1.1.1", "object.assign": "^4.1.4", "regexp.prototype.flags": "^1.5.1", "side-channel": "^1.0.4", "which-boxed-primitive": "^1.0.2", "which-collection": "^1.0.1", "which-typed-array": "^1.1.13" } }, "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA=="], + + "@testing-library/dom/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "@tufjs/models/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "@yarnpkg/parsers/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "babel-jest/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "babel-loader/schema-utils/ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + + "babel-plugin-istanbul/istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "babel-plugin-macros/cosmiconfig/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "boxen/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "boxen/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "boxen/wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "browserify-sign/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "browserify-sign/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "browserify-sign/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "cacache/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "cacache/tar/fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + + "cacache/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "cacache/tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "chalk-template/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "child-process-promise/cross-spawn/lru-cache": ["lru-cache@4.1.5", "", { "dependencies": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" } }, "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g=="], + + "child-process-promise/cross-spawn/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + + "clipboardy/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "clipboardy/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "clipboardy/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "clipboardy/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "clipboardy/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "clipboardy/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "clipboardy/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "conventional-changelog-core/normalize-package-data/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], + + "conventional-changelog-core/read-pkg/load-json-file": ["load-json-file@4.0.0", "", { "dependencies": { "graceful-fs": "^4.1.2", "parse-json": "^4.0.0", "pify": "^3.0.0", "strip-bom": "^3.0.0" } }, "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw=="], + + "conventional-changelog-core/read-pkg/normalize-package-data": ["normalize-package-data@2.5.0", "", { "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" } }, "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA=="], + + "conventional-changelog-core/read-pkg/path-type": ["path-type@3.0.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg=="], + + "conventional-changelog-core/read-pkg-up/find-up": ["find-up@2.1.0", "", { "dependencies": { "locate-path": "^2.0.0" } }, "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ=="], + + "copy-webpack-plugin/schema-utils/ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + + "cosmiconfig/import-fresh/resolve-from": ["resolve-from@3.0.0", "", {}, "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw=="], + + "cosmiconfig/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "create-jest/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "cross-env/cross-spawn/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], + + "cross-env/cross-spawn/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "cross-env/cross-spawn/shebang-command": ["shebang-command@1.2.0", "", { "dependencies": { "shebang-regex": "^1.0.0" } }, "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg=="], + + "cross-env/cross-spawn/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + + "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], + + "cypress/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "cypress/execa/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + + "cypress/execa/human-signals": ["human-signals@1.1.1", "", {}, "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw=="], + + "cypress/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "cypress/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "cypress/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "cypress/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "cypress/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "decompress-tar/tar-stream/bl": ["bl@1.2.3", "", { "dependencies": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" } }, "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww=="], + + "decompress-tar/tar-stream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "decompress/make-dir/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], + + "default-gateway/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "default-gateway/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "default-gateway/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "default-gateway/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "default-gateway/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "default-gateway/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "default-gateway/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "del/globby/array-union": ["array-union@1.0.2", "", { "dependencies": { "array-uniq": "^1.0.1" } }, "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng=="], + + "del/globby/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "del/globby/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "del/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "detect-package-manager/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "detect-package-manager/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "detect-package-manager/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "detect-package-manager/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "detect-package-manager/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "detect-package-manager/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "detect-package-manager/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "dicom-microscopy-viewer/mathjs/fraction.js": ["fraction.js@4.3.4", "", {}, "sha512-pwiTgt0Q7t+GHZA4yaLjObx4vXmmdcS0iSJ19o8d/goUGgItX9UZWKWNnLHehxviD8wU2IWRsnR8cD5+yOJP2Q=="], + + "duplexify/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "duplexify/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "duplexify/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "eslint-loader/loader-utils/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "eslint-loader/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "eslint-webpack-plugin/schema-utils/ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + + "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "expect/jest-matcher-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "expect/jest-matcher-utils/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "extract-css-chunks-webpack-plugin/schema-utils/ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + + "file-loader/schema-utils/ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + + "filelist/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "flat-cache/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "fork-ts-checker-webpack-plugin/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "fork-ts-checker-webpack-plugin/cosmiconfig/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "fork-ts-checker-webpack-plugin/schema-utils/ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + + "get-pkg-repo/hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "giget/tar/fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + + "giget/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "giget/tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "gunzip-maybe/browserify-zlib/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], + + "hpack.js/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "hpack.js/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "hpack.js/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "htmlparser2/domutils/dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="], + + "husky/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "husky/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "husky/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "husky/execa/cross-spawn": ["cross-spawn@6.0.6", "", { "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw=="], + + "husky/execa/get-stream": ["get-stream@4.1.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="], + + "husky/execa/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="], + + "husky/execa/npm-run-path": ["npm-run-path@2.0.2", "", { "dependencies": { "path-key": "^2.0.0" } }, "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw=="], + + "husky/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "ignore-walk/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "init-package-json/npm-package-arg/hosted-git-info": ["hosted-git-info@6.1.3", "", { "dependencies": { "lru-cache": "^7.5.1" } }, "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw=="], + + "inquirer/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "ipfs-unixfs-exporter/p-queue/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "ipfs-unixfs-exporter/p-queue/p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], + + "itk-wasm/axios/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "itk-wasm/glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + + "jake/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-changed-files/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "jest-changed-files/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "jest-changed-files/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "jest-changed-files/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "jest-changed-files/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "jest-changed-files/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "jest-changed-files/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "jest-circus/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-circus/jest-matcher-utils/jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], + + "jest-circus/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "jest-circus/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "jest-cli/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-cli/jest-validate/jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], + + "jest-cli/jest-validate/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "jest-cli/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "jest-config/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-config/parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "jest-config/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "jest-config/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "jest-diff/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-diff/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "jest-diff/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "jest-each/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-each/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "jest-each/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "jest-junit/strip-ansi/ansi-regex": ["ansi-regex@3.0.1", "", {}, "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw=="], + + "jest-leak-detector/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "jest-leak-detector/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "jest-matcher-utils/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-matcher-utils/jest-diff/diff-sequences": ["diff-sequences@27.5.1", "", {}, "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ=="], + + "jest-message-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-message-util/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "jest-message-util/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "jest-resolve/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-resolve/jest-validate/jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], + + "jest-resolve/jest-validate/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "jest-runner/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-runner/source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "jest-runtime/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-snapshot/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-snapshot/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "jest-snapshot/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jest-validate/@jest/types/@types/istanbul-reports": ["@types/istanbul-reports@1.1.2", "", { "dependencies": { "@types/istanbul-lib-coverage": "*", "@types/istanbul-lib-report": "*" } }, "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw=="], + + "jest-validate/@jest/types/@types/yargs": ["@types/yargs@13.0.12", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qCxJE1qgz2y0hA4pIxjBR+PelCH0U5CK1XJXFwCNqfmliatKp47UCXXE9Dyk1OXBDLvsCF57TqQEJaeLfDYEOQ=="], + + "jest-validate/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "jest-validate/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "jest-validate/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "jest-validate/pretty-format/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], + + "jest-validate/pretty-format/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "jest-watcher/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jscodeshift/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "jscodeshift/write-file-atomic/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "jsdom/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="], + + "lerna/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "lerna/cosmiconfig/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "lerna/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "lerna/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "lerna/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "lerna/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "lerna/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "lerna/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "lerna/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "libnpmaccess/npm-package-arg/hosted-git-info": ["hosted-git-info@6.1.3", "", { "dependencies": { "lru-cache": "^7.5.1" } }, "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw=="], + + "libnpmpublish/normalize-package-data/hosted-git-info": ["hosted-git-info@6.1.3", "", { "dependencies": { "lru-cache": "^7.5.1" } }, "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw=="], + + "libnpmpublish/npm-package-arg/hosted-git-info": ["hosted-git-info@6.1.3", "", { "dependencies": { "lru-cache": "^7.5.1" } }, "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw=="], + + "libnpmpublish/ssri/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "lint-staged/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "lint-staged/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "lint-staged/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "lint-staged/del/globby": ["globby@10.0.2", "", { "dependencies": { "@types/glob": "^7.1.1", "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.0.3", "glob": "^7.1.3", "ignore": "^5.1.1", "merge2": "^1.2.3", "slash": "^3.0.0" } }, "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg=="], + + "lint-staged/del/p-map": ["p-map@3.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ=="], + + "lint-staged/del/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "lint-staged/execa/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + + "lint-staged/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "lint-staged/execa/npm-run-path": ["npm-run-path@3.1.0", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg=="], + + "lint-staged/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "lint-staged/execa/p-finally": ["p-finally@2.0.1", "", {}, "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw=="], + + "lint-staged/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "lint-staged/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "listr-update-renderer/chalk/ansi-styles": ["ansi-styles@2.2.1", "", {}, "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA=="], + + "listr-update-renderer/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "listr-update-renderer/chalk/supports-color": ["supports-color@2.0.0", "", {}, "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g=="], + + "listr-update-renderer/cli-truncate/slice-ansi": ["slice-ansi@0.0.4", "", {}, "sha512-up04hB2hR92PgjpyU3y/eg91yIBILyjVY26NvvciY3EVVPjybkMszMpXQ9QAkcS3I5rtJBDLoTxxg+qvW8c7rw=="], + + "listr-update-renderer/cli-truncate/string-width": ["string-width@1.0.2", "", { "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", "strip-ansi": "^3.0.0" } }, "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw=="], + + "listr-update-renderer/figures/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "listr-update-renderer/log-update/ansi-escapes": ["ansi-escapes@3.2.0", "", {}, "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ=="], + + "listr-update-renderer/log-update/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], + + "listr-update-renderer/log-update/wrap-ansi": ["wrap-ansi@3.0.1", "", { "dependencies": { "string-width": "^2.1.1", "strip-ansi": "^4.0.0" } }, "sha512-iXR3tDXpbnTpzjKSylUJRkLuOrEC7hwEB221cgn6wtF8wpmz28puFXAEfPT5zrjM3wahygB//VuWEr1vTkDcNQ=="], + + "listr-update-renderer/strip-ansi/ansi-regex": ["ansi-regex@2.1.1", "", {}, "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA=="], + + "listr-verbose-renderer/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "listr-verbose-renderer/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "listr-verbose-renderer/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "listr-verbose-renderer/cli-cursor/restore-cursor": ["restore-cursor@2.0.0", "", { "dependencies": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" } }, "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q=="], + + "listr-verbose-renderer/figures/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "listr/rxjs/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "load-json-file/parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "loader-fs-cache/find-cache-dir/pkg-dir": ["pkg-dir@1.0.0", "", { "dependencies": { "find-up": "^1.0.0" } }, "sha512-c6pv3OE78mcZ92ckebVDqg0aWSoKhOTbwCV6qbCWMk546mAL9pZln0+QsN/yQ7fkucd4+yJPLrCBXNt8Ruk+Eg=="], + + "log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "make-fetch-happen/ssri/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "mdast-util-to-markdown/unist-util-visit/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], + + "mdast-util-to-markdown/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], + + "meow/normalize-package-data/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], + + "node-gyp/make-fetch-happen/cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="], + + "node-gyp/make-fetch-happen/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "node-gyp/make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "node-gyp/make-fetch-happen/minipass-fetch": ["minipass-fetch@2.1.2", "", { "dependencies": { "minipass": "^3.1.6", "minipass-sized": "^1.0.3", "minizlib": "^2.1.2" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA=="], + + "node-gyp/tar/fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + + "node-gyp/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "node-gyp/tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "normalize-url/query-string/strict-uri-encode": ["strict-uri-encode@1.1.0", "", {}, "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ=="], + + "npm-package-arg/validate-npm-package-name/builtins": ["builtins@1.0.3", "", {}, "sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ=="], + + "npm-packlist/glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + + "npm-pick-manifest/npm-package-arg/hosted-git-info": ["hosted-git-info@6.1.3", "", { "dependencies": { "lru-cache": "^7.5.1" } }, "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw=="], + + "npm-registry-fetch/npm-package-arg/hosted-git-info": ["hosted-git-info@6.1.3", "", { "dependencies": { "lru-cache": "^7.5.1" } }, "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw=="], + + "nx/axios/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "nx/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "nx/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "nx/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "ora/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "pacote/npm-package-arg/hosted-git-info": ["hosted-git-info@6.1.3", "", { "dependencies": { "lru-cache": "^7.5.1" } }, "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw=="], + + "pacote/npm-packlist/ignore-walk": ["ignore-walk@6.0.5", "", { "dependencies": { "minimatch": "^9.0.0" } }, "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A=="], + + "pacote/ssri/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "pacote/tar/fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + + "pacote/tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "pkg-install/execa/cross-spawn": ["cross-spawn@6.0.6", "", { "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw=="], + + "pkg-install/execa/get-stream": ["get-stream@4.1.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="], + + "pkg-install/execa/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="], + + "pkg-install/execa/npm-run-path": ["npm-run-path@2.0.2", "", { "dependencies": { "path-key": "^2.0.0" } }, "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw=="], + + "pkg-install/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + + "postcss-loader/cosmiconfig/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "postcss-svgo/svgo/css-select": ["css-select@4.3.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", "domhandler": "^4.3.1", "domutils": "^2.8.0", "nth-check": "^2.0.1" } }, "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ=="], + + "postcss-svgo/svgo/css-tree": ["css-tree@1.1.3", "", { "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" } }, "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q=="], + + "postcss-svgo/svgo/csso": ["csso@4.2.0", "", { "dependencies": { "css-tree": "^1.1.2" } }, "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA=="], + + "promzard/read/mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="], + + "puppeteer-core/extract-zip/concat-stream": ["concat-stream@1.6.2", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw=="], + + "puppeteer-core/extract-zip/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "puppeteer-core/https-proxy-agent/agent-base": ["agent-base@5.1.1", "", {}, "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g=="], + + "puppeteer-core/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "read-package-json/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "read-package-json/glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "read-package-json/normalize-package-data/hosted-git-info": ["hosted-git-info@6.1.3", "", { "dependencies": { "lru-cache": "^7.5.1" } }, "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw=="], + + "read-pkg-up/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "read-pkg/parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "renderkid/css-select/domhandler": ["domhandler@4.3.1", "", { "dependencies": { "domelementtype": "^2.2.0" } }, "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ=="], + + "renderkid/css-select/domutils": ["domutils@2.8.0", "", { "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", "domhandler": "^4.2.0" } }, "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A=="], + + "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "rollup-plugin-terser/jest-worker/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "serve-handler/mime-types/mime-db": ["mime-db@1.33.0", "", {}, "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="], + + "serve-index/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "serve-index/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="], + + "serve-index/http-errors/inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="], + + "serve-index/http-errors/setprototypeof": ["setprototypeof@1.1.0", "", {}, "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="], + + "serve-index/http-errors/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], + + "serve/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "shader-loader/loader-utils/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "start-server-and-test/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + + "start-server-and-test/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "start-server-and-test/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "start-server-and-test/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "start-server-and-test/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "start-server-and-test/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "start-server-and-test/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "start-server-and-test/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "style-loader/schema-utils/ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + + "temp/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "tempy/del/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "through2/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "through2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "through2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "unused-webpack-plugin/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "unused-webpack-plugin/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "unused-webpack-plugin/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "url-loader/schema-utils/ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + + "webpack-dev-server/compression/bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "webpack-dev-server/compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "webpack-dev-server/del/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "webpack-dev-server/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + + "webpack/schema-utils/ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + + "widest-line/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "workbox-build/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "workbox-build/source-map/whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="], + + "workbox-build/tempy/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "workbox-build/tempy/temp-dir": ["temp-dir@2.0.0", "", {}, "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg=="], + + "workbox-build/tempy/type-fest": ["type-fest@0.16.0", "", {}, "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg=="], + + "worker-loader/schema-utils/ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + + "write-json-file/make-dir/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "write-json-file/sort-keys/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], + + "write-json-file/write-file-atomic/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "write-pkg/sort-keys/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], + + "xmlbuilder2/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "@babel/register/find-cache-dir/pkg-dir/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "@itk-wasm/dam/tar/fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "@lerna/child-process/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "@lerna/create/cosmiconfig/parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "@lerna/create/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "@lerna/create/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "@lerna/create/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "@octokit/request/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "@octokit/request/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "@ohif/mode-segmentation/@babel/preset-env/babel-plugin-polyfill-corejs3/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.4.4", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA=="], + + "@ohif/mode-segmentation/@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.5.0", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q=="], + + "@ohif/mode-segmentation/copy-webpack-plugin/globby/array-union": ["array-union@3.0.1", "", {}, "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw=="], + + "@ohif/mode-segmentation/copy-webpack-plugin/globby/slash": ["slash@4.0.0", "", {}, "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew=="], + + "@ohif/ui/babel-loader/find-cache-dir/pkg-dir": ["pkg-dir@7.0.0", "", { "dependencies": { "find-up": "^6.3.0" } }, "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA=="], + + "@ohif/ui/postcss-loader/cosmiconfig/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "@storybook/builder-webpack5/babel-loader/find-cache-dir/pkg-dir": ["pkg-dir@7.0.0", "", { "dependencies": { "find-up": "^6.3.0" } }, "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA=="], + + "@storybook/cli/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-callback-ref": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.0.3", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-primitive": "1.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-popper/@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/rect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-popper/@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/react-use-layout-effect": "1.0.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0" }, "optionalPeers": ["@types/react"] }, "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g=="], + + "@storybook/components/@radix-ui/react-select/@radix-ui/react-popper/@radix-ui/rect": ["@radix-ui/rect@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ=="], + + "@storybook/core-common/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "@storybook/core-common/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "@storybook/core-common/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "@storybook/core-server/compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "@svgr/core/cosmiconfig/parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "@svgr/plugin-svgo/cosmiconfig/parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "babel-plugin-macros/cosmiconfig/parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "boxen/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "cacache/tar/fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "child-process-promise/cross-spawn/lru-cache/yallist": ["yallist@2.1.2", "", {}, "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A=="], + + "clipboardy/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "conventional-changelog-core/normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "conventional-changelog-core/read-pkg-up/find-up/locate-path": ["locate-path@2.0.0", "", { "dependencies": { "p-locate": "^2.0.0", "path-exists": "^3.0.0" } }, "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA=="], + + "conventional-changelog-core/read-pkg/load-json-file/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], + + "conventional-changelog-core/read-pkg/normalize-package-data/hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="], + + "conventional-changelog-core/read-pkg/normalize-package-data/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "conventional-changelog-core/read-pkg/path-type/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], + + "cross-env/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], + + "cypress/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "decompress-tar/tar-stream/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "decompress-tar/tar-stream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "decompress-tar/tar-stream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "default-gateway/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "detect-package-manager/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "expect/jest-matcher-utils/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "expect/jest-matcher-utils/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "expect/jest-matcher-utils/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "fork-ts-checker-webpack-plugin/cosmiconfig/parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "giget/tar/fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "husky/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "husky/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "husky/execa/cross-spawn/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], + + "husky/execa/cross-spawn/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "husky/execa/cross-spawn/shebang-command": ["shebang-command@1.2.0", "", { "dependencies": { "shebang-regex": "^1.0.0" } }, "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg=="], + + "husky/execa/cross-spawn/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + + "husky/execa/npm-run-path/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], + + "init-package-json/npm-package-arg/hosted-git-info/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "itk-wasm/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "jest-changed-files/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "jest-cli/jest-validate/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "jest-cli/jest-validate/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "jest-resolve/jest-validate/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "jest-resolve/jest-validate/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "jest-validate/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "jest-validate/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "jest-validate/pretty-format/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "lerna/cosmiconfig/parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "lerna/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "lerna/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "lerna/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "libnpmaccess/npm-package-arg/hosted-git-info/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "libnpmpublish/normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "libnpmpublish/npm-package-arg/hosted-git-info/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "lint-staged/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "lint-staged/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "lint-staged/del/globby/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "lint-staged/del/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "lint-staged/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "listr-update-renderer/cli-truncate/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@1.0.0", "", { "dependencies": { "number-is-nan": "^1.0.0" } }, "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw=="], + + "listr-update-renderer/log-update/cli-cursor/restore-cursor": ["restore-cursor@2.0.0", "", { "dependencies": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" } }, "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q=="], + + "listr-update-renderer/log-update/wrap-ansi/string-width": ["string-width@2.1.1", "", { "dependencies": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" } }, "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw=="], + + "listr-update-renderer/log-update/wrap-ansi/strip-ansi": ["strip-ansi@4.0.0", "", { "dependencies": { "ansi-regex": "^3.0.0" } }, "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow=="], + + "listr-verbose-renderer/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "listr-verbose-renderer/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "listr-verbose-renderer/cli-cursor/restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="], + + "listr-verbose-renderer/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "loader-fs-cache/find-cache-dir/pkg-dir/find-up": ["find-up@1.1.2", "", { "dependencies": { "path-exists": "^2.0.0", "pinkie-promise": "^2.0.0" } }, "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA=="], + + "meow/normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "node-gyp/make-fetch-happen/cacache/@npmcli/fs": ["@npmcli/fs@2.1.2", "", { "dependencies": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" } }, "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ=="], + + "node-gyp/make-fetch-happen/cacache/fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + + "node-gyp/make-fetch-happen/cacache/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], + + "node-gyp/make-fetch-happen/cacache/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "node-gyp/make-fetch-happen/cacache/unique-filename": ["unique-filename@2.0.1", "", { "dependencies": { "unique-slug": "^3.0.0" } }, "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A=="], + + "node-gyp/tar/fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "npm-packlist/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "npm-pick-manifest/npm-package-arg/hosted-git-info/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "npm-registry-fetch/npm-package-arg/hosted-git-info/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "pacote/npm-package-arg/hosted-git-info/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "pacote/npm-packlist/ignore-walk/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "pacote/tar/fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "pkg-install/execa/cross-spawn/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], + + "pkg-install/execa/cross-spawn/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "pkg-install/execa/cross-spawn/shebang-command": ["shebang-command@1.2.0", "", { "dependencies": { "shebang-regex": "^1.0.0" } }, "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg=="], + + "pkg-install/execa/cross-spawn/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], + + "pkg-install/execa/npm-run-path/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], + + "pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], + + "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + + "postcss-loader/cosmiconfig/parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "postcss-svgo/svgo/css-select/domhandler": ["domhandler@4.3.1", "", { "dependencies": { "domelementtype": "^2.2.0" } }, "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ=="], + + "postcss-svgo/svgo/css-select/domutils": ["domutils@2.8.0", "", { "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", "domhandler": "^4.2.0" } }, "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A=="], + + "postcss-svgo/svgo/css-tree/mdn-data": ["mdn-data@2.0.14", "", {}, "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="], + + "postcss-svgo/svgo/css-tree/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "puppeteer-core/extract-zip/concat-stream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "puppeteer-core/extract-zip/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "read-package-json/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "read-package-json/normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "read-pkg-up/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "renderkid/css-select/domutils/dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="], + + "start-server-and-test/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "tempy/del/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "unused-webpack-plugin/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "unused-webpack-plugin/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "webpack-dev-server/compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "webpack-dev-server/del/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "widest-line/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "workbox-build/source-map/whatwg-url/tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="], + + "workbox-build/source-map/whatwg-url/webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="], + + "@babel/register/find-cache-dir/pkg-dir/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "@ohif/ui/babel-loader/find-cache-dir/pkg-dir/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="], + + "@ohif/ui/postcss-loader/cosmiconfig/parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "@storybook/builder-webpack5/babel-loader/find-cache-dir/pkg-dir/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="], + + "conventional-changelog-core/read-pkg-up/find-up/locate-path/p-locate": ["p-locate@2.0.0", "", { "dependencies": { "p-limit": "^1.1.0" } }, "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg=="], + + "conventional-changelog-core/read-pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + + "husky/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "husky/execa/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], + + "jest-validate/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "jest-validate/pretty-format/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "lint-staged/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "listr-update-renderer/log-update/cli-cursor/restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="], + + "listr-update-renderer/log-update/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "listr-update-renderer/log-update/wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@2.0.0", "", {}, "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w=="], + + "listr-update-renderer/log-update/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@3.0.1", "", {}, "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw=="], + + "listr-verbose-renderer/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "listr-verbose-renderer/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], + + "loader-fs-cache/find-cache-dir/pkg-dir/find-up/path-exists": ["path-exists@2.1.0", "", { "dependencies": { "pinkie-promise": "^2.0.0" } }, "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ=="], + + "node-gyp/make-fetch-happen/cacache/glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + + "node-gyp/make-fetch-happen/cacache/unique-filename/unique-slug": ["unique-slug@3.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w=="], + + "pacote/npm-packlist/ignore-walk/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "pkg-install/execa/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], + + "pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "postcss-svgo/svgo/css-select/domutils/dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="], + + "puppeteer-core/extract-zip/concat-stream/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "puppeteer-core/extract-zip/concat-stream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "puppeteer-core/extract-zip/concat-stream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "renderkid/css-select/domutils/dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], + + "unused-webpack-plugin/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "@babel/register/find-cache-dir/pkg-dir/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], + + "@babel/register/find-cache-dir/pkg-dir/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + + "@ohif/ui/babel-loader/find-cache-dir/pkg-dir/find-up/locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="], + + "@ohif/ui/babel-loader/find-cache-dir/pkg-dir/find-up/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="], + + "@storybook/builder-webpack5/babel-loader/find-cache-dir/pkg-dir/find-up/locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="], + + "@storybook/builder-webpack5/babel-loader/find-cache-dir/pkg-dir/find-up/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="], + + "conventional-changelog-core/read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@1.3.0", "", { "dependencies": { "p-try": "^1.0.0" } }, "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q=="], + + "listr-update-renderer/log-update/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], + + "node-gyp/make-fetch-happen/cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "postcss-svgo/svgo/css-select/domutils/dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], + + "@babel/register/find-cache-dir/pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "@ohif/ui/babel-loader/find-cache-dir/pkg-dir/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], + + "@storybook/builder-webpack5/babel-loader/find-cache-dir/pkg-dir/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], + + "conventional-changelog-core/read-pkg-up/find-up/locate-path/p-locate/p-limit/p-try": ["p-try@1.0.0", "", {}, "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww=="], + + "@ohif/ui/babel-loader/find-cache-dir/pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], + + "@storybook/builder-webpack5/babel-loader/find-cache-dir/pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], + + "@ohif/ui/babel-loader/find-cache-dir/pkg-dir/find-up/locate-path/p-locate/p-limit/yocto-queue": ["yocto-queue@1.1.1", "", {}, "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g=="], + + "@storybook/builder-webpack5/babel-loader/find-cache-dir/pkg-dir/find-up/locate-path/p-locate/p-limit/yocto-queue": ["yocto-queue@1.1.1", "", {}, "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g=="], + } +} diff --git a/commit.txt b/commit.txt new file mode 100644 index 0000000..0cdaa53 --- /dev/null +++ b/commit.txt @@ -0,0 +1 @@ +fdb073c216013477c8545db34d254a9ad328fe48 \ No newline at end of file diff --git a/eslintAliasesResolver.js b/eslintAliasesResolver.js new file mode 100644 index 0000000..146d8d1 --- /dev/null +++ b/eslintAliasesResolver.js @@ -0,0 +1,8 @@ +module.exports.interfaceVersion = 2; + +module.exports.resolve = (source, file, aliases) => { + if (aliases[source]) { + return { found: true, path: aliases[source] }; + } + return { found: false }; +}; diff --git a/extensions/cornerstone-dicom-pmap/.webpack/webpack.dev.js b/extensions/cornerstone-dicom-pmap/.webpack/webpack.dev.js new file mode 100644 index 0000000..6aea859 --- /dev/null +++ b/extensions/cornerstone-dicom-pmap/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/extensions/cornerstone-dicom-pmap/.webpack/webpack.prod.js b/extensions/cornerstone-dicom-pmap/.webpack/webpack.prod.js new file mode 100644 index 0000000..0ae7d6c --- /dev/null +++ b/extensions/cornerstone-dicom-pmap/.webpack/webpack.prod.js @@ -0,0 +1,54 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +const pkg = require('./../package.json'); + +const ROOT_DIR = path.join(__dirname, '../'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +const outputName = `ohif-${pkg.name.split('/').pop()}`; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: true, + }, + output: { + path: ROOT_DIR, + library: 'ohif-extension-cornerstone-dicom-pmap', + libraryTarget: 'umd', + filename: pkg.main, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + new MiniCssExtractPlugin({ + filename: `./dist/${outputName}.css`, + chunkFilename: `./dist/${outputName}.css`, + }), + ], + }); +}; diff --git a/extensions/cornerstone-dicom-pmap/CHANGELOG.md b/extensions/cornerstone-dicom-pmap/CHANGELOG.md new file mode 100644 index 0000000..2b1d3fe --- /dev/null +++ b/extensions/cornerstone-dicom-pmap/CHANGELOG.md @@ -0,0 +1,1315 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + + +### Bug Fixes + +* **core:** Address 3D reconstruction and Android compatibility issues and clean up 4D data mode ([#4762](https://github.com/OHIF/Viewers/issues/4762)) ([149d6d0](https://github.com/OHIF/Viewers/commit/149d6d049cd333b9e5846576b403ff387558a66f)) + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + + +### Features + +* Add customization support for more UI components ([#4634](https://github.com/OHIF/Viewers/issues/4634)) ([f15eb44](https://github.com/OHIF/Viewers/commit/f15eb44b4cf49de1b73a22512571cec02effaef3)) + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + + +### Bug Fixes + +* **seg:** sphere scissor on stack and cpu rendering reset properties was broken ([#4721](https://github.com/OHIF/Viewers/issues/4721)) ([f00d182](https://github.com/OHIF/Viewers/commit/f00d18292f02e8910215d913edfc994850a68d88)) + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + + +### Bug Fixes + +* Having sop instance in a per-frame or shared attribute breaks load ([#4560](https://github.com/OHIF/Viewers/issues/4560)) ([cded082](https://github.com/OHIF/Viewers/commit/cded08261788143e0d5be57a55c927fd96aafb22)) + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + + +### Bug Fixes + +* **docker:** publish manifest for multiarch and update cs3d ([#4650](https://github.com/OHIF/Viewers/issues/4650)) ([836e67a](https://github.com/OHIF/Viewers/commit/836e67a6ab8de66d8908c75856774318729544f4)) + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + + +### Bug Fixes + +* **tools:** enable additional tools in volume viewport ([#4620](https://github.com/OHIF/Viewers/issues/4620)) ([1992002](https://github.com/OHIF/Viewers/commit/1992002d2dced171c17b9a0163baf707fc551e3d)) + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + + +### Bug Fixes + +* **seg:** jump to the first slice in SEG and RT that has data ([#4605](https://github.com/OHIF/Viewers/issues/4605)) ([9bf24d6](https://github.com/OHIF/Viewers/commit/9bf24d6dc58ed8f65c90899a17c11044b792cf40)) + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + + +### Features + +* **node:** move to node 20 ([#4594](https://github.com/OHIF/Viewers/issues/4594)) ([1f04d6c](https://github.com/OHIF/Viewers/commit/1f04d6c1be729a26fe7bcda923770a1cd461053c)) + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + + +### Bug Fixes + +* **CinePlayer:** always show cine player for dynamic data ([#4575](https://github.com/OHIF/Viewers/issues/4575)) ([b8e8bbe](https://github.com/OHIF/Viewers/commit/b8e8bbe482b66e8cbe9167d03e9d8dedd2d3b6c5)) + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + + +### Bug Fixes + +* **multiframe:** metadata handling of NM studies and loading order ([#4554](https://github.com/OHIF/Viewers/issues/4554)) ([7624ccb](https://github.com/OHIF/Viewers/commit/7624ccb5e495c0a151227a458d8d5bfb8babb22c)) + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + + +### Bug Fixes + +* **colorlut:** use the correct colorlut index and update vtk ([#4544](https://github.com/OHIF/Viewers/issues/4544)) ([b9c26e7](https://github.com/OHIF/Viewers/commit/b9c26e775a49044673473418dd5bdee2e5562ab9)) + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + + +### Bug Fixes + +* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed)) + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + + +### Bug Fixes + +* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30)) + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + + +### Bug Fixes + +* **tmtv:** fix toggle one up weird behaviours ([#4473](https://github.com/OHIF/Viewers/issues/4473)) ([aa2b649](https://github.com/OHIF/Viewers/commit/aa2b649444eb4fe5422e72ea7830a709c4d24a90)) + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + + +### Bug Fixes + +* build ([#4471](https://github.com/OHIF/Viewers/issues/4471)) ([3d11ef2](https://github.com/OHIF/Viewers/commit/3d11ef28f213361ec7586809317bd219fa70e742)) + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + + +### Features + +* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3)) + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + + +### Features + +* **pmap:** added support for parametric map ([#4284](https://github.com/OHIF/Viewers/issues/4284)) ([fc0064f](https://github.com/OHIF/Viewers/commit/fc0064fd9d8cdc8fde81b81f0e71fd5d077ca22b)) diff --git a/extensions/cornerstone-dicom-pmap/LICENSE b/extensions/cornerstone-dicom-pmap/LICENSE new file mode 100644 index 0000000..983c5ef --- /dev/null +++ b/extensions/cornerstone-dicom-pmap/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2023 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/extensions/cornerstone-dicom-pmap/README.md b/extensions/cornerstone-dicom-pmap/README.md new file mode 100644 index 0000000..af90336 --- /dev/null +++ b/extensions/cornerstone-dicom-pmap/README.md @@ -0,0 +1,12 @@ +# dicom-pmap +## Description + +DICOM PMAP read workflow. This extension will allow you to load a DICOM Parametric +Map image and display it on OHIF. + +## Author + +OHIF + +## License +MIT diff --git a/extensions/cornerstone-dicom-pmap/babel.config.js b/extensions/cornerstone-dicom-pmap/babel.config.js new file mode 100644 index 0000000..e514425 --- /dev/null +++ b/extensions/cornerstone-dicom-pmap/babel.config.js @@ -0,0 +1,44 @@ +module.exports = { + plugins: ['@babel/plugin-proposal-class-properties'], + env: { + test: { + presets: [ + [ + // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2 + '@babel/preset-env', + { + modules: 'commonjs', + debug: false, + }, + '@babel/preset-typescript', + ], + '@babel/preset-react', + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-regenerator', + '@babel/plugin-transform-runtime', + ], + }, + production: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + development: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + plugins: ['react-hot-loader/babel'], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + }, +}; diff --git a/extensions/cornerstone-dicom-pmap/package.json b/extensions/cornerstone-dicom-pmap/package.json new file mode 100644 index 0000000..3f293af --- /dev/null +++ b/extensions/cornerstone-dicom-pmap/package.json @@ -0,0 +1,54 @@ +{ + "name": "@ohif/extension-cornerstone-dicom-pmap", + "version": "3.10.0-beta.111", + "description": "DICOM Parametric Map read workflow", + "author": "OHIF", + "license": "MIT", + "main": "dist/ohif-extension-cornerstone-dicom-pmap.umd.js", + "module": "src/index.tsx", + "files": [ + "dist/**", + "public/**", + "README.md" + ], + "repository": "OHIF/Viewers", + "keywords": [ + "ohif-extension" + ], + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.18.0" + }, + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:dicom-pmap": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package-1": "yarn run build", + "start": "yarn run dev" + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.111", + "@ohif/extension-cornerstone": "3.10.0-beta.111", + "@ohif/extension-default": "3.10.0-beta.111", + "@ohif/i18n": "3.10.0-beta.111", + "prop-types": "^15.6.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^12.2.2", + "react-router": "^6.8.1", + "react-router-dom": "^6.8.1" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "@cornerstonejs/adapters": "^2.19.14", + "@cornerstonejs/core": "^2.19.14", + "@kitware/vtk.js": "32.1.1", + "react-color": "^2.19.3" + } +} diff --git a/extensions/cornerstone-dicom-pmap/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-pmap/src/getSopClassHandlerModule.ts new file mode 100644 index 0000000..dc21ba3 --- /dev/null +++ b/extensions/cornerstone-dicom-pmap/src/getSopClassHandlerModule.ts @@ -0,0 +1,244 @@ +import { utils } from '@ohif/core'; +import { metaData, cache, utilities as csUtils, volumeLoader } from '@cornerstonejs/core'; +import { adaptersPMAP } from '@cornerstonejs/adapters'; +import { SOPClassHandlerId } from './id'; +import { dicomLoaderService } from '@ohif/extension-cornerstone'; + +const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume'; +const sopClassUids = ['1.2.840.10008.5.1.4.1.1.30']; + +function _getDisplaySetsFromSeries( + instances, + servicesManager: AppTypes.ServicesManager, + extensionManager +) { + const instance = instances[0]; + + const { + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + SeriesDescription, + SeriesNumber, + SeriesDate, + SOPClassUID, + wadoRoot, + wadoUri, + wadoUriRoot, + } = instance; + + const displaySet = { + // Parametric map use to have the same modality as its referenced volume but + // "PMAP" is used in the viewer even though this is not a valid DICOM modality + Modality: 'PMAP', + isReconstructable: true, // by default for now + displaySetInstanceUID: `pmap.${utils.guid()}`, + SeriesDescription, + SeriesNumber, + SeriesDate, + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + SOPClassHandlerId, + SOPClassUID, + referencedImages: null, + referencedSeriesInstanceUID: null, + referencedDisplaySetInstanceUID: null, + referencedVolumeURI: null, + referencedVolumeId: null, + isDerivedDisplaySet: true, + loadStatus: { + loading: false, + loaded: false, + }, + sopClassUids, + instance, + instances: [instance], + wadoRoot, + wadoUriRoot, + wadoUri, + isOverlayDisplaySet: true, + }; + + const referencedSeriesSequence = instance.ReferencedSeriesSequence; + + if (!referencedSeriesSequence) { + console.error('ReferencedSeriesSequence is missing for the parametric map'); + return; + } + + const referencedSeries = referencedSeriesSequence[0] || referencedSeriesSequence; + + displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence; + displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID; + + // Does not get the referenced displaySet during parametric displaySet creation + // because it is still not available (getDisplaySetByUID returns `undefined`). + displaySet.getReferenceDisplaySet = () => { + const { displaySetService } = servicesManager.services; + + if (displaySet.referencedDisplaySetInstanceUID) { + return displaySetService.getDisplaySetByUID(displaySet.referencedDisplaySetInstanceUID); + } + + const referencedDisplaySets = displaySetService.getDisplaySetsForSeries( + displaySet.referencedSeriesInstanceUID + ); + + if (!referencedDisplaySets || referencedDisplaySets.length === 0) { + throw new Error('Referenced displaySet is missing for the parametric map'); + } + + const referencedDisplaySet = referencedDisplaySets[0]; + + displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID; + + return referencedDisplaySet; + }; + + // Does not get the referenced volumeId during parametric displaySet creation because the + // referenced displaySet is still not available (getDisplaySetByUID returns `undefined`). + displaySet.getReferencedVolumeId = () => { + if (displaySet.referencedVolumeId) { + return displaySet.referencedVolumeId; + } + + const referencedDisplaySet = displaySet.getReferenceDisplaySet(); + const referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID; + const referencedVolumeId = `${VOLUME_LOADER_SCHEME}:${referencedVolumeURI}`; + + displaySet.referencedVolumeURI = referencedVolumeURI; + displaySet.referencedVolumeId = referencedVolumeId; + + return referencedVolumeId; + }; + + displaySet.load = async ({ headers }) => + await _load(displaySet, servicesManager, extensionManager, headers); + + return [displaySet]; +} + +const getRangeFromPixelData = (pixelData: Float32Array) => { + let lowest = pixelData[0]; + let highest = pixelData[0]; + + for (let i = 1; i < pixelData.length; i++) { + if (pixelData[i] < lowest) { + lowest = pixelData[i]; + } + if (pixelData[i] > highest) { + highest = pixelData[i]; + } + } + + return [lowest, highest]; +}; + +async function _load( + displaySet, + servicesManager: AppTypes.ServicesManager, + extensionManager, + headers +) { + const volumeId = `${VOLUME_LOADER_SCHEME}:${displaySet.displaySetInstanceUID}`; + const volumeLoadObject = cache.getVolumeLoadObject(volumeId); + + if (volumeLoadObject) { + return volumeLoadObject.promise; + } + + displaySet.loading = true; + displaySet.isLoaded = false; + + // We don't want to fire multiple loads, so we'll wait for the first to finish + // and also return the same promise to any other callers. + const promise = _loadParametricMap({ + extensionManager, + displaySet, + headers, + }); + + cache.putVolumeLoadObject(volumeId, { promise }).catch(err => { + throw err; + }); + + promise + .then(() => { + displaySet.loading = false; + displaySet.isLoaded = true; + // Broadcast that loading is complete + servicesManager.services.segmentationService._broadcastEvent( + servicesManager.services.segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE, + { + pmapDisplaySet: displaySet, + } + ); + }) + .catch(err => { + displaySet.loading = false; + displaySet.isLoaded = false; + throw err; + }); + + return promise; +} + +async function _loadParametricMap({ displaySet, headers }: withAppTypes) { + const arrayBuffer = await dicomLoaderService.findDicomDataPromise(displaySet, null, headers); + const referencedVolumeId = displaySet.getReferencedVolumeId(); + const cachedReferencedVolume = cache.getVolume(referencedVolumeId); + + // Parametric map can be loaded only if its referenced volume exists otherwise it will fail + if (!cachedReferencedVolume) { + throw new Error( + 'Referenced Volume is missing for the PMAP, and stack viewport PMAP is not supported yet' + ); + } + + const { imageIds } = cachedReferencedVolume; + const results = await adaptersPMAP.Cornerstone3D.ParametricMap.generateToolState( + imageIds, + arrayBuffer, + metaData + ); + const { pixelData } = results; + const TypedArrayConstructor = pixelData.constructor; + const paramMapId = displaySet.displaySetInstanceUID; + + const derivedVolume = await volumeLoader.createAndCacheDerivedVolume(referencedVolumeId, { + volumeId: paramMapId, + targetBuffer: { + type: TypedArrayConstructor.name, + }, + }); + + const newPixelData = new TypedArrayConstructor(pixelData.length); + for (let i = 0; i < pixelData.length; i++) { + newPixelData[i] = pixelData[i] * 100; + } + derivedVolume.voxelManager.setCompleteScalarDataArray(newPixelData); + const range = getRangeFromPixelData(newPixelData); + const windowLevel = csUtils.windowLevel.toWindowLevel(range[0], range[1]); + + derivedVolume.metadata.voiLut = [windowLevel]; + derivedVolume.loadStatus = { loaded: true }; + + return derivedVolume; +} + +function getSopClassHandlerModule({ servicesManager, extensionManager }) { + const getDisplaySetsFromSeries = instances => { + return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager); + }; + + return [ + { + name: 'dicom-pmap', + sopClassUids, + getDisplaySetsFromSeries, + }, + ]; +} + +export default getSopClassHandlerModule; diff --git a/extensions/cornerstone-dicom-pmap/src/id.js b/extensions/cornerstone-dicom-pmap/src/id.js new file mode 100644 index 0000000..4ec5d59 --- /dev/null +++ b/extensions/cornerstone-dicom-pmap/src/id.js @@ -0,0 +1,7 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; +const SOPClassHandlerName = 'dicom-pmap'; +const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`; + +export { id, SOPClassHandlerId, SOPClassHandlerName }; diff --git a/extensions/cornerstone-dicom-pmap/src/index.tsx b/extensions/cornerstone-dicom-pmap/src/index.tsx new file mode 100644 index 0000000..916f8a0 --- /dev/null +++ b/extensions/cornerstone-dicom-pmap/src/index.tsx @@ -0,0 +1,39 @@ +import { id } from './id'; +import React from 'react'; +import getSopClassHandlerModule from './getSopClassHandlerModule'; + +const Component = React.lazy(() => { + return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstonePMAPViewport'); +}); + +const OHIFCornerstonePMAPViewport = props => { + return ( + Loading...}> + + + ); +}; + +/** + * You can remove any of the following modules if you don't need them. + */ +const extension = { + id, + getViewportModule({ servicesManager, extensionManager, commandsManager }) { + const ExtendedOHIFCornerstonePMAPViewport = props => { + return ( + + ); + }; + + return [{ name: 'dicom-pmap', component: ExtendedOHIFCornerstonePMAPViewport }]; + }, + getSopClassHandlerModule, +}; + +export default extension; diff --git a/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx b/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx new file mode 100644 index 0000000..2d33fe3 --- /dev/null +++ b/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx @@ -0,0 +1,210 @@ +import PropTypes from 'prop-types'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useViewportGrid } from '@ohif/ui-next'; + +function OHIFCornerstonePMAPViewport(props: withAppTypes) { + const { + displaySets, + children, + viewportOptions, + displaySetOptions, + servicesManager, + extensionManager, + } = props; + const viewportId = viewportOptions.viewportId; + const { displaySetService, segmentationService, uiNotificationService, customizationService } = + servicesManager.services; + + // PMAP viewport will always have a single display set + if (displaySets.length !== 1) { + throw new Error('PMAP viewport must have a single display set'); + } + + const LoadingIndicatorTotalPercent = customizationService.getCustomization( + 'ui.loadingIndicatorTotalPercent' + ); + + const pmapDisplaySet = displaySets[0]; + const [viewportGrid, viewportGridService] = useViewportGrid(); + const referencedDisplaySetRef = useRef(null); + const { viewports, activeViewportId } = viewportGrid; + const referencedDisplaySet = pmapDisplaySet.getReferenceDisplaySet(); + const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata( + referencedDisplaySet, + pmapDisplaySet + ); + + referencedDisplaySetRef.current = { + displaySet: referencedDisplaySet, + metadata: referencedDisplaySetMetadata, + }; + + const [pmapIsLoading, setPmapIsLoading] = useState(!pmapDisplaySet.isLoaded); + + // Add effect to listen for loading complete + useEffect(() => { + const { unsubscribe } = segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE, + evt => { + if (evt.pmapDisplaySet?.displaySetInstanceUID === pmapDisplaySet.displaySetInstanceUID) { + setPmapIsLoading(false); + } + } + ); + + return () => { + unsubscribe(); + }; + }, [pmapDisplaySet]); + + const getCornerstoneViewport = useCallback(() => { + const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current; + const { component: Component } = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.viewportModule.cornerstone' + ); + + displaySetOptions.unshift({}); + const [pmapDisplaySetOptions] = displaySetOptions; + + // Make sure `options` exists + pmapDisplaySetOptions.options = pmapDisplaySetOptions.options ?? {}; + + Object.assign(pmapDisplaySetOptions.options, { + colormap: { + name: 'rainbow_2', + opacity: [ + { value: 0, opacity: 0 }, + { value: 0.25, opacity: 0.25 }, + { value: 0.5, opacity: 0.5 }, + { value: 0.75, opacity: 0.75 }, + { value: 0.9, opacity: 0.99 }, + ], + }, + voi: { + windowCenter: 50, + windowWidth: 100, + }, + }); + + uiNotificationService.show({ + title: 'Parametric Map', + type: 'warning', + message: 'The values are multiplied by 100 in the viewport for better visibility', + }); + + return ( + + ); + }, [ + extensionManager, + displaySetOptions, + props, + pmapDisplaySet, + viewportOptions.orientation, + viewportOptions.viewportId, + ]); + + // Cleanup the PMAP viewport when the viewport is destroyed + useEffect(() => { + const onDisplaySetsRemovedSubscription = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SETS_REMOVED, + ({ displaySetInstanceUIDs }) => { + const activeViewport = viewports.get(activeViewportId); + if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) { + viewportGridService.setDisplaySetsForViewport({ + viewportId: activeViewportId, + displaySetInstanceUIDs: [], + }); + } + } + ); + + return () => { + onDisplaySetsRemovedSubscription.unsubscribe(); + }; + }, [activeViewportId, displaySetService, viewportGridService, viewports]); + + let childrenWithProps = null; + + if (children && children.length) { + childrenWithProps = children.map((child, index) => { + return ( + child && + React.cloneElement(child, { + viewportId, + key: index, + }) + ); + }); + } + + return ( + <> +
+ {pmapIsLoading && ( + + )} + {getCornerstoneViewport()} + {childrenWithProps} +
+ + ); +} + +OHIFCornerstonePMAPViewport.propTypes = { + displaySets: PropTypes.arrayOf(PropTypes.object), + viewportId: PropTypes.string.isRequired, + dataSource: PropTypes.object, + children: PropTypes.node, +}; + +function _getReferencedDisplaySetMetadata(referencedDisplaySet, pmapDisplaySet) { + const { SharedFunctionalGroupsSequence } = pmapDisplaySet.instance; + + const SharedFunctionalGroup = Array.isArray(SharedFunctionalGroupsSequence) + ? SharedFunctionalGroupsSequence[0] + : SharedFunctionalGroupsSequence; + + const { PixelMeasuresSequence } = SharedFunctionalGroup; + + const PixelMeasures = Array.isArray(PixelMeasuresSequence) + ? PixelMeasuresSequence[0] + : PixelMeasuresSequence; + + const { SpacingBetweenSlices, SliceThickness } = PixelMeasures; + + const image0 = referencedDisplaySet.images[0]; + const referencedDisplaySetMetadata = { + PatientID: image0.PatientID, + PatientName: image0.PatientName, + PatientSex: image0.PatientSex, + PatientAge: image0.PatientAge, + SliceThickness: image0.SliceThickness || SliceThickness, + StudyDate: image0.StudyDate, + SeriesDescription: image0.SeriesDescription, + SeriesInstanceUID: image0.SeriesInstanceUID, + SeriesNumber: image0.SeriesNumber, + ManufacturerModelName: image0.ManufacturerModelName, + SpacingBetweenSlices: image0.SpacingBetweenSlices || SpacingBetweenSlices, + }; + + return referencedDisplaySetMetadata; +} + +export default OHIFCornerstonePMAPViewport; diff --git a/extensions/cornerstone-dicom-rt/.webpack/webpack.dev.js b/extensions/cornerstone-dicom-rt/.webpack/webpack.dev.js new file mode 100644 index 0000000..6aea859 --- /dev/null +++ b/extensions/cornerstone-dicom-rt/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/extensions/cornerstone-dicom-rt/.webpack/webpack.prod.js b/extensions/cornerstone-dicom-rt/.webpack/webpack.prod.js new file mode 100644 index 0000000..4ee5cc9 --- /dev/null +++ b/extensions/cornerstone-dicom-rt/.webpack/webpack.prod.js @@ -0,0 +1,48 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const pkg = require('./../package.json'); +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; + +const ROOT_DIR = path.join(__dirname, './..'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, ENTRY, DIST_DIR }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: false, + }, + output: { + path: ROOT_DIR, + library: 'ohif-extension-cornerstone-dicom-rt', + libraryTarget: 'umd', + filename: pkg.main, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + // new BundleAnalyzerPlugin(), + ], + }); +}; diff --git a/extensions/cornerstone-dicom-rt/CHANGELOG.md b/extensions/cornerstone-dicom-rt/CHANGELOG.md new file mode 100644 index 0000000..645ac38 --- /dev/null +++ b/extensions/cornerstone-dicom-rt/CHANGELOG.md @@ -0,0 +1,3044 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + + +### Features + +* Add customization support for more UI components ([#4634](https://github.com/OHIF/Viewers/issues/4634)) ([f15eb44](https://github.com/OHIF/Viewers/commit/f15eb44b4cf49de1b73a22512571cec02effaef3)) + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + + +### Features + +* **customization:** new customization service api ([#4688](https://github.com/OHIF/Viewers/issues/4688)) ([55ad8ef](https://github.com/OHIF/Viewers/commit/55ad8efbabc3fabd8031fc08927b2f92ae5aec69)) + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + + +### Features + +* **dev:** move to rsbuild for dev - faster ([#4674](https://github.com/OHIF/Viewers/issues/4674)) ([d4a4267](https://github.com/OHIF/Viewers/commit/d4a4267429c02916dd51f6aefb290d96dd1c3b04)) + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + + +### Features + +* **measurements:** Provide for the Load (SR) measurements button to optionally clear existing measurements prior to loading the SR. ([#4586](https://github.com/OHIF/Viewers/issues/4586)) ([4d3d5e7](https://github.com/OHIF/Viewers/commit/4d3d5e794cb99212eba06bf91dbb30a258725efe)) + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + + +### Features + +* migrate icons to ui-next ([#4606](https://github.com/OHIF/Viewers/issues/4606)) ([4e2ae32](https://github.com/OHIF/Viewers/commit/4e2ae328744ed95589c2cdf7a531454a25bf88b5)) + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + + +### Bug Fixes + +* **seg:** jump to the first slice in SEG and RT that has data ([#4605](https://github.com/OHIF/Viewers/issues/4605)) ([9bf24d6](https://github.com/OHIF/Viewers/commit/9bf24d6dc58ed8f65c90899a17c11044b792cf40)) + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + + +### Bug Fixes + +* **rt:** dont convert to volume for RTSTRUCT ([#4157](https://github.com/OHIF/Viewers/issues/4157)) ([7745c09](https://github.com/OHIF/Viewers/commit/7745c092bb3edf0090f32fbbbae2f0776128d5a2)) + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + + +### Bug Fixes + +* **bugs:** enhancements and bugs in several areas ([#4086](https://github.com/OHIF/Viewers/issues/4086)) ([730f434](https://github.com/OHIF/Viewers/commit/730f4349100f21b4489a21707dbb2dca9dbfbba2)) + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + + +### Bug Fixes + +* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025)) + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637)) + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + + +### Features + +* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1)) + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + + +### Bug Fixes + +* **viewport-sync:** remember synced viewports bw stack and volume and RENAME StackImageSync to ImageSliceSync ([#3849](https://github.com/OHIF/Viewers/issues/3849)) ([e4a116b](https://github.com/OHIF/Viewers/commit/e4a116b074fcb85c8cbcc9db44fdec565f3386db)) + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + + +### Bug Fixes + +* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6)) + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + + +### Features + +* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd)) + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + + +### Features + +* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1)) + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt diff --git a/extensions/cornerstone-dicom-rt/LICENSE b/extensions/cornerstone-dicom-rt/LICENSE new file mode 100644 index 0000000..983c5ef --- /dev/null +++ b/extensions/cornerstone-dicom-rt/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2023 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/extensions/cornerstone-dicom-rt/README.md b/extensions/cornerstone-dicom-rt/README.md new file mode 100644 index 0000000..a23ee41 --- /dev/null +++ b/extensions/cornerstone-dicom-rt/README.md @@ -0,0 +1,13 @@ +# dicom-rt +## Description + +DICOM RT read workflow. This extension will allow you to load a DICOM RTSS image +and display it in OHIF. + + +## Author + +OHIF + +## License +MIT diff --git a/extensions/cornerstone-dicom-rt/babel.config.js b/extensions/cornerstone-dicom-rt/babel.config.js new file mode 100644 index 0000000..a35080a --- /dev/null +++ b/extensions/cornerstone-dicom-rt/babel.config.js @@ -0,0 +1,43 @@ +module.exports = { + plugins: ['@babel/plugin-proposal-class-properties'], + env: { + test: { + presets: [ + [ + // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2 + '@babel/preset-env', + { + modules: 'commonjs', + debug: false, + }, + '@babel/preset-typescript', + ], + '@babel/preset-react', + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-regenerator', + '@babel/plugin-transform-runtime', + ], + }, + production: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + development: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + }, +}; diff --git a/extensions/cornerstone-dicom-rt/package.json b/extensions/cornerstone-dicom-rt/package.json new file mode 100644 index 0000000..9d8d68c --- /dev/null +++ b/extensions/cornerstone-dicom-rt/package.json @@ -0,0 +1,51 @@ +{ + "name": "@ohif/extension-cornerstone-dicom-rt", + "version": "3.10.0-beta.111", + "description": "DICOM RT read workflow", + "author": "OHIF", + "license": "MIT", + "main": "dist/ohif-extension-cornerstone-dicom-rt.umd.js", + "module": "src/index.tsx", + "files": [ + "dist/**", + "public/**", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "repository": "OHIF/Viewers", + "keywords": [ + "ohif-extension" + ], + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.18.0" + }, + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:dicom-seg": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package-1": "yarn run build", + "start": "yarn run dev" + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.111", + "@ohif/extension-cornerstone": "3.10.0-beta.111", + "@ohif/extension-default": "3.10.0-beta.111", + "@ohif/i18n": "3.10.0-beta.111", + "prop-types": "^15.6.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^10.11.0", + "react-router": "^6.23.1", + "react-router-dom": "^6.23.1" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "react-color": "^2.19.3" + } +} diff --git a/extensions/cornerstone-dicom-rt/src/getCommandsModule.ts b/extensions/cornerstone-dicom-rt/src/getCommandsModule.ts new file mode 100644 index 0000000..d2737c5 --- /dev/null +++ b/extensions/cornerstone-dicom-rt/src/getCommandsModule.ts @@ -0,0 +1,55 @@ +import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; + +const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => { + const services = servicesManager.services; + const { displaySetService, viewportGridService } = services; + + const actions = { + hydrateRTSDisplaySet: ({ displaySet, viewportId }) => { + if (displaySet.Modality !== 'RTSTRUCT') { + throw new Error('Display set is not an RTSTRUCT'); + } + + const referencedDisplaySet = displaySetService.getDisplaySetByUID( + displaySet.referencedDisplaySetInstanceUID + ); + + // update the previously stored segmentationPresentation with the new viewportId + // presentation so that when we put the referencedDisplaySet back in the viewport + // it will have the correct segmentation representation hydrated + commandsManager.runCommand('updateStoredSegmentationPresentation', { + displaySet: displaySet, + type: SegmentationRepresentations.Contour, + }); + + // update the previously stored positionPresentation with the new viewportId + // presentation so that when we put the referencedDisplaySet back in the viewport + // it will be in the correct position zoom and pan + commandsManager.runCommand('updateStoredPositionPresentation', { + viewportId, + displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID, + }); + + viewportGridService.setDisplaySetsForViewport({ + viewportId, + displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID], + }); + }, + }; + + const definitions = { + hydrateRTSDisplaySet: { + commandFn: actions.hydrateRTSDisplaySet, + storeContexts: [], + options: {}, + }, + }; + + return { + actions, + definitions, + defaultContext: 'cornerstone-dicom-rt', + }; +}; + +export default commandsModule; diff --git a/extensions/cornerstone-dicom-rt/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-rt/src/getSopClassHandlerModule.ts new file mode 100644 index 0000000..505819d --- /dev/null +++ b/extensions/cornerstone-dicom-rt/src/getSopClassHandlerModule.ts @@ -0,0 +1,199 @@ +import { utils } from '@ohif/core'; + +import { SOPClassHandlerId } from './id'; +import loadRTStruct from './loadRTStruct'; + +const sopClassUids = ['1.2.840.10008.5.1.4.1.1.481.3']; + +const loadPromises = {}; + +function _getDisplaySetsFromSeries( + instances, + servicesManager: AppTypes.ServicesManager, + extensionManager +) { + const instance = instances[0]; + + const { + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + SeriesDescription, + SeriesNumber, + SeriesDate, + SOPClassUID, + wadoRoot, + wadoUri, + wadoUriRoot, + } = instance; + + const displaySet = { + Modality: 'RTSTRUCT', + loading: false, + isReconstructable: false, // by default for now since it is a volumetric SEG currently + displaySetInstanceUID: utils.guid(), + SeriesDescription, + SeriesNumber, + SeriesDate, + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + SOPClassHandlerId, + SOPClassUID, + referencedImages: null, + referencedSeriesInstanceUID: null, + referencedDisplaySetInstanceUID: null, + isDerivedDisplaySet: true, + isLoaded: false, + isHydrated: false, + structureSet: null, + sopClassUids, + instance, + wadoRoot, + wadoUriRoot, + wadoUri, + isOverlayDisplaySet: true, + }; + + let referencedSeriesSequence = instance.ReferencedSeriesSequence; + if (instance.ReferencedFrameOfReferenceSequence && !instance.ReferencedSeriesSequence) { + instance.ReferencedSeriesSequence = _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence( + instance.ReferencedFrameOfReferenceSequence + ); + referencedSeriesSequence = instance.ReferencedSeriesSequence; + } + + if (!referencedSeriesSequence) { + throw new Error('ReferencedSeriesSequence is missing for the RTSTRUCT'); + } + + const referencedSeries = referencedSeriesSequence[0]; + + displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence; + displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID; + + const { displaySetService } = servicesManager.services; + const referencedDisplaySets = displaySetService.getDisplaySetsForSeries( + displaySet.referencedSeriesInstanceUID + ); + + if (!referencedDisplaySets || referencedDisplaySets.length === 0) { + // Instead of throwing error, subscribe to display sets added + const { unsubscribe } = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SETS_ADDED, + ({ displaySetsAdded }) => { + const addedDisplaySet = displaySetsAdded[0]; + if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) { + displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID; + unsubscribe(); + } + } + ); + } else { + const referencedDisplaySet = referencedDisplaySets[0]; + displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID; + } + + displaySet.load = ({ headers }) => _load(displaySet, servicesManager, extensionManager, headers); + + return [displaySet]; +} + +function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensionManager, headers) { + const { SOPInstanceUID } = rtDisplaySet; + const { segmentationService } = servicesManager.services; + if ( + (rtDisplaySet.loading || rtDisplaySet.isLoaded) && + loadPromises[SOPInstanceUID] && + _segmentationExistsInCache(rtDisplaySet, segmentationService) + ) { + return loadPromises[SOPInstanceUID]; + } + + rtDisplaySet.loading = true; + + // We don't want to fire multiple loads, so we'll wait for the first to finish + // and also return the same promise to any other callers. + loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => { + if (!rtDisplaySet.structureSet) { + const structureSet = await loadRTStruct(extensionManager, rtDisplaySet, headers); + + rtDisplaySet.structureSet = structureSet; + } + + segmentationService + .createSegmentationForRTDisplaySet(rtDisplaySet) + .then(() => { + rtDisplaySet.loading = false; + resolve(); + }) + .catch(error => { + rtDisplaySet.loading = false; + reject(error); + }); + }); + + return loadPromises[SOPInstanceUID]; +} + +function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence( + ReferencedFrameOfReferenceSequence +) { + const ReferencedSeriesSequence = []; + + ReferencedFrameOfReferenceSequence.forEach(referencedFrameOfReference => { + const { RTReferencedStudySequence } = referencedFrameOfReference; + + RTReferencedStudySequence.forEach(rtReferencedStudy => { + const { RTReferencedSeriesSequence } = rtReferencedStudy; + + RTReferencedSeriesSequence.forEach(rtReferencedSeries => { + const ReferencedInstanceSequence = []; + const { ContourImageSequence, SeriesInstanceUID } = rtReferencedSeries; + + ContourImageSequence.forEach(contourImage => { + ReferencedInstanceSequence.push({ + ReferencedSOPInstanceUID: contourImage.ReferencedSOPInstanceUID, + ReferencedSOPClassUID: contourImage.ReferencedSOPClassUID, + }); + }); + + const referencedSeries = { + SeriesInstanceUID, + ReferencedInstanceSequence, + }; + + ReferencedSeriesSequence.push(referencedSeries); + }); + }); + }); + + return ReferencedSeriesSequence; +} + +function _segmentationExistsInCache( + rtDisplaySet, + segmentationService: AppTypes.SegmentationService +) { + // Todo: fix this + return false; + // This should be abstracted with the CornerstoneCacheService + const rtContourId = rtDisplaySet.displaySetInstanceUID; + const contour = segmentationService.getContour(rtContourId); + + return contour !== undefined; +} + +function getSopClassHandlerModule({ servicesManager, extensionManager }) { + return [ + { + name: 'dicom-rt', + sopClassUids, + getDisplaySetsFromSeries: instances => { + return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager); + }, + }, + ]; +} + +export default getSopClassHandlerModule; diff --git a/extensions/cornerstone-dicom-rt/src/id.js b/extensions/cornerstone-dicom-rt/src/id.js new file mode 100644 index 0000000..2c8691f --- /dev/null +++ b/extensions/cornerstone-dicom-rt/src/id.js @@ -0,0 +1,7 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; +const SOPClassHandlerName = 'dicom-rt'; +const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`; + +export { id, SOPClassHandlerId, SOPClassHandlerName }; diff --git a/extensions/cornerstone-dicom-rt/src/index.tsx b/extensions/cornerstone-dicom-rt/src/index.tsx new file mode 100644 index 0000000..6d4e4db --- /dev/null +++ b/extensions/cornerstone-dicom-rt/src/index.tsx @@ -0,0 +1,63 @@ +import { id } from './id'; +import React from 'react'; +import { Types } from '@ohif/core'; +import getSopClassHandlerModule from './getSopClassHandlerModule'; +import getCommandsModule from './getCommandsModule'; + +const Component = React.lazy(() => { + return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport'); +}); + +const OHIFCornerstoneRTViewport = props => { + return ( + Loading...}> + + + ); +}; + +/** + * You can remove any of the following modules if you don't need them. + */ +const extension: Types.Extensions.Extension = { + /** + * Only required property. Should be a unique value across all extensions. + * You ID can be anything you want, but it should be unique. + */ + id, + getCommandsModule, + + /** + * PanelModule should provide a list of panels that will be available in OHIF + * for Modes to consume and render. Each panel is defined by a {name, + * iconName, iconLabel, label, component} object. Example of a panel module + * is the StudyBrowserPanel that is provided by the default extension in OHIF. + */ + getViewportModule({ + servicesManager, + extensionManager, + commandsManager, + }: Types.Extensions.ExtensionParams) { + const ExtendedOHIFCornerstoneRTViewport = props => { + return ( + + ); + }; + + return [{ name: 'dicom-rt', component: ExtendedOHIFCornerstoneRTViewport }]; + }, + /** + * SopClassHandlerModule should provide a list of sop class handlers that will be + * available in OHIF for Modes to consume and use to create displaySets from Series. + * Each sop class handler is defined by a { name, sopClassUids, getDisplaySetsFromSeries}. + * Examples include the default sop class handler provided by the default extension + */ + getSopClassHandlerModule, +}; + +export default extension; diff --git a/extensions/cornerstone-dicom-rt/src/loadRTStruct.js b/extensions/cornerstone-dicom-rt/src/loadRTStruct.js new file mode 100644 index 0000000..0e9ab38 --- /dev/null +++ b/extensions/cornerstone-dicom-rt/src/loadRTStruct.js @@ -0,0 +1,267 @@ +import dcmjs from 'dcmjs'; +const { DicomMessage, DicomMetaDictionary } = dcmjs.data; +const dicomlab2RGB = dcmjs.data.Colors.dicomlab2RGB; + +async function checkAndLoadContourData(instance, datasource) { + if (!instance || !instance.ROIContourSequence) { + return Promise.reject('Invalid instance object or ROIContourSequence'); + } + + const promisesMap = new Map(); + + for (const ROIContour of instance.ROIContourSequence) { + const referencedROINumber = ROIContour.ReferencedROINumber; + if (!ROIContour || !ROIContour.ContourSequence) { + promisesMap.set(referencedROINumber, [Promise.resolve([])]); + continue; + } + + for (const Contour of ROIContour.ContourSequence) { + if (!Contour || !Contour.ContourData) { + return Promise.reject('Invalid Contour or ContourData'); + } + + const contourData = Contour.ContourData; + + if (Array.isArray(contourData)) { + promisesMap.has(referencedROINumber) + ? promisesMap.get(referencedROINumber).push(Promise.resolve(contourData)) + : promisesMap.set(referencedROINumber, [Promise.resolve(contourData)]); + } else if (contourData && contourData.BulkDataURI) { + const bulkDataURI = contourData.BulkDataURI; + + if (!datasource || !datasource.retrieve || !datasource.retrieve.bulkDataURI) { + return Promise.reject('Invalid datasource object or retrieve function'); + } + + const bulkDataPromise = datasource.retrieve.bulkDataURI({ + BulkDataURI: bulkDataURI, + StudyInstanceUID: instance.StudyInstanceUID, + SeriesInstanceUID: instance.SeriesInstanceUID, + SOPInstanceUID: instance.SOPInstanceUID, + }); + + promisesMap.has(referencedROINumber) + ? promisesMap.get(referencedROINumber).push(bulkDataPromise) + : promisesMap.set(referencedROINumber, [bulkDataPromise]); + } else { + return Promise.reject(`Invalid ContourData: ${contourData}`); + } + } + } + + const resolvedPromisesMap = new Map(); + for (const [key, promiseArray] of promisesMap.entries()) { + resolvedPromisesMap.set(key, await Promise.allSettled(promiseArray)); + } + + instance.ROIContourSequence.forEach(ROIContour => { + try { + const referencedROINumber = ROIContour.ReferencedROINumber; + const resolvedPromises = resolvedPromisesMap.get(referencedROINumber); + + if (ROIContour.ContourSequence) { + ROIContour.ContourSequence.forEach((Contour, index) => { + const promise = resolvedPromises[index]; + if (promise.status === 'fulfilled') { + if (Array.isArray(promise.value) && promise.value.every(Number.isFinite)) { + // If promise.value is already an array of numbers, use it directly + Contour.ContourData = promise.value; + } else { + // If the resolved promise value is a byte array (Blob), it needs to be decoded + const uint8Array = new Uint8Array(promise.value); + const textDecoder = new TextDecoder(); + const dataUint8Array = textDecoder.decode(uint8Array); + if (typeof dataUint8Array === 'string' && dataUint8Array.includes('\\')) { + Contour.ContourData = dataUint8Array.split('\\').map(parseFloat); + } else { + Contour.ContourData = []; + } + } + } else { + console.error(promise.reason); + } + }); + } + } catch (error) { + console.error(error); + } + }); +} + +export default async function loadRTStruct(extensionManager, rtStructDisplaySet, headers) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.common' + ); + const dataSource = extensionManager.getActiveDataSource()[0]; + const { bulkDataURI } = dataSource.getConfig?.() || {}; + + const { dicomLoaderService } = utilityModule.exports; + + // Set here is loading is asynchronous. + // If this function throws its set back to false. + rtStructDisplaySet.isLoaded = true; + let instance = rtStructDisplaySet.instance; + + if (!bulkDataURI || !bulkDataURI.enabled) { + const segArrayBuffer = await dicomLoaderService.findDicomDataPromise( + rtStructDisplaySet, + null, + headers + ); + + const dicomData = DicomMessage.readFile(segArrayBuffer); + const rtStructDataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict); + rtStructDataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta); + instance = rtStructDataset; + } else { + await checkAndLoadContourData(instance, dataSource); + } + + const { StructureSetROISequence, ROIContourSequence, RTROIObservationsSequence } = instance; + + // Define our structure set entry and add it to the rtstruct module state. + const structureSet = { + StructureSetLabel: instance.StructureSetLabel, + SeriesInstanceUID: instance.SeriesInstanceUID, + ROIContours: [], + visible: true, + ReferencedSOPInstanceUIDsSet: new Set(), + }; + + for (let i = 0; i < ROIContourSequence.length; i++) { + const ROIContour = ROIContourSequence[i]; + const { ContourSequence } = ROIContour; + + if (!ContourSequence) { + continue; + } + + const isSupported = false; + + const ContourSequenceArray = _toArray(ContourSequence); + + const contourPoints = []; + for (let c = 0; c < ContourSequenceArray.length; c++) { + const { ContourData, NumberOfContourPoints, ContourGeometricType, ContourImageSequence } = + ContourSequenceArray[c]; + + let isSupported = false; + + const points = []; + for (let p = 0; p < NumberOfContourPoints * 3; p += 3) { + points.push({ + x: ContourData[p], + y: ContourData[p + 1], + z: ContourData[p + 2], + }); + } + + switch (ContourGeometricType) { + case 'CLOSED_PLANAR': + case 'OPEN_PLANAR': + case 'POINT': + isSupported = true; + + break; + default: + continue; + } + + contourPoints.push({ + numberOfPoints: NumberOfContourPoints, + points, + type: ContourGeometricType, + isSupported, + }); + + if (ContourImageSequence?.ReferencedSOPInstanceUID) { + structureSet.ReferencedSOPInstanceUIDsSet.add( + ContourImageSequence?.ReferencedSOPInstanceUID + ); + } + } + + _setROIContourMetadata( + structureSet, + StructureSetROISequence, + RTROIObservationsSequence, + ROIContour, + contourPoints, + isSupported + ); + } + return structureSet; +} + +function _setROIContourMetadata( + structureSet, + StructureSetROISequence, + RTROIObservationsSequence, + ROIContour, + contourPoints, + isSupported +) { + const StructureSetROI = StructureSetROISequence.find( + structureSetROI => structureSetROI.ROINumber === ROIContour.ReferencedROINumber + ); + + const ROIContourData = { + ROINumber: StructureSetROI.ROINumber, + ROIName: StructureSetROI.ROIName, + ROIGenerationAlgorithm: StructureSetROI.ROIGenerationAlgorithm, + ROIDescription: StructureSetROI.ROIDescription, + isSupported, + contourPoints, + visible: true, + }; + + _setROIContourDataColor(ROIContour, ROIContourData); + + if (RTROIObservationsSequence) { + // If present, add additional RTROIObservations metadata. + _setROIContourRTROIObservations( + ROIContourData, + RTROIObservationsSequence, + ROIContour.ReferencedROINumber + ); + } + + structureSet.ROIContours.push(ROIContourData); +} + +function _setROIContourDataColor(ROIContour, ROIContourData) { + let { ROIDisplayColor, RecommendedDisplayCIELabValue } = ROIContour; + + if (!ROIDisplayColor && RecommendedDisplayCIELabValue) { + // If ROIDisplayColor is absent, try using the RecommendedDisplayCIELabValue color. + ROIDisplayColor = dicomlab2RGB(RecommendedDisplayCIELabValue); + } + + if (ROIDisplayColor) { + ROIContourData.colorArray = [...ROIDisplayColor]; + } +} + +function _setROIContourRTROIObservations(ROIContourData, RTROIObservationsSequence, ROINumber) { + const RTROIObservations = RTROIObservationsSequence.find( + RTROIObservations => RTROIObservations.ReferencedROINumber === ROINumber + ); + + if (RTROIObservations) { + // Deep copy so we don't keep the reference to the dcmjs dataset entry. + const { ObservationNumber, ROIObservationDescription, RTROIInterpretedType, ROIInterpreter } = + RTROIObservations; + + ROIContourData.RTROIObservations = { + ObservationNumber, + ROIObservationDescription, + RTROIInterpretedType, + ROIInterpreter, + }; + } +} + +function _toArray(objOrArray) { + return Array.isArray(objOrArray) ? objOrArray : [objOrArray]; +} diff --git a/extensions/cornerstone-dicom-rt/src/utils/initRTToolGroup.ts b/extensions/cornerstone-dicom-rt/src/utils/initRTToolGroup.ts new file mode 100644 index 0000000..45a3c98 --- /dev/null +++ b/extensions/cornerstone-dicom-rt/src/utils/initRTToolGroup.ts @@ -0,0 +1,7 @@ +function createRTToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) { + const tools = customizationService.getCustomization('cornerstone.overlayViewportTools'); + + return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools); +} + +export default createRTToolGroupAndAddTools; diff --git a/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts b/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts new file mode 100644 index 0000000..89abd66 --- /dev/null +++ b/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts @@ -0,0 +1,82 @@ +import { ButtonEnums } from '@ohif/ui'; + +const RESPONSE = { + NO_NEVER: -1, + CANCEL: 0, + HYDRATE_SEG: 5, +}; + +function promptHydrateRT({ + servicesManager, + rtDisplaySet, + viewportId, + preHydrateCallbacks, + hydrateRTDisplaySet, +}: withAppTypes) { + const { uiViewportDialogService } = servicesManager.services; + const extensionManager = servicesManager._extensionManager; + const appConfig = extensionManager._appConfig; + return new Promise(async function (resolve, reject) { + const promptResult = appConfig?.disableConfirmationPrompts + ? RESPONSE.HYDRATE_SEG + : await _askHydrate(uiViewportDialogService, viewportId); + + if (promptResult === RESPONSE.HYDRATE_SEG) { + preHydrateCallbacks?.forEach(callback => { + callback(); + }); + + const isHydrated = await hydrateRTDisplaySet({ + rtDisplaySet, + viewportId, + servicesManager, + }); + + resolve(isHydrated); + } + }); +} + +function _askHydrate(uiViewportDialogService: AppTypes.UIViewportDialogService, viewportId) { + return new Promise(function (resolve, reject) { + const message = 'Do you want to open this Segmentation?'; + const actions = [ + { + id: 'no-hydrate', + type: ButtonEnums.type.secondary, + text: 'No', + value: RESPONSE.CANCEL, + }, + { + id: 'yes-hydrate', + type: ButtonEnums.type.primary, + text: 'Yes', + value: RESPONSE.HYDRATE_SEG, + }, + ]; + const onSubmit = result => { + uiViewportDialogService.hide(); + resolve(result); + }; + + uiViewportDialogService.show({ + id: 'promptHydrateRT', + viewportId, + type: 'info', + message, + actions, + onSubmit, + onOutsideClick: () => { + uiViewportDialogService.hide(); + resolve(RESPONSE.CANCEL); + }, + onKeyPress: event => { + if (event.key === 'Enter') { + onSubmit(RESPONSE.HYDRATE_SEG); + } + }, + }); + }); +} + +export default promptHydrateRT; diff --git a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx new file mode 100644 index 0000000..6dc0b4a --- /dev/null +++ b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx @@ -0,0 +1,397 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import { ViewportActionArrows } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; +import { utils } from '@ohif/extension-cornerstone'; + +import promptHydrateRT from '../utils/promptHydrateRT'; +import _getStatusComponent from './_getStatusComponent'; + +import createRTToolGroupAndAddTools from '../utils/initRTToolGroup'; +import { usePositionPresentationStore } from '@ohif/extension-cornerstone'; + +const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup'; + +function OHIFCornerstoneRTViewport(props: withAppTypes) { + const { + children, + displaySets, + viewportOptions, + servicesManager, + extensionManager, + commandsManager, + } = props; + + const { + displaySetService, + toolGroupService, + segmentationService, + uiNotificationService, + customizationService, + viewportActionCornersService, + } = servicesManager.services; + + const viewportId = viewportOptions.viewportId; + + const toolGroupId = `${RT_TOOLGROUP_BASE_NAME}-${viewportId}`; + + // RT viewport will always have a single display set + if (displaySets.length > 1) { + throw new Error('RT viewport should only have a single display set'); + } + + const LoadingIndicatorTotalPercent = customizationService.getCustomization( + 'ui.loadingIndicatorTotalPercent' + ); + + const rtDisplaySet = displaySets[0]; + + const [viewportGrid, viewportGridService] = useViewportGrid(); + + // States + const selectedSegmentObjectIndex: number = 0; + const { setPositionPresentation } = usePositionPresentationStore(); + + // Hydration means that the RT is opened and segments are loaded into the + // segmentation panel, and RT is also rendered on any viewport that is in the + // same frameOfReferenceUID as the referencedSeriesUID of the RT. However, + // loading basically means RT loading over network and bit unpacking of the + // RT data. + const [isHydrated, setIsHydrated] = useState(rtDisplaySet.isHydrated); + const [rtIsLoading, setRtIsLoading] = useState(!rtDisplaySet.isLoaded); + const [element, setElement] = useState(null); + const [processingProgress, setProcessingProgress] = useState({ + percentComplete: null, + totalSegments: null, + }); + + // refs + const referencedDisplaySetRef = useRef(null); + + const { viewports, activeViewportId } = viewportGrid; + + const referencedDisplaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID; + const referencedDisplaySet = displaySetService.getDisplaySetByUID( + referencedDisplaySetInstanceUID + ); + const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(referencedDisplaySet); + + referencedDisplaySetRef.current = { + displaySet: referencedDisplaySet, + metadata: referencedDisplaySetMetadata, + }; + /** + * OnElementEnabled callback which is called after the cornerstoneExtension + * has enabled the element. Note: we delegate all the image rendering to + * cornerstoneExtension, so we don't need to do anything here regarding + * the image rendering, element enabling etc. + */ + const onElementEnabled = evt => { + setElement(evt.detail.element); + }; + + const onElementDisabled = () => { + setElement(null); + }; + + const storePresentationState = useCallback(() => { + viewportGrid?.viewports.forEach(({ viewportId }) => { + commandsManager.runCommand('storePresentation', { + viewportId, + }); + }); + }, [viewportGrid]); + + const hydrateRTDisplaySet = useCallback( + ({ rtDisplaySet, viewportId }) => { + commandsManager.runCommand('hydrateRTSDisplaySet', { + displaySet: rtDisplaySet, + viewportId, + }); + }, + [commandsManager] + ); + + const getCornerstoneViewport = useCallback(() => { + const { component: Component } = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.viewportModule.cornerstone' + ); + + const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current; + + // Todo: jump to the center of the first segment + return ( + { + props.onElementEnabled?.(evt); + onElementEnabled(evt); + }} + onElementDisabled={onElementDisabled} + > + ); + }, [viewportId, rtDisplaySet, toolGroupId]); + + const onSegmentChange = useCallback( + direction => { + utils.handleSegmentChange({ + direction, + segDisplaySet: rtDisplaySet, + viewportId, + selectedSegmentObjectIndex, + segmentationService, + }); + }, + [selectedSegmentObjectIndex] + ); + + useEffect(() => { + if (rtIsLoading) { + return; + } + + promptHydrateRT({ + servicesManager, + viewportId, + rtDisplaySet, + preHydrateCallbacks: [storePresentationState], + hydrateRTDisplaySet, + }).then(isHydrated => { + if (isHydrated) { + setIsHydrated(true); + } + }); + }, [servicesManager, viewportId, rtDisplaySet, rtIsLoading]); + + useEffect(() => { + // I'm not sure what is this, since in RT we support Overlapping segments + // via contours + const { unsubscribe } = segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE, + evt => { + if (evt.rtDisplaySet.displaySetInstanceUID === rtDisplaySet.displaySetInstanceUID) { + setRtIsLoading(false); + } + + if (rtDisplaySet?.firstSegmentedSliceImageId && viewportOptions?.presentationIds) { + const { firstSegmentedSliceImageId } = rtDisplaySet; + const { presentationIds } = viewportOptions; + + setPositionPresentation(presentationIds.positionPresentationId, { + viewportType: 'stack', + viewReference: { + referencedImageId: firstSegmentedSliceImageId, + }, + viewPresentation: {}, + }); + } + + if (evt.overlappingSegments) { + uiNotificationService.show({ + title: 'Overlapping Segments', + message: 'Overlapping segments detected which is not currently supported', + type: 'warning', + }); + } + } + ); + + return () => { + unsubscribe(); + }; + }, [rtDisplaySet]); + + useEffect(() => { + const { unsubscribe } = segmentationService.subscribe( + segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, + ({ percentComplete, numSegments }) => { + setProcessingProgress({ + percentComplete, + totalSegments: numSegments, + }); + } + ); + + return () => { + unsubscribe(); + }; + }, [rtDisplaySet]); + + /** + Cleanup the SEG viewport when the viewport is destroyed + */ + useEffect(() => { + const onDisplaySetsRemovedSubscription = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SETS_REMOVED, + ({ displaySetInstanceUIDs }) => { + const activeViewport = viewports.get(activeViewportId); + if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) { + viewportGridService.setDisplaySetsForViewport({ + viewportId: activeViewportId, + displaySetInstanceUIDs: [], + }); + } + } + ); + + return () => { + onDisplaySetsRemovedSubscription.unsubscribe(); + }; + }, []); + + useEffect(() => { + let toolGroup = toolGroupService.getToolGroup(toolGroupId); + + if (toolGroup) { + return; + } + + toolGroup = createRTToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId); + + return () => { + // remove the segmentation representations if seg displayset changed + segmentationService.removeSegmentationRepresentations(viewportId); + + toolGroupService.destroyToolGroup(toolGroupId); + }; + }, []); + + useEffect(() => { + setIsHydrated(rtDisplaySet.isHydrated); + + return () => { + // remove the segmentation representations if seg displayset changed + segmentationService.removeSegmentationRepresentations(viewportId); + referencedDisplaySetRef.current = null; + }; + }, [rtDisplaySet]); + + const onStatusClick = useCallback(async () => { + // Before hydrating a RT and make it added to all viewports in the grid + // that share the same frameOfReferenceUID, we need to store the viewport grid + // presentation state, so that we can restore it after hydrating the RT. This is + // required if the user has changed the viewport (other viewport than RT viewport) + // presentation state (w/l and invert) and then opens the RT. If we don't store + // the presentation state, the viewport will be reset to the default presentation + storePresentationState(); + const isHydrated = await hydrateRTDisplaySet({ + rtDisplaySet, + viewportId, + }); + + setIsHydrated(isHydrated); + }, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + let childrenWithProps = null; + + if ( + !referencedDisplaySetRef.current || + referencedDisplaySet.displaySetInstanceUID !== + referencedDisplaySetRef.current.displaySet.displaySetInstanceUID + ) { + return null; + } + + if (children && children.length) { + childrenWithProps = children.map((child, index) => { + return ( + child && + React.cloneElement(child, { + viewportId, + key: index, + }) + ); + }); + } + + useEffect(() => { + viewportActionCornersService.addComponents([ + { + viewportId, + id: 'viewportStatusComponent', + component: _getStatusComponent({ + isHydrated, + onStatusClick, + }), + indexPriority: -100, + location: viewportActionCornersService.LOCATIONS.topLeft, + }, + { + viewportId, + id: 'viewportActionArrowsComponent', + component: ( + + ), + indexPriority: 0, + location: viewportActionCornersService.LOCATIONS.topRight, + }, + ]); + }, [ + activeViewportId, + isHydrated, + onSegmentChange, + onStatusClick, + viewportActionCornersService, + viewportId, + ]); + + return ( + <> +
+ {rtIsLoading && ( + + )} + {getCornerstoneViewport()} + {childrenWithProps} +
+ + ); +} + +OHIFCornerstoneRTViewport.propTypes = { + displaySets: PropTypes.arrayOf(PropTypes.object), + viewportId: PropTypes.string.isRequired, + dataSource: PropTypes.object, + children: PropTypes.node, +}; + +function _getReferencedDisplaySetMetadata(referencedDisplaySet) { + const image0 = referencedDisplaySet.images[0]; + const referencedDisplaySetMetadata = { + PatientID: image0.PatientID, + PatientName: image0.PatientName, + PatientSex: image0.PatientSex, + PatientAge: image0.PatientAge, + SliceThickness: image0.SliceThickness, + StudyDate: image0.StudyDate, + SeriesDescription: image0.SeriesDescription, + SeriesInstanceUID: image0.SeriesInstanceUID, + SeriesNumber: image0.SeriesNumber, + ManufacturerModelName: image0.ManufacturerModelName, + SpacingBetweenSlices: image0.SpacingBetweenSlices, + }; + + return referencedDisplaySetMetadata; +} + +export default OHIFCornerstoneRTViewport; diff --git a/extensions/cornerstone-dicom-rt/src/viewports/_getStatusComponent.tsx b/extensions/cornerstone-dicom-rt/src/viewports/_getStatusComponent.tsx new file mode 100644 index 0000000..78b36ca --- /dev/null +++ b/extensions/cornerstone-dicom-rt/src/viewports/_getStatusComponent.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ViewportActionButton } from '@ohif/ui'; +import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next'; + +export default function _getStatusComponent({ isHydrated, onStatusClick }) { + let ToolTipMessage = null; + let StatusIcon = null; + + switch (isHydrated) { + case true: + StatusIcon = () => ; + ToolTipMessage = () =>
This Segmentation is loaded in the segmentation panel
; + break; + case false: + StatusIcon = () => ( + + ); + ToolTipMessage = () =>
Click LOAD to load RTSTRUCT.
; + } + + const StatusArea = () => { + const { t } = useTranslation('Common'); + const loadStr = t('LOAD'); + + return ( +
+
+ + RTSTRUCT +
+ {!isHydrated && ( + {loadStr} + )} +
+ ); + }; + + return ( + <> + {ToolTipMessage && ( + + + + + + + + + + + )} + {!ToolTipMessage && } + + ); +} \ No newline at end of file diff --git a/extensions/cornerstone-dicom-seg/.webpack/webpack.dev.js b/extensions/cornerstone-dicom-seg/.webpack/webpack.dev.js new file mode 100644 index 0000000..6aea859 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/extensions/cornerstone-dicom-seg/.webpack/webpack.prod.js b/extensions/cornerstone-dicom-seg/.webpack/webpack.prod.js new file mode 100644 index 0000000..3f6eb4b --- /dev/null +++ b/extensions/cornerstone-dicom-seg/.webpack/webpack.prod.js @@ -0,0 +1,54 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +const pkg = require('./../package.json'); + +const ROOT_DIR = path.join(__dirname, '../'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +const outputName = `ohif-${pkg.name.split('/').pop()}`; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: true, + }, + output: { + path: ROOT_DIR, + library: 'ohif-extension-cornerstone-dicom-seg', + libraryTarget: 'umd', + filename: pkg.main, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + new MiniCssExtractPlugin({ + filename: `./dist/${outputName}.css`, + chunkFilename: `./dist/${outputName}.css`, + }), + ], + }); +}; diff --git a/extensions/cornerstone-dicom-seg/CHANGELOG.md b/extensions/cornerstone-dicom-seg/CHANGELOG.md new file mode 100644 index 0000000..068fe56 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/CHANGELOG.md @@ -0,0 +1,3206 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + + +### Bug Fixes + +* cache thumbnail in display set ([#4782](https://github.com/OHIF/Viewers/issues/4782)) ([2410c6a](https://github.com/OHIF/Viewers/commit/2410c6a50904c1235993900e837876cc26af019b)) + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + + +### Bug Fixes + +* **core:** Address 3D reconstruction and Android compatibility issues and clean up 4D data mode ([#4762](https://github.com/OHIF/Viewers/issues/4762)) ([149d6d0](https://github.com/OHIF/Viewers/commit/149d6d049cd333b9e5846576b403ff387558a66f)) + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + + +### Features + +* Add customization support for more UI components ([#4634](https://github.com/OHIF/Viewers/issues/4634)) ([f15eb44](https://github.com/OHIF/Viewers/commit/f15eb44b4cf49de1b73a22512571cec02effaef3)) + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + + +### Features + +* **customization:** new customization service api ([#4688](https://github.com/OHIF/Viewers/issues/4688)) ([55ad8ef](https://github.com/OHIF/Viewers/commit/55ad8efbabc3fabd8031fc08927b2f92ae5aec69)) + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + + +### Bug Fixes + +* **seg:** sphere scissor on stack and cpu rendering reset properties was broken ([#4721](https://github.com/OHIF/Viewers/issues/4721)) ([f00d182](https://github.com/OHIF/Viewers/commit/f00d18292f02e8910215d913edfc994850a68d88)) + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + + +### Bug Fixes + +* Having sop instance in a per-frame or shared attribute breaks load ([#4560](https://github.com/OHIF/Viewers/issues/4560)) ([cded082](https://github.com/OHIF/Viewers/commit/cded08261788143e0d5be57a55c927fd96aafb22)) + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + + +### Features + +* **dev:** move to rsbuild for dev - faster ([#4674](https://github.com/OHIF/Viewers/issues/4674)) ([d4a4267](https://github.com/OHIF/Viewers/commit/d4a4267429c02916dd51f6aefb290d96dd1c3b04)) + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + + +### Bug Fixes + +* **docker:** publish manifest for multiarch and update cs3d ([#4650](https://github.com/OHIF/Viewers/issues/4650)) ([836e67a](https://github.com/OHIF/Viewers/commit/836e67a6ab8de66d8908c75856774318729544f4)) + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + + +### Bug Fixes + +* **tools:** enable additional tools in volume viewport ([#4620](https://github.com/OHIF/Viewers/issues/4620)) ([1992002](https://github.com/OHIF/Viewers/commit/1992002d2dced171c17b9a0163baf707fc551e3d)) + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + + +### Features + +* **measurements:** Provide for the Load (SR) measurements button to optionally clear existing measurements prior to loading the SR. ([#4586](https://github.com/OHIF/Viewers/issues/4586)) ([4d3d5e7](https://github.com/OHIF/Viewers/commit/4d3d5e794cb99212eba06bf91dbb30a258725efe)) + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + + +### Features + +* migrate icons to ui-next ([#4606](https://github.com/OHIF/Viewers/issues/4606)) ([4e2ae32](https://github.com/OHIF/Viewers/commit/4e2ae328744ed95589c2cdf7a531454a25bf88b5)) + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + + +### Bug Fixes + +* **seg:** jump to the first slice in SEG and RT that has data ([#4605](https://github.com/OHIF/Viewers/issues/4605)) ([9bf24d6](https://github.com/OHIF/Viewers/commit/9bf24d6dc58ed8f65c90899a17c11044b792cf40)) + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + + +### Features + +* **node:** move to node 20 ([#4594](https://github.com/OHIF/Viewers/issues/4594)) ([1f04d6c](https://github.com/OHIF/Viewers/commit/1f04d6c1be729a26fe7bcda923770a1cd461053c)) + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + + +### Bug Fixes + +* **CinePlayer:** always show cine player for dynamic data ([#4575](https://github.com/OHIF/Viewers/issues/4575)) ([b8e8bbe](https://github.com/OHIF/Viewers/commit/b8e8bbe482b66e8cbe9167d03e9d8dedd2d3b6c5)) + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + + +### Bug Fixes + +* **multiframe:** metadata handling of NM studies and loading order ([#4554](https://github.com/OHIF/Viewers/issues/4554)) ([7624ccb](https://github.com/OHIF/Viewers/commit/7624ccb5e495c0a151227a458d8d5bfb8babb22c)) + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + + +### Bug Fixes + +* **colorlut:** use the correct colorlut index and update vtk ([#4544](https://github.com/OHIF/Viewers/issues/4544)) ([b9c26e7](https://github.com/OHIF/Viewers/commit/b9c26e775a49044673473418dd5bdee2e5562ab9)) + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + + +### Bug Fixes + +* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed)) + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + + +### Bug Fixes + +* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30)) + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + + +### Bug Fixes + +* **tmtv:** fix toggle one up weird behaviours ([#4473](https://github.com/OHIF/Viewers/issues/4473)) ([aa2b649](https://github.com/OHIF/Viewers/commit/aa2b649444eb4fe5422e72ea7830a709c4d24a90)) + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + + +### Bug Fixes + +* build ([#4471](https://github.com/OHIF/Viewers/issues/4471)) ([3d11ef2](https://github.com/OHIF/Viewers/commit/3d11ef28f213361ec7586809317bd219fa70e742)) + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + + +### Bug Fixes + +* **ui:** display error in ui while loading seg ([#4433](https://github.com/OHIF/Viewers/issues/4433)) ([2e96371](https://github.com/OHIF/Viewers/commit/2e96371b0631a9e5d411b0142300708ab8ba7d27)) + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + + +### Features + +* **new-study-panel:** default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser ([#4417](https://github.com/OHIF/Viewers/issues/4417)) ([a7fd9fa](https://github.com/OHIF/Viewers/commit/a7fd9fa5bfff7a1b533d99cb96f7147a35fd528f)) + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + + +### Bug Fixes + +* **createReport:** early return on cancel in prompt ([#4243](https://github.com/OHIF/Viewers/issues/4243)) ([2ec4692](https://github.com/OHIF/Viewers/commit/2ec4692eaf2349e21b141a2c0b5b104ee10f7a28)) + + +### Features + +* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3)) + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + + +### Features + +* **studies-panel:** New OHIF study panel - under experimental flag ([#4254](https://github.com/OHIF/Viewers/issues/4254)) ([7a96406](https://github.com/OHIF/Viewers/commit/7a96406a116e46e62c396855fa64f434e2984b58)) + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + + +### Bug Fixes + +* Cobb angle not working in basic-test mode and open contour ([#4280](https://github.com/OHIF/Viewers/issues/4280)) ([6fd3c7e](https://github.com/OHIF/Viewers/commit/6fd3c7e293fec851dd30e650c1347cc0bc7a99ee)) + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + + +### Bug Fixes + +* **seg:** maintain algorithm name and algorithm type when DICOM seg is exported or downloaded ([#4203](https://github.com/OHIF/Viewers/issues/4203)) ([a29e94d](https://github.com/OHIF/Viewers/commit/a29e94de803f79bbb3372d00ad8eb14b4224edc2)) + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + + +### Bug Fixes + +* **hydration:** Maintain the same slice that the user was on pre hydration in post hydration for SR and SEG. ([#4200](https://github.com/OHIF/Viewers/issues/4200)) ([430330f](https://github.com/OHIF/Viewers/commit/430330f7e384d503cb6fc695a7a9642ddfaac313)) + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + + +### Bug Fixes + +* **tmtv:** crosshairs should not have viewport indicators ([#4197](https://github.com/OHIF/Viewers/issues/4197)) ([f85da32](https://github.com/OHIF/Viewers/commit/f85da32f34389ef7cecae03c07e0af26468b52a6)) + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + + +### Bug Fixes + +* segmentation creation and segmentation mode viewport rendering ([#4193](https://github.com/OHIF/Viewers/issues/4193)) ([2174026](https://github.com/OHIF/Viewers/commit/217402678981f74293dff615f6b6812e54216d37)) + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + + +### Bug Fixes + +* **ultrasound:** Upgrade cornerstone3D version to resolve coloring issues ([#4181](https://github.com/OHIF/Viewers/issues/4181)) ([75a71db](https://github.com/OHIF/Viewers/commit/75a71db7f89840250ad1c2b35df5a35aceb8be7d)) + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + + +### Features + +* **measurements:** show untracked measurements in measurement panel under additional findings ([#4160](https://github.com/OHIF/Viewers/issues/4160)) ([18686c2](https://github.com/OHIF/Viewers/commit/18686c2caf13ede3e881303100bd4cc34b8b135f)) + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + + +### Bug Fixes + +* **rt:** dont convert to volume for RTSTRUCT ([#4157](https://github.com/OHIF/Viewers/issues/4157)) ([7745c09](https://github.com/OHIF/Viewers/commit/7745c092bb3edf0090f32fbbbae2f0776128d5a2)) + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + + +### Bug Fixes + +* **crosshairs:** reset angle, position, and slabthickness for crosshairs when reset viewport tool is used ([#4113](https://github.com/OHIF/Viewers/issues/4113)) ([73d9e99](https://github.com/OHIF/Viewers/commit/73d9e99d5d6f38ab6c36f4471d54f18798feacb4)) + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + + +### Bug Fixes + +* dicom json for orthanc by Update package versions for [@cornerstonejs](https://github.com/cornerstonejs) dependencies ([#4165](https://github.com/OHIF/Viewers/issues/4165)) ([34c7d72](https://github.com/OHIF/Viewers/commit/34c7d72142847486b98c9c52469940083eeaf87e)) + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + + +### Bug Fixes + +* **segmentation:** Address issue where segmentation creation failed on layout change ([#4153](https://github.com/OHIF/Viewers/issues/4153)) ([29944c8](https://github.com/OHIF/Viewers/commit/29944c8512c35718af03c03ef82bc43675ee1872)) + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + + +### Bug Fixes + +* **bugs:** enhancements and bugs in several areas ([#4086](https://github.com/OHIF/Viewers/issues/4086)) ([730f434](https://github.com/OHIF/Viewers/commit/730f4349100f21b4489a21707dbb2dca9dbfbba2)) + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + + +### Bug Fixes + +* **rt:** enhanced RT support, utilize SVGs for rendering. ([#4074](https://github.com/OHIF/Viewers/issues/4074)) ([0156bc4](https://github.com/OHIF/Viewers/commit/0156bc426f1840ae0d090223e94a643726e856cb)) + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + + +### Bug Fixes + +* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f)) + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + + +### Bug Fixes + +* **bugs:** fix patient header for doc, track ball rotate resize observer and add segmentation button not being enabled on viewport data change ([#4068](https://github.com/OHIF/Viewers/issues/4068)) ([c09311d](https://github.com/OHIF/Viewers/commit/c09311d3b7df05fcd00a9f36a7233e9d7e5589d0)) + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + + +### Bug Fixes + +* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951)) + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + + +### Features + +* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb)) + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + + +### Bug Fixes + +* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af)) + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes - final ([#4048](https://github.com/OHIF/Viewers/issues/4048)) ([170bb96](https://github.com/OHIF/Viewers/commit/170bb96983082c39b22b7352e0c54aacf3e73b02)) + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + + +### Bug Fixes + +* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025)) + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637)) + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + + +### Features + +* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13)) + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + + +### Features + +* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2)) + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + + +### Bug Fixes + +* **cornerstone-dicom-sr:** Freehand SR hydration support ([#3996](https://github.com/OHIF/Viewers/issues/3996)) ([5645ac1](https://github.com/OHIF/Viewers/commit/5645ac1b271e1ed8c57f5d71100809362447267e)) + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + + +### Features + +* **segmentation:** Enhanced segmentation panel design for TMTV ([#3988](https://github.com/OHIF/Viewers/issues/3988)) ([9f3235f](https://github.com/OHIF/Viewers/commit/9f3235ff096636aafa88d8a42859e8dc85d9036d)) + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + + +### Features + +* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1)) + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + + +### Bug Fixes + +* **new layout:** address black screen bugs ([#4008](https://github.com/OHIF/Viewers/issues/4008)) ([158a181](https://github.com/OHIF/Viewers/commit/158a1816703e0ad66cae08cb9bd1ffb93bbd8d43)) + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + + +### Features + +* **layout:** new layout selector with 3D volume rendering ([#3923](https://github.com/OHIF/Viewers/issues/3923)) ([617043f](https://github.com/OHIF/Viewers/commit/617043fe0da5de91fbea4ac33a27f1df16ae1ca6)) + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + + +### Bug Fixes + +* **SR display:** and the token based navigation ([#3995](https://github.com/OHIF/Viewers/issues/3995)) ([feed230](https://github.com/OHIF/Viewers/commit/feed2304c124dc2facc7a7371ed9851548c223c5)) + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + + +### Bug Fixes + +* **demo:** Deploy issue ([#3951](https://github.com/OHIF/Viewers/issues/3951)) ([21e8a2b](https://github.com/OHIF/Viewers/commit/21e8a2bd0b7cc72f90a31e472d285d761be15d30)) + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + + +### Bug Fixes + +* Update CS3D to fix second render ([#3892](https://github.com/OHIF/Viewers/issues/3892)) ([d00a86b](https://github.com/OHIF/Viewers/commit/d00a86b022742ea089d246d06cfd691f43b64412)) + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + + +### Bug Fixes + +* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6)) + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + + +### Bug Fixes + +* **auth:** fix the issue with oauth at a non root path ([#3840](https://github.com/OHIF/Viewers/issues/3840)) ([6651008](https://github.com/OHIF/Viewers/commit/6651008fbb35dabd5991c7f61128e6ef324012df)) + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + + +### Bug Fixes + +* Update the CS3D packages to add the most recent HTJ2K TSUIDS ([#3806](https://github.com/OHIF/Viewers/issues/3806)) ([9d1884d](https://github.com/OHIF/Viewers/commit/9d1884d7d8b6b2a1cdc26965a96995838aa72682)) + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + + +### Bug Fixes + +* **voi:** should publish voi change event on reset ([#3707](https://github.com/OHIF/Viewers/issues/3707)) ([52f34c6](https://github.com/OHIF/Viewers/commit/52f34c64d014f433ec1661a39b47e7fb27f15332)) + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + + +### Bug Fixes + +* **modality unit:** fix the modality unit per target via upgrade of cs3d ([#3706](https://github.com/OHIF/Viewers/issues/3706)) ([0a42d57](https://github.com/OHIF/Viewers/commit/0a42d573bbca7f2551a831a46d3aa6b56674a580)) + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + + +### Features + +* **Segmentation:** download RTSS from Labelmap([#3692](https://github.com/OHIF/Viewers/issues/3692)) ([40673f6](https://github.com/OHIF/Viewers/commit/40673f64b36b1150149c55632aa1825178a39e65)) + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + + +### Bug Fixes + +* **segmentation scroll:** and hydration bugs ([#3701](https://github.com/OHIF/Viewers/issues/3701)) ([1fd98d9](https://github.com/OHIF/Viewers/commit/1fd98d922094d10fe0c6e9df726314ec9fce49e8)) + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + + +### Bug Fixes + +* **measurement and microscopy:** various small fixes for measurement and microscopy side panel ([#3696](https://github.com/OHIF/Viewers/issues/3696)) ([c1d5ee7](https://github.com/OHIF/Viewers/commit/c1d5ee7e3f7f4c0c6bed9ae81eba5519741c5155)) + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + + +### Bug Fixes + +* **config:** support more values for the useSharedArrayBuffer ([#3688](https://github.com/OHIF/Viewers/issues/3688)) ([1129c15](https://github.com/OHIF/Viewers/commit/1129c155d2c7d46c98a5df7c09879aa3d459fa7e)) + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) +* **SidePanel:** new side panel tab look-and-feel ([#3657](https://github.com/OHIF/Viewers/issues/3657)) ([85c899b](https://github.com/OHIF/Viewers/commit/85c899b399e2521480724be145538993721b9378)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + + +### Features + +* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd)) + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + + +### Features + +* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1)) + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg diff --git a/extensions/cornerstone-dicom-seg/LICENSE b/extensions/cornerstone-dicom-seg/LICENSE new file mode 100644 index 0000000..983c5ef --- /dev/null +++ b/extensions/cornerstone-dicom-seg/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2023 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/extensions/cornerstone-dicom-seg/README.md b/extensions/cornerstone-dicom-seg/README.md new file mode 100644 index 0000000..9057fbc --- /dev/null +++ b/extensions/cornerstone-dicom-seg/README.md @@ -0,0 +1,18 @@ +# dicom-seg +## Description + +DICOM SEG read workflow. This extension will allow you to load a DICOM SEG image +and display it on OHIF. Currently Segmentations are loaded as a volumetric labelmap +and displayed as a 3D volume. + +This extension provides a SEG viewport, which enables rendering and reviewing +of the DICOM SEG images. However, in order to fully load all the segments +you will need to click on the SEG Pill button on the viewport action bar +to fully load the segments. + +## Author + +OHIF + +## License +MIT diff --git a/extensions/cornerstone-dicom-seg/babel.config.js b/extensions/cornerstone-dicom-seg/babel.config.js new file mode 100644 index 0000000..a35080a --- /dev/null +++ b/extensions/cornerstone-dicom-seg/babel.config.js @@ -0,0 +1,43 @@ +module.exports = { + plugins: ['@babel/plugin-proposal-class-properties'], + env: { + test: { + presets: [ + [ + // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2 + '@babel/preset-env', + { + modules: 'commonjs', + debug: false, + }, + '@babel/preset-typescript', + ], + '@babel/preset-react', + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-regenerator', + '@babel/plugin-transform-runtime', + ], + }, + production: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + development: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + }, +}; diff --git a/extensions/cornerstone-dicom-seg/package.json b/extensions/cornerstone-dicom-seg/package.json new file mode 100644 index 0000000..5e69b52 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/package.json @@ -0,0 +1,54 @@ +{ + "name": "@ohif/extension-cornerstone-dicom-seg", + "version": "3.10.0-beta.111", + "description": "DICOM SEG read workflow", + "author": "OHIF", + "license": "MIT", + "main": "dist/ohif-extension-cornerstone-dicom-seg.umd.js", + "module": "src/index.tsx", + "files": [ + "dist/**", + "public/**", + "README.md" + ], + "repository": "OHIF/Viewers", + "keywords": [ + "ohif-extension" + ], + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.18.0" + }, + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:dicom-seg": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package-1": "yarn run build", + "start": "yarn run dev" + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.111", + "@ohif/extension-cornerstone": "3.10.0-beta.111", + "@ohif/extension-default": "3.10.0-beta.111", + "@ohif/i18n": "3.10.0-beta.111", + "prop-types": "^15.6.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^12.2.2", + "react-router": "^6.23.1", + "react-router-dom": "^6.23.1" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "@cornerstonejs/adapters": "^2.19.14", + "@cornerstonejs/core": "^2.19.14", + "@kitware/vtk.js": "32.1.1", + "react-color": "^2.19.3" + } +} diff --git a/extensions/cornerstone-dicom-seg/src/commandsModule.ts b/extensions/cornerstone-dicom-seg/src/commandsModule.ts new file mode 100644 index 0000000..b3c63ed --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/commandsModule.ts @@ -0,0 +1,379 @@ +import dcmjs from 'dcmjs'; +import { createReportDialogPrompt } from '@ohif/extension-default'; +import { Types } from '@ohif/core'; +import { cache, metaData } from '@cornerstonejs/core'; +import { + segmentation as cornerstoneToolsSegmentation, + Enums as cornerstoneToolsEnums, + utilities, +} from '@cornerstonejs/tools'; +import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters'; +import { classes, DicomMetadataStore } from '@ohif/core'; + +import vtkImageMarchingSquares from '@kitware/vtk.js/Filters/General/ImageMarchingSquares'; +import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; +import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; + +const { segmentation: segmentationUtils } = utilities; + +const { datasetToBlob } = dcmjs.data; + +const getTargetViewport = ({ viewportId, viewportGridService }) => { + const { viewports, activeViewportId } = viewportGridService.getState(); + const targetViewportId = viewportId || activeViewportId; + + const viewport = viewports.get(targetViewportId); + + return viewport; +}; + +const { + Cornerstone3D: { + Segmentation: { generateSegmentation }, + }, +} = adaptersSEG; + +const { + Cornerstone3D: { + RTSS: { generateRTSSFromSegmentations }, + }, +} = adaptersRT; + +const { downloadDICOMData } = helpers; + +const commandsModule = ({ + servicesManager, + extensionManager, +}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => { + const { + segmentationService, + uiDialogService, + displaySetService, + viewportGridService, + toolGroupService, + } = servicesManager.services as AppTypes.Services; + + const actions = { + /** + * Loads segmentations for a specified viewport. + * The function prepares the viewport for rendering, then loads the segmentation details. + * Additionally, if the segmentation has scalar data, it is set for the corresponding label map volume. + * + * @param {Object} params - Parameters for the function. + * @param params.segmentations - Array of segmentations to be loaded. + * @param params.viewportId - the target viewport ID. + * + */ + loadSegmentationsForViewport: async ({ segmentations, viewportId }) => { + // Todo: handle adding more than one segmentation + const viewport = getTargetViewport({ viewportId, viewportGridService }); + const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0]; + + const segmentation = segmentations[0]; + const segmentationId = segmentation.segmentationId; + const label = segmentation.config.label; + const segments = segmentation.config.segments; + + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + + await segmentationService.createLabelmapForDisplaySet(displaySet, { + segmentationId, + segments, + label, + }); + + segmentationService.addOrUpdateSegmentation(segmentation); + + await segmentationService.addSegmentationRepresentation(viewport.viewportId, { + segmentationId, + }); + + return segmentationId; + }, + /** + * Generates a segmentation from a given segmentation ID. + * This function retrieves the associated segmentation and + * its referenced volume, extracts label maps from the + * segmentation volume, and produces segmentation data + * alongside associated metadata. + * + * @param {Object} params - Parameters for the function. + * @param params.segmentationId - ID of the segmentation to be generated. + * @param params.options - Optional configuration for the generation process. + * + * @returns Returns the generated segmentation data. + */ + generateSegmentation: ({ segmentationId, options = {} }) => { + const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId); + + const { imageIds } = segmentation.representationData.Labelmap; + + const segImages = imageIds.map(imageId => cache.getImage(imageId)); + const referencedImages = segImages.map(image => cache.getImage(image.referencedImageId)); + + const labelmaps2D = []; + + let z = 0; + + for (const segImage of segImages) { + const segmentsOnLabelmap = new Set(); + const pixelData = segImage.getPixelData(); + const { rows, columns } = segImage; + + // Use a single pass through the pixel data + for (let i = 0; i < pixelData.length; i++) { + const segment = pixelData[i]; + if (segment !== 0) { + segmentsOnLabelmap.add(segment); + } + } + + labelmaps2D[z++] = { + segmentsOnLabelmap: Array.from(segmentsOnLabelmap), + pixelData, + rows, + columns, + }; + } + + const allSegmentsOnLabelmap = labelmaps2D.map(labelmap => labelmap.segmentsOnLabelmap); + + const labelmap3D = { + segmentsOnLabelmap: Array.from(new Set(allSegmentsOnLabelmap.flat())), + metadata: [], + labelmaps2D, + }; + + const segmentationInOHIF = segmentationService.getSegmentation(segmentationId); + const representations = segmentationService.getRepresentationsForSegmentation(segmentationId); + + Object.entries(segmentationInOHIF.segments).forEach(([segmentIndex, segment]) => { + // segmentation service already has a color for each segment + if (!segment) { + return; + } + + const { label } = segment; + + const firstRepresentation = representations[0]; + const color = segmentationService.getSegmentColor( + firstRepresentation.viewportId, + segmentationId, + segment.segmentIndex + ); + + const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB( + color.slice(0, 3).map(value => value / 255) + ).map(value => Math.round(value)); + + const segmentMetadata = { + SegmentNumber: segmentIndex.toString(), + SegmentLabel: label, + SegmentAlgorithmType: segment?.algorithmType || 'MANUAL', + SegmentAlgorithmName: segment?.algorithmName || 'OHIF Brush', + RecommendedDisplayCIELabValue, + SegmentedPropertyCategoryCodeSequence: { + CodeValue: 'T-D0050', + CodingSchemeDesignator: 'SRT', + CodeMeaning: 'Tissue', + }, + SegmentedPropertyTypeCodeSequence: { + CodeValue: 'T-D0050', + CodingSchemeDesignator: 'SRT', + CodeMeaning: 'Tissue', + }, + }; + labelmap3D.metadata[segmentIndex] = segmentMetadata; + }); + + const generatedSegmentation = generateSegmentation( + referencedImages, + labelmap3D, + metaData, + options + ); + + return generatedSegmentation; + }, + /** + * Downloads a segmentation based on the provided segmentation ID. + * This function retrieves the associated segmentation and + * uses it to generate the corresponding DICOM dataset, which + * is then downloaded with an appropriate filename. + * + * @param {Object} params - Parameters for the function. + * @param params.segmentationId - ID of the segmentation to be downloaded. + * + */ + downloadSegmentation: ({ segmentationId }) => { + const segmentationInOHIF = segmentationService.getSegmentation(segmentationId); + const generatedSegmentation = actions.generateSegmentation({ + segmentationId, + }); + + downloadDICOMData(generatedSegmentation.dataset, `${segmentationInOHIF.label}`); + }, + /** + * Stores a segmentation based on the provided segmentationId into a specified data source. + * The SeriesDescription is derived from user input or defaults to the segmentation label, + * and in its absence, defaults to 'Research Derived Series'. + * + * @param {Object} params - Parameters for the function. + * @param params.segmentationId - ID of the segmentation to be stored. + * @param params.dataSource - Data source where the generated segmentation will be stored. + * + * @returns {Object|void} Returns the naturalized report if successfully stored, + * otherwise throws an error. + */ + storeSegmentation: async ({ segmentationId, dataSource }) => { + const promptResult = await createReportDialogPrompt(uiDialogService, { + extensionManager, + }); + + if (promptResult.action !== 1 && !promptResult.value) { + return; + } + + const segmentation = segmentationService.getSegmentation(segmentationId); + + if (!segmentation) { + throw new Error('No segmentation found'); + } + + const { label } = segmentation; + const SeriesDescription = promptResult.value || label || 'Research Derived Series'; + + const generatedData = actions.generateSegmentation({ + segmentationId, + options: { + SeriesDescription, + }, + }); + + if (!generatedData || !generatedData.dataset) { + throw new Error('Error during segmentation generation'); + } + + const { dataset: naturalizedReport } = generatedData; + + await dataSource.store.dicom(naturalizedReport); + + // The "Mode" route listens for DicomMetadataStore changes + // When a new instance is added, it listens and + // automatically calls makeDisplaySets + + // add the information for where we stored it to the instance as well + naturalizedReport.wadoRoot = dataSource.getConfig().wadoRoot; + + DicomMetadataStore.addInstances([naturalizedReport], true); + + return naturalizedReport; + }, + /** + * Converts segmentations into RTSS for download. + * This sample function retrieves all segentations and passes to + * cornerstone tool adapter to convert to DICOM RTSS format. It then + * converts dataset to downloadable blob. + * + */ + downloadRTSS: ({ segmentationId }) => { + const segmentations = segmentationService.getSegmentation(segmentationId); + const vtkUtils = { + vtkImageMarchingSquares, + vtkDataArray, + vtkImageData, + }; + + const RTSS = generateRTSSFromSegmentations( + segmentations, + classes.MetadataProvider, + DicomMetadataStore, + cache, + cornerstoneToolsEnums, + vtkUtils + ); + + try { + const reportBlob = datasetToBlob(RTSS); + + //Create a URL for the binary. + const objectUrl = URL.createObjectURL(reportBlob); + window.location.assign(objectUrl); + } catch (e) { + console.warn(e); + } + }, + setBrushSize: ({ value, toolNames }) => { + const brushSize = Number(value); + + toolGroupService.getToolGroupIds()?.forEach(toolGroupId => { + if (toolNames?.length === 0) { + segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize); + } else { + toolNames?.forEach(toolName => { + segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName); + }); + } + }); + }, + setThresholdRange: ({ + value, + toolNames = ['ThresholdCircularBrush', 'ThresholdSphereBrush'], + }) => { + toolGroupService.getToolGroupIds()?.forEach(toolGroupId => { + const toolGroup = toolGroupService.getToolGroup(toolGroupId); + toolNames?.forEach(toolName => { + toolGroup.setToolConfiguration(toolName, { + strategySpecificConfiguration: { + THRESHOLD: { + threshold: value, + }, + }, + }); + }); + }); + }, + }; + + const definitions = { + /** + * Obsolete? + */ + loadSegmentationDisplaySetsForViewport: { + commandFn: actions.loadSegmentationDisplaySetsForViewport, + }, + /** + * Obsolete? + */ + loadSegmentationsForViewport: { + commandFn: actions.loadSegmentationsForViewport, + }, + + generateSegmentation: { + commandFn: actions.generateSegmentation, + }, + downloadSegmentation: { + commandFn: actions.downloadSegmentation, + }, + storeSegmentation: { + commandFn: actions.storeSegmentation, + }, + downloadRTSS: { + commandFn: actions.downloadRTSS, + }, + setBrushSize: { + commandFn: actions.setBrushSize, + }, + setThresholdRange: { + commandFn: actions.setThresholdRange, + }, + }; + + return { + actions, + definitions, + defaultContext: 'SEGMENTATION', + }; +}; + +export default commandsModule; diff --git a/extensions/cornerstone-dicom-seg/src/getHangingProtocolModule.ts b/extensions/cornerstone-dicom-seg/src/getHangingProtocolModule.ts new file mode 100644 index 0000000..700153f --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/getHangingProtocolModule.ts @@ -0,0 +1,101 @@ +import { Types } from '@ohif/core'; + +const segProtocol: Types.HangingProtocol.Protocol = { + id: '@ohif/seg', + // Don't store this hanging protocol as it applies to the currently active + // display set by default + // cacheId: null, + name: 'Segmentations', + // Just apply this one when specifically listed + protocolMatchingRules: [], + toolGroupIds: ['default'], + // -1 would be used to indicate active only, whereas other values are + // the number of required priors referenced - so 0 means active with + // 0 or more priors. + numberOfPriorsReferenced: 0, + // Default viewport is used to define the viewport when + // additional viewports are added using the layout tool + defaultViewport: { + viewportOptions: { + viewportType: 'stack', + toolGroupId: 'default', + allowUnmatchedView: true, + syncGroups: [ + { + type: 'hydrateseg', + id: 'sameFORId', + source: true, + target: true, + // options: { + // matchingRules: ['sameFOR'], + // }, + }, + ], + }, + displaySets: [ + { + id: 'segDisplaySetId', + matchedDisplaySetsIndex: -1, + }, + ], + }, + displaySetSelectors: { + segDisplaySetId: { + seriesMatchingRules: [ + { + attribute: 'Modality', + constraint: { + equals: 'SEG', + }, + }, + ], + }, + }, + stages: [ + { + name: 'Segmentations', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 1, + }, + }, + viewports: [ + { + viewportOptions: { + allowUnmatchedView: true, + syncGroups: [ + { + type: 'hydrateseg', + id: 'sameFORId', + source: true, + target: true, + // options: { + // matchingRules: ['sameFOR'], + // }, + }, + ], + }, + displaySets: [ + { + id: 'segDisplaySetId', + }, + ], + }, + ], + }, + ], +}; + +function getHangingProtocolModule() { + return [ + { + name: segProtocol.id, + protocol: segProtocol, + }, + ]; +} + +export default getHangingProtocolModule; +export { segProtocol }; diff --git a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts new file mode 100644 index 0000000..746c540 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.ts @@ -0,0 +1,255 @@ +import { utils } from '@ohif/core'; +import { metaData, triggerEvent, eventTarget } from '@cornerstonejs/core'; +import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools'; +import { adaptersSEG, Enums } from '@cornerstonejs/adapters'; + +import { SOPClassHandlerId } from './id'; +import { dicomlabToRGB } from './utils/dicomlabToRGB'; + +const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4']; + +const loadPromises = {}; + +function _getDisplaySetsFromSeries( + instances, + servicesManager: AppTypes.ServicesManager, + extensionManager +) { + const instance = instances[0]; + + const { + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + SeriesDescription, + SeriesNumber, + SeriesDate, + SOPClassUID, + wadoRoot, + wadoUri, + wadoUriRoot, + } = instance; + + const displaySet = { + Modality: 'SEG', + loading: false, + isReconstructable: true, // by default for now since it is a volumetric SEG currently + displaySetInstanceUID: utils.guid(), + SeriesDescription, + SeriesNumber, + SeriesDate, + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + SOPClassHandlerId, + SOPClassUID, + referencedImages: null, + referencedSeriesInstanceUID: null, + referencedDisplaySetInstanceUID: null, + isDerivedDisplaySet: true, + isLoaded: false, + isHydrated: false, + segments: {}, + sopClassUids, + instance, + instances: [instance], + wadoRoot, + wadoUriRoot, + wadoUri, + isOverlayDisplaySet: true, + }; + + const referencedSeriesSequence = instance.ReferencedSeriesSequence; + + if (!referencedSeriesSequence) { + console.error('ReferencedSeriesSequence is missing for the SEG'); + return; + } + + const referencedSeries = referencedSeriesSequence[0] || referencedSeriesSequence; + + displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence; + displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID; + const { displaySetService } = servicesManager.services; + const referencedDisplaySets = displaySetService.getDisplaySetsForSeries( + displaySet.referencedSeriesInstanceUID + ); + + const referencedDisplaySet = referencedDisplaySets[0]; + + if (!referencedDisplaySet) { + // subscribe to display sets added which means at some point it will be available + const { unsubscribe } = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SETS_ADDED, + ({ displaySetsAdded }) => { + // here we can also do a little bit of search, since sometimes DICOM SEG + // does not contain the referenced display set uid , and we can just + // see which of the display sets added is more similar and assign it + // to the referencedDisplaySet + const addedDisplaySet = displaySetsAdded[0]; + if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) { + displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID; + unsubscribe(); + } + } + ); + } else { + displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID; + } + + displaySet.load = async ({ headers }) => + await _load(displaySet, servicesManager, extensionManager, headers); + + return [displaySet]; +} + +function _load( + segDisplaySet, + servicesManager: AppTypes.ServicesManager, + extensionManager, + headers +) { + const { SOPInstanceUID } = segDisplaySet; + const { segmentationService } = servicesManager.services; + + if ( + (segDisplaySet.loading || segDisplaySet.isLoaded) && + loadPromises[SOPInstanceUID] && + _segmentationExists(segDisplaySet) + ) { + return loadPromises[SOPInstanceUID]; + } + + segDisplaySet.loading = true; + + // We don't want to fire multiple loads, so we'll wait for the first to finish + // and also return the same promise to any other callers. + loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => { + if (!segDisplaySet.segments || Object.keys(segDisplaySet.segments).length === 0) { + try { + await _loadSegments({ + extensionManager, + servicesManager, + segDisplaySet, + headers, + }); + } catch (e) { + segDisplaySet.loading = false; + return reject(e); + } + } + + segmentationService + .createSegmentationForSEGDisplaySet(segDisplaySet) + .then(() => { + segDisplaySet.loading = false; + resolve(); + }) + .catch(error => { + segDisplaySet.loading = false; + reject(error); + }); + }); + + return loadPromises[SOPInstanceUID]; +} + +async function _loadSegments({ + extensionManager, + servicesManager, + segDisplaySet, + headers, +}: withAppTypes) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.common' + ); + + const { segmentationService, uiNotificationService } = servicesManager.services; + + const { dicomLoaderService } = utilityModule.exports; + const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers); + + const referencedDisplaySet = servicesManager.services.displaySetService.getDisplaySetByUID( + segDisplaySet.referencedDisplaySetInstanceUID + ); + + if (!referencedDisplaySet) { + throw new Error('referencedDisplaySet is missing for SEG'); + } + + const { instances: images } = referencedDisplaySet; + const imageIds = images.map(({ imageId }) => imageId); + + // Todo: what should be defaults here + const tolerance = 0.001; + const skipOverlapping = true; + eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => { + const { percentComplete } = evt.detail; + segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, { + percentComplete, + }); + }); + + const results = await adaptersSEG.Cornerstone3D.Segmentation.generateToolState( + imageIds, + arrayBuffer, + metaData, + { skipOverlapping, tolerance, eventTarget, triggerEvent } + ); + + let usedRecommendedDisplayCIELabValue = true; + results.segMetadata.data.forEach((data, i) => { + if (i > 0) { + data.rgba = data.RecommendedDisplayCIELabValue; + + if (data.rgba) { + data.rgba = dicomlabToRGB(data.rgba); + } else { + usedRecommendedDisplayCIELabValue = false; + data.rgba = CONSTANTS.COLOR_LUT[i % CONSTANTS.COLOR_LUT.length]; + } + } + }); + + if (results.overlappingSegments) { + uiNotificationService.show({ + title: 'Overlapping Segments', + message: + 'Unsupported overlapping segments detected, segmentation rendering results may be incorrect.', + type: 'warning', + }); + } + + if (!usedRecommendedDisplayCIELabValue) { + // Display a notification about the non-utilization of RecommendedDisplayCIELabValue + uiNotificationService.show({ + title: 'DICOM SEG import', + message: + 'RecommendedDisplayCIELabValue not found for one or more segments. The default color was used instead.', + type: 'warning', + duration: 5000, + }); + } + + Object.assign(segDisplaySet, results); +} + +function _segmentationExists(segDisplaySet) { + return cstSegmentation.state.getSegmentation(segDisplaySet.displaySetInstanceUID); +} + +function getSopClassHandlerModule({ servicesManager, extensionManager }) { + const getDisplaySetsFromSeries = instances => { + return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager); + }; + + return [ + { + name: 'dicom-seg', + sopClassUids, + getDisplaySetsFromSeries, + }, + ]; +} + +export default getSopClassHandlerModule; diff --git a/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts b/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts new file mode 100644 index 0000000..fdd32c1 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/getToolbarModule.ts @@ -0,0 +1,65 @@ +export function getToolbarModule({ servicesManager }: withAppTypes) { + const { segmentationService, toolbarService, toolGroupService } = servicesManager.services; + return [ + { + name: 'evaluate.cornerstone.hasSegmentation', + evaluate: ({ viewportId }) => { + const segmentations = segmentationService.getSegmentationRepresentations(viewportId); + return { + disabled: !segmentations?.length, + }; + }, + }, + { + name: 'evaluate.cornerstone.segmentation', + evaluate: ({ viewportId, button, toolNames, disabledText }) => { + // Todo: we need to pass in the button section Id since we are kind of + // forcing the button to have black background since initially + // it is designed for the toolbox not the toolbar on top + // we should then branch the buttonSectionId to have different styles + const segmentations = segmentationService.getSegmentationRepresentations(viewportId); + if (!segmentations?.length) { + return { + disabled: true, + disabledText: disabledText ?? 'No segmentations available', + }; + } + + const activeSegmentation = segmentationService.getActiveSegmentation(viewportId); + if (!Object.keys(activeSegmentation.segments).length) { + return { + disabled: true, + disabledText: 'Add segment to enable this tool', + }; + } + + const toolGroup = toolGroupService.getToolGroupForViewport(viewportId); + + if (!toolGroup) { + return { + disabled: true, + disabledText: disabledText ?? 'Not available on the current viewport', + }; + } + + const toolName = toolbarService.getToolNameForButton(button); + + if (!toolGroup.hasTool(toolName) && !toolNames) { + return { + disabled: true, + disabledText: disabledText ?? 'Not available on the current viewport', + }; + } + + const isPrimaryActive = toolNames + ? toolNames.includes(toolGroup.getActivePrimaryMouseButtonTool()) + : toolGroup.getActivePrimaryMouseButtonTool() === toolName; + + return { + disabled: false, + isActive: isPrimaryActive, + }; + }, + }, + ]; +} diff --git a/extensions/cornerstone-dicom-seg/src/id.js b/extensions/cornerstone-dicom-seg/src/id.js new file mode 100644 index 0000000..7b5d940 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/id.js @@ -0,0 +1,7 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; +const SOPClassHandlerName = 'dicom-seg'; +const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`; + +export { id, SOPClassHandlerId, SOPClassHandlerName }; diff --git a/extensions/cornerstone-dicom-seg/src/index.tsx b/extensions/cornerstone-dicom-seg/src/index.tsx new file mode 100644 index 0000000..e1315b6 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/index.tsx @@ -0,0 +1,56 @@ +import { id } from './id'; +import React from 'react'; + +import getSopClassHandlerModule from './getSopClassHandlerModule'; +import getHangingProtocolModule from './getHangingProtocolModule'; +import getCommandsModule from './commandsModule'; +import { getToolbarModule } from './getToolbarModule'; + +const Component = React.lazy(() => { + return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport'); +}); + +const OHIFCornerstoneSEGViewport = props => { + return ( + Loading...}> + + + ); +}; + +/** + * You can remove any of the following modules if you don't need them. + */ +const extension = { + /** + * Only required property. Should be a unique value across all extensions. + * You ID can be anything you want, but it should be unique. + */ + id, + getCommandsModule, + getToolbarModule, + getViewportModule({ servicesManager, extensionManager, commandsManager }) { + const ExtendedOHIFCornerstoneSEGViewport = props => { + return ( + + ); + }; + + return [{ name: 'dicom-seg', component: ExtendedOHIFCornerstoneSEGViewport }]; + }, + /** + * SopClassHandlerModule should provide a list of sop class handlers that will be + * available in OHIF for Modes to consume and use to create displaySets from Series. + * Each sop class handler is defined by a { name, sopClassUids, getDisplaySetsFromSeries}. + * Examples include the default sop class handler provided by the default extension + */ + getSopClassHandlerModule, + getHangingProtocolModule, +}; + +export default extension; diff --git a/extensions/cornerstone-dicom-seg/src/types/segmentation.tsx b/extensions/cornerstone-dicom-seg/src/types/segmentation.tsx new file mode 100644 index 0000000..170c09c --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/types/segmentation.tsx @@ -0,0 +1,4 @@ +export enum SegmentationPanelMode { + Expanded = 'expanded', + Dropdown = 'dropdown', +} diff --git a/extensions/cornerstone-dicom-seg/src/utils/dicomlabToRGB.ts b/extensions/cornerstone-dicom-seg/src/utils/dicomlabToRGB.ts new file mode 100644 index 0000000..34ce1e5 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/utils/dicomlabToRGB.ts @@ -0,0 +1,14 @@ +import dcmjs from 'dcmjs'; + +/** + * Converts a CIELAB color to an RGB color using the dcmjs library. + * @param cielab - The CIELAB color to convert. + * @returns The RGB color as an array of three integers between 0 and 255. + */ +function dicomlabToRGB(cielab: number[]): number[] { + const rgb = dcmjs.data.Colors.dicomlab2RGB(cielab).map(x => Math.round(x * 255)); + + return rgb; +} + +export { dicomlabToRGB }; diff --git a/extensions/cornerstone-dicom-seg/src/utils/initSEGToolGroup.ts b/extensions/cornerstone-dicom-seg/src/utils/initSEGToolGroup.ts new file mode 100644 index 0000000..9db899b --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/utils/initSEGToolGroup.ts @@ -0,0 +1,7 @@ +function createSEGToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) { + const tools = customizationService.getCustomization('cornerstone.overlayViewportTools'); + + return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools); +} + +export default createSEGToolGroupAndAddTools; diff --git a/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts b/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts new file mode 100644 index 0000000..9d3a8c8 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts @@ -0,0 +1,83 @@ +import { ButtonEnums } from '@ohif/ui'; + +const RESPONSE = { + NO_NEVER: -1, + CANCEL: 0, + HYDRATE_SEG: 5, +}; + +function promptHydrateSEG({ + servicesManager, + segDisplaySet, + viewportId, + preHydrateCallbacks, + hydrateCallback, +}: withAppTypes) { + const { uiViewportDialogService } = servicesManager.services; + const extensionManager = servicesManager._extensionManager; + const appConfig = extensionManager._appConfig; + + return new Promise(async function (resolve, reject) { + const promptResult = appConfig?.disableConfirmationPrompts + ? RESPONSE.HYDRATE_SEG + : await _askHydrate(uiViewportDialogService, viewportId); + + if (promptResult === RESPONSE.HYDRATE_SEG) { + preHydrateCallbacks?.forEach(callback => { + callback(); + }); + + window.setTimeout(async () => { + const isHydrated = await hydrateCallback({ + segDisplaySet, + viewportId, + }); + + resolve(isHydrated); + }, 0); + } + }); +} + +function _askHydrate(uiViewportDialogService, viewportId) { + return new Promise(function (resolve, reject) { + const message = 'Do you want to open this Segmentation?'; + const actions = [ + { + id: 'no-hydrate', + type: ButtonEnums.type.secondary, + text: 'No', + value: RESPONSE.CANCEL, + }, + { + id: 'yes-hydrate', + type: ButtonEnums.type.primary, + text: 'Yes', + value: RESPONSE.HYDRATE_SEG, + }, + ]; + const onSubmit = result => { + uiViewportDialogService.hide(); + resolve(result); + }; + + uiViewportDialogService.show({ + viewportId, + type: 'info', + message, + actions, + onSubmit, + onOutsideClick: () => { + uiViewportDialogService.hide(); + resolve(RESPONSE.CANCEL); + }, + onKeyPress: event => { + if (event.key === 'Enter') { + onSubmit(RESPONSE.HYDRATE_SEG); + } + }, + }); + }); +} + +export default promptHydrateSEG; diff --git a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx new file mode 100644 index 0000000..2cf4d0c --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx @@ -0,0 +1,408 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ViewportActionArrows } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; +import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup'; +import promptHydrateSEG from '../utils/promptHydrateSEG'; +import _getStatusComponent from './_getStatusComponent'; +import { usePositionPresentationStore } from '@ohif/extension-cornerstone'; +import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; +import { utils } from '@ohif/extension-cornerstone'; + +const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup'; + +function OHIFCornerstoneSEGViewport(props: withAppTypes) { + const { + children, + displaySets, + viewportOptions, + servicesManager, + extensionManager, + commandsManager, + } = props; + + const { t } = useTranslation('SEGViewport'); + const viewportId = viewportOptions.viewportId; + + const { + displaySetService, + toolGroupService, + segmentationService, + customizationService, + viewportActionCornersService, + } = servicesManager.services; + + const LoadingIndicatorTotalPercent = customizationService.getCustomization( + 'ui.loadingIndicatorTotalPercent' + ); + + const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportId}`; + + // SEG viewport will always have a single display set + if (displaySets.length > 1) { + throw new Error('SEG viewport should only have a single display set'); + } + + const segDisplaySet = displaySets[0]; + const [viewportGrid, viewportGridService] = useViewportGrid(); + + // States + let selectedSegmentObjectIndex: number = 0; + const { setPositionPresentation } = usePositionPresentationStore(); + + // Hydration means that the SEG is opened and segments are loaded into the + // segmentation panel, and SEG is also rendered on any viewport that is in the + // same frameOfReferenceUID as the referencedSeriesUID of the SEG. However, + // loading basically means SEG loading over network and bit unpacking of the + // SEG data. + const [isHydrated, setIsHydrated] = useState(segDisplaySet.isHydrated); + const [segIsLoading, setSegIsLoading] = useState(!segDisplaySet.isLoaded); + const [element, setElement] = useState(null); + const [processingProgress, setProcessingProgress] = useState({ + percentComplete: null, + totalSegments: null, + }); + + // refs + const referencedDisplaySetRef = useRef(null); + + const { viewports, activeViewportId } = viewportGrid; + + const referencedDisplaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID; + const referencedDisplaySet = displaySetService.getDisplaySetByUID( + referencedDisplaySetInstanceUID + ); + + const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata( + referencedDisplaySet, + segDisplaySet + ); + + referencedDisplaySetRef.current = { + displaySet: referencedDisplaySet, + metadata: referencedDisplaySetMetadata, + }; + /** + * OnElementEnabled callback which is called after the cornerstoneExtension + * has enabled the element. Note: we delegate all the image rendering to + * cornerstoneExtension, so we don't need to do anything here regarding + * the image rendering, element enabling etc. + */ + const onElementEnabled = evt => { + setElement(evt.detail.element); + }; + + const onElementDisabled = () => { + setElement(null); + }; + + const storePresentationState = useCallback(() => { + viewportGrid?.viewports.forEach(({ viewportId }) => { + commandsManager.runCommand('storePresentation', { + viewportId, + }); + }); + }, [viewportGrid]); + + const getCornerstoneViewport = useCallback(() => { + const { component: Component } = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.viewportModule.cornerstone' + ); + + // Todo: jump to the center of the first segment + return ( + { + props.onElementEnabled?.(evt); + onElementEnabled(evt); + }} + onElementDisabled={onElementDisabled} + > + ); + }, [viewportId, segDisplaySet, toolGroupId]); + + const onSegmentChange = useCallback( + direction => { + utils.handleSegmentChange({ + direction, + segDisplaySet: segDisplaySet, + viewportId, + selectedSegmentObjectIndex, + segmentationService, + }); + }, + [selectedSegmentObjectIndex] + ); + + const hydrateSEG = useCallback(() => { + // update the previously stored segmentationPresentation with the new viewportId + // presentation so that when we put the referencedDisplaySet back in the viewport + // it will have the correct segmentation representation hydrated + commandsManager.runCommand('updateStoredSegmentationPresentation', { + displaySet: segDisplaySet, + type: SegmentationRepresentations.Labelmap, + }); + + // update the previously stored positionPresentation with the new viewportId + // presentation so that when we put the referencedDisplaySet back in the viewport + // it will be in the correct position zoom and pan + commandsManager.runCommand('updateStoredPositionPresentation', { + viewportId, + displaySetInstanceUID: referencedDisplaySet.displaySetInstanceUID, + }); + + viewportGridService.setDisplaySetsForViewport({ + viewportId, + displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID], + }); + }, [commandsManager, viewportId, referencedDisplaySet, segDisplaySet]); + + useEffect(() => { + if (segIsLoading) { + return; + } + + promptHydrateSEG({ + servicesManager, + viewportId, + segDisplaySet, + preHydrateCallbacks: [storePresentationState], + hydrateCallback: hydrateSEG, + }).then(isHydrated => { + if (isHydrated) { + setIsHydrated(true); + } + }); + }, [servicesManager, viewportId, segDisplaySet, segIsLoading, hydrateSEG]); + + useEffect(() => { + // on new seg display set, remove all segmentations from all viewports + segmentationService.clearSegmentationRepresentations(viewportId); + + const { unsubscribe } = segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE, + evt => { + if (evt.segDisplaySet.displaySetInstanceUID === segDisplaySet.displaySetInstanceUID) { + setSegIsLoading(false); + } + + if (segDisplaySet?.firstSegmentedSliceImageId && viewportOptions?.presentationIds) { + const { firstSegmentedSliceImageId } = segDisplaySet; + const { presentationIds } = viewportOptions; + + setPositionPresentation(presentationIds.positionPresentationId, { + viewReference: { + referencedImageId: firstSegmentedSliceImageId, + }, + }); + } + } + ); + + return () => { + unsubscribe(); + }; + }, [segDisplaySet]); + + useEffect(() => { + const { unsubscribe } = segmentationService.subscribe( + segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, + ({ percentComplete, numSegments }) => { + setProcessingProgress({ + percentComplete, + totalSegments: numSegments, + }); + } + ); + + return () => { + unsubscribe(); + }; + }, [segDisplaySet]); + + /** + Cleanup the SEG viewport when the viewport is destroyed + */ + useEffect(() => { + const onDisplaySetsRemovedSubscription = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SETS_REMOVED, + ({ displaySetInstanceUIDs }) => { + const activeViewport = viewports.get(activeViewportId); + if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) { + viewportGridService.setDisplaySetsForViewport({ + viewportId: activeViewportId, + displaySetInstanceUIDs: [], + }); + } + } + ); + + return () => { + onDisplaySetsRemovedSubscription.unsubscribe(); + }; + }, []); + + useEffect(() => { + let toolGroup = toolGroupService.getToolGroup(toolGroupId); + + if (toolGroup) { + return; + } + + // keep the already stored segmentationPresentation for this viewport in memory + // so that we can restore it after hydrating the SEG + commandsManager.runCommand('updateStoredSegmentationPresentation', { + displaySet: segDisplaySet, + type: SegmentationRepresentations.Labelmap, + }); + + // always start fresh for this viewport since it is special type of viewport + // that should only show one segmentation at a time. + segmentationService.clearSegmentationRepresentations(viewportId); + + // This creates a custom tool group which has the lifetime of this view + // only, and does NOT interfere with currently displayed segmentations. + toolGroup = createSEGToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId); + + return () => { + // remove the segmentation representations if seg displayset changed + // e.g., another seg displayset is dragged into the viewport + segmentationService.clearSegmentationRepresentations(viewportId); + + // Only destroy the viewport specific implementation + toolGroupService.destroyToolGroup(toolGroupId); + }; + }, []); + + const onStatusClick = useCallback(async () => { + // Before hydrating a SEG and make it added to all viewports in the grid + // that share the same frameOfReferenceUID, we need to store the viewport grid + // presentation state, so that we can restore it after hydrating the SEG. This is + // required if the user has changed the viewport (other viewport than SEG viewport) + // presentation state (w/l and invert) and then opens the SEG. If we don't store + // the presentation state, the viewport will be reset to the default presentation + storePresentationState(); + hydrateSEG(); + }, [storePresentationState, hydrateSEG]); + + useEffect(() => { + viewportActionCornersService.addComponents([ + { + viewportId, + id: 'viewportStatusComponent', + component: _getStatusComponent({ + isHydrated, + onStatusClick, + }), + indexPriority: -100, + location: viewportActionCornersService.LOCATIONS.topLeft, + }, + { + viewportId, + id: 'viewportActionArrowsComponent', + component: ( + + ), + indexPriority: 0, + location: viewportActionCornersService.LOCATIONS.topRight, + }, + ]); + }, [ + activeViewportId, + isHydrated, + onSegmentChange, + onStatusClick, + viewportActionCornersService, + viewportId, + ]); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + let childrenWithProps = null; + + if ( + !referencedDisplaySetRef.current || + referencedDisplaySet.displaySetInstanceUID !== + referencedDisplaySetRef.current.displaySet.displaySetInstanceUID + ) { + return null; + } + + if (children && children.length) { + childrenWithProps = children.map((child, index) => { + return ( + child && + React.cloneElement(child, { + viewportId, + key: index, + }) + ); + }); + } + + return ( + <> +
+ {segIsLoading && ( + + )} + {getCornerstoneViewport()} + {childrenWithProps} +
+ + ); +} + +function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) { + const { SharedFunctionalGroupsSequence } = segDisplaySet.instance; + + const SharedFunctionalGroup = Array.isArray(SharedFunctionalGroupsSequence) + ? SharedFunctionalGroupsSequence[0] + : SharedFunctionalGroupsSequence; + + const { PixelMeasuresSequence } = SharedFunctionalGroup; + + const PixelMeasures = Array.isArray(PixelMeasuresSequence) + ? PixelMeasuresSequence[0] + : PixelMeasuresSequence; + + const { SpacingBetweenSlices, SliceThickness } = PixelMeasures; + + const image0 = referencedDisplaySet.images[0]; + const referencedDisplaySetMetadata = { + PatientID: image0.PatientID, + PatientName: image0.PatientName, + PatientSex: image0.PatientSex, + PatientAge: image0.PatientAge, + SliceThickness: image0.SliceThickness || SliceThickness, + StudyDate: image0.StudyDate, + SeriesDescription: image0.SeriesDescription, + SeriesInstanceUID: image0.SeriesInstanceUID, + SeriesNumber: image0.SeriesNumber, + ManufacturerModelName: image0.ManufacturerModelName, + SpacingBetweenSlices: image0.SpacingBetweenSlices || SpacingBetweenSlices, + }; + + return referencedDisplaySetMetadata; +} + +export default OHIFCornerstoneSEGViewport; diff --git a/extensions/cornerstone-dicom-seg/src/viewports/_getStatusComponent.tsx b/extensions/cornerstone-dicom-seg/src/viewports/_getStatusComponent.tsx new file mode 100644 index 0000000..836dca5 --- /dev/null +++ b/extensions/cornerstone-dicom-seg/src/viewports/_getStatusComponent.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ViewportActionButton } from '@ohif/ui'; +import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next'; + +export default function _getStatusComponent({ isHydrated, onStatusClick }) { + let ToolTipMessage = null; + let StatusIcon = null; + + switch (isHydrated) { + case true: + StatusIcon = () => ; + ToolTipMessage = () =>
This Segmentation is loaded in the segmentation panel
; + break; + case false: + StatusIcon = () => ( + + ); + ToolTipMessage = () =>
Click LOAD to load segmentation.
; + } + + const StatusArea = () => { + const { t } = useTranslation('Common'); + const loadStr = t('LOAD'); + + return ( +
+
+ + SEG +
+ {!isHydrated && ( + {loadStr} + )} +
+ ); + }; + + return ( + <> + {ToolTipMessage && ( + + + + + + + + + + + )} + {!ToolTipMessage && } + + ); +} \ No newline at end of file diff --git a/extensions/cornerstone-dicom-sr/.webpack/webpack.dev.js b/extensions/cornerstone-dicom-sr/.webpack/webpack.dev.js new file mode 100644 index 0000000..6aea859 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/extensions/cornerstone-dicom-sr/.webpack/webpack.prod.js b/extensions/cornerstone-dicom-sr/.webpack/webpack.prod.js new file mode 100644 index 0000000..9e07dd8 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/.webpack/webpack.prod.js @@ -0,0 +1,55 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +const pkg = require('./../package.json'); + +const ROOT_DIR = path.join(__dirname, '../'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +const outputName = `ohif-${pkg.name.split('/').pop()}`; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: true, + }, + output: { + path: ROOT_DIR, + library: 'ohif-extension-cornerstone-dicom-sr', + libraryTarget: 'umd', + filename: pkg.main, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + // new MiniCssExtractPlugin({ + // filename: `./dist/${outputName}.css`, + // chunkFilename: `./dist/${outputName}.css`, + // }), + ], + }); +}; diff --git a/extensions/cornerstone-dicom-sr/CHANGELOG.md b/extensions/cornerstone-dicom-sr/CHANGELOG.md new file mode 100644 index 0000000..051aa03 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/CHANGELOG.md @@ -0,0 +1,3215 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + + +### Bug Fixes + +* **core:** Address 3D reconstruction and Android compatibility issues and clean up 4D data mode ([#4762](https://github.com/OHIF/Viewers/issues/4762)) ([149d6d0](https://github.com/OHIF/Viewers/commit/149d6d049cd333b9e5846576b403ff387558a66f)) + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + + +### Features + +* **customization:** new customization service api ([#4688](https://github.com/OHIF/Viewers/issues/4688)) ([55ad8ef](https://github.com/OHIF/Viewers/commit/55ad8efbabc3fabd8031fc08927b2f92ae5aec69)) + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + + +### Bug Fixes + +* **seg:** sphere scissor on stack and cpu rendering reset properties was broken ([#4721](https://github.com/OHIF/Viewers/issues/4721)) ([f00d182](https://github.com/OHIF/Viewers/commit/f00d18292f02e8910215d913edfc994850a68d88)) + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + + +### Bug Fixes + +* Inconsistent Handling of Patient Name Tag ([#4703](https://github.com/OHIF/Viewers/issues/4703)) ([8aedb2e](https://github.com/OHIF/Viewers/commit/8aedb2ec54a0ccf2550f745fed6f0b8aa184a860)) + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + + +### Bug Fixes + +* Having sop instance in a per-frame or shared attribute breaks load ([#4560](https://github.com/OHIF/Viewers/issues/4560)) ([cded082](https://github.com/OHIF/Viewers/commit/cded08261788143e0d5be57a55c927fd96aafb22)) + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + + +### Bug Fixes + +* cs dicom sr commands module ([#4683](https://github.com/OHIF/Viewers/issues/4683)) ([2d611d0](https://github.com/OHIF/Viewers/commit/2d611d06ed759bbd1e83ccfac7dceeff9eb6238e)) + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + + +### Features + +* Start using group filtering to define measurements table layout ([#4501](https://github.com/OHIF/Viewers/issues/4501)) ([82440e8](https://github.com/OHIF/Viewers/commit/82440e88d5debe808f0b14281b77e430c2489779)) + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + + +### Bug Fixes + +* Inconsistencies and update the style setting on load for embedded styles from codingValues ([#4599](https://github.com/OHIF/Viewers/issues/4599)) ([e0088ec](https://github.com/OHIF/Viewers/commit/e0088ec91807fa6a8e11e1e6942f51cedd080cc9)) + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + + +### Bug Fixes + +* **docker:** publish manifest for multiarch and update cs3d ([#4650](https://github.com/OHIF/Viewers/issues/4650)) ([836e67a](https://github.com/OHIF/Viewers/commit/836e67a6ab8de66d8908c75856774318729544f4)) + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + + +### Bug Fixes + +* **tools:** enable additional tools in volume viewport ([#4620](https://github.com/OHIF/Viewers/issues/4620)) ([1992002](https://github.com/OHIF/Viewers/commit/1992002d2dced171c17b9a0163baf707fc551e3d)) + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + + +### Bug Fixes + +* **SR:** Bring back the onModeEnter for the cornerstone-dicom-sr extension that was accidentally removed by PR [#4586](https://github.com/OHIF/Viewers/issues/4586) ([#4616](https://github.com/OHIF/Viewers/issues/4616)) ([2df8e1d](https://github.com/OHIF/Viewers/commit/2df8e1d5cd7a203bdde1cac6230b60a0b87bfcdd)) + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + + +### Features + +* **measurements:** Provide for the Load (SR) measurements button to optionally clear existing measurements prior to loading the SR. ([#4586](https://github.com/OHIF/Viewers/issues/4586)) ([4d3d5e7](https://github.com/OHIF/Viewers/commit/4d3d5e794cb99212eba06bf91dbb30a258725efe)) + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + + +### Features + +* migrate icons to ui-next ([#4606](https://github.com/OHIF/Viewers/issues/4606)) ([4e2ae32](https://github.com/OHIF/Viewers/commit/4e2ae328744ed95589c2cdf7a531454a25bf88b5)) + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + + +### Bug Fixes + +* **seg:** jump to the first slice in SEG and RT that has data ([#4605](https://github.com/OHIF/Viewers/issues/4605)) ([9bf24d6](https://github.com/OHIF/Viewers/commit/9bf24d6dc58ed8f65c90899a17c11044b792cf40)) + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + + +### Features + +* **node:** move to node 20 ([#4594](https://github.com/OHIF/Viewers/issues/4594)) ([1f04d6c](https://github.com/OHIF/Viewers/commit/1f04d6c1be729a26fe7bcda923770a1cd461053c)) + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + + +### Bug Fixes + +* **sr:** correct jump to first image via viewRef ([#4576](https://github.com/OHIF/Viewers/issues/4576)) ([6ec04ca](https://github.com/OHIF/Viewers/commit/6ec04ca65ea2f0fe95eaf624652911b87a6f81e6)) + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + + +### Bug Fixes + +* **CinePlayer:** always show cine player for dynamic data ([#4575](https://github.com/OHIF/Viewers/issues/4575)) ([b8e8bbe](https://github.com/OHIF/Viewers/commit/b8e8bbe482b66e8cbe9167d03e9d8dedd2d3b6c5)) + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + + +### Bug Fixes + +* **multiframe:** metadata handling of NM studies and loading order ([#4554](https://github.com/OHIF/Viewers/issues/4554)) ([7624ccb](https://github.com/OHIF/Viewers/commit/7624ccb5e495c0a151227a458d8d5bfb8babb22c)) + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + + +### Bug Fixes + +* **colorlut:** use the correct colorlut index and update vtk ([#4544](https://github.com/OHIF/Viewers/issues/4544)) ([b9c26e7](https://github.com/OHIF/Viewers/commit/b9c26e775a49044673473418dd5bdee2e5562ab9)) + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + + +### Bug Fixes + +* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed)) + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + + +### Bug Fixes + +* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30)) + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + + +### Bug Fixes + +* **tmtv:** fix toggle one up weird behaviours ([#4473](https://github.com/OHIF/Viewers/issues/4473)) ([aa2b649](https://github.com/OHIF/Viewers/commit/aa2b649444eb4fe5422e72ea7830a709c4d24a90)) + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + + +### Bug Fixes + +* build ([#4471](https://github.com/OHIF/Viewers/issues/4471)) ([3d11ef2](https://github.com/OHIF/Viewers/commit/3d11ef28f213361ec7586809317bd219fa70e742)) + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + + +### Features + +* **new-study-panel:** default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser ([#4417](https://github.com/OHIF/Viewers/issues/4417)) ([a7fd9fa](https://github.com/OHIF/Viewers/commit/a7fd9fa5bfff7a1b533d99cb96f7147a35fd528f)) + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + + +### Bug Fixes + +* **sr:** load existing point, if there is 2nd point in renderableData (Fix rotation in arrow annotation) ([#4356](https://github.com/OHIF/Viewers/issues/4356)) ([7353f7f](https://github.com/OHIF/Viewers/commit/7353f7f069446f8484278c2cff5b09149cfa23eb)) + + +### Features + +* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3)) + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + + +### Features + +* Add CS3D WSI and Video Viewports and add annotation navigation for MPR ([#4182](https://github.com/OHIF/Viewers/issues/4182)) ([7599ec9](https://github.com/OHIF/Viewers/commit/7599ec9421129dcade94e6fa6ec7908424ab3134)) + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + + +### Features + +* **SR:** text structured report (TEXT, CODE, NUM, PNAME, DATE, TIME and DATETIME) ([#4287](https://github.com/OHIF/Viewers/issues/4287)) ([246ebab](https://github.com/OHIF/Viewers/commit/246ebab6ebf5431a704a1861a5804045b9644ba4)) + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + + +### Bug Fixes + +* Cobb angle not working in basic-test mode and open contour ([#4280](https://github.com/OHIF/Viewers/issues/4280)) ([6fd3c7e](https://github.com/OHIF/Viewers/commit/6fd3c7e293fec851dd30e650c1347cc0bc7a99ee)) +* webpack import bugs showing warnings on import ([#4265](https://github.com/OHIF/Viewers/issues/4265)) ([24c511f](https://github.com/OHIF/Viewers/commit/24c511f4bc04c4143bbd3d0d48029f41f7f36014)) + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + + +### Bug Fixes + +* **sr:** rendering issue by running loadSR before updateSR ([#4226](https://github.com/OHIF/Viewers/issues/4226)) ([6971287](https://github.com/OHIF/Viewers/commit/69712874603109aa4f655d47daf15d72167a49ff)) + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + + +### Bug Fixes + +* **seg:** maintain algorithm name and algorithm type when DICOM seg is exported or downloaded ([#4203](https://github.com/OHIF/Viewers/issues/4203)) ([a29e94d](https://github.com/OHIF/Viewers/commit/a29e94de803f79bbb3372d00ad8eb14b4224edc2)) + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + + +### Bug Fixes + +* **tmtv:** crosshairs should not have viewport indicators ([#4197](https://github.com/OHIF/Viewers/issues/4197)) ([f85da32](https://github.com/OHIF/Viewers/commit/f85da32f34389ef7cecae03c07e0af26468b52a6)) + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + + +### Bug Fixes + +* **ultrasound:** Upgrade cornerstone3D version to resolve coloring issues ([#4181](https://github.com/OHIF/Viewers/issues/4181)) ([75a71db](https://github.com/OHIF/Viewers/commit/75a71db7f89840250ad1c2b35df5a35aceb8be7d)) + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + + +### Features + +* **measurements:** show untracked measurements in measurement panel under additional findings ([#4160](https://github.com/OHIF/Viewers/issues/4160)) ([18686c2](https://github.com/OHIF/Viewers/commit/18686c2caf13ede3e881303100bd4cc34b8b135f)) + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + + +### Bug Fixes + +* **crosshairs:** reset angle, position, and slabthickness for crosshairs when reset viewport tool is used ([#4113](https://github.com/OHIF/Viewers/issues/4113)) ([73d9e99](https://github.com/OHIF/Viewers/commit/73d9e99d5d6f38ab6c36f4471d54f18798feacb4)) + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + + +### Bug Fixes + +* dicom json for orthanc by Update package versions for [@cornerstonejs](https://github.com/cornerstonejs) dependencies ([#4165](https://github.com/OHIF/Viewers/issues/4165)) ([34c7d72](https://github.com/OHIF/Viewers/commit/34c7d72142847486b98c9c52469940083eeaf87e)) + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + + +### Bug Fixes + +* **segmentation:** Address issue where segmentation creation failed on layout change ([#4153](https://github.com/OHIF/Viewers/issues/4153)) ([29944c8](https://github.com/OHIF/Viewers/commit/29944c8512c35718af03c03ef82bc43675ee1872)) + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + + +### Bug Fixes + +* **rt:** enhanced RT support, utilize SVGs for rendering. ([#4074](https://github.com/OHIF/Viewers/issues/4074)) ([0156bc4](https://github.com/OHIF/Viewers/commit/0156bc426f1840ae0d090223e94a643726e856cb)) + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + + +### Bug Fixes + +* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f)) + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + + +### Bug Fixes + +* **bugs:** fix patient header for doc, track ball rotate resize observer and add segmentation button not being enabled on viewport data change ([#4068](https://github.com/OHIF/Viewers/issues/4068)) ([c09311d](https://github.com/OHIF/Viewers/commit/c09311d3b7df05fcd00a9f36a7233e9d7e5589d0)) + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + + +### Bug Fixes + +* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951)) + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + + +### Features + +* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb)) + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + + +### Bug Fixes + +* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af)) + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes - final ([#4048](https://github.com/OHIF/Viewers/issues/4048)) ([170bb96](https://github.com/OHIF/Viewers/commit/170bb96983082c39b22b7352e0c54aacf3e73b02)) + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + + +### Bug Fixes + +* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025)) + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + + +### Features + +* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13)) + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + + +### Features + +* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2)) + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + + +### Bug Fixes + +* **cornerstone-dicom-sr:** Freehand SR hydration support ([#3996](https://github.com/OHIF/Viewers/issues/3996)) ([5645ac1](https://github.com/OHIF/Viewers/commit/5645ac1b271e1ed8c57f5d71100809362447267e)) + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + + +### Features + +* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1)) + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + + +### Bug Fixes + +* **new layout:** address black screen bugs ([#4008](https://github.com/OHIF/Viewers/issues/4008)) ([158a181](https://github.com/OHIF/Viewers/commit/158a1816703e0ad66cae08cb9bd1ffb93bbd8d43)) + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + + +### Bug Fixes + +* **SR display:** and the token based navigation ([#3995](https://github.com/OHIF/Viewers/issues/3995)) ([feed230](https://github.com/OHIF/Viewers/commit/feed2304c124dc2facc7a7371ed9851548c223c5)) + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + + +### Bug Fixes + +* **demo:** Deploy issue ([#3951](https://github.com/OHIF/Viewers/issues/3951)) ([21e8a2b](https://github.com/OHIF/Viewers/commit/21e8a2bd0b7cc72f90a31e472d285d761be15d30)) + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + + +### Bug Fixes + +* Update CS3D to fix second render ([#3892](https://github.com/OHIF/Viewers/issues/3892)) ([d00a86b](https://github.com/OHIF/Viewers/commit/d00a86b022742ea089d246d06cfd691f43b64412)) + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + + +### Bug Fixes + +* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6)) + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + + +### Features + +* **customizationService:** Enable saving and loading of private tags in SRs ([#3842](https://github.com/OHIF/Viewers/issues/3842)) ([e1f55e6](https://github.com/OHIF/Viewers/commit/e1f55e65f2d2a34136ad5d0b1ada77d337a0ea23)) + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + + +### Bug Fixes + +* **auth:** fix the issue with oauth at a non root path ([#3840](https://github.com/OHIF/Viewers/issues/3840)) ([6651008](https://github.com/OHIF/Viewers/commit/6651008fbb35dabd5991c7f61128e6ef324012df)) + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + + +### Bug Fixes + +* Update the CS3D packages to add the most recent HTJ2K TSUIDS ([#3806](https://github.com/OHIF/Viewers/issues/3806)) ([9d1884d](https://github.com/OHIF/Viewers/commit/9d1884d7d8b6b2a1cdc26965a96995838aa72682)) + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + + +### Features + +* **dicomJSON:** Add Loading Other Display Sets and JSON Metadata Generation script ([#3777](https://github.com/OHIF/Viewers/issues/3777)) ([43b1c17](https://github.com/OHIF/Viewers/commit/43b1c17209502e4876ad59bae09ed9442eda8024)) + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + + +### Bug Fixes + +* **sr:** dcm4chee requires the patient name for an SR to match what is in the original study ([#3739](https://github.com/OHIF/Viewers/issues/3739)) ([d98439f](https://github.com/OHIF/Viewers/commit/d98439fe7f3825076dbc87b664a1d1480ff414d3)) + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + + +### Bug Fixes + +* **voi:** should publish voi change event on reset ([#3707](https://github.com/OHIF/Viewers/issues/3707)) ([52f34c6](https://github.com/OHIF/Viewers/commit/52f34c64d014f433ec1661a39b47e7fb27f15332)) + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + + +### Bug Fixes + +* **modality unit:** fix the modality unit per target via upgrade of cs3d ([#3706](https://github.com/OHIF/Viewers/issues/3706)) ([0a42d57](https://github.com/OHIF/Viewers/commit/0a42d573bbca7f2551a831a46d3aa6b56674a580)) + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + + +### Features + +* **Segmentation:** download RTSS from Labelmap([#3692](https://github.com/OHIF/Viewers/issues/3692)) ([40673f6](https://github.com/OHIF/Viewers/commit/40673f64b36b1150149c55632aa1825178a39e65)) + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + + +### Bug Fixes + +* **measurement and microscopy:** various small fixes for measurement and microscopy side panel ([#3696](https://github.com/OHIF/Viewers/issues/3696)) ([c1d5ee7](https://github.com/OHIF/Viewers/commit/c1d5ee7e3f7f4c0c6bed9ae81eba5519741c5155)) + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + + +### Bug Fixes + +* **config:** support more values for the useSharedArrayBuffer ([#3688](https://github.com/OHIF/Viewers/issues/3688)) ([1129c15](https://github.com/OHIF/Viewers/commit/1129c155d2c7d46c98a5df7c09879aa3d459fa7e)) + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + + +### Performance Improvements + +* **memory:** add 16 bit texture via configuration - reduces memory by half ([#3662](https://github.com/OHIF/Viewers/issues/3662)) ([2bd3b26](https://github.com/OHIF/Viewers/commit/2bd3b26a6aa54b211ef988f3ad64ef1fe5648bab)) + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + + +### Features + +* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f)) + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + + +### Features + +* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd)) + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + + +### Features + +* **data source UI config:** Popup the configuration dialogue whenever a data source is not fully configured ([#3620](https://github.com/OHIF/Viewers/issues/3620)) ([adedc8c](https://github.com/OHIF/Viewers/commit/adedc8c382e18a2e86a569e3d023cc55a157363f)) + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + + +### Features + +* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1)) + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + + +### Bug Fixes + +* **memory leak:** array buffer was sticking around in volume viewports ([#3611](https://github.com/OHIF/Viewers/issues/3611)) ([65b49ae](https://github.com/OHIF/Viewers/commit/65b49aeb1b5f38224e4892bdf32453500ee351f8)) diff --git a/extensions/cornerstone-dicom-sr/LICENSE b/extensions/cornerstone-dicom-sr/LICENSE new file mode 100644 index 0000000..19e20dd --- /dev/null +++ b/extensions/cornerstone-dicom-sr/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/cornerstone-dicom-sr/README.md b/extensions/cornerstone-dicom-sr/README.md new file mode 100644 index 0000000..e69de29 diff --git a/extensions/cornerstone-dicom-sr/babel.config.js b/extensions/cornerstone-dicom-sr/babel.config.js new file mode 100644 index 0000000..325ca2a --- /dev/null +++ b/extensions/cornerstone-dicom-sr/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/extensions/cornerstone-dicom-sr/package.json b/extensions/cornerstone-dicom-sr/package.json new file mode 100644 index 0000000..280261c --- /dev/null +++ b/extensions/cornerstone-dicom-sr/package.json @@ -0,0 +1,54 @@ +{ + "name": "@ohif/extension-cornerstone-dicom-sr", + "version": "3.10.0-beta.111", + "description": "OHIF extension for an SR Cornerstone Viewport", + "author": "OHIF", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/ohif-extension-cornerstone-dicom-sr.umd.js", + "module": "src/index.tsx", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "files": [ + "dist", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "keywords": [ + "ohif-extension" + ], + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:cornerstone": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package-1": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.111", + "@ohif/extension-cornerstone": "3.10.0-beta.111", + "@ohif/extension-measurement-tracking": "3.10.0-beta.111", + "@ohif/ui": "3.10.0-beta.111", + "dcmjs": "*", + "dicom-parser": "^1.8.9", + "hammerjs": "^2.0.8", + "prop-types": "^15.6.2", + "react": "^18.3.1" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "@cornerstonejs/adapters": "^2.19.14", + "@cornerstonejs/core": "^2.19.14", + "@cornerstonejs/tools": "^2.19.14", + "classnames": "^2.3.2" + } +} diff --git a/extensions/cornerstone-dicom-sr/src/commandsModule.ts b/extensions/cornerstone-dicom-sr/src/commandsModule.ts new file mode 100644 index 0000000..d65278d --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/commandsModule.ts @@ -0,0 +1,188 @@ +import { metaData, utilities } from '@cornerstonejs/core'; + +import OHIF, { DicomMetadataStore } from '@ohif/core'; +import dcmjs from 'dcmjs'; +import { adaptersSR } from '@cornerstonejs/adapters'; + +import getFilteredCornerstoneToolState from './utils/getFilteredCornerstoneToolState'; +import hydrateStructuredReport from './utils/hydrateStructuredReport'; + +const { MeasurementReport } = adaptersSR.Cornerstone3D; +const { log } = OHIF; + +/** + * @param measurementData An array of measurements from the measurements service + * that you wish to serialize. + * @param additionalFindingTypes toolTypes that should be stored with labels as Findings + * @param options Naturalized DICOM JSON headers to merge into the displaySet. + * + */ +const _generateReport = (measurementData, additionalFindingTypes, options = {}) => { + const filteredToolState = getFilteredCornerstoneToolState( + measurementData, + additionalFindingTypes + ); + + const report = MeasurementReport.generateReport( + filteredToolState, + metaData, + utilities.worldToImageCoords, + options + ); + + const { dataset } = report; + + // Set the default character set as UTF-8 + // https://dicom.innolitics.com/ciods/nm-image/sop-common/00080005 + if (typeof dataset.SpecificCharacterSet === 'undefined') { + dataset.SpecificCharacterSet = 'ISO_IR 192'; + } + return dataset; +}; + +const commandsModule = (props: withAppTypes) => { + const { servicesManager, extensionManager } = props; + const { customizationService, viewportGridService, displaySetService } = servicesManager.services; + + const actions = { + changeColorMeasurement: ({ uid }) => { + // When this gets supported, it probably belongs in cornerstone, not sr + throw new Error('Unsupported operation: changeColorMeasurement'); + // const { color } = measurementService.getMeasurement(uid); + // const rgbaColor = { + // r: color[0], + // g: color[1], + // b: color[2], + // a: color[3] / 255.0, + // }; + // colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => { + // if (actionId === 'cancel') { + // return; + // } + + // const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0]; + // segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color); + // }); + }, + + /** + * + * @param measurementData An array of measurements from the measurements service + * @param additionalFindingTypes toolTypes that should be stored with labels as Findings + * @param options Naturalized DICOM JSON headers to merge into the displaySet. + * as opposed to Finding Sites. + * that you wish to serialize. + */ + downloadReport: ({ measurementData, additionalFindingTypes, options = {} }) => { + const srDataset = _generateReport(measurementData, additionalFindingTypes, options); + const reportBlob = dcmjs.data.datasetToBlob(srDataset); + + //Create a URL for the binary. + const objectUrl = URL.createObjectURL(reportBlob); + window.location.assign(objectUrl); + }, + + /** + * + * @param measurementData An array of measurements from the measurements service + * that you wish to serialize. + * @param dataSource The dataSource that you wish to use to persist the data. + * @param additionalFindingTypes toolTypes that should be stored with labels as Findings + * @param options Naturalized DICOM JSON headers to merge into the displaySet. + * @return The naturalized report + */ + storeMeasurements: async ({ + measurementData, + dataSource, + additionalFindingTypes, + options = {}, + }) => { + // Use the @cornerstonejs adapter for converting to/from DICOM + // But it is good enough for now whilst we only have cornerstone as a datasource. + log.info('[DICOMSR] storeMeasurements'); + + if (!dataSource || !dataSource.store || !dataSource.store.dicom) { + log.error('[DICOMSR] datasource has no dataSource.store.dicom endpoint!'); + return Promise.reject({}); + } + + try { + const naturalizedReport = _generateReport(measurementData, additionalFindingTypes, options); + + const { StudyInstanceUID, ContentSequence } = naturalizedReport; + // The content sequence has 5 or more elements, of which + // the `[4]` element contains the annotation data, so this is + // checking that there is some annotation data present. + if (!ContentSequence?.[4].ContentSequence?.length) { + console.log('naturalizedReport missing imaging content', naturalizedReport); + throw new Error('Invalid report, no content'); + } + + const onBeforeDicomStore = customizationService.getCustomization('onBeforeDicomStore'); + + let dicomDict; + if (typeof onBeforeDicomStore === 'function') { + dicomDict = onBeforeDicomStore({ dicomDict, measurementData, naturalizedReport }); + } + + await dataSource.store.dicom(naturalizedReport, null, dicomDict); + + if (StudyInstanceUID) { + dataSource.deleteStudyMetadataPromise(StudyInstanceUID); + } + + // The "Mode" route listens for DicomMetadataStore changes + // When a new instance is added, it listens and + // automatically calls makeDisplaySets + DicomMetadataStore.addInstances([naturalizedReport], true); + + return naturalizedReport; + } catch (error) { + console.warn(error); + log.error(`[DICOMSR] Error while saving the measurements: ${error.message}`); + throw new Error(error.message || 'Error while saving the measurements.'); + } + }, + + /** + * Loads measurements by hydrating and loading the SR for the given display set instance UID + * and displays it in the active viewport. + */ + loadSRMeasurements: ({ displaySetInstanceUID }) => { + const { SeriesInstanceUIDs } = hydrateStructuredReport( + { servicesManager, extensionManager, commandsManager }, + displaySetInstanceUID + ); + + const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]); + if (displaySets.length) { + viewportGridService.setDisplaySetsForViewports([ + { + viewportId: viewportGridService.getActiveViewportId(), + displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID], + }, + ]); + } + }, + }; + + const definitions = { + downloadReport: { + commandFn: actions.downloadReport, + }, + storeMeasurements: { + commandFn: actions.storeMeasurements, + }, + loadSRMeasurements: { + commandFn: actions.loadSRMeasurements, + }, + }; + + return { + actions, + definitions, + defaultContext: 'CORNERSTONE_STRUCTURED_REPORT', + }; +}; + +export default commandsModule; diff --git a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRContainer.tsx b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRContainer.tsx new file mode 100644 index 0000000..461eba1 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRContainer.tsx @@ -0,0 +1,78 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { OHIFCornerstoneSRContentItem } from './OHIFCornerstoneSRContentItem'; + +export function OHIFCornerstoneSRContainer(props) { + const { container, nodeIndexesTree = [0], containerNumberedTree = [1] } = props; + const { ContinuityOfContent, ConceptNameCodeSequence } = container; + const { CodeMeaning } = ConceptNameCodeSequence ?? {}; + let childContainerIndex = 1; + const contentItems = container.ContentSequence?.map((contentItem, i) => { + const { ValueType } = contentItem; + const childNodeLevel = [...nodeIndexesTree, i]; + const key = childNodeLevel.join('.'); + + let Component; + let componentProps; + + if (ValueType === 'CONTAINER') { + const childContainerNumberedTree = [...containerNumberedTree, childContainerIndex++]; + + Component = OHIFCornerstoneSRContainer; + componentProps = { + container: contentItem, + nodeIndexesTree: childNodeLevel, + containerNumberedTree: childContainerNumberedTree, + }; + } else { + Component = OHIFCornerstoneSRContentItem; + componentProps = { + contentItem, + nodeIndexesTree: childNodeLevel, + continuityOfContent: ContinuityOfContent, + }; + } + + return ( + + ); + }); + + return ( +
+
+ {containerNumberedTree.join('.')}.  + {CodeMeaning} +
+
{contentItems}
+
+ ); +} + +OHIFCornerstoneSRContainer.propTypes = { + /** + * A tree node that may contain another container or one or more content items + * (text, code, uidref, pname, etc.) + */ + container: PropTypes.object, + /** + * A 0-based index list + */ + nodeIndexesTree: PropTypes.arrayOf(PropTypes.number), + /** + * A 1-based index list that represents a container in a multi-level numbered + * list (tree). + * + * Example: + * 1. History + * 1.1. Chief Complaint + * 1.2. Present Illness + * 1.3. Past History + * 1.4. Family History + * 2. Findings + * */ + containerNumberedTree: PropTypes.arrayOf(PropTypes.number), +}; diff --git a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRContentItem.tsx b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRContentItem.tsx new file mode 100644 index 0000000..bca5901 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRContentItem.tsx @@ -0,0 +1,63 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { CodeNameCodeSequenceValues } from '../enums'; +import formatContentItemValue from '../utils/formatContentItem'; + +const EMPTY_TAG_VALUE = '[empty]'; + +function OHIFCornerstoneSRContentItem(props) { + const { contentItem, nodeIndexesTree, continuityOfContent } = props; + const { ConceptNameCodeSequence } = contentItem; + const { CodeValue, CodeMeaning } = ConceptNameCodeSequence; + const isChildFirstNode = nodeIndexesTree[nodeIndexesTree.length - 1] === 0; + const formattedValue = formatContentItemValue(contentItem) ?? EMPTY_TAG_VALUE; + const startWithAlphaNumCharRegEx = /^[a-zA-Z0-9]/; + const isContinuous = continuityOfContent === 'CONTINUOUS'; + const isFinding = CodeValue === CodeNameCodeSequenceValues.Finding; + const addExtraSpace = + isContinuous && !isChildFirstNode && startWithAlphaNumCharRegEx.test(formattedValue?.[0]); + + // Collapse sequences of white space preserving newline characters + let className = 'whitespace-pre-line'; + + if (CodeValue === CodeNameCodeSequenceValues.Finding) { + // Preserve spaces because it is common to see tabular text in a + // "Findings" ConceptNameCodeSequence + className = 'whitespace-pre-wrap'; + } + + if (isContinuous) { + return ( + <> + + {addExtraSpace ? ' ' : ''} + {formattedValue} + + + ); + } + + return ( + <> +
+ {CodeMeaning}: + {isFinding ? ( +
{formattedValue}
+ ) : ( + {formattedValue} + )} +
+ + ); +} + +OHIFCornerstoneSRContentItem.propTypes = { + contentItem: PropTypes.object, + nodeIndexesTree: PropTypes.arrayOf(PropTypes.number), + continuityOfContent: PropTypes.string, +}; + +export { OHIFCornerstoneSRContentItem }; diff --git a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx new file mode 100644 index 0000000..bd6e7d0 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx @@ -0,0 +1,491 @@ +import PropTypes from 'prop-types'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ExtensionManager, useToolbar } from '@ohif/core'; + +import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule'; + +import { ViewportActionArrows } from '@ohif/ui'; +import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet'; +import { usePositionPresentationStore } from '@ohif/extension-cornerstone'; +import { useViewportGrid } from '@ohif/ui-next'; +import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next'; + +const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking'; + +const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup'; + +function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) { + const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } = + props; + + const { displaySetService, viewportActionCornersService } = servicesManager.services; + + const viewportId = viewportOptions.viewportId; + + // SR viewport will always have a single display set + if (displaySets.length > 1) { + throw new Error('SR viewport should only have a single display set'); + } + + const srDisplaySet = displaySets[0]; + + const { setPositionPresentation } = usePositionPresentationStore(); + + const [viewportGrid, viewportGridService] = useViewportGrid(); + const [measurementSelected, setMeasurementSelected] = useState(0); + const [measurementCount, setMeasurementCount] = useState(1); + const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState(null); + const [referencedDisplaySetMetadata, setReferencedDisplaySetMetadata] = useState(null); + const [element, setElement] = useState(null); + const { viewports, activeViewportId } = viewportGrid; + + const { t } = useTranslation('Common'); + + // Optional hook into tracking extension, if present. + let trackedMeasurements; + + const hasMeasurementTrackingExtension = extensionManager.registeredExtensionIds.includes( + MEASUREMENT_TRACKING_EXTENSION_ID + ); + + if (hasMeasurementTrackingExtension) { + const contextModule = extensionManager.getModuleEntry( + '@ohif/extension-measurement-tracking.contextModule.TrackedMeasurementsContext' + ); + + const tracked = useContext(contextModule.context); + trackedMeasurements = tracked?.[0]; + } + + /** + * Todo: what is this, not sure what it does regarding the react aspect, + * it is updating a local variable? which is not state. + */ + const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0); + /** + * Store the tracking identifiers per viewport in order to be able to + * show the SR measurements on the referenced image on the correct viewport, + * when multiple viewports are used. + */ + const setTrackingIdentifiers = useCallback( + measurementSelected => { + const { measurements } = srDisplaySet; + + setTrackingUniqueIdentifiersForElement( + element, + measurements.map(measurement => measurement.TrackingUniqueIdentifier), + measurementSelected + ); + }, + [element, measurementSelected, srDisplaySet] + ); + + /** + * OnElementEnabled callback which is called after the cornerstoneExtension + * has enabled the element. Note: we delegate all the image rendering to + * cornerstoneExtension, so we don't need to do anything here regarding + * the image rendering, element enabling etc. + */ + const onElementEnabled = evt => { + setElement(evt.detail.element); + }; + + const updateViewport = useCallback( + newMeasurementSelected => { + const { StudyInstanceUID, displaySetInstanceUID, sopClassUids } = srDisplaySet; + + if (!StudyInstanceUID || !displaySetInstanceUID) { + return; + } + + if (sopClassUids && sopClassUids.length > 1) { + // Todo: what happens if there are multiple SOP Classes? Why we are + // not throwing an error? + console.warn('More than one SOPClassUID in the same series is not yet supported.'); + } + + // if (!srDisplaySet.measurements || !srDisplaySet.measurements.length) { + // return; + // } + + _getViewportReferencedDisplaySetData( + srDisplaySet, + newMeasurementSelected, + displaySetService + ).then(({ referencedDisplaySet, referencedDisplaySetMetadata }) => { + if (!referencedDisplaySet || !referencedDisplaySetMetadata) { + return; + } + + setMeasurementSelected(newMeasurementSelected); + + setActiveImageDisplaySetData(referencedDisplaySet); + setReferencedDisplaySetMetadata(referencedDisplaySetMetadata); + + const { presentationIds } = viewportOptions; + const measurement = srDisplaySet.measurements[newMeasurementSelected]; + setPositionPresentation(presentationIds.positionPresentationId, { + viewReference: { + referencedImageId: measurement.imageId, + }, + }); + }); + }, + [dataSource, srDisplaySet, activeImageDisplaySetData, viewportId] + ); + + const getCornerstoneViewport = useCallback(() => { + if (!activeImageDisplaySetData) { + return null; + } + + const { component: Component } = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.viewportModule.cornerstone' + ); + + const { measurements } = srDisplaySet; + const measurement = measurements[measurementSelected]; + + if (!measurement) { + return null; + } + + return ( + { + props.onElementEnabled?.(evt); + onElementEnabled(evt); + }} + isJumpToMeasurementDisabled={true} + > + ); + }, [activeImageDisplaySetData, viewportId, measurementSelected]); + + const onMeasurementChange = useCallback( + direction => { + let newMeasurementSelected = measurementSelected; + + newMeasurementSelected += direction; + if (newMeasurementSelected >= measurementCount) { + newMeasurementSelected = 0; + } else if (newMeasurementSelected < 0) { + newMeasurementSelected = measurementCount - 1; + } + + setTrackingIdentifiers(newMeasurementSelected); + updateViewport(newMeasurementSelected); + }, + [measurementSelected, measurementCount, updateViewport, setTrackingIdentifiers] + ); + + /** + Cleanup the SR viewport when the viewport is destroyed + */ + useEffect(() => { + const onDisplaySetsRemovedSubscription = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SETS_REMOVED, + ({ displaySetInstanceUIDs }) => { + const activeViewport = viewports.get(activeViewportId); + if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) { + viewportGridService.setDisplaySetsForViewport({ + viewportId: activeViewportId, + displaySetInstanceUIDs: [], + }); + } + } + ); + + return () => { + onDisplaySetsRemovedSubscription.unsubscribe(); + }; + }, []); + + /** + * Loading the measurements from the SR viewport, which goes through the + * isHydratable check, the outcome for the isHydrated state here is always FALSE + * since we don't do the hydration here. Todo: can't we just set it as false? why + * we are changing the state here? isHydrated is always false at this stage, and + * if it is hydrated we don't even use the SR viewport. + */ + useEffect(() => { + const loadSR = async () => { + if (!srDisplaySet.isLoaded) { + await srDisplaySet.load(); + } + const numMeasurements = srDisplaySet.measurements.length; + setMeasurementCount(numMeasurements); + updateViewport(measurementSelected); + }; + loadSR(); + }, [srDisplaySet]); + + /** + * Hook to update the tracking identifiers when the selected measurement changes or + * the element changes + */ + useEffect(() => { + const updateSR = async () => { + if (!srDisplaySet.isLoaded) { + await srDisplaySet.load(); + } + if (!element || !srDisplaySet.isLoaded) { + return; + } + setTrackingIdentifiers(measurementSelected); + }; + updateSR(); + }, [measurementSelected, element, setTrackingIdentifiers, srDisplaySet]); + + useEffect(() => { + setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0); + }, [trackedMeasurements]); + + useEffect(() => { + viewportActionCornersService.addComponents([ + { + viewportId, + id: 'viewportStatusComponent', + component: _getStatusComponent({ + srDisplaySet, + viewportId, + isRehydratable: srDisplaySet.isRehydratable, + isLocked, + t, + servicesManager, + }), + indexPriority: -100, + location: viewportActionCornersService.LOCATIONS.topLeft, + }, + { + viewportId, + id: 'viewportActionArrowsComponent', + index: 0, + component: ( + + ), + indexPriority: 0, + location: viewportActionCornersService.LOCATIONS.topRight, + }, + ]); + }, [isLocked, onMeasurementChange, srDisplaySet, t, viewportActionCornersService, viewportId]); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + let childrenWithProps = null; + + if (!activeImageDisplaySetData || !referencedDisplaySetMetadata) { + return null; + } + + if (children && children.length) { + childrenWithProps = children.map((child, index) => { + return ( + child && + React.cloneElement(child, { + viewportId, + key: index, + }) + ); + }); + } + + return ( + <> +
+ {getCornerstoneViewport()} + {childrenWithProps} +
+ + ); +} + +OHIFCornerstoneSRMeasurementViewport.propTypes = { + displaySets: PropTypes.arrayOf(PropTypes.object), + viewportId: PropTypes.string.isRequired, + dataSource: PropTypes.object, + children: PropTypes.node, + viewportLabel: PropTypes.string, + viewportOptions: PropTypes.object, + servicesManager: PropTypes.object.isRequired, + extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired, +}; + +async function _getViewportReferencedDisplaySetData( + displaySet, + measurementSelected, + displaySetService +) { + const { measurements } = displaySet; + const measurement = measurements[measurementSelected]; + + const { displaySetInstanceUID } = measurement; + if (!displaySet.keyImageDisplaySet) { + // Create a new display set, and preserve a reference to it here, + // so that it can be re-displayed and shown inside the SR viewport. + // This is only for ease of redisplay - the display set is stored in the + // usual manner in the display set service. + displaySet.keyImageDisplaySet = createReferencedImageDisplaySet(displaySetService, displaySet); + } + + if (!displaySetInstanceUID) { + return { referencedDisplaySetMetadata: null, referencedDisplaySet: null }; + } + + const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + + const image0 = referencedDisplaySet.images[0]; + const referencedDisplaySetMetadata = { + PatientID: image0.PatientID, + PatientName: image0.PatientName, + PatientSex: image0.PatientSex, + PatientAge: image0.PatientAge, + SliceThickness: image0.SliceThickness, + StudyDate: image0.StudyDate, + SeriesDescription: image0.SeriesDescription, + SeriesInstanceUID: image0.SeriesInstanceUID, + SeriesNumber: image0.SeriesNumber, + ManufacturerModelName: image0.ManufacturerModelName, + SpacingBetweenSlices: image0.SpacingBetweenSlices, + }; + + return { referencedDisplaySetMetadata, referencedDisplaySet }; +} + +function _getStatusComponent({ + srDisplaySet, + viewportId, + isRehydratable, + isLocked, + t, + servicesManager, +}) { + const loadStr = t('LOAD'); + + // 1 - Incompatible + // 2 - Locked + // 3 - Rehydratable / Open + const state = isRehydratable && !isLocked ? 3 : isRehydratable && isLocked ? 2 : 1; + let ToolTipMessage = null; + let StatusIcon = null; + + switch (state) { + case 1: + StatusIcon = () => ; + + ToolTipMessage = () => ( +
+ This structured report is not compatible +
+ with this application. +
+ ); + break; + case 2: + StatusIcon = () => ; + + ToolTipMessage = () => ( +
+ This structured report is currently read-only +
+ because you are tracking measurements in +
+ another viewport. +
+ ); + break; + case 3: + StatusIcon = () => ( + + ); + + ToolTipMessage = () =>
{`Click ${loadStr} to restore measurements.`}
; + } + + const StatusArea = () => { + const { toolbarButtons: loadSRMeasurementsButtons, onInteraction } = useToolbar({ + servicesManager, + buttonSection: 'loadSRMeasurements', + }); + + const commandOptions = { + displaySetInstanceUID: srDisplaySet.displaySetInstanceUID, + viewportId, + }; + + return ( +
+
+ + SR +
+ {state === 3 && ( + <> + {loadSRMeasurementsButtons.map(toolDef => { + if (!toolDef) { + return null; + } + const { id, Component, componentProps } = toolDef; + const tool = ( + onInteraction({ ...args, ...commandOptions })} + {...componentProps} + /> + ); + + return
{tool}
; + })} + + )} +
+ ); + }; + + return ( + <> + {ToolTipMessage && ( + + + + + + + + + + + )} + {!ToolTipMessage && } + + ); +} + +export default OHIFCornerstoneSRMeasurementViewport; diff --git a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRTextViewport.tsx b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRTextViewport.tsx new file mode 100644 index 0000000..78d4929 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRTextViewport.tsx @@ -0,0 +1,32 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { ExtensionManager } from '@ohif/core'; +import { OHIFCornerstoneSRContainer } from './OHIFCornerstoneSRContainer'; + +function OHIFCornerstoneSRTextViewport(props: withAppTypes) { + const { displaySets } = props; + const displaySet = displaySets[0]; + const instance = displaySet.instances[0]; + + return ( +
+
+ {/* The root level is always a container */} + +
+
+ ); +} + +OHIFCornerstoneSRTextViewport.propTypes = { + displaySets: PropTypes.arrayOf(PropTypes.object), + viewportId: PropTypes.string.isRequired, + dataSource: PropTypes.object, + children: PropTypes.node, + viewportLabel: PropTypes.string, + viewportOptions: PropTypes.object, + servicesManager: PropTypes.object.isRequired, + extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired, +}; + +export default OHIFCornerstoneSRTextViewport; diff --git a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRViewport.tsx b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRViewport.tsx new file mode 100644 index 0000000..3bbb73a --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRViewport.tsx @@ -0,0 +1,30 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { ExtensionManager } from '@ohif/core'; + +import OHIFCornerstoneSRMeasurementViewport from './OHIFCornerstoneSRMeasurementViewport'; +import OHIFCornerstoneSRTextViewport from './OHIFCornerstoneSRTextViewport'; + +function OHIFCornerstoneSRViewport(props: withAppTypes) { + const { displaySets } = props; + const { isImagingMeasurementReport } = displaySets[0]; + + if (isImagingMeasurementReport) { + return ; + } + + return ; +} + +OHIFCornerstoneSRViewport.propTypes = { + displaySets: PropTypes.arrayOf(PropTypes.object), + viewportId: PropTypes.string.isRequired, + dataSource: PropTypes.object, + children: PropTypes.node, + viewportLabel: PropTypes.string, + viewportOptions: PropTypes.object, + servicesManager: PropTypes.object.isRequired, + extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired, +}; + +export default OHIFCornerstoneSRViewport; diff --git a/extensions/cornerstone-dicom-sr/src/enums.ts b/extensions/cornerstone-dicom-sr/src/enums.ts new file mode 100644 index 0000000..e53688c --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/enums.ts @@ -0,0 +1,44 @@ +import { adaptersSR } from '@cornerstonejs/adapters'; + +const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D; + +export const SCOORDTypes = { + POINT: 'POINT', + MULTIPOINT: 'MULTIPOINT', + POLYLINE: 'POLYLINE', + CIRCLE: 'CIRCLE', + ELLIPSE: 'ELLIPSE', +}; + +export const CodeNameCodeSequenceValues = { + ImagingMeasurementReport: '126000', + ImageLibrary: '111028', + ImagingMeasurements: '126010', + MeasurementGroup: '125007', + ImageLibraryGroup: '126200', + TrackingUniqueIdentifier: '112040', + TrackingIdentifier: '112039', + Finding: '121071', + FindingSite: 'G-C0E3', // SRT + FindingSiteSCT: '363698007', // SCT +}; + +export const CodingSchemeDesignators = { + SRT: 'SRT', + SCT: 'SCT', + CornerstoneCodeSchemes: [Cornerstone3DCodeScheme.CodingSchemeDesignator, 'CST4'], +}; + +export const RelationshipType = { + INFERRED_FROM: 'INFERRED FROM', + CONTAINS: 'CONTAINS', +}; + +const enums = { + CodeNameCodeSequenceValues, + CodingSchemeDesignators, + RelationshipType, + SCOORDTypes, +}; + +export default enums; diff --git a/extensions/cornerstone-dicom-sr/src/getHangingProtocolModule.ts b/extensions/cornerstone-dicom-sr/src/getHangingProtocolModule.ts new file mode 100644 index 0000000..8d47aca --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/getHangingProtocolModule.ts @@ -0,0 +1,77 @@ +import { Types } from '@ohif/core'; + +const srProtocol: Types.HangingProtocol.Protocol = { + id: '@ohif/sr', + // Don't store this hanging protocol as it applies to the currently active + // display set by default + // cacheId: null, + name: 'SR Key Images', + // Just apply this one when specifically listed + protocolMatchingRules: [], + toolGroupIds: ['default'], + // -1 would be used to indicate active only, whereas other values are + // the number of required priors referenced - so 0 means active with + // 0 or more priors. + numberOfPriorsReferenced: 0, + // Default viewport is used to define the viewport when + // additional viewports are added using the layout tool + defaultViewport: { + viewportOptions: { + viewportType: 'stack', + toolGroupId: 'default', + allowUnmatchedView: true, + }, + displaySets: [ + { + id: 'srDisplaySetId', + matchedDisplaySetsIndex: -1, + }, + ], + }, + displaySetSelectors: { + srDisplaySetId: { + seriesMatchingRules: [ + { + attribute: 'Modality', + constraint: { + equals: 'SR', + }, + }, + ], + }, + }, + stages: [ + { + name: 'SR Key Images', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 1, + }, + }, + viewports: [ + { + viewportOptions: { allowUnmatchedView: true }, + displaySets: [ + { + id: 'srDisplaySetId', + }, + ], + }, + ], + }, + ], +}; + +function getHangingProtocolModule() { + return [ + { + name: srProtocol.id, + protocol: srProtocol, + }, + ]; +} + +export default getHangingProtocolModule; +export { srProtocol }; diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts new file mode 100644 index 0000000..0c67cd0 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts @@ -0,0 +1,724 @@ +import { utils, classes, DisplaySetService, Types } from '@ohif/core'; +import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone'; +import { adaptersSR } from '@cornerstonejs/adapters'; + +import addSRAnnotation from './utils/addSRAnnotation'; +import isRehydratable from './utils/isRehydratable'; +import { + SOPClassHandlerName, + SOPClassHandlerId, + SOPClassHandlerId3D, + SOPClassHandlerName3D, +} from './id'; +import { CodeNameCodeSequenceValues, CodingSchemeDesignators } from './enums'; + +const { sopClassDictionary } = utils; +const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums; +const { ImageSet, MetadataProvider: metadataProvider } = classes; +const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D; + +type InstanceMetadata = Types.InstanceMetadata; + +/** + * TODO + * - [ ] Add SR thumbnail + * - [ ] Make viewport + * - [ ] Get stacks from referenced displayInstanceUID and load into wrapped CornerStone viewport + */ + +const sopClassUids = [ + sopClassDictionary.BasicTextSR, + sopClassDictionary.EnhancedSR, + sopClassDictionary.ComprehensiveSR, +]; + +const validateSameStudyUID = (uid: string, instances): void => { + instances.forEach(it => { + if (it.StudyInstanceUID !== uid) { + console.warn('Not all instances have the same UID', uid, it); + throw new Error(`Instances ${it.SOPInstanceUID} does not belong to ${uid}`); + } + }); +}; + +/** + * Adds instances to the DICOM SR series, rather than creating a new + * series, so that as SR's are saved, they append to the series, and the + * key image display set gets updated as well, containing just the new series. + * @param instances is a list of instances from THIS series that are not + * in this DICOM SR Display Set already. + */ +function addInstances(instances: InstanceMetadata[], displaySetService: DisplaySetService) { + this.instances.push(...instances); + utils.sortStudyInstances(this.instances); + // The last instance is the newest one, so is the one most interesting. + // Eventually, the SR viewer should have the ability to choose which SR + // gets loaded, and to navigate among them. + this.instance = this.instances[this.instances.length - 1]; + this.isLoaded = false; + return this; +} + +/** + * DICOM SR SOP Class Handler + * For all referenced images in the TID 1500/300 sections, add an image to the + * display. + * @param instances is a set of instances all from the same series + * @param servicesManager is the services that can be used for creating + * @returns The list of display sets created for the given instances object + */ +function _getDisplaySetsFromSeries( + instances, + servicesManager: AppTypes.ServicesManager, + extensionManager +) { + // If the series has no instances, stop here + if (!instances || !instances.length) { + throw new Error('No instances were provided'); + } + + utils.sortStudyInstances(instances); + // The last instance is the newest one, so is the one most interesting. + // Eventually, the SR viewer should have the ability to choose which SR + // gets loaded, and to navigate among them. + const instance = instances[instances.length - 1]; + + const { + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + SeriesDescription, + SeriesNumber, + SeriesDate, + ConceptNameCodeSequence, + SOPClassUID, + } = instance; + validateSameStudyUID(instance.StudyInstanceUID, instances); + + const is3DSR = SOPClassUID === sopClassDictionary.Comprehensive3DSR; + + const isImagingMeasurementReport = + ConceptNameCodeSequence?.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurementReport; + + const displaySet = { + Modality: 'SR', + displaySetInstanceUID: utils.guid(), + SeriesDescription, + SeriesNumber, + SeriesDate, + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + SOPClassHandlerId: is3DSR ? SOPClassHandlerId3D : SOPClassHandlerId, + SOPClassUID, + instances, + referencedImages: null, + measurements: null, + isDerivedDisplaySet: true, + isLoaded: false, + isImagingMeasurementReport, + sopClassUids, + instance, + addInstances, + }; + + displaySet.load = () => _load(displaySet, servicesManager, extensionManager); + + return [displaySet]; +} + +/** + * Loads the display set with the given services and extension manager. + * @param srDisplaySet - The display set to load. + * @param servicesManager - The services manager containing displaySetService and measurementService. + * @param extensionManager - The extension manager containing data sources. + */ +async function _load( + srDisplaySet: Types.DisplaySet, + servicesManager: AppTypes.ServicesManager, + extensionManager: AppTypes.ExtensionManager +) { + const { displaySetService, measurementService } = servicesManager.services; + const dataSources = extensionManager.getDataSources(); + const dataSource = dataSources[0]; + const { ContentSequence } = srDisplaySet.instance; + + async function retrieveBulkData(obj, parentObj = null, key = null) { + for (const prop in obj) { + if (typeof obj[prop] === 'object' && obj[prop] !== null) { + await retrieveBulkData(obj[prop], obj, prop); + } else if (Array.isArray(obj[prop])) { + await Promise.all(obj[prop].map(item => retrieveBulkData(item, obj, prop))); + } else if (prop === 'BulkDataURI') { + const value = await dataSource.retrieve.bulkDataURI({ + BulkDataURI: obj[prop], + StudyInstanceUID: srDisplaySet.instance.StudyInstanceUID, + SeriesInstanceUID: srDisplaySet.instance.SeriesInstanceUID, + SOPInstanceUID: srDisplaySet.instance.SOPInstanceUID, + }); + if (parentObj && key) { + parentObj[key] = new Float32Array(value); + } + } + } + } + + if (srDisplaySet.isLoaded !== true) { + await retrieveBulkData(ContentSequence); + } + + if (srDisplaySet.isImagingMeasurementReport) { + srDisplaySet.referencedImages = _getReferencedImagesList(ContentSequence); + srDisplaySet.measurements = _getMeasurements(ContentSequence); + } else { + srDisplaySet.referencedImages = []; + srDisplaySet.measurements = []; + } + + const mappings = measurementService.getSourceMappings( + CORNERSTONE_3D_TOOLS_SOURCE_NAME, + CORNERSTONE_3D_TOOLS_SOURCE_VERSION + ); + + srDisplaySet.isHydrated = false; + srDisplaySet.isRehydratable = isRehydratable(srDisplaySet, mappings); + srDisplaySet.isLoaded = true; + + /** Check currently added displaySets and add measurements if the sources exist */ + displaySetService.activeDisplaySets.forEach(activeDisplaySet => { + _checkIfCanAddMeasurementsToDisplaySet( + srDisplaySet, + activeDisplaySet, + dataSource, + servicesManager + ); + }); + + /** Subscribe to new displaySets as the source may come in after */ + displaySetService.subscribe(displaySetService.EVENTS.DISPLAY_SETS_ADDED, data => { + const { displaySetsAdded } = data; + /** + * If there are still some measurements that have not yet been loaded into cornerstone, + * See if we can load them onto any of the new displaySets. + */ + displaySetsAdded.forEach(newDisplaySet => { + _checkIfCanAddMeasurementsToDisplaySet( + srDisplaySet, + newDisplaySet, + dataSource, + servicesManager + ); + }); + }); +} + +/** + * Checks if measurements can be added to a display set. + * + * @param srDisplaySet - The source display set containing measurements. + * @param newDisplaySet - The new display set to check if measurements can be added. + * @param dataSource - The data source used to retrieve image IDs. + * @param servicesManager - The services manager. + */ +function _checkIfCanAddMeasurementsToDisplaySet( + srDisplaySet, + newDisplaySet, + dataSource, + servicesManager: AppTypes.ServicesManager +) { + const { customizationService } = servicesManager.services; + + const unloadedMeasurements = srDisplaySet.measurements.filter( + measurement => measurement.loaded === false + ); + + if ( + unloadedMeasurements.length === 0 || + !(newDisplaySet instanceof ImageSet) || + newDisplaySet.unsupported + ) { + return; + } + + // const { sopClassUids } = newDisplaySet; + // Create a Set for faster lookups + // const sopClassUidSet = new Set(sopClassUids); + + // Create a Map to efficiently look up ImageIds by SOPInstanceUID and frame number + const imageIdMap = new Map(); + const imageIds = dataSource.getImageIdsForDisplaySet(newDisplaySet); + + for (const imageId of imageIds) { + const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(imageId); + const key = `${SOPInstanceUID}:${frameNumber || 1}`; + imageIdMap.set(key, imageId); + } + + if (!unloadedMeasurements?.length) { + return; + } + + const is3DSR = srDisplaySet.SOPClassUID === sopClassDictionary.Comprehensive3DSR; + + for (let j = unloadedMeasurements.length - 1; j >= 0; j--) { + let measurement = unloadedMeasurements[j]; + + const onBeforeSRAddMeasurement = customizationService.getCustomization( + 'onBeforeSRAddMeasurement' + ); + + if (typeof onBeforeSRAddMeasurement === 'function') { + measurement = onBeforeSRAddMeasurement({ + measurement, + StudyInstanceUID: srDisplaySet.StudyInstanceUID, + SeriesInstanceUID: srDisplaySet.SeriesInstanceUID, + }); + } + + // if it is 3d SR we can just add the SR annotation + if (is3DSR) { + addSRAnnotation(measurement, null, null); + measurement.loaded = true; + continue; + } + + const referencedSOPSequence = measurement.coords[0].ReferencedSOPSequence; + if (!referencedSOPSequence) { + continue; + } + + const { ReferencedSOPInstanceUID } = referencedSOPSequence; + const frame = referencedSOPSequence.ReferencedFrameNumber || 1; + const key = `${ReferencedSOPInstanceUID}:${frame}`; + const imageId = imageIdMap.get(key); + + if ( + imageId && + _measurementReferencesSOPInstanceUID(measurement, ReferencedSOPInstanceUID, frame) + ) { + addSRAnnotation(measurement, imageId, frame); + + // Update measurement properties + measurement.loaded = true; + measurement.imageId = imageId; + measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID; + measurement.ReferencedSOPInstanceUID = ReferencedSOPInstanceUID; + measurement.frameNumber = frame; + + unloadedMeasurements.splice(j, 1); + } + } +} + +/** + * Checks if a measurement references a specific SOP Instance UID. + * @param measurement - The measurement object. + * @param SOPInstanceUID - The SOP Instance UID to check against. + * @param frameNumber - The frame number to check against (optional). + * @returns True if the measurement references the specified SOP Instance UID, false otherwise. + */ +function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber) { + const { coords } = measurement; + + /** + * NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM + * Standard. But for now, we will support only one ReferenceFrameNumber. + */ + const ReferencedFrameNumber = + (measurement.coords[0].ReferencedSOPSequence && + measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) || + 1; + + if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) { + return false; + } + + for (let j = 0; j < coords.length; j++) { + const coord = coords[j]; + const { ReferencedSOPInstanceUID } = coord.ReferencedSOPSequence; + if (ReferencedSOPInstanceUID === SOPInstanceUID) { + return true; + } + } + + return false; +} + +/** + * Retrieves the SOP class handler module. + * + * @param {Object} options - The options for retrieving the SOP class handler module. + * @param {Object} options.servicesManager - The services manager. + * @param {Object} options.extensionManager - The extension manager. + * @returns {Array} An array containing the SOP class handler module. + */ +function getSopClassHandlerModule({ servicesManager, extensionManager }) { + const getDisplaySetsFromSeries = instances => { + return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager); + }; + return [ + { + name: SOPClassHandlerName, + sopClassUids, + getDisplaySetsFromSeries, + }, + { + name: SOPClassHandlerName3D, + sopClassUids: [sopClassDictionary.Comprehensive3DSR], + getDisplaySetsFromSeries, + }, + ]; +} + +/** + * Retrieves the measurements from the ImagingMeasurementReportContentSequence. + * + * @param {Array} ImagingMeasurementReportContentSequence - The ImagingMeasurementReportContentSequence array. + * @returns {Array} - The array of measurements. + */ +function _getMeasurements(ImagingMeasurementReportContentSequence) { + const ImagingMeasurements = ImagingMeasurementReportContentSequence.find( + item => + item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurements + ); + + if (!ImagingMeasurements) { + return []; + } + + const MeasurementGroups = _getSequenceAsArray(ImagingMeasurements.ContentSequence).filter( + item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.MeasurementGroup + ); + + const mergedContentSequencesByTrackingUniqueIdentifiers = + _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups); + const measurements = []; + + Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach( + trackingUniqueIdentifier => { + const mergedContentSequence = + mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier]; + + const measurement = _processMeasurement(mergedContentSequence); + if (measurement) { + measurements.push(measurement); + } + } + ); + + return measurements; +} + +/** + * Retrieves merged content sequences by tracking unique identifiers. + * + * @param {Array} MeasurementGroups - The measurement groups. + * @returns {Object} - The merged content sequences by tracking unique identifiers. + */ +function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups) { + const mergedContentSequencesByTrackingUniqueIdentifiers = {}; + + MeasurementGroups.forEach(MeasurementGroup => { + const ContentSequence = _getSequenceAsArray(MeasurementGroup.ContentSequence); + + const TrackingUniqueIdentifierItem = ContentSequence.find( + item => + item.ConceptNameCodeSequence.CodeValue === + CodeNameCodeSequenceValues.TrackingUniqueIdentifier + ); + if (!TrackingUniqueIdentifierItem) { + console.warn('No Tracking Unique Identifier, skipping ambiguous measurement.'); + } + + const trackingUniqueIdentifier = TrackingUniqueIdentifierItem.UID; + + if (mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier] === undefined) { + // Add the full ContentSequence + mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier] = [ + ...ContentSequence, + ]; + } else { + // Add the ContentSequence minus the tracking identifier, as we have this + // Information in the merged ContentSequence anyway. + ContentSequence.forEach(item => { + if ( + item.ConceptNameCodeSequence.CodeValue !== + CodeNameCodeSequenceValues.TrackingUniqueIdentifier + ) { + mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier].push(item); + } + }); + } + }); + + return mergedContentSequencesByTrackingUniqueIdentifiers; +} + +/** + * Processes the measurement based on the merged content sequence. + * If the merged content sequence contains SCOORD or SCOORD3D value types, + * it calls the _processTID1410Measurement function. + * Otherwise, it calls the _processNonGeometricallyDefinedMeasurement function. + * + * @param {Array} mergedContentSequence - The merged content sequence to process. + * @returns {any} - The processed measurement result. + */ +function _processMeasurement(mergedContentSequence) { + if ( + mergedContentSequence.some( + group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D' + ) + ) { + return _processTID1410Measurement(mergedContentSequence); + } + + return _processNonGeometricallyDefinedMeasurement(mergedContentSequence); +} + +/** + * Processes TID 1410 style measurements from the mergedContentSequence. + * TID 1410 style measurements have a SCOORD or SCOORD3D at the top level, + * and non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D. + * + * @param mergedContentSequence - The merged content sequence containing the measurements. + * @returns The measurement object containing the loaded status, labels, coordinates, tracking unique identifier, and tracking identifier. + */ +function _processTID1410Measurement(mergedContentSequence) { + // Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level, + // And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D + + const graphicItem = mergedContentSequence.find( + group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D' + ); + + const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF'); + + const TrackingIdentifierContentItem = mergedContentSequence.find( + item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.TrackingIdentifier + ); + + if (!graphicItem) { + console.warn( + `graphic ValueType ${graphicItem.ValueType} not currently supported, skipping annotation.` + ); + return; + } + + const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM'); + + const measurement = { + loaded: false, + labels: [], + coords: [_getCoordsFromSCOORDOrSCOORD3D(graphicItem)], + TrackingUniqueIdentifier: UIDREFContentItem.UID, + TrackingIdentifier: TrackingIdentifierContentItem.TextValue, + }; + + NUMContentItems.forEach(item => { + const { ConceptNameCodeSequence, MeasuredValueSequence } = item; + if (MeasuredValueSequence) { + measurement.labels.push( + _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence) + ); + } + }); + + const findingSites = mergedContentSequence.filter( + item => + item.ConceptNameCodeSequence.CodingSchemeDesignator === CodingSchemeDesignators.SCT && + item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSiteSCT + ); + if (findingSites.length) { + measurement.labels.push({ + label: CodeNameCodeSequenceValues.FindingSiteSCT, + value: findingSites[0].ConceptCodeSequence.CodeMeaning, + }); + } + + return measurement; +} + +/** + * Processes the non-geometrically defined measurement from the merged content sequence. + * + * @param mergedContentSequence The merged content sequence containing the measurement data. + * @returns The processed measurement object. + */ +function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { + const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM'); + const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF'); + + const TrackingIdentifierContentItem = mergedContentSequence.find( + item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.TrackingIdentifier + ); + + const finding = mergedContentSequence.find( + item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.Finding + ); + + const findingSites = mergedContentSequence.filter( + item => + item.ConceptNameCodeSequence.CodingSchemeDesignator === CodingSchemeDesignators.SRT && + item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSite + ); + + const measurement = { + loaded: false, + labels: [], + coords: [], + TrackingUniqueIdentifier: UIDREFContentItem.UID, + TrackingIdentifier: TrackingIdentifierContentItem.TextValue, + }; + + if ( + finding && + CodingSchemeDesignators.CornerstoneCodeSchemes.includes( + finding.ConceptCodeSequence.CodingSchemeDesignator + ) && + finding.ConceptCodeSequence.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT + ) { + measurement.labels.push({ + label: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, + value: finding.ConceptCodeSequence.CodeMeaning, + }); + } + + // TODO -> Eventually hopefully support SNOMED or some proper code library, just free text for now. + if (findingSites.length) { + const cornerstoneFreeTextFindingSite = findingSites.find( + FindingSite => + CodingSchemeDesignators.CornerstoneCodeSchemes.includes( + FindingSite.ConceptCodeSequence.CodingSchemeDesignator + ) && + FindingSite.ConceptCodeSequence.CodeValue === + Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT + ); + + if (cornerstoneFreeTextFindingSite) { + measurement.labels.push({ + label: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, + value: cornerstoneFreeTextFindingSite.ConceptCodeSequence.CodeMeaning, + }); + } + } + + NUMContentItems.forEach(item => { + const { ConceptNameCodeSequence, ContentSequence, MeasuredValueSequence } = item; + + const { ValueType } = ContentSequence; + if (!ValueType === 'SCOORD') { + console.warn(`Graphic ${ValueType} not currently supported, skipping annotation.`); + return; + } + + const coords = _getCoordsFromSCOORDOrSCOORD3D(ContentSequence); + if (coords) { + measurement.coords.push(coords); + } + + if (MeasuredValueSequence) { + measurement.labels.push( + _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence) + ); + } + }); + + return measurement; +} + +/** + * Extracts coordinates from a graphic item of type SCOORD or SCOORD3D. + * @param {object} graphicItem - The graphic item containing the coordinates. + * @returns {object} - The extracted coordinates. + */ +const _getCoordsFromSCOORDOrSCOORD3D = graphicItem => { + const { ValueType, GraphicType, GraphicData } = graphicItem; + const coords = { ValueType, GraphicType, GraphicData }; + coords.ReferencedSOPSequence = graphicItem.ContentSequence?.ReferencedSOPSequence; + coords.ReferencedFrameOfReferenceSequence = + graphicItem.ReferencedFrameOfReferenceUID || + graphicItem.ContentSequence?.ReferencedFrameOfReferenceSequence; + return coords; +}; + +/** + * Retrieves the label and value from the provided ConceptNameCodeSequence and MeasuredValueSequence. + * @param {Object} ConceptNameCodeSequence - The ConceptNameCodeSequence object. + * @param {Object} MeasuredValueSequence - The MeasuredValueSequence object. + * @returns {Object} - An object containing the label and value. + * The label represents the CodeMeaning from the ConceptNameCodeSequence. + * The value represents the formatted NumericValue and CodeValue from the MeasuredValueSequence. + * Example: { label: 'Long Axis', value: '31.00 mm' } + */ +function _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence) { + const { CodeMeaning } = ConceptNameCodeSequence; + const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence; + const { CodeValue } = MeasurementUnitsCodeSequence; + const formatedNumericValue = NumericValue ? Number(NumericValue).toFixed(2) : ''; + return { + label: CodeMeaning, + value: `${formatedNumericValue} ${CodeValue}`, + }; // E.g. Long Axis: 31.0 mm +} + +/** + * Retrieves a list of referenced images from the Imaging Measurement Report Content Sequence. + * + * @param {Array} ImagingMeasurementReportContentSequence - The Imaging Measurement Report Content Sequence. + * @returns {Array} - The list of referenced images. + */ +function _getReferencedImagesList(ImagingMeasurementReportContentSequence) { + const ImageLibrary = ImagingMeasurementReportContentSequence.find( + item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibrary + ); + + if (!ImageLibrary) { + return []; + } + + const ImageLibraryGroup = _getSequenceAsArray(ImageLibrary.ContentSequence).find( + item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibraryGroup + ); + if (!ImageLibraryGroup) { + return []; + } + + const referencedImages = []; + + _getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => { + const { ReferencedSOPSequence } = item; + if (!ReferencedSOPSequence) { + return; + } + for (const ref of _getSequenceAsArray(ReferencedSOPSequence)) { + if (ref.ReferencedSOPClassUID) { + const { ReferencedSOPClassUID, ReferencedSOPInstanceUID } = ref; + + referencedImages.push({ + ReferencedSOPClassUID, + ReferencedSOPInstanceUID, + }); + } + } + }); + + return referencedImages; +} + +/** + * Converts a DICOM sequence to an array. + * If the sequence is null or undefined, an empty array is returned. + * If the sequence is already an array, it is returned as is. + * Otherwise, the sequence is wrapped in an array and returned. + * + * @param {any} sequence - The DICOM sequence to convert. + * @returns {any[]} - The converted array. + */ +function _getSequenceAsArray(sequence) { + if (!sequence) { + return []; + } + return Array.isArray(sequence) ? sequence : [sequence]; +} + +export default getSopClassHandlerModule; diff --git a/extensions/cornerstone-dicom-sr/src/id.js b/extensions/cornerstone-dicom-sr/src/id.js new file mode 100644 index 0000000..f670697 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/id.js @@ -0,0 +1,11 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +const SOPClassHandlerName = 'dicom-sr'; +const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`; + +const SOPClassHandlerName3D = 'dicom-sr-3d'; +const SOPClassHandlerId3D = `${id}.sopClassHandlerModule.${SOPClassHandlerName3D}`; + +export { SOPClassHandlerName, SOPClassHandlerId, SOPClassHandlerName3D, SOPClassHandlerId3D, id }; diff --git a/extensions/cornerstone-dicom-sr/src/index.tsx b/extensions/cornerstone-dicom-sr/src/index.tsx new file mode 100644 index 0000000..b6ca6ef --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/index.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import getSopClassHandlerModule from './getSopClassHandlerModule'; +import { srProtocol } from './getHangingProtocolModule'; +import onModeEnter from './onModeEnter'; +import getCommandsModule from './commandsModule'; +import preRegistration from './init'; +import { id } from './id.js'; +import toolNames from './tools/toolNames'; +import hydrateStructuredReport from './utils/hydrateStructuredReport'; +import createReferencedImageDisplaySet from './utils/createReferencedImageDisplaySet'; +import Enums from './enums'; + +const Component = React.lazy(() => { + return import(/* webpackPrefetch: true */ './components/OHIFCornerstoneSRViewport'); +}); + +const OHIFCornerstoneSRViewport = props => { + return ( + Loading...}> + + + ); +}; + +/** + * + */ +const dicomSRExtension = { + /** + * Only required property. Should be a unique value across all extensions. + */ + id, + + onModeEnter, + + preRegistration, + + /** + * + * + * @param {object} [configuration={}] + * @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools` + */ + getViewportModule({ servicesManager, extensionManager }) { + const ExtendedOHIFCornerstoneSRViewport = props => { + return ( + + ); + }; + + return [{ name: 'dicom-sr', component: ExtendedOHIFCornerstoneSRViewport }]; + }, + getCommandsModule, + getSopClassHandlerModule, + // Include dynamically computed values such as toolNames not known till instantiation + getUtilityModule({ servicesManager }) { + return [ + { + name: 'tools', + exports: { + toolNames, + }, + }, + ]; + }, +}; + +export default dicomSRExtension; + +// Put static exports here so they can be type checked +export { hydrateStructuredReport, createReferencedImageDisplaySet, srProtocol, Enums, toolNames }; diff --git a/extensions/cornerstone-dicom-sr/src/init.ts b/extensions/cornerstone-dicom-sr/src/init.ts new file mode 100644 index 0000000..c2e4ff3 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/init.ts @@ -0,0 +1,104 @@ +import { + AngleTool, + annotation, + ArrowAnnotateTool, + BidirectionalTool, + CobbAngleTool, + EllipticalROITool, + CircleROITool, + LengthTool, + PlanarFreehandROITool, + RectangleROITool, + utilities as csToolsUtils, +} from '@cornerstonejs/tools'; +import { Types, MeasurementService } from '@ohif/core'; +import { StackViewport, utilities as csUtils } from '@cornerstonejs/core'; +import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone'; +import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool'; +import SCOORD3DPointTool from './tools/SCOORD3DPointTool'; +import SRSCOOR3DProbeMapper from './utils/SRSCOOR3DProbeMapper'; +import addToolInstance from './utils/addToolInstance'; +import toolNames from './tools/toolNames'; + +const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums; + +/** + * @param {object} configuration + */ +export default function init({ + configuration = {}, + servicesManager, +}: Types.Extensions.ExtensionParams): void { + const { measurementService, cornerstoneViewportService } = servicesManager.services; + + addToolInstance(toolNames.DICOMSRDisplay, DICOMSRDisplayTool); + addToolInstance(toolNames.SRLength, LengthTool); + addToolInstance(toolNames.SRBidirectional, BidirectionalTool); + addToolInstance(toolNames.SREllipticalROI, EllipticalROITool); + addToolInstance(toolNames.SRCircleROI, CircleROITool); + addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool); + addToolInstance(toolNames.SRAngle, AngleTool); + addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool); + addToolInstance(toolNames.SRRectangleROI, RectangleROITool); + addToolInstance(toolNames.SRSCOORD3DPoint, SCOORD3DPointTool); + + // TODO - fix the SR display of Cobb Angle, as it joins the two lines + addToolInstance(toolNames.SRCobbAngle, CobbAngleTool); + + const csTools3DVer1MeasurementSource = measurementService.getSource( + CORNERSTONE_3D_TOOLS_SOURCE_NAME, + CORNERSTONE_3D_TOOLS_SOURCE_VERSION + ); + + const { POINT } = measurementService.VALUE_TYPES; + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'SRSCOORD3DPoint', + POINT, + SRSCOOR3DProbeMapper.toAnnotation, + SRSCOOR3DProbeMapper.toMeasurement + ); + + // Modify annotation tools to use dashed lines on SR + const dashedLine = { + lineDash: '4,4', + }; + annotation.config.style.setToolGroupToolStyles('SRToolGroup', { + [toolNames.DICOMSRDisplay]: dashedLine, + SRLength: dashedLine, + SRBidirectional: dashedLine, + SREllipticalROI: dashedLine, + SRCircleROI: dashedLine, + SRArrowAnnotate: dashedLine, + SRCobbAngle: dashedLine, + SRAngle: dashedLine, + SRPlanarFreehandROI: dashedLine, + SRRectangleROI: dashedLine, + global: {}, + }); + + measurementService.subscribe( + MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_LAYOUT, + ({ viewportId, measurement, isConsumed }) => { + if (isConsumed) { + return; + } + try { + const currentViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + const { viewPlaneNormal } = currentViewport.getCamera(); + const referencedImageId = csToolsUtils.getClosestImageIdForStackViewport( + currentViewport as StackViewport, + measurement.points[0], + viewPlaneNormal + ); + const imageIndex = (currentViewport as StackViewport) + .getImageIds() + .indexOf(referencedImageId); + csUtils.jumpToSlice(currentViewport.element, { imageIndex }); + } catch (error) { + console.warn('Unable to jump to image based on measurement coordinate', error); + } + } + ); +} diff --git a/extensions/cornerstone-dicom-sr/src/onModeEnter.tsx b/extensions/cornerstone-dicom-sr/src/onModeEnter.tsx new file mode 100644 index 0000000..603e46f --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/onModeEnter.tsx @@ -0,0 +1,39 @@ +import React from 'react'; + +import { SOPClassHandlerId, SOPClassHandlerId3D } from './id'; +import { ViewportActionButton } from '@ohif/ui'; +import i18n from '@ohif/i18n'; + +export default function onModeEnter({ servicesManager }) { + const { displaySetService, toolbarService } = servicesManager.services; + const displaySetCache = displaySetService.getDisplaySetCache(); + + const srDisplaySets = [...displaySetCache.values()].filter( + ds => ds.SOPClassHandlerId === SOPClassHandlerId || ds.SOPClassHandlerId === SOPClassHandlerId3D + ); + + srDisplaySets.forEach(ds => { + // New mode route, allow SRs to be hydrated again + ds.isHydrated = false; + }); + + toolbarService.addButtons([ + { + // A base/default button for loading measurements. It is added to the toolbar below. + // Customizations to this button can be made in the mode or by another extension. + // For example, the button label can be changed and/or the command to clear + // the measurements can be dropped. + id: 'loadSRMeasurements', + component: props => ( + {i18n.t('Common:LOAD')} + ), + props: { + commands: ['clearMeasurements', 'loadSRMeasurements'], + }, + }, + ]); + + // The toolbar used in the viewport's status bar. Modes and extensions can further customize + // it to optionally add other buttons. + toolbarService.createButtonSection('loadSRMeasurements', ['loadSRMeasurements']); +} diff --git a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts new file mode 100644 index 0000000..60d1c47 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts @@ -0,0 +1,407 @@ +import { Types, metaData, utilities as csUtils } from '@cornerstonejs/core'; +import { + AnnotationTool, + annotation, + drawing, + utilities, + Types as cs3DToolsTypes, +} from '@cornerstonejs/tools'; +import { getTrackingUniqueIdentifiersForElement } from './modules/dicomSRModule'; +import { SCOORDTypes } from '../enums'; +import toolNames from './toolNames'; + +export default class DICOMSRDisplayTool extends AnnotationTool { + static toolName = toolNames.DICOMSRDisplay; + + constructor( + toolProps = {}, + defaultToolProps = { + configuration: {}, + } + ) { + super(toolProps, defaultToolProps); + } + + _getTextBoxLinesFromLabels(labels) { + // TODO -> max 5 for now (label + shortAxis + longAxis), need a generic solution for this! + + const labelLength = Math.min(labels.length, 5); + const lines = []; + + for (let i = 0; i < labelLength; i++) { + const labelEntry = labels[i]; + lines.push(`${_labelToShorthand(labelEntry.label)}: ${labelEntry.value}`); + } + + return lines; + } + + // This tool should not inherit from AnnotationTool and we should not need + // to add the following lines. + isPointNearTool = () => null; + getHandleNearImagePoint = () => null; + + renderAnnotation = (enabledElement: Types.IEnabledElement, svgDrawingHelper: any): void => { + const { viewport } = enabledElement; + const { element } = viewport; + + let annotations = annotation.state.getAnnotations(this.getToolName(), element); + + // Todo: We don't need this anymore, filtering happens in triggerAnnotationRender + if (!annotations?.length) { + return; + } + + annotations = this.filterInteractableAnnotationsForElement(element, annotations); + + if (!annotations?.length) { + return; + } + + const trackingUniqueIdentifiersForElement = getTrackingUniqueIdentifiersForElement(element); + + const { activeIndex, trackingUniqueIdentifiers } = trackingUniqueIdentifiersForElement; + + const activeTrackingUniqueIdentifier = trackingUniqueIdentifiers[activeIndex]; + + // Filter toolData to only render the data for the active SR. + const filteredAnnotations = annotations.filter(annotation => + trackingUniqueIdentifiers.includes(annotation.data?.TrackingUniqueIdentifier) + ); + + if (!viewport._actors?.size) { + return; + } + + const styleSpecifier: cs3DToolsTypes.AnnotationStyle.StyleSpecifier = { + toolGroupId: this.toolGroupId, + toolName: this.getToolName(), + viewportId: enabledElement.viewport.id, + }; + const { style: annotationStyle } = annotation.config; + + for (let i = 0; i < filteredAnnotations.length; i++) { + const annotation = filteredAnnotations[i]; + const annotationUID = annotation.annotationUID; + const { renderableData, TrackingUniqueIdentifier } = annotation.data; + const { referencedImageId } = annotation.metadata; + + styleSpecifier.annotationUID = annotationUID; + + const groupStyle = annotationStyle.getToolGroupToolStyles(this.toolGroupId)[ + this.getToolName() + ]; + + const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation); + const lineDash = this.getStyle('lineDash', styleSpecifier, annotation); + const color = + TrackingUniqueIdentifier === activeTrackingUniqueIdentifier + ? 'rgb(0, 255, 0)' + : this.getStyle('color', styleSpecifier, annotation); + + const options = { + color, + lineDash, + lineWidth, + ...groupStyle, + }; + + Object.keys(renderableData).forEach(GraphicType => { + const renderableDataForGraphicType = renderableData[GraphicType]; + + let renderMethod; + let canvasCoordinatesAdapter; + + switch (GraphicType) { + case SCOORDTypes.POINT: + renderMethod = this.renderPoint; + break; + case SCOORDTypes.MULTIPOINT: + renderMethod = this.renderMultipoint; + break; + case SCOORDTypes.POLYLINE: + renderMethod = this.renderPolyLine; + break; + case SCOORDTypes.CIRCLE: + renderMethod = this.renderEllipse; + break; + case SCOORDTypes.ELLIPSE: + renderMethod = this.renderEllipse; + canvasCoordinatesAdapter = utilities.math.ellipse.getCanvasEllipseCorners; + break; + default: + throw new Error(`Unsupported GraphicType: ${GraphicType}`); + } + + const canvasCoordinates = renderMethod( + svgDrawingHelper, + viewport, + renderableDataForGraphicType, + annotationUID, + referencedImageId, + options + ); + + this.renderTextBox( + svgDrawingHelper, + viewport, + canvasCoordinates, + canvasCoordinatesAdapter, + annotation, + styleSpecifier, + options + ); + }); + } + }; + + renderPolyLine( + svgDrawingHelper, + viewport, + renderableData, + annotationUID, + referencedImageId, + options + ) { + const drawingOptions = { + color: options.color, + width: options.lineWidth, + lineDash: options.lineDash, + }; + let allCanvasCoordinates = []; + renderableData.map((data, index) => { + const canvasCoordinates = data.map(p => viewport.worldToCanvas(p)); + const lineUID = `${index}`; + + if (canvasCoordinates.length === 2) { + drawing.drawLine( + svgDrawingHelper, + annotationUID, + lineUID, + canvasCoordinates[0], + canvasCoordinates[1], + drawingOptions + ); + } else { + drawing.drawPolyline( + svgDrawingHelper, + annotationUID, + lineUID, + canvasCoordinates, + drawingOptions + ); + } + + allCanvasCoordinates = allCanvasCoordinates.concat(canvasCoordinates); + }); + + return allCanvasCoordinates; // used for drawing textBox + } + + renderMultipoint( + svgDrawingHelper, + viewport, + renderableData, + annotationUID, + referencedImageId, + options + ) { + let canvasCoordinates; + renderableData.map((data, index) => { + canvasCoordinates = data.map(p => viewport.worldToCanvas(p)); + const handleGroupUID = '0'; + drawing.drawHandles(svgDrawingHelper, annotationUID, handleGroupUID, canvasCoordinates, { + color: options.color, + }); + }); + } + + renderPoint( + svgDrawingHelper, + viewport, + renderableData, + annotationUID, + referencedImageId, + options + ) { + const canvasCoordinates = []; + renderableData.map((data, index) => { + const point = data[0]; + // This gives us one point for arrow + canvasCoordinates.push(viewport.worldToCanvas(point)); + + if (data[1] !== undefined) { + canvasCoordinates.push(viewport.worldToCanvas(data[1])); + } + else{ + // We get the other point for the arrow by using the image size + const imagePixelModule = metaData.get('imagePixelModule', referencedImageId); + + let xOffset = 10; + let yOffset = 10; + + if (imagePixelModule) { + const { columns, rows } = imagePixelModule; + xOffset = columns / 10; + yOffset = rows / 10; + } + + const imagePoint = csUtils.worldToImageCoords(referencedImageId, point); + const arrowEnd = csUtils.imageToWorldCoords(referencedImageId, [ + imagePoint[0] + xOffset, + imagePoint[1] + yOffset, + ]); + + canvasCoordinates.push(viewport.worldToCanvas(arrowEnd)); + + } + + + const arrowUID = `${index}`; + + // Todo: handle drawing probe as probe, currently we are drawing it as an arrow + drawing.drawArrow( + svgDrawingHelper, + annotationUID, + arrowUID, + canvasCoordinates[1], + canvasCoordinates[0], + { + color: options.color, + width: options.lineWidth, + } + ); + }); + + return canvasCoordinates; // used for drawing textBox + } + + renderEllipse( + svgDrawingHelper, + viewport, + renderableData, + annotationUID, + referencedImageId, + options + ) { + let canvasCoordinates; + renderableData.map((data, index) => { + if (data.length === 0) { + // since oblique ellipse is not supported for hydration right now + // we just return + return; + } + + const ellipsePointsWorld = data; + + const rotation = viewport.getRotation(); + + canvasCoordinates = ellipsePointsWorld.map(p => viewport.worldToCanvas(p)); + let canvasCorners; + if (rotation == 90 || rotation == 270) { + canvasCorners = utilities.math.ellipse.getCanvasEllipseCorners([ + canvasCoordinates[2], + canvasCoordinates[3], + canvasCoordinates[0], + canvasCoordinates[1], + ]) as Array; + } else { + canvasCorners = utilities.math.ellipse.getCanvasEllipseCorners( + canvasCoordinates + ) as Array; + } + + const lineUID = `${index}`; + drawing.drawEllipse( + svgDrawingHelper, + annotationUID, + lineUID, + canvasCorners[0], + canvasCorners[1], + { + color: options.color, + width: options.lineWidth, + lineDash: options.lineDash, + } + ); + }); + + return canvasCoordinates; + } + + renderTextBox( + svgDrawingHelper, + viewport, + canvasCoordinates, + canvasCoordinatesAdapter, + annotation, + styleSpecifier, + options = {} + ) { + if (!canvasCoordinates || !annotation) { + return; + } + + const { annotationUID, data = {} } = annotation; + const { labels } = data; + const { color } = options; + + let adaptedCanvasCoordinates = canvasCoordinates; + // adapt coordinates if there is an adapter + if (typeof canvasCoordinatesAdapter === 'function') { + adaptedCanvasCoordinates = canvasCoordinatesAdapter(canvasCoordinates); + } + const textLines = this._getTextBoxLinesFromLabels(labels); + const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates); + + if (!annotation.data?.handles?.textBox?.worldPosition) { + annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords); + } + + const textBoxPosition = viewport.worldToCanvas(annotation.data.handles.textBox.worldPosition); + + const textBoxUID = '1'; + const textBoxOptions = this.getLinkedTextBoxStyle(styleSpecifier, annotation); + + const boundingBox = drawing.drawLinkedTextBox( + svgDrawingHelper, + annotationUID, + textBoxUID, + textLines, + textBoxPosition, + canvasCoordinates, + {}, + { + ...textBoxOptions, + color, + } + ); + + const { x: left, y: top, width, height } = boundingBox; + + annotation.data.handles.textBox.worldBoundingBox = { + topLeft: viewport.canvasToWorld([left, top]), + topRight: viewport.canvasToWorld([left + width, top]), + bottomLeft: viewport.canvasToWorld([left, top + height]), + bottomRight: viewport.canvasToWorld([left + width, top + height]), + }; + } +} + +const SHORT_HAND_MAP = { + 'Short Axis': 'W: ', + 'Long Axis': 'L: ', + AREA: 'Area: ', + Length: '', + CORNERSTONEFREETEXT: '', +}; + +function _labelToShorthand(label) { + const shortHand = SHORT_HAND_MAP[label]; + + if (shortHand !== undefined) { + return shortHand; + } + + return label; +} diff --git a/extensions/cornerstone-dicom-sr/src/tools/SCOORD3DPointTool.ts b/extensions/cornerstone-dicom-sr/src/tools/SCOORD3DPointTool.ts new file mode 100644 index 0000000..e412609 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/tools/SCOORD3DPointTool.ts @@ -0,0 +1,203 @@ +import { Types, metaData, utilities as csUtils } from '@cornerstonejs/core'; +import { + annotation, + drawing, + utilities, + Types as cs3DToolsTypes, + AnnotationDisplayTool, +} from '@cornerstonejs/tools'; +import toolNames from './toolNames'; +import { Annotation } from '@cornerstonejs/tools/dist/types/types'; + +export default class SCOORD3DPointTool extends AnnotationDisplayTool { + static toolName = toolNames.SRSCOORD3DPoint; + + constructor( + toolProps = {}, + defaultToolProps = { + configuration: {}, + } + ) { + super(toolProps, defaultToolProps); + } + + _getTextBoxLinesFromLabels(labels) { + // TODO -> max 5 for now (label + shortAxis + longAxis), need a generic solution for this! + + const labelLength = Math.min(labels.length, 5); + const lines = []; + + return lines; + } + + // This tool should not inherit from AnnotationTool and we should not need + // to add the following lines. + isPointNearTool = () => null; + getHandleNearImagePoint = () => null; + + renderAnnotation = (enabledElement: Types.IEnabledElement, svgDrawingHelper: any): void => { + const { viewport } = enabledElement; + const { element } = viewport; + + const annotations = annotation.state.getAnnotations(this.getToolName(), element); + + // Todo: We don't need this anymore, filtering happens in triggerAnnotationRender + if (!annotations?.length) { + return; + } + + // Filter toolData to only render the data for the active SR. + const filteredAnnotations = annotations; + if (!viewport._actors?.size) { + return; + } + + const styleSpecifier: cs3DToolsTypes.AnnotationStyle.StyleSpecifier = { + toolGroupId: this.toolGroupId, + toolName: this.getToolName(), + viewportId: enabledElement.viewport.id, + }; + + for (let i = 0; i < filteredAnnotations.length; i++) { + const annotation = filteredAnnotations[i]; + + const annotationUID = annotation.annotationUID; + const { renderableData } = annotation.data; + const { POINT: points } = renderableData; + + styleSpecifier.annotationUID = annotationUID; + + const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation); + const lineDash = this.getStyle('lineDash', styleSpecifier, annotation); + const color = this.getStyle('color', styleSpecifier, annotation); + + const options = { + color, + lineDash, + lineWidth, + }; + + const point = points[0][0]; + + // check if viewport can render it + const viewable = viewport.isReferenceViewable( + { FrameOfReferenceUID: annotation.metadata.FrameOfReferenceUID, cameraFocalPoint: point }, + { asNearbyProjection: true } + ); + + if (!viewable) { + continue; + } + + // render the point + const arrowPointCanvas = viewport.worldToCanvas(point); + // Todo: configure this + const arrowEndCanvas = [arrowPointCanvas[0] + 20, arrowPointCanvas[1] + 20]; + const canvasCoordinates = [arrowPointCanvas, arrowEndCanvas]; + + drawing.drawArrow( + svgDrawingHelper, + annotationUID, + '1', + canvasCoordinates[1], + canvasCoordinates[0], + { + color: options.color, + width: options.lineWidth, + } + ); + + this.renderTextBox( + svgDrawingHelper, + viewport, + canvasCoordinates, + annotation, + styleSpecifier, + options + ); + } + }; + + renderTextBox( + svgDrawingHelper, + viewport, + canvasCoordinates, + annotation, + styleSpecifier, + options = {} + ) { + if (!canvasCoordinates || !annotation) { + return; + } + + const { annotationUID, data = {} } = annotation; + const { labels } = data; + + const textLines = []; + + for (const label of labels) { + // make this generic + // fix this + if (label.label === '363698007') { + textLines.push(`Finding Site: ${label.value}`); + } + } + + const { color } = options; + + const adaptedCanvasCoordinates = canvasCoordinates; + // adapt coordinates if there is an adapter + const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates); + + if (!annotation.data?.handles?.textBox?.worldPosition) { + annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords); + } + + const textBoxPosition = viewport.worldToCanvas(annotation.data.handles.textBox.worldPosition); + + const textBoxUID = '1'; + const textBoxOptions = this.getLinkedTextBoxStyle(styleSpecifier, annotation); + + const boundingBox = drawing.drawLinkedTextBox( + svgDrawingHelper, + annotationUID, + textBoxUID, + textLines, + textBoxPosition, + canvasCoordinates, + {}, + { + ...textBoxOptions, + color, + } + ); + + const { x: left, y: top, width, height } = boundingBox; + + annotation.data.handles.textBox.worldBoundingBox = { + topLeft: viewport.canvasToWorld([left, top]), + topRight: viewport.canvasToWorld([left + width, top]), + bottomLeft: viewport.canvasToWorld([left, top + height]), + bottomRight: viewport.canvasToWorld([left + width, top + height]), + }; + } + + public getLinkedTextBoxStyle( + specifications: cs3DToolsTypes.AnnotationStyle.StyleSpecifier, + annotation?: Annotation + ): Record { + // Todo: this function can be used to set different styles for different toolMode + // for the textBox. + + return { + visibility: this.getStyle('textBoxVisibility', specifications, annotation), + fontFamily: this.getStyle('textBoxFontFamily', specifications, annotation), + fontSize: this.getStyle('textBoxFontSize', specifications, annotation), + color: this.getStyle('textBoxColor', specifications, annotation), + shadow: this.getStyle('textBoxShadow', specifications, annotation), + background: this.getStyle('textBoxBackground', specifications, annotation), + lineWidth: this.getStyle('textBoxLinkLineWidth', specifications, annotation), + lineDash: this.getStyle('textBoxLinkLineDash', specifications, annotation), + }; + } +} diff --git a/extensions/cornerstone-dicom-sr/src/tools/modules/dicomSRModule.js b/extensions/cornerstone-dicom-sr/src/tools/modules/dicomSRModule.js new file mode 100644 index 0000000..5636e65 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/tools/modules/dicomSRModule.js @@ -0,0 +1,60 @@ +import { getEnabledElement } from '@cornerstonejs/core'; + +const state = { + TrackingUniqueIdentifier: null, + trackingIdentifiersByViewportId: {}, +}; + +/** + * This file is being used to store the per-viewport state of the SR tools, + * Since, all the toolStates are added to the cornerstoneTools, when displaying the SRTools, + * if there are two viewports rendering the same imageId, we don't want to show + * the same SR annotation twice on irrelevant viewport, hence, we are storing the state + * of the SR tools in state here, so that we can filter them later. + */ + +function setTrackingUniqueIdentifiersForElement( + element, + trackingUniqueIdentifiers, + activeIndex = 0 +) { + const enabledElement = getEnabledElement(element); + const { viewport } = enabledElement; + + state.trackingIdentifiersByViewportId[viewport.id] = { + trackingUniqueIdentifiers, + activeIndex, + }; +} + +function setActiveTrackingUniqueIdentifierForElement(element, TrackingUniqueIdentifier) { + const enabledElement = getEnabledElement(element); + const { viewport } = enabledElement; + + const trackingIdentifiersForElement = state.trackingIdentifiersByViewportId[viewport.id]; + + if (trackingIdentifiersForElement) { + const activeIndex = trackingIdentifiersForElement.trackingUniqueIdentifiers.findIndex( + tuid => tuid === TrackingUniqueIdentifier + ); + + trackingIdentifiersForElement.activeIndex = activeIndex; + } +} + +function getTrackingUniqueIdentifiersForElement(element) { + const enabledElement = getEnabledElement(element); + const { viewport } = enabledElement; + + if (state.trackingIdentifiersByViewportId[viewport.id]) { + return state.trackingIdentifiersByViewportId[viewport.id]; + } + + return { trackingUniqueIdentifiers: [] }; +} + +export { + setTrackingUniqueIdentifiersForElement, + setActiveTrackingUniqueIdentifierForElement, + getTrackingUniqueIdentifiersForElement, +}; diff --git a/extensions/cornerstone-dicom-sr/src/tools/toolNames.ts b/extensions/cornerstone-dicom-sr/src/tools/toolNames.ts new file mode 100644 index 0000000..535c11f --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/tools/toolNames.ts @@ -0,0 +1,15 @@ +const toolNames = { + DICOMSRDisplay: 'DICOMSRDisplay', + SRLength: 'SRLength', + SRBidirectional: 'SRBidirectional', + SREllipticalROI: 'SREllipticalROI', + SRCircleROI: 'SRCircleROI', + SRArrowAnnotate: 'SRArrowAnnotate', + SRAngle: 'SRAngle', + SRCobbAngle: 'SRCobbAngle', + SRRectangleROI: 'SRRectangleROI', + SRPlanarFreehandROI: 'SRPlanarFreehandROI', + SRSCOORD3DPoint: 'SRSCOORD3DPoint', +}; + +export default toolNames; diff --git a/extensions/cornerstone-dicom-sr/src/utils/SRSCOOR3DProbeMapper.ts b/extensions/cornerstone-dicom-sr/src/utils/SRSCOOR3DProbeMapper.ts new file mode 100644 index 0000000..b67c887 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/SRSCOOR3DProbeMapper.ts @@ -0,0 +1,62 @@ +const SRSCOOR3DProbe = { + toAnnotation: measurement => {}, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} cornerstone Cornerstone event data + * @return {Measurement} Measurement instance + */ + toMeasurement: ( + csToolsEventDetail, + displaySetService, + CornerstoneViewportService, + getValueTypeFromToolType, + customizationService + ) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + if (!metadata || !data) { + console.warn('Probe tool: Missing metadata or data'); + return null; + } + + const { toolName } = metadata; + const { points } = data.handles; + + const displayText = getDisplayText(annotation); + return { + uid: annotationUID, + points, + metadata, + toolName: metadata.toolName, + label: data.label, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType?.(toolName) ?? null, + }; + }, +}; + +function getDisplayText(annotation) { + const { data } = annotation; + + if (!data) { + return ['']; + } + const { labels } = data; + + const displayText = []; + + for (const label of labels) { + // make this generic + if (label.label === '33636980076') { + displayText.push(`Finding Site: ${label.value}`); + } + } + + return displayText; +} + +export default SRSCOOR3DProbe; diff --git a/extensions/cornerstone-dicom-sr/src/utils/addSRAnnotation.ts b/extensions/cornerstone-dicom-sr/src/utils/addSRAnnotation.ts new file mode 100644 index 0000000..f50a0cc --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/addSRAnnotation.ts @@ -0,0 +1,67 @@ +import { Types, annotation } from '@cornerstonejs/tools'; +import { metaData } from '@cornerstonejs/core'; + +import getRenderableData from './getRenderableData'; +import toolNames from '../tools/toolNames'; + +export default function addSRAnnotation(measurement, imageId, frameNumber) { + let toolName = toolNames.DICOMSRDisplay; + const renderableData = measurement.coords.reduce((acc, coordProps) => { + acc[coordProps.GraphicType] = acc[coordProps.GraphicType] || []; + acc[coordProps.GraphicType].push(getRenderableData({ ...coordProps, imageId })); + return acc; + }, {}); + + const { TrackingUniqueIdentifier } = measurement; + const { ValueType: valueType, GraphicType: graphicType } = measurement.coords[0]; + const graphicTypePoints = renderableData[graphicType]; + + /** TODO: Read the tool name from the DICOM SR identification type in the future. */ + let frameOfReferenceUID = null; + + if (imageId) { + const imagePlaneModule = metaData.get('imagePlaneModule', imageId); + frameOfReferenceUID = imagePlaneModule?.frameOfReferenceUID; + } + + if (valueType === 'SCOORD3D') { + toolName = toolNames.SRSCOORD3DPoint; + + // get the ReferencedFrameOfReferenceUID from the measurement + frameOfReferenceUID = measurement.coords[0].ReferencedFrameOfReferenceSequence; + } + + const SRAnnotation: Types.Annotation = { + annotationUID: TrackingUniqueIdentifier, + highlighted: false, + isLocked: false, + invalidated: false, + metadata: { + toolName, + valueType, + graphicType, + FrameOfReferenceUID: frameOfReferenceUID, + referencedImageId: imageId, + }, + data: { + label: measurement.labels?.[0]?.value || undefined, + displayText: measurement.displayText || undefined, + handles: { + textBox: measurement.textBox ?? {}, + points: graphicTypePoints[0], + }, + cachedStats: {}, + frameNumber, + renderableData, + TrackingUniqueIdentifier, + labels: measurement.labels, + }, + }; + + /** + * const annotationManager = annotation.annotationState.getAnnotationManager(); + * was not triggering annotation_added events. + */ + annotation.state.addAnnotation(SRAnnotation); + console.debug('Adding SR annotation:', SRAnnotation); +} diff --git a/extensions/cornerstone-dicom-sr/src/utils/addToolInstance.ts b/extensions/cornerstone-dicom-sr/src/utils/addToolInstance.ts new file mode 100644 index 0000000..717b9dc --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/addToolInstance.ts @@ -0,0 +1,14 @@ +import { addTool } from '@cornerstonejs/tools'; + +export default function addToolInstance(name: string, toolClass, configuration = {}): void { + class InstanceClass extends toolClass { + static toolName = name; + constructor(toolProps, defaultToolProps) { + toolProps.configuration = toolProps.configuration + ? { ...toolProps.configuration, ...configuration } + : configuration; + super(toolProps, defaultToolProps); + } + } + addTool(InstanceClass); +} diff --git a/extensions/cornerstone-dicom-sr/src/utils/createReferencedImageDisplaySet.ts b/extensions/cornerstone-dicom-sr/src/utils/createReferencedImageDisplaySet.ts new file mode 100644 index 0000000..5e615f0 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/createReferencedImageDisplaySet.ts @@ -0,0 +1,95 @@ +import { DisplaySetService, classes } from '@ohif/core'; + +const ImageSet = classes.ImageSet; + +const findInstance = (measurement, displaySetService: DisplaySetService) => { + const { displaySetInstanceUID, ReferencedSOPInstanceUID: sopUid } = measurement; + const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + if (!referencedDisplaySet.images) { + return; + } + return referencedDisplaySet.images.find(it => it.SOPInstanceUID === sopUid); +}; + +/** Finds references to display sets inside the measurements + * contained within the provided display set. + * @return an array of instances referenced. + */ +const findReferencedInstances = (displaySetService: DisplaySetService, displaySet) => { + const instances = []; + const instanceById = {}; + for (const measurement of displaySet.measurements) { + const { imageId } = measurement; + if (!imageId) { + continue; + } + if (instanceById[imageId]) { + continue; + } + + const instance = findInstance(measurement, displaySetService); + if (!instance) { + console.log('Measurement', measurement, 'had no instances found'); + continue; + } + + instanceById[imageId] = instance; + instances.push(instance); + } + return instances; +}; + +/** + * Creates a new display set containing a single image instance for each + * referenced image. + * + * @param displaySetService + * @param displaySet - containing measurements referencing images. + * @returns A new (registered/active) display set containing the referenced images + */ +const createReferencedImageDisplaySet = (displaySetService, displaySet) => { + const instances = findReferencedInstances(displaySetService, displaySet); + // This will be a member function of the created image set + const updateInstances = function () { + this.images.splice( + 0, + this.images.length, + ...findReferencedInstances(displaySetService, displaySet) + ); + this.numImageFrames = this.images.length; + }; + + const imageSet = new ImageSet(instances); + const instance = instances[0]; + + if (!instance) { + return; + } + + imageSet.setAttributes({ + displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID + SeriesDate: instance.SeriesDate, + SeriesTime: instance.SeriesTime, + SeriesInstanceUID: imageSet.uid, + StudyInstanceUID: instance.StudyInstanceUID, + SeriesNumber: instance.SeriesNumber || 0, + SOPClassUID: instance.SOPClassUID, + SeriesDescription: `${displaySet.SeriesDescription} KO ${displaySet.instance.SeriesNumber}`, + Modality: 'KO', + isMultiFrame: false, + numImageFrames: instances.length, + SOPClassHandlerId: `@ohif/extension-default.sopClassHandlerModule.stack`, + isReconstructable: false, + // This object is made of multiple instances from other series + isCompositeStack: true, + madeInClient: true, + excludeFromThumbnailBrowser: true, + updateInstances, + }); + + displaySetService.addDisplaySets(imageSet); + + return imageSet; +}; + +export default createReferencedImageDisplaySet; diff --git a/extensions/cornerstone-dicom-sr/src/utils/findInstanceMetadataBySopInstanceUid.js b/extensions/cornerstone-dicom-sr/src/utils/findInstanceMetadataBySopInstanceUid.js new file mode 100644 index 0000000..30e6815 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/findInstanceMetadataBySopInstanceUid.js @@ -0,0 +1,26 @@ +/** + * Should Find the requested instance metadata into the displaySets and return + * + * @param {Array} displaySets - List of displaySets + * @param {string} SOPInstanceUID - sopInstanceUID to look for + * @returns {Object} - instance metadata found + */ +const findInstanceMetadataBySopInstanceUID = (displaySets, SOPInstanceUID) => { + let instanceFound; + + displaySets.find(displaySet => { + if (!displaySet.images) { + return false; + } + + instanceFound = displaySet.images.find( + instanceMetadata => instanceMetadata.getSOPInstanceUID() === SOPInstanceUID + ); + + return !!instanceFound; + }); + + return instanceFound; +}; + +export default findInstanceMetadataBySopInstanceUID; diff --git a/extensions/cornerstone-dicom-sr/src/utils/findMostRecentStructuredReport.js b/extensions/cornerstone-dicom-sr/src/utils/findMostRecentStructuredReport.js new file mode 100644 index 0000000..138d72d --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/findMostRecentStructuredReport.js @@ -0,0 +1,61 @@ +/** + * Should find the most recent Structured Report metadata + * + * @param {Array} studies + * @returns {Object} Series + */ +const findMostRecentStructuredReport = studies => { + let mostRecentStructuredReport; + + studies.forEach(study => { + const allSeries = study.getSeries ? study.getSeries() : []; + allSeries.forEach(series => { + // Skip series that may not have instances yet + // This can happen if we have retrieved just the initial + // details about the series via QIDO-RS, but not the full metadata + if (!series.instances.length) { + return; + } + + if (isStructuredReportSeries(series)) { + if (!mostRecentStructuredReport || compareSeriesDate(series, mostRecentStructuredReport)) { + mostRecentStructuredReport = series; + } + } + }); + }); + + return mostRecentStructuredReport; +}; + +/** + * Checks if series sopClassUID matches with the supported Structured Reports sopClassUID + * + * @param {Object} series - Series metadata + * @returns {boolean} + */ +const isStructuredReportSeries = series => { + const supportedSopClassUIDs = ['1.2.840.10008.5.1.4.1.1.88.22', '1.2.840.10008.5.1.4.1.1.11.1']; + + const firstInstance = series.getFirstInstance(); + const SOPClassUID = firstInstance.getData().metadata.SOPClassUID; + + return supportedSopClassUIDs.includes(SOPClassUID); +}; + +/** + * Checks if series1 is newer than series2 + * + * @param {Object} series1 - Series Metadata 1 + * @param {Object} series2 - Series Metadata 2 + * @returns {boolean} true/false if series1 is newer than series2 + */ +const compareSeriesDate = (series1, series2) => { + return ( + series1._data.SeriesDate > series2._data.SeriesDate || + (series1._data.SeriesDate === series2._data.SeriesDate && + series1._data.SeriesTime > series2._data.SeriesTime) + ); +}; + +export default findMostRecentStructuredReport; diff --git a/extensions/cornerstone-dicom-sr/src/utils/formatContentItem.ts b/extensions/cornerstone-dicom-sr/src/utils/formatContentItem.ts new file mode 100644 index 0000000..064a8a2 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/formatContentItem.ts @@ -0,0 +1,67 @@ +import { utils } from '@ohif/core'; + +/** + * Formatters used to format each of the content items (SR "nodes") which can be + * text, code, UID ref, number, person name, date, time and date time. Each + * formatter must be a function with the following signature: + * + * [VALUE_TYPE]: (contentItem) => string + * + */ +const contentItemFormatters = { + TEXT: contentItem => contentItem.TextValue, + CODE: contentItem => contentItem.ConceptCodeSequence?.[0]?.CodeMeaning, + UIDREF: contentItem => contentItem.UID, + NUM: contentItem => { + const measuredValue = contentItem.MeasuredValueSequence?.[0]; + + if (!measuredValue) { + return; + } + + const { NumericValue, MeasurementUnitsCodeSequence } = measuredValue; + const { CodeValue } = MeasurementUnitsCodeSequence; + + return `${NumericValue} ${CodeValue}`; + }, + PNAME: contentItem => { + const personName = contentItem.PersonName?.[0]; + return personName ? utils.formatPN(personName) : undefined; + }, + DATE: contentItem => { + const { Date } = contentItem; + return Date ? utils.formatDate(Date) : undefined; + }, + TIME: contentItem => { + const { Time } = contentItem; + return Time ? utils.formatTime(Time) : undefined; + }, + DATETIME: contentItem => { + const { DateTime } = contentItem; + + if (typeof DateTime !== 'string') { + return; + } + + // 14 characters because it should be something like 20180614113714 + if (DateTime.length < 14) { + return DateTime; + } + + const dicomDate = DateTime.substring(0, 8); + const dicomTime = DateTime.substring(8, 14); + const formattedDate = utils.formatDate(dicomDate); + const formattedTime = utils.formatTime(dicomTime); + + return `${formattedDate} ${formattedTime}`; + }, +}; + +function formatContentItemValue(contentItem) { + const { ValueType } = contentItem; + const fnFormat = contentItemFormatters[ValueType]; + + return fnFormat ? fnFormat(contentItem) : `[${ValueType} is not supported]`; +} + +export { formatContentItemValue as default, formatContentItemValue }; diff --git a/extensions/cornerstone-dicom-sr/src/utils/getAllDisplaySets.js b/extensions/cornerstone-dicom-sr/src/utils/getAllDisplaySets.js new file mode 100644 index 0000000..a8eda16 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/getAllDisplaySets.js @@ -0,0 +1,19 @@ +/** + * Retrieve a list of all displaySets of all studies + * + * @param {Object} studies - List of studies loaded into the viewer + * @returns {Object} List of DisplaySets + */ +const getAllDisplaySets = studies => { + let allDisplaySets = []; + + studies.forEach(study => { + if (study.getDisplaySets) { + allDisplaySets = allDisplaySets.concat(study.getDisplaySets()); + } + }); + + return allDisplaySets; +}; + +export default getAllDisplaySets; diff --git a/extensions/cornerstone-dicom-sr/src/utils/getFilteredCornerstoneToolState.ts b/extensions/cornerstone-dicom-sr/src/utils/getFilteredCornerstoneToolState.ts new file mode 100644 index 0000000..2c28311 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/getFilteredCornerstoneToolState.ts @@ -0,0 +1,103 @@ +import OHIF from '@ohif/core'; +import { annotation } from '@cornerstonejs/tools'; +const { log } = OHIF; + +function getFilteredCornerstoneToolState(measurementData, additionalFindingTypes) { + const filteredToolState = {}; + + function addToFilteredToolState(annotation, toolType) { + if (!annotation.metadata?.referencedImageId) { + log.warn(`[DICOMSR] No referencedImageId found for ${toolType} ${annotation.id}`); + return; + } + + const imageId = annotation.metadata.referencedImageId; + + if (!filteredToolState[imageId]) { + filteredToolState[imageId] = {}; + } + + const imageIdSpecificToolState = filteredToolState[imageId]; + + if (!imageIdSpecificToolState[toolType]) { + imageIdSpecificToolState[toolType] = { + data: [], + }; + } + + const measurementDataI = measurementData.find(md => md.uid === annotation.annotationUID); + const toolData = imageIdSpecificToolState[toolType].data; + + let { finding } = measurementDataI; + const findingSites = []; + + // NOTE -> We use the CORNERSTONEJS coding schemeDesignator which we have + // defined in the @cornerstonejs/adapters + if (measurementDataI.label) { + if (additionalFindingTypes.includes(toolType)) { + finding = { + CodeValue: 'CORNERSTONEFREETEXT', + CodingSchemeDesignator: 'CORNERSTONEJS', + CodeMeaning: measurementDataI.label, + }; + } else { + findingSites.push({ + CodeValue: 'CORNERSTONEFREETEXT', + CodingSchemeDesignator: 'CORNERSTONEJS', + CodeMeaning: measurementDataI.label, + }); + } + } + + if (measurementDataI.findingSites) { + findingSites.push(...measurementDataI.findingSites); + } + + const measurement = Object.assign({}, annotation, { + finding, + findingSites, + }); + + toolData.push(measurement); + } + + const uidFilter = measurementData.map(md => md.uid); + const uids = uidFilter.slice(); + + const annotationManager = annotation.state.getAnnotationManager(); + const framesOfReference = annotationManager.getFramesOfReference(); + + for (let i = 0; i < framesOfReference.length; i++) { + const frameOfReference = framesOfReference[i]; + + const frameOfReferenceAnnotations = annotationManager.getAnnotations(frameOfReference); + + const toolTypes = Object.keys(frameOfReferenceAnnotations); + + for (let j = 0; j < toolTypes.length; j++) { + const toolType = toolTypes[j]; + + const annotations = frameOfReferenceAnnotations[toolType]; + + if (annotations) { + for (let k = 0; k < annotations.length; k++) { + const annotation = annotations[k]; + const uidIndex = uids.findIndex(uid => uid === annotation.annotationUID); + + if (uidIndex !== -1) { + addToFilteredToolState(annotation, toolType); + uids.splice(uidIndex, 1); + + if (!uids.length) { + return filteredToolState; + } + } + } + } + } + } + + return filteredToolState; +} + +export default getFilteredCornerstoneToolState; diff --git a/extensions/cornerstone-dicom-sr/src/utils/getLabelFromDCMJSImportedToolData.js b/extensions/cornerstone-dicom-sr/src/utils/getLabelFromDCMJSImportedToolData.js new file mode 100644 index 0000000..b814b42 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/getLabelFromDCMJSImportedToolData.js @@ -0,0 +1,27 @@ +import { adaptersSR } from '@cornerstonejs/adapters'; + +const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D; + +/** + * Extracts the label from the toolData imported from dcmjs. We need to do this + * as dcmjs does not depeend on OHIF/the measurementService, it just produces data for cornestoneTools. + * This optional data is available for the consumer to process if they wish to. + * @param {object} toolData The tooldata relating to the + * + * @returns {string} The extracted label. + */ +export default function getLabelFromDCMJSImportedToolData(toolData) { + const { findingSites = [], finding } = toolData; + + let freeTextLabel = findingSites.find( + fs => fs.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT + ); + + if (freeTextLabel) { + return freeTextLabel.CodeMeaning; + } + + if (finding && finding.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT) { + return finding.CodeMeaning; + } +} diff --git a/extensions/cornerstone-dicom-sr/src/utils/getRenderableData.ts b/extensions/cornerstone-dicom-sr/src/utils/getRenderableData.ts new file mode 100644 index 0000000..a8546a2 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/getRenderableData.ts @@ -0,0 +1,142 @@ +import { vec3 } from 'gl-matrix'; +import { metaData, utilities, Types as csTypes } from '@cornerstonejs/core'; + +import { SCOORDTypes } from '../enums'; + +const EPSILON = 1e-4; + +const getRenderableCoords = ({ GraphicData, ValueType, imageId }) => { + const renderableData = []; + if (ValueType === 'SCOORD3D') { + for (let i = 0; i < GraphicData.length; i += 3) { + renderableData.push([GraphicData[i], GraphicData[i + 1], GraphicData[i + 2]]); + } + } else { + for (let i = 0; i < GraphicData.length; i += 2) { + const worldPos = utilities.imageToWorldCoords(imageId, [GraphicData[i], GraphicData[i + 1]]); + renderableData.push(worldPos); + } + } + return renderableData; +}; + +function getRenderableData({ GraphicType, GraphicData, ValueType, imageId }) { + let renderableData = []; + + switch (GraphicType) { + case SCOORDTypes.POINT: + case SCOORDTypes.MULTIPOINT: + case SCOORDTypes.POLYLINE: { + renderableData = getRenderableCoords({ GraphicData, ValueType, imageId }); + break; + } + case SCOORDTypes.CIRCLE: { + const pointsWorld: csTypes.Point3[] = getRenderableCoords({ + GraphicData, + ValueType, + imageId, + }); + // We do not have an explicit draw circle svg helper in Cornerstone3D at + // this time, but we can use the ellipse svg helper to draw a circle, so + // here we reshape the data for that purpose. + const center = pointsWorld[0]; + const onPerimeter = pointsWorld[1]; + const radius = vec3.distance(center, onPerimeter); + + const imagePlaneModule = metaData.get('imagePlaneModule', imageId); + if (!imagePlaneModule) { + throw new Error('No imagePlaneModule found'); + } + + const { + columnCosines, + rowCosines, + }: { + columnCosines: csTypes.Point3; + rowCosines: csTypes.Point3; + } = imagePlaneModule; + + // we need to get major/minor axis (which are both the same size major = minor) + + const firstAxisStart = vec3.create(); + vec3.scaleAndAdd(firstAxisStart, center, columnCosines, radius); + + const firstAxisEnd = vec3.create(); + vec3.scaleAndAdd(firstAxisEnd, center, columnCosines, -radius); + + const secondAxisStart = vec3.create(); + vec3.scaleAndAdd(secondAxisStart, center, rowCosines, radius); + + const secondAxisEnd = vec3.create(); + vec3.scaleAndAdd(secondAxisEnd, center, rowCosines, -radius); + + renderableData = [ + firstAxisStart as csTypes.Point3, + firstAxisEnd as csTypes.Point3, + secondAxisStart as csTypes.Point3, + secondAxisEnd as csTypes.Point3, + ]; + + break; + } + case SCOORDTypes.ELLIPSE: { + // GraphicData is ordered as [majorAxisStartX, majorAxisStartY, majorAxisEndX, majorAxisEndY, minorAxisStartX, minorAxisStartY, minorAxisEndX, minorAxisEndY] + // But Cornerstone3D points are ordered as top, bottom, left, right for the + // ellipse so we need to identify if the majorAxis is horizontal or vertical + // and then choose the correct points to use for the ellipse. + const pointsWorld: csTypes.Point3[] = getRenderableCoords({ + GraphicData, + ValueType, + imageId, + }); + + const majorAxisStart = vec3.fromValues(...pointsWorld[0]); + const majorAxisEnd = vec3.fromValues(...pointsWorld[1]); + const minorAxisStart = vec3.fromValues(...pointsWorld[2]); + const minorAxisEnd = vec3.fromValues(...pointsWorld[3]); + + const majorAxisVec = vec3.create(); + vec3.sub(majorAxisVec, majorAxisEnd, majorAxisStart); + + // normalize majorAxisVec to avoid scaling issues + vec3.normalize(majorAxisVec, majorAxisVec); + + const minorAxisVec = vec3.create(); + vec3.sub(minorAxisVec, minorAxisEnd, minorAxisStart); + vec3.normalize(minorAxisVec, minorAxisVec); + + const imagePlaneModule = metaData.get('imagePlaneModule', imageId); + + if (!imagePlaneModule) { + throw new Error('imageId does not have imagePlaneModule metadata'); + } + + const { columnCosines }: { columnCosines: csTypes.Point3 } = imagePlaneModule; + + // find which axis is parallel to the columnCosines + const columnCosinesVec = vec3.fromValues(...columnCosines); + + const projectedMajorAxisOnColVec = Math.abs(vec3.dot(columnCosinesVec, majorAxisVec)); + const projectedMinorAxisOnColVec = Math.abs(vec3.dot(columnCosinesVec, minorAxisVec)); + + const absoluteOfMajorDotProduct = Math.abs(projectedMajorAxisOnColVec); + const absoluteOfMinorDotProduct = Math.abs(projectedMinorAxisOnColVec); + + renderableData = []; + if (Math.abs(absoluteOfMajorDotProduct - 1) < EPSILON) { + renderableData = [pointsWorld[0], pointsWorld[1], pointsWorld[2], pointsWorld[3]]; + } else if (Math.abs(absoluteOfMinorDotProduct - 1) < EPSILON) { + renderableData = [pointsWorld[2], pointsWorld[3], pointsWorld[0], pointsWorld[1]]; + } else { + console.warn('OBLIQUE ELLIPSE NOT YET SUPPORTED'); + } + break; + } + default: + console.warn('Unsupported GraphicType:', GraphicType); + } + + return renderableData; +} + +export default getRenderableData; diff --git a/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts new file mode 100644 index 0000000..bafeba2 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts @@ -0,0 +1,298 @@ +import { utilities, metaData } from '@cornerstonejs/core'; +import OHIF, { DicomMetadataStore } from '@ohif/core'; +import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData'; +import { adaptersSR } from '@cornerstonejs/adapters'; +import { annotation as CsAnnotation } from '@cornerstonejs/tools'; +import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone'; + +const { locking } = CsAnnotation; +const { guid } = OHIF.utils; +const { MeasurementReport, CORNERSTONE_3D_TAG } = adaptersSR.Cornerstone3D; +const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums; +const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; + +const convertCode = (codingValues, code) => { + if (!code || code.CodingSchemeDesignator === 'CORNERSTONEJS') { + return; + } + const ref = `${code.CodingSchemeDesignator}:${code.CodeValue}`; + const ret = { ...codingValues[ref], ref, ...code, text: code.CodeMeaning }; + return ret; +}; + +const convertSites = (codingValues, sites) => { + if (!sites || !sites.length) { + return; + } + const ret = []; + // Do as a loop to convert away from Proxy instances + for (let i = 0; i < sites.length; i++) { + // Deal with irregular conversion from dcmjs + const site = convertCode(codingValues, sites[i][0] || sites[i]); + if (site) { + ret.push(site); + } + } + return (ret.length && ret) || undefined; +}; + +/** + * Hydrates a structured report, for default viewports. + * + */ +export default function hydrateStructuredReport( + { servicesManager, extensionManager, commandsManager }: withAppTypes, + displaySetInstanceUID +) { + const dataSource = extensionManager.getActiveDataSource()[0]; + const { measurementService, displaySetService, customizationService } = servicesManager.services; + + const codingValues = customizationService.getCustomization('codingValues'); + const disableEditing = customizationService.getCustomization('panelMeasurement.disableEditing'); + + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + + // TODO -> We should define a strict versioning somewhere. + const mappings = measurementService.getSourceMappings( + CORNERSTONE_3D_TOOLS_SOURCE_NAME, + CORNERSTONE_3D_TOOLS_SOURCE_VERSION + ); + + if (!mappings || !mappings.length) { + throw new Error( + `Attempting to hydrate measurements service when no mappings present. This shouldn't be reached.` + ); + } + + const instance = DicomMetadataStore.getInstance( + displaySet.StudyInstanceUID, + displaySet.SeriesInstanceUID, + displaySet.SOPInstanceUID + ); + + const sopInstanceUIDToImageId = {}; + const imageIdsForToolState = {}; + + displaySet.measurements.forEach(measurement => { + const { ReferencedSOPInstanceUID, imageId, frameNumber } = measurement; + + if (!sopInstanceUIDToImageId[ReferencedSOPInstanceUID]) { + sopInstanceUIDToImageId[ReferencedSOPInstanceUID] = imageId; + imageIdsForToolState[ReferencedSOPInstanceUID] = []; + } + if (!imageIdsForToolState[ReferencedSOPInstanceUID][frameNumber]) { + imageIdsForToolState[ReferencedSOPInstanceUID][frameNumber] = imageId; + } + }); + + const datasetToUse = _mapLegacyDataSet(instance); + + // Use dcmjs to generate toolState. + let storedMeasurementByAnnotationType = MeasurementReport.generateToolState( + datasetToUse, + // NOTE: we need to pass in the imageIds to dcmjs since the we use them + // for the imageToWorld transformation. The following assumes that the order + // that measurements were added to the display set are the same order as + // the measurementGroups in the instance. + sopInstanceUIDToImageId, + utilities.imageToWorldCoords, + metaData + ); + + const onBeforeSRHydration = customizationService.getCustomization('onBeforeSRHydration')?.value; + + if (typeof onBeforeSRHydration === 'function') { + storedMeasurementByAnnotationType = onBeforeSRHydration({ + storedMeasurementByAnnotationType, + displaySet, + }); + } + + // Filter what is found by DICOM SR to measurements we support. + const mappingDefinitions = mappings.map(m => m.annotationType); + const hydratableMeasurementsInSR = {}; + + Object.keys(storedMeasurementByAnnotationType).forEach(key => { + if (mappingDefinitions.includes(key)) { + hydratableMeasurementsInSR[key] = storedMeasurementByAnnotationType[key]; + } + }); + + // Set the series touched as tracked. + const imageIds = []; + + // TODO: notification if no hydratable? + Object.keys(hydratableMeasurementsInSR).forEach(annotationType => { + const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType]; + + toolDataForAnnotationType.forEach(toolData => { + // Add the measurement to toolState + // dcmjs and Cornerstone3D has structural defect in supporting multi-frame + // files, and looking up the imageId from sopInstanceUIDToImageId results + // in the wrong value. + const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1; + const imageId = + imageIdsForToolState[toolData.sopInstanceUid][frameNumber] || + sopInstanceUIDToImageId[toolData.sopInstanceUid]; + + if (!imageIds.includes(imageId)) { + imageIds.push(imageId); + } + }); + }); + + let targetStudyInstanceUID; + const SeriesInstanceUIDs = []; + + for (let i = 0; i < imageIds.length; i++) { + const imageId = imageIds[i]; + const { SeriesInstanceUID, StudyInstanceUID } = metaData.get('instance', imageId); + + if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) { + SeriesInstanceUIDs.push(SeriesInstanceUID); + } + + if (!targetStudyInstanceUID) { + targetStudyInstanceUID = StudyInstanceUID; + } else if (targetStudyInstanceUID !== StudyInstanceUID) { + console.warn('NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.'); + } + } + + Object.keys(hydratableMeasurementsInSR).forEach(annotationType => { + const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType]; + + toolDataForAnnotationType.forEach(toolData => { + // Add the measurement to toolState + // dcmjs and Cornerstone3D has structural defect in supporting multi-frame + // files, and looking up the imageId from sopInstanceUIDToImageId results + // in the wrong value. + const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1; + const imageId = + imageIdsForToolState[toolData.sopInstanceUid][frameNumber] || + sopInstanceUIDToImageId[toolData.sopInstanceUid]; + + toolData.uid = guid(); + + const instance = metaData.get('instance', imageId); + const { + FrameOfReferenceUID, + // SOPInstanceUID, + // SeriesInstanceUID, + // StudyInstanceUID, + } = instance; + + const annotation = { + annotationUID: toolData.annotation.annotationUID, + data: toolData.annotation.data, + metadata: { + toolName: annotationType, + referencedImageId: imageId, + FrameOfReferenceUID, + }, + }; + + const source = measurementService.getSource( + CORNERSTONE_3D_TOOLS_SOURCE_NAME, + CORNERSTONE_3D_TOOLS_SOURCE_VERSION + ); + annotation.data.label = getLabelFromDCMJSImportedToolData(toolData); + annotation.data.finding = convertCode(codingValues, toolData.finding?.[0]); + annotation.data.findingSites = convertSites(codingValues, toolData.findingSites); + annotation.data.findingSites?.forEach(site => { + if (site.type) { + annotation.data[site.type] = site; + } + }); + + const matchingMapping = mappings.find(m => m.annotationType === annotationType); + + const newAnnotationUID = measurementService.addRawMeasurement( + source, + annotationType, + { annotation }, + matchingMapping.toMeasurementSchema, + dataSource + ); + + commandsManager.runCommand('updateMeasurement', { + uid: newAnnotationUID, + code: annotation.data.finding, + }); + + if (disableEditing) { + locking.setAnnotationLocked(newAnnotationUID, true); + } + + if (!imageIds.includes(imageId)) { + imageIds.push(imageId); + } + }); + }); + + displaySet.isHydrated = true; + + return { + StudyInstanceUID: targetStudyInstanceUID, + SeriesInstanceUIDs, + }; +} + +function _mapLegacyDataSet(dataset) { + const REPORT = 'Imaging Measurements'; + const GROUP = 'Measurement Group'; + const TRACKING_IDENTIFIER = 'Tracking Identifier'; + + // Identify the Imaging Measurements + const imagingMeasurementContent = toArray(dataset.ContentSequence).find( + codeMeaningEquals(REPORT) + ); + + // Retrieve the Measurements themselves + const measurementGroups = toArray(imagingMeasurementContent.ContentSequence).filter( + codeMeaningEquals(GROUP) + ); + + // For each of the supported measurement types, compute the measurement data + const measurementData = {}; + + const cornerstoneToolClasses = MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE; + + const registeredToolClasses = []; + + Object.keys(cornerstoneToolClasses).forEach(key => { + registeredToolClasses.push(cornerstoneToolClasses[key]); + measurementData[key] = []; + }); + + measurementGroups.forEach((measurementGroup, index) => { + const measurementGroupContentSequence = toArray(measurementGroup.ContentSequence); + + const TrackingIdentifierGroup = measurementGroupContentSequence.find( + contentItem => contentItem.ConceptNameCodeSequence.CodeMeaning === TRACKING_IDENTIFIER + ); + + const TrackingIdentifier = TrackingIdentifierGroup.TextValue; + + let [cornerstoneTag, toolName] = TrackingIdentifier.split(':'); + if (supportedLegacyCornerstoneTags.includes(cornerstoneTag)) { + cornerstoneTag = CORNERSTONE_3D_TAG; + } + + const mappedTrackingIdentifier = `${cornerstoneTag}:${toolName}`; + + TrackingIdentifierGroup.TextValue = mappedTrackingIdentifier; + }); + + return dataset; +} + +const toArray = function (x) { + return Array.isArray(x) ? x : [x]; +}; + +const codeMeaningEquals = codeMeaningName => { + return contentItem => { + return contentItem.ConceptNameCodeSequence.CodeMeaning === codeMeaningName; + }; +}; diff --git a/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js b/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js new file mode 100644 index 0000000..31705c3 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/isRehydratable.js @@ -0,0 +1,60 @@ +import { adaptersSR } from '@cornerstonejs/adapters'; + +const cornerstoneAdapters = + adaptersSR.Cornerstone3D.MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE; + +const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0']; +const CORNERSTONE_3D_TAG = adaptersSR.Cornerstone3D.CORNERSTONE_3D_TAG; + +/** + * Checks if the given `displaySet`can be rehydrated into the `measurementService`. + * + * @param {object} displaySet The SR `displaySet` to check. + * @param {object[]} mappings The CornerstoneTools 4 mappings to the `measurementService`. + * @returns {boolean} True if the SR can be rehydrated into the `measurementService`. + */ +export default function isRehydratable(displaySet, mappings) { + if (!mappings || !mappings.length) { + return false; + } + + const mappingDefinitions = mappings.map(m => m.annotationType); + const { measurements } = displaySet; + + const adapterKeys = Object.keys(cornerstoneAdapters).filter( + adapterKey => + typeof cornerstoneAdapters[adapterKey].isValidCornerstoneTrackingIdentifier === 'function' + ); + + const adapters = []; + + adapterKeys.forEach(key => { + if (mappingDefinitions.includes(key)) { + // Must have both a dcmjs adapter and a measurementService + // Definition in order to be a candidate for import. + adapters.push(cornerstoneAdapters[key]); + } + }); + + for (let i = 0; i < measurements.length; i++) { + const { TrackingIdentifier } = measurements[i] || {}; + const hydratable = adapters.some(adapter => { + let [cornerstoneTag, toolName] = TrackingIdentifier.split(':'); + if (supportedLegacyCornerstoneTags.includes(cornerstoneTag)) { + cornerstoneTag = CORNERSTONE_3D_TAG; + } + + const mappedTrackingIdentifier = `${cornerstoneTag}:${toolName}`; + + return adapter.isValidCornerstoneTrackingIdentifier(mappedTrackingIdentifier); + }); + + if (hydratable) { + return true; + } + console.log('Measurement is not rehydratable', TrackingIdentifier, measurements[i]); + } + + console.log('No measurements found which were rehydratable'); + return false; +} diff --git a/extensions/cornerstone-dicom-sr/src/utils/isToolSupported.js b/extensions/cornerstone-dicom-sr/src/utils/isToolSupported.js new file mode 100644 index 0000000..cdbb2e7 --- /dev/null +++ b/extensions/cornerstone-dicom-sr/src/utils/isToolSupported.js @@ -0,0 +1,14 @@ +import { adaptersSR } from '@cornerstonejs/adapters'; + +/** + * Checks if dcmjs has support to determined tool + * + * @param {string} toolName + * @returns {boolean} + */ +const isToolSupported = toolName => { + const adapter = adaptersSR.Cornerstone3D; + return !!adapter[toolName]; +}; + +export default isToolSupported; diff --git a/extensions/cornerstone-dynamic-volume/.webpack/webpack.dev.js b/extensions/cornerstone-dynamic-volume/.webpack/webpack.dev.js new file mode 100644 index 0000000..1ae3084 --- /dev/null +++ b/extensions/cornerstone-dynamic-volume/.webpack/webpack.dev.js @@ -0,0 +1,8 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR }); +}; diff --git a/extensions/cornerstone-dynamic-volume/.webpack/webpack.prod.js b/extensions/cornerstone-dynamic-volume/.webpack/webpack.prod.js new file mode 100644 index 0000000..66a3e9f --- /dev/null +++ b/extensions/cornerstone-dynamic-volume/.webpack/webpack.prod.js @@ -0,0 +1,54 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +const pkg = require('./../package.json'); + +const ROOT_DIR = path.join(__dirname, '../'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); +const ENTRY = { + app: `${SRC_DIR}/index.ts`, +}; + +const outputName = `ohif-${pkg.name.split('/').pop()}`; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: true, + }, + output: { + path: ROOT_DIR, + library: 'ohif-extension-cornerstone', + libraryTarget: 'umd', + filename: pkg.main, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + new MiniCssExtractPlugin({ + filename: `./dist/${outputName}.css`, + chunkFilename: `./dist/${outputName}.css`, + }), + ], + }); +}; diff --git a/extensions/cornerstone-dynamic-volume/CHANGELOG.md b/extensions/cornerstone-dynamic-volume/CHANGELOG.md new file mode 100644 index 0000000..6489c79 --- /dev/null +++ b/extensions/cornerstone-dynamic-volume/CHANGELOG.md @@ -0,0 +1,2098 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + + +### Bug Fixes + +* **core:** Address 3D reconstruction and Android compatibility issues and clean up 4D data mode ([#4762](https://github.com/OHIF/Viewers/issues/4762)) ([149d6d0](https://github.com/OHIF/Viewers/commit/149d6d049cd333b9e5846576b403ff387558a66f)) + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + + +### Features + +* **customization:** new customization service api ([#4688](https://github.com/OHIF/Viewers/issues/4688)) ([55ad8ef](https://github.com/OHIF/Viewers/commit/55ad8efbabc3fabd8031fc08927b2f92ae5aec69)) + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + + +### Bug Fixes + +* **seg:** sphere scissor on stack and cpu rendering reset properties was broken ([#4721](https://github.com/OHIF/Viewers/issues/4721)) ([f00d182](https://github.com/OHIF/Viewers/commit/f00d18292f02e8910215d913edfc994850a68d88)) + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + + +### Bug Fixes + +* **ui:** Update dependencies and add missing icons ([#4699](https://github.com/OHIF/Viewers/issues/4699)) ([cf97fa9](https://github.com/OHIF/Viewers/commit/cf97fa9b7b9687a9b73c1cf6926bc9fbc39b6512)) + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + + +### Bug Fixes + +* Having sop instance in a per-frame or shared attribute breaks load ([#4560](https://github.com/OHIF/Viewers/issues/4560)) ([cded082](https://github.com/OHIF/Viewers/commit/cded08261788143e0d5be57a55c927fd96aafb22)) + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + + +### Bug Fixes + +* **docker:** publish manifest for multiarch and update cs3d ([#4650](https://github.com/OHIF/Viewers/issues/4650)) ([836e67a](https://github.com/OHIF/Viewers/commit/836e67a6ab8de66d8908c75856774318729544f4)) + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + + +### Bug Fixes + +* **tools:** enable additional tools in volume viewport ([#4620](https://github.com/OHIF/Viewers/issues/4620)) ([1992002](https://github.com/OHIF/Viewers/commit/1992002d2dced171c17b9a0163baf707fc551e3d)) + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + + +### Features + +* migrate icons to ui-next ([#4606](https://github.com/OHIF/Viewers/issues/4606)) ([4e2ae32](https://github.com/OHIF/Viewers/commit/4e2ae328744ed95589c2cdf7a531454a25bf88b5)) + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + + +### Features + +* **node:** move to node 20 ([#4594](https://github.com/OHIF/Viewers/issues/4594)) ([1f04d6c](https://github.com/OHIF/Viewers/commit/1f04d6c1be729a26fe7bcda923770a1cd461053c)) + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + + +### Bug Fixes + +* **CinePlayer:** always show cine player for dynamic data ([#4575](https://github.com/OHIF/Viewers/issues/4575)) ([b8e8bbe](https://github.com/OHIF/Viewers/commit/b8e8bbe482b66e8cbe9167d03e9d8dedd2d3b6c5)) + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + + +### Bug Fixes + +* **multiframe:** metadata handling of NM studies and loading order ([#4554](https://github.com/OHIF/Viewers/issues/4554)) ([7624ccb](https://github.com/OHIF/Viewers/commit/7624ccb5e495c0a151227a458d8d5bfb8babb22c)) + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + + +### Bug Fixes + +* **colorlut:** use the correct colorlut index and update vtk ([#4544](https://github.com/OHIF/Viewers/issues/4544)) ([b9c26e7](https://github.com/OHIF/Viewers/commit/b9c26e775a49044673473418dd5bdee2e5562ab9)) + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + + +### Bug Fixes + +* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed)) + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + + +### Bug Fixes + +* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30)) + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + + +### Bug Fixes + +* **tmtv:** fix toggle one up weird behaviours ([#4473](https://github.com/OHIF/Viewers/issues/4473)) ([aa2b649](https://github.com/OHIF/Viewers/commit/aa2b649444eb4fe5422e72ea7830a709c4d24a90)) + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + + +### Bug Fixes + +* build ([#4471](https://github.com/OHIF/Viewers/issues/4471)) ([3d11ef2](https://github.com/OHIF/Viewers/commit/3d11ef28f213361ec7586809317bd219fa70e742)) + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + + +### Features + +* **new-study-panel:** default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser ([#4417](https://github.com/OHIF/Viewers/issues/4417)) ([a7fd9fa](https://github.com/OHIF/Viewers/commit/a7fd9fa5bfff7a1b533d99cb96f7147a35fd528f)) + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + + +### Features + +* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3)) + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + + +### Features + +* **studies-panel:** New OHIF study panel - under experimental flag ([#4254](https://github.com/OHIF/Viewers/issues/4254)) ([7a96406](https://github.com/OHIF/Viewers/commit/7a96406a116e46e62c396855fa64f434e2984b58)) + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + + +### Features + +* Add CS3D WSI and Video Viewports and add annotation navigation for MPR ([#4182](https://github.com/OHIF/Viewers/issues/4182)) ([7599ec9](https://github.com/OHIF/Viewers/commit/7599ec9421129dcade94e6fa6ec7908424ab3134)) + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + + +### Bug Fixes + +* Cobb angle not working in basic-test mode and open contour ([#4280](https://github.com/OHIF/Viewers/issues/4280)) ([6fd3c7e](https://github.com/OHIF/Viewers/commit/6fd3c7e293fec851dd30e650c1347cc0bc7a99ee)) +* webpack import bugs showing warnings on import ([#4265](https://github.com/OHIF/Viewers/issues/4265)) ([24c511f](https://github.com/OHIF/Viewers/commit/24c511f4bc04c4143bbd3d0d48029f41f7f36014)) + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) + + +### Features + +* customization service append and customize functionality should run once ([#4238](https://github.com/OHIF/Viewers/issues/4238)) ([e462fd3](https://github.com/OHIF/Viewers/commit/e462fd31f7944acfee34f08cfbc28cfd9de16169)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + + +### Bug Fixes + +* **seg:** maintain algorithm name and algorithm type when DICOM seg is exported or downloaded ([#4203](https://github.com/OHIF/Viewers/issues/4203)) ([a29e94d](https://github.com/OHIF/Viewers/commit/a29e94de803f79bbb3372d00ad8eb14b4224edc2)) + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + + +### Bug Fixes + +* **tmtv:** crosshairs should not have viewport indicators ([#4197](https://github.com/OHIF/Viewers/issues/4197)) ([f85da32](https://github.com/OHIF/Viewers/commit/f85da32f34389ef7cecae03c07e0af26468b52a6)) + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + + +### Bug Fixes + +* **ultrasound:** Upgrade cornerstone3D version to resolve coloring issues ([#4181](https://github.com/OHIF/Viewers/issues/4181)) ([75a71db](https://github.com/OHIF/Viewers/commit/75a71db7f89840250ad1c2b35df5a35aceb8be7d)) + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + + +### Features + +* **measurements:** show untracked measurements in measurement panel under additional findings ([#4160](https://github.com/OHIF/Viewers/issues/4160)) ([18686c2](https://github.com/OHIF/Viewers/commit/18686c2caf13ede3e881303100bd4cc34b8b135f)) + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + + +### Bug Fixes + +* **crosshairs:** reset angle, position, and slabthickness for crosshairs when reset viewport tool is used ([#4113](https://github.com/OHIF/Viewers/issues/4113)) ([73d9e99](https://github.com/OHIF/Viewers/commit/73d9e99d5d6f38ab6c36f4471d54f18798feacb4)) + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + + +### Bug Fixes + +* dicom json for orthanc by Update package versions for [@cornerstonejs](https://github.com/cornerstonejs) dependencies ([#4165](https://github.com/OHIF/Viewers/issues/4165)) ([34c7d72](https://github.com/OHIF/Viewers/commit/34c7d72142847486b98c9c52469940083eeaf87e)) + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + + +### Bug Fixes + +* **segmentation:** Address issue where segmentation creation failed on layout change ([#4153](https://github.com/OHIF/Viewers/issues/4153)) ([29944c8](https://github.com/OHIF/Viewers/commit/29944c8512c35718af03c03ef82bc43675ee1872)) + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + + +### Bug Fixes + +* **rt:** enhanced RT support, utilize SVGs for rendering. ([#4074](https://github.com/OHIF/Viewers/issues/4074)) ([0156bc4](https://github.com/OHIF/Viewers/commit/0156bc426f1840ae0d090223e94a643726e856cb)) + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + + +### Bug Fixes + +* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f)) + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + + +### Bug Fixes + +* **bugs:** fix patient header for doc, track ball rotate resize observer and add segmentation button not being enabled on viewport data change ([#4068](https://github.com/OHIF/Viewers/issues/4068)) ([c09311d](https://github.com/OHIF/Viewers/commit/c09311d3b7df05fcd00a9f36a7233e9d7e5589d0)) + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + + +### Bug Fixes + +* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951)) + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + + +### Features + +* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb)) + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + + +### Bug Fixes + +* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af)) + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + + +### Bug Fixes + +* **bugs:** and replace seriesInstanceUID and seriesInstanceUIDs URL with seriesInstanceUIDs ([#4049](https://github.com/OHIF/Viewers/issues/4049)) ([da7c1a5](https://github.com/OHIF/Viewers/commit/da7c1a5d8c54bfa1d3f97bbc500386bf76e7fd9d)) + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes - final ([#4048](https://github.com/OHIF/Viewers/issues/4048)) ([170bb96](https://github.com/OHIF/Viewers/commit/170bb96983082c39b22b7352e0c54aacf3e73b02)) + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes - more ([#4043](https://github.com/OHIF/Viewers/issues/4043)) ([3754c22](https://github.com/OHIF/Viewers/commit/3754c224b4dab28182adb0a41e37d890942144d8)) + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + + +### Bug Fixes + +* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025)) + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637)) + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + + +### Features + +* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13)) + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + + +### Features + +* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2)) diff --git a/extensions/cornerstone-dynamic-volume/LICENSE b/extensions/cornerstone-dynamic-volume/LICENSE new file mode 100644 index 0000000..24728a7 --- /dev/null +++ b/extensions/cornerstone-dynamic-volume/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2023 cornerstone-dynamic-volume () + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/extensions/cornerstone-dynamic-volume/README.md b/extensions/cornerstone-dynamic-volume/README.md new file mode 100644 index 0000000..7094980 --- /dev/null +++ b/extensions/cornerstone-dynamic-volume/README.md @@ -0,0 +1,8 @@ +# cornerstone-dynamic-volume +## Description + +## Author +OHIF + +## License +MIT diff --git a/extensions/cornerstone-dynamic-volume/babel.config.js b/extensions/cornerstone-dynamic-volume/babel.config.js new file mode 100644 index 0000000..325ca2a --- /dev/null +++ b/extensions/cornerstone-dynamic-volume/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/extensions/cornerstone-dynamic-volume/package.json b/extensions/cornerstone-dynamic-volume/package.json new file mode 100644 index 0000000..d157ac2 --- /dev/null +++ b/extensions/cornerstone-dynamic-volume/package.json @@ -0,0 +1,49 @@ +{ + "name": "@ohif/extension-cornerstone-dynamic-volume", + "version": "3.10.0-beta.111", + "description": "OHIF extension for 4D volumes data", + "author": "OHIF", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/ohif-extension-cornerstone-dynamic-volume.umd.js", + "module": "src/index.ts", + "exports": { + ".": "./src/index.ts", + "./types": "./src/types/index.ts" + }, + "files": [ + "dist", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.111", + "@ohif/extension-cornerstone": "3.10.0-beta.111", + "@ohif/extension-default": "3.10.0-beta.111", + "@ohif/i18n": "3.10.0-beta.111", + "@ohif/ui": "3.10.0-beta.111", + "dcmjs": "*", + "dicom-parser": "^1.8.21", + "hammerjs": "^2.0.8", + "prop-types": "^15.6.2", + "react": "^18.3.1" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "@cornerstonejs/core": "^2.19.14", + "@cornerstonejs/tools": "^2.19.14", + "classnames": "^2.3.2" + } +} diff --git a/extensions/cornerstone-dynamic-volume/src/actions/index.ts b/extensions/cornerstone-dynamic-volume/src/actions/index.ts new file mode 100644 index 0000000..45d2adf --- /dev/null +++ b/extensions/cornerstone-dynamic-volume/src/actions/index.ts @@ -0,0 +1,3 @@ +import updateSegmentationsChartDisplaySet from './updateSegmentationsChartDisplaySet'; + +export { updateSegmentationsChartDisplaySet }; diff --git a/extensions/cornerstone-dynamic-volume/src/actions/updateSegmentationsChartDisplaySet.ts b/extensions/cornerstone-dynamic-volume/src/actions/updateSegmentationsChartDisplaySet.ts new file mode 100644 index 0000000..78df25a --- /dev/null +++ b/extensions/cornerstone-dynamic-volume/src/actions/updateSegmentationsChartDisplaySet.ts @@ -0,0 +1,278 @@ +import { DicomMetadataStore, utils } from '@ohif/core'; + +import * as cs from '@cornerstonejs/core'; +import * as csTools from '@cornerstonejs/tools'; + +const CHART_MODALITY = 'CHT'; +const SEG_CHART_INSTANCE_UID = utils.guid(); + +// Private SOPClassUid for chart data +const ChartDataSOPClassUid = '1.9.451.13215.7.3.2.7.6.1'; + +const { utilities: csToolsUtils } = csTools; + +function _getDateTimeStr() { + const now = new Date(); + const date = + now.getFullYear() + ('0' + now.getUTCMonth()).slice(-2) + ('0' + now.getUTCDate()).slice(-2); + const time = + ('0' + now.getUTCHours()).slice(-2) + + ('0' + now.getUTCMinutes()).slice(-2) + + ('0' + now.getUTCSeconds()).slice(-2); + + return { date, time }; +} + +function _getTimePointsDataByTagName(volume, timePointsTag) { + const uniqueTimePoints = volume.imageIds.reduce((timePoints, imageId) => { + const instance = DicomMetadataStore.getInstanceByImageId(imageId); + const timePointValue = instance[timePointsTag]; + + if (timePointValue !== undefined) { + timePoints.add(timePointValue); + } + + return timePoints; + }, new Set()); + + return Array.from(uniqueTimePoints).sort((a: number, b: number) => a - b); +} + +function _convertTimePointsUnit(timePoints, timePointsUnit) { + const validUnits = ['ms', 's', 'm', 'h']; + const divisors = [1000, 60, 60]; + const currentUnitIndex = validUnits.indexOf(timePointsUnit); + let divisor = 1; + + if (currentUnitIndex !== -1) { + for (let i = currentUnitIndex; i < validUnits.length - 1; i++) { + const newDivisor = divisor * divisors[i]; + const greaterThanDivisorCount = timePoints.filter(timePoint => timePoint > newDivisor).length; + + // Change the scale only if more than 50% of the time points are + // greater than the new divisor. + if (greaterThanDivisorCount <= timePoints.length / 2) { + break; + } + + divisor = newDivisor; + timePointsUnit = validUnits[i + 1]; + } + + if (divisor > 1) { + timePoints = timePoints.map(timePoint => timePoint / divisor); + } + } + + return { timePoints, timePointsUnit }; +} + +// It currently supports only one tag but a few other will be added soon +// Supported 4D Tags +// (0018,1060) Trigger Time [NOK] +// (0018,0081) Echo Time [NOK] +// (0018,0086) Echo Number [NOK] +// (0020,0100) Temporal Position Identifier [NOK] +// (0054,1300) FrameReferenceTime [OK] +function _getTimePointsData(volume) { + const timePointsTags = { + FrameReferenceTime: { + unit: 'ms', + }, + }; + + const timePointsTagNames = Object.keys(timePointsTags); + let timePoints; + let timePointsUnit; + + for (let i = 0; i < timePointsTagNames.length; i++) { + const tagName = timePointsTagNames[i]; + const curTimePoints = _getTimePointsDataByTagName(volume, tagName); + + if (curTimePoints.length) { + timePoints = curTimePoints; + timePointsUnit = timePointsTags[tagName].unit; + break; + } + } + + if (!timePoints.length) { + const concatTagNames = timePointsTagNames.join(', '); + + throw new Error(`Could not extract time points data for the following tags: ${concatTagNames}`); + } + + const convertedTimePoints = _convertTimePointsUnit(timePoints, timePointsUnit); + + timePoints = convertedTimePoints.timePoints; + timePointsUnit = convertedTimePoints.timePointsUnit; + + return { timePoints, timePointsUnit }; +} + +function _getSegmentationData( + segmentation, + volumesTimePointsCache, + { servicesManager }: { servicesManager: AppTypes.ServicesManager } +) { + const { displaySetService, segmentationService, viewportGridService } = servicesManager.services; + const displaySets = displaySetService.getActiveDisplaySets(); + + const dynamic4DDisplaySet = displaySets.find(displaySet => { + const anInstance = displaySet.instances?.[0]; + + if (anInstance) { + return ( + anInstance.FrameReferenceTime !== undefined || anInstance.NumberOfTimeSlices !== undefined + ); + } + + return false; + }); + + // const referencedDynamicVolume = cs.cache.getVolume(dynamic4DDisplaySet.displaySetInstanceUID); + let volumeCacheKey: string | undefined; + const volumeId = dynamic4DDisplaySet.displaySetInstanceUID; + + for (const [key] of cs.cache._volumeCache) { + if (key.includes(volumeId)) { + volumeCacheKey = key; + break; + } + } + + let referencedDynamicVolume; + if (volumeCacheKey) { + referencedDynamicVolume = cs.cache.getVolume(volumeCacheKey); + } + + const { StudyInstanceUID, StudyDescription } = DicomMetadataStore.getInstanceByImageId( + referencedDynamicVolume.imageIds[0] + ); + + const segmentationVolume = segmentationService.getLabelmapVolume(segmentation.segmentationId); + const maskVolumeId = segmentationVolume?.volumeId; + + const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, { + maskVolumeId, + }) as number[][]; + + const pixelCount = timeData.length; + + if (pixelCount === 0) { + return []; + } + + // Todo: this is useless we should be able to grab color with just segRepUID and segmentIndex + // const color = csTools.segmentation.config.color.getSegmentIndexColor( + // segmentationRepresentationUID, + // 1 // segmentIndex + // ); + const viewportId = viewportGridService.getActiveViewportId(); + const color = segmentationService.getSegmentColor(viewportId, segmentation.segmentationId, 1); + + const hexColor = cs.utilities.color.rgbToHex(color[0], color[1], color[2]); + let timePointsData = volumesTimePointsCache.get(referencedDynamicVolume); + + if (!timePointsData) { + timePointsData = _getTimePointsData(referencedDynamicVolume); + volumesTimePointsCache.set(referencedDynamicVolume, timePointsData); + } + + const { timePoints, timePointsUnit } = timePointsData; + + if (timePoints.length !== timeData[0].length) { + throw new Error('Invalid number of time points returned'); + } + + const timepointsCount = timePoints.length; + const chartSeriesData = new Array(timepointsCount); + + for (let i = 0; i < timepointsCount; i++) { + const average = timeData.reduce((acc, cur) => acc + cur[i] / pixelCount, 0); + + chartSeriesData[i] = [timePoints[i], average]; + } + + return { + StudyInstanceUID, + StudyDescription, + chartData: { + series: { + label: segmentation.label, + points: chartSeriesData, + color: hexColor, + }, + axis: { + x: { + label: `Time (${timePointsUnit})`, + }, + y: { + label: `Vl (Bq/ml)`, + }, + }, + }, + }; +} + +function _getInstanceFromSegmentations(segmentations, { servicesManager }) { + if (!segmentations.length) { + return; + } + + const volumesTimePointsCache = new WeakMap(); + const segmentationsData = segmentations.map(segmentation => + _getSegmentationData(segmentation, volumesTimePointsCache, { servicesManager }) + ); + + const { date: seriesDate, time: seriesTime } = _getDateTimeStr(); + const series = segmentationsData.reduce((allSeries, curSegData) => { + return [...allSeries, curSegData.chartData.series]; + }, []); + + const instance = { + SOPClassUID: ChartDataSOPClassUid, + Modality: CHART_MODALITY, + SOPInstanceUID: utils.guid(), + SeriesDate: seriesDate, + SeriesTime: seriesTime, + SeriesInstanceUID: SEG_CHART_INSTANCE_UID, + StudyInstanceUID: segmentationsData[0].StudyInstanceUID, + StudyDescription: segmentationsData[0].StudyDescription, + SeriesNumber: 100, + SeriesDescription: 'Segmentation chart series data', + chartData: { + series, + axis: { ...segmentationsData[0].chartData.axis }, + }, + }; + + const seriesMetadata = { + StudyInstanceUID: instance.StudyInstanceUID, + StudyDescription: instance.StudyDescription, + SeriesInstanceUID: instance.SeriesInstanceUID, + SeriesDescription: instance.SeriesDescription, + SeriesNumber: instance.SeriesNumber, + SeriesTime: instance.SeriesTime, + SOPClassUID: instance.SOPClassUID, + Modality: instance.Modality, + }; + + return { seriesMetadata, instance }; +} + +function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void { + debugger; + const { segmentationService } = servicesManager.services; + const segmentations = segmentationService.getSegmentations(); + const { seriesMetadata, instance } = + _getInstanceFromSegmentations(segmentations, { servicesManager }) ?? {}; + + if (seriesMetadata && instance) { + // An event is triggered after adding the instance and the displaySet is created + DicomMetadataStore.addSeriesMetadata([seriesMetadata], true); + DicomMetadataStore.addInstances([instance], true); + } +} + +export { updateSegmentationsChartDisplaySet as default }; diff --git a/extensions/cornerstone-dynamic-volume/src/commandsModule.ts b/extensions/cornerstone-dynamic-volume/src/commandsModule.ts new file mode 100644 index 0000000..34422be --- /dev/null +++ b/extensions/cornerstone-dynamic-volume/src/commandsModule.ts @@ -0,0 +1,412 @@ +import * as importedActions from './actions'; +import { utilities, Enums } from '@cornerstonejs/tools'; +import { cache } from '@cornerstonejs/core'; + +const LABELMAP = Enums.SegmentationRepresentations.Labelmap; + +const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => { + const services = servicesManager.services; + const { displaySetService, viewportGridService, segmentationService } = services; + + const actions = { + ...importedActions, + getDynamic4DDisplaySet: () => { + const displaySets = displaySetService.getActiveDisplaySets(); + + const dynamic4DDisplaySet = displaySets.find(displaySet => { + const anInstance = displaySet.instances?.[0]; + + if (anInstance) { + return ( + anInstance.FrameReferenceTime !== undefined || + anInstance.NumberOfTimeSlices !== undefined || + anInstance.TemporalPositionIdentifier !== undefined + ); + } + + return false; + }); + + return dynamic4DDisplaySet; + }, + getComputedDisplaySets: () => { + const displaySetCache = displaySetService.getDisplaySetCache(); + const cachedDisplaySets = [...displaySetCache.values()]; + const computedDisplaySets = cachedDisplaySets.filter(displaySet => { + return displaySet.isDerived; + }); + return computedDisplaySets; + }, + exportTimeReportCSV: ({ segmentations, config, options, summaryStats }) => { + const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet(); + + const volumeId = dynamic4DDisplaySet?.displaySetInstanceUID; + + // cache._volumeCache is a map that has a key that includes the volumeId + // it is not exactly the volumeId, but it is the key that includes the volumeId + // so we can't do cache._volumeCache.get(volumeId) we should iterate + // over the keys and find the one that includes the volumeId + let volumeCacheKey: string | undefined; + + for (const [key] of cache._volumeCache) { + if (key.includes(volumeId)) { + volumeCacheKey = key; + break; + } + } + + let dynamicVolume; + if (volumeCacheKey) { + dynamicVolume = cache.getVolume(volumeCacheKey); + } + + const instance = dynamic4DDisplaySet.instances[0]; + + const csv = []; + + // CSV header information with placeholder empty values for the metadata lines + csv.push(`Patient ID,${instance.PatientID},`); + csv.push(`Study Date,${instance.StudyDate},`); + csv.push(`StudyInstanceUID,${instance.StudyInstanceUID},`); + csv.push(`StudyDescription,${instance.StudyDescription},`); + csv.push(`SeriesInstanceUID,${instance.SeriesInstanceUID},`); + + // empty line + csv.push(''); + csv.push(''); + + // Helper function to calculate standard deviation + function calculateStandardDeviation(data) { + const n = data.length; + const mean = data.reduce((acc, value) => acc + value, 0) / n; + const squaredDifferences = data.map(value => (value - mean) ** 2); + const variance = squaredDifferences.reduce((acc, value) => acc + value, 0) / n; + const stdDeviation = Math.sqrt(variance); + return stdDeviation; + } + // Iterate through each segmentation to get the timeData and ijkCoords + segmentations.forEach(segmentation => { + const volume = segmentationService.getLabelmapVolume(segmentation.segmentationId); + const [timeData, ijkCoords] = utilities.dynamicVolume.getDataInTime(dynamicVolume, { + maskVolumeId: volume.volumeId, + }) as number[][]; + + if (summaryStats) { + // Adding column headers for pixel identifier and segmentation label ids + let headers = 'Operation,Segmentation Label ID'; + const maxLength = dynamicVolume.numTimePoints; + for (let t = 0; t < maxLength; t++) { + headers += `,Time Point ${t}`; + } + csv.push(headers); + // // perform summary statistics on the timeData including for each time point, mean, median, min, max, and standard deviation for + // // all the voxels in the ROI + const mean = []; + const min = []; + const minIJK = []; + const max = []; + const maxIJK = []; + const std = []; + + const numVoxels = timeData.length; + // Helper function to calculate standard deviation + for (let timeIndex = 0; timeIndex < maxLength; timeIndex++) { + // for each voxel in the ROI, get the value at the current time point + const voxelValues = []; + let sum = 0; + let minValue = Infinity; + let maxValue = -Infinity; + let minIndex = 0; + let maxIndex = 0; + + // Single pass through the data to collect all needed values + for (let voxelIndex = 0; voxelIndex < numVoxels; voxelIndex++) { + const value = timeData[voxelIndex][timeIndex]; + voxelValues.push(value); + sum += value; + + if (value < minValue) { + minValue = value; + minIndex = voxelIndex; + } + if (value > maxValue) { + maxValue = value; + maxIndex = voxelIndex; + } + } + + mean.push(sum / numVoxels); + min.push(minValue); + minIJK.push(ijkCoords[minIndex]); + max.push(maxValue); + maxIJK.push(ijkCoords[maxIndex]); + std.push(calculateStandardDeviation(voxelValues)); + } + + let row = `Mean,${segmentation.label}`; + // Generate separate rows for each statistic + for (let t = 0; t < maxLength; t++) { + row += `,${mean[t]}`; + } + + csv.push(row); + + row = `Standard Deviation,${segmentation.label}`; + for (let t = 0; t < maxLength; t++) { + row += `,${std[t]}`; + } + + csv.push(row); + + row = `Min,${segmentation.label}`; + for (let t = 0; t < maxLength; t++) { + row += `,${min[t]}`; + } + + csv.push(row); + + row = `Max,${segmentation.label}`; + for (let t = 0; t < maxLength; t++) { + row += `,${max[t]}`; + } + + csv.push(row); + } else { + // Adding column headers for pixel identifier and segmentation label ids + let headers = 'Pixel Identifier (IJK),Segmentation Label ID'; + const maxLength = dynamicVolume.numTimePoints; + for (let t = 0; t < maxLength; t++) { + headers += `,Time Point ${t}`; + } + csv.push(headers); + // Assuming timeData and ijkCoords are of the same length + for (let i = 0; i < timeData.length; i++) { + // Generate the pixel identifier + const pixelIdentifier = `${ijkCoords[i][0]}_${ijkCoords[i][1]}_${ijkCoords[i][2]}`; + + // Start a new row for the current pixel + let row = `${pixelIdentifier},${segmentation.label}`; + + // Add time data points for this pixel + for (let t = 0; t < timeData[i].length; t++) { + row += `,${timeData[i][t]}`; + } + + // Append the row to the CSV array + csv.push(row); + } + } + }); + + // Convert to CSV string + const csvContent = csv.join('\n'); + + // Generate filename and trigger download + const filename = `${instance.PatientID}.csv`; + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + const url = URL.createObjectURL(blob); + link.setAttribute('href', url); + link.setAttribute('download', filename); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }, + swapDynamicWithComputedDisplaySet: ({ displaySet }) => { + const computedDisplaySet = displaySet; + + const displaySetCache = displaySetService.getDisplaySetCache(); + const cachedDisplaySetKeys = [displaySetCache.keys()]; + const { displaySetInstanceUID } = computedDisplaySet; + // Check to see if computed display set is already in cache + if (!cachedDisplaySetKeys.includes(displaySetInstanceUID)) { + displaySetCache.set(displaySetInstanceUID, computedDisplaySet); + } + + // Get all viewports and their corresponding indices + const { viewports } = viewportGridService.getState(); + + // get the viewports in the grid + // iterate over them and find the ones that are showing a dynamic + // volume (displaySet), and replace that exact displaySet with the + // computed displaySet + + const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet(); + + const viewportsToUpdate = []; + + for (const [key, value] of viewports) { + const viewport = value; + const viewportOptions = viewport.viewportOptions; + const { displaySetInstanceUIDs } = viewport; + const displaySetInstanceUIDIndex = displaySetInstanceUIDs.indexOf( + dynamic4DDisplaySet.displaySetInstanceUID + ); + if (displaySetInstanceUIDIndex !== -1) { + const newViewport = { + viewportId: viewport.viewportId, + // merge the other displaySetInstanceUIDs with the new one + displaySetInstanceUIDs: [ + ...displaySetInstanceUIDs.slice(0, displaySetInstanceUIDIndex), + displaySetInstanceUID, + ...displaySetInstanceUIDs.slice(displaySetInstanceUIDIndex + 1), + ], + viewportOptions: { + initialImageOptions: viewportOptions.initialImageOptions, + viewportType: 'volume', + orientation: viewportOptions.orientation, + background: viewportOptions.background, + }, + }; + viewportsToUpdate.push(newViewport); + } + } + + viewportGridService.setDisplaySetsForViewports(viewportsToUpdate); + }, + swapComputedWithDynamicDisplaySet: () => { + // Todo: this assumes there is only one dynamic display set in the viewer + const dynamicDisplaySet = actions.getDynamic4DDisplaySet(); + + const displaySetCache = displaySetService.getDisplaySetCache(); + const cachedDisplaySetKeys = [...displaySetCache.keys()]; // Fix: Spread to get the array + const { displaySetInstanceUID } = dynamicDisplaySet; + + // Check to see if dynamic display set is already in cache + if (!cachedDisplaySetKeys.includes(displaySetInstanceUID)) { + displaySetCache.set(displaySetInstanceUID, dynamicDisplaySet); + } + + // Get all viewports and their corresponding indices + const { viewports } = viewportGridService.getState(); + + // Get the computed 4D display set + const computed4DDisplaySet = actions.getComputedDisplaySets()[0]; + + const viewportsToUpdate = []; + + for (const [key, value] of viewports) { + const viewport = value; + const viewportOptions = viewport.viewportOptions; + const { displaySetInstanceUIDs } = viewport; + const displaySetInstanceUIDIndex = displaySetInstanceUIDs.indexOf( + computed4DDisplaySet.displaySetInstanceUID + ); + if (displaySetInstanceUIDIndex !== -1) { + const newViewport = { + viewportId: viewport.viewportId, + // merge the other displaySetInstanceUIDs with the new one + displaySetInstanceUIDs: [ + ...displaySetInstanceUIDs.slice(0, displaySetInstanceUIDIndex), + displaySetInstanceUID, + ...displaySetInstanceUIDs.slice(displaySetInstanceUIDIndex + 1), + ], + viewportOptions: { + initialImageOptions: viewportOptions.initialImageOptions, + viewportType: 'volume', + orientation: viewportOptions.orientation, + background: viewportOptions.background, + }, + }; + viewportsToUpdate.push(newViewport); + } + } + + viewportGridService.setDisplaySetsForViewports(viewportsToUpdate); + }, + createNewLabelMapForDynamicVolume: async ({ label }) => { + const { viewports, activeViewportId } = viewportGridService.getState(); + + // get the dynamic 4D display set + const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet(); + const dynamic4DDisplaySetInstanceUID = dynamic4DDisplaySet.displaySetInstanceUID; + + // check if the dynamic 4D display set is in the display, if not we might have + // the computed volumes and we should choose them for the segmentation + // creation + + let referenceDisplaySet; + + const activeViewport = viewports.get(activeViewportId); + const activeDisplaySetInstanceUIDs = activeViewport.displaySetInstanceUIDs; + const dynamicIsInActiveViewport = activeDisplaySetInstanceUIDs.includes( + dynamic4DDisplaySetInstanceUID + ); + + if (dynamicIsInActiveViewport) { + referenceDisplaySet = dynamic4DDisplaySet; + } + + if (!referenceDisplaySet) { + // try to see if there is any derived displaySet in the active viewport + // which is referencing the dynamic 4D display set + + // Todo: this is wrong but I don't have time to fix it now + const cachedDisplaySets = displaySetService.getDisplaySetCache(); + for (const [key, displaySet] of cachedDisplaySets) { + if (displaySet.referenceDisplaySetUID === dynamic4DDisplaySetInstanceUID) { + referenceDisplaySet = displaySet; + break; + } + } + } + + if (!referenceDisplaySet) { + throw new Error('No reference display set found based on the dynamic data'); + } + + const displaySet = displaySetService.getDisplaySetByUID( + referenceDisplaySet.displaySetInstanceUID + ); + + const segmentationId = await segmentationService.createLabelmapForDisplaySet(displaySet, { + label, + }); + + const firstViewport = viewports.values().next().value; + + await segmentationService.addSegmentationRepresentation(firstViewport.viewportId, { + segmentationId, + }); + + return segmentationId; + }, + }; + + const definitions = { + updateSegmentationsChartDisplaySet: { + commandFn: actions.updateSegmentationsChartDisplaySet, + storeContexts: [], + options: {}, + }, + exportTimeReportCSV: { + commandFn: actions.exportTimeReportCSV, + storeContexts: [], + options: {}, + }, + swapDynamicWithComputedDisplaySet: { + commandFn: actions.swapDynamicWithComputedDisplaySet, + storeContexts: [], + options: {}, + }, + createNewLabelMapForDynamicVolume: { + commandFn: actions.createNewLabelMapForDynamicVolume, + storeContexts: [], + options: {}, + }, + swapComputedWithDynamicDisplaySet: { + commandFn: actions.swapComputedWithDynamicDisplaySet, + storeContexts: [], + options: {}, + }, + }; + + return { + actions, + definitions, + defaultContext: 'DYNAMIC-VOLUME:CORNERSTONE', + }; +}; + +export default commandsModule; diff --git a/extensions/cornerstone-dynamic-volume/src/getHangingProtocolModule.ts b/extensions/cornerstone-dynamic-volume/src/getHangingProtocolModule.ts new file mode 100644 index 0000000..4ee5ed0 --- /dev/null +++ b/extensions/cornerstone-dynamic-volume/src/getHangingProtocolModule.ts @@ -0,0 +1,681 @@ +const DEFAULT_COLORMAP = '2hot'; +const toolGroupIds = { + pt: 'dynamic4D-pt', + fusion: 'dynamic4D-fusion', + ct: 'dynamic4D-ct', +}; + +function getPTOptions({ + colormap, + voiInverted, +}: { + colormap?: { + name: string; + opacity: + | number + | { + value: number; + opacity: number; + }[]; + }; + voiInverted?: boolean; +} = {}) { + return { + blendMode: 'MIP', + colormap, + voi: { + windowWidth: 5, + windowCenter: 2.5, + }, + voiInverted, + }; +} + +function getPTViewports() { + const ptOptionsParams = { + colormap: { + name: DEFAULT_COLORMAP, + opacity: [ + { value: 0, opacity: 0 }, + { value: 0.1, opacity: 1 }, + { value: 1, opacity: 1 }, + ], + }, + voiInverted: false, + }; + + return [ + { + viewportOptions: { + viewportId: 'ptAxial', + viewportType: 'volume', + orientation: 'axial', + toolGroupId: toolGroupIds.pt, + initialImageOptions: { + preset: 'middle', // 'first', 'last', 'middle' + }, + syncGroups: [ + { + type: 'cameraPosition', + id: 'axialSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ptWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ptDisplaySet', + options: { ...getPTOptions(ptOptionsParams) }, + }, + ], + }, + { + viewportOptions: { + viewportId: 'ptSagittal', + viewportType: 'volume', + orientation: 'sagittal', + toolGroupId: toolGroupIds.pt, + initialImageOptions: { + preset: 'middle', // 'first', 'last', 'middle' + }, + syncGroups: [ + { + type: 'cameraPosition', + id: 'sagittalSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ptWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ptDisplaySet', + options: { ...getPTOptions(ptOptionsParams) }, + }, + ], + }, + { + viewportOptions: { + viewportId: 'ptCoronal', + viewportType: 'volume', + orientation: 'coronal', + toolGroupId: toolGroupIds.pt, + initialImageOptions: { + preset: 'middle', // 'first', 'last', 'middle' + }, + syncGroups: [ + { + type: 'cameraPosition', + id: 'coronalSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ptWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ptDisplaySet', + options: { ...getPTOptions(ptOptionsParams) }, + }, + ], + }, + ]; +} + +function getFusionViewports() { + const ptOptionsParams = { + colormap: { + name: DEFAULT_COLORMAP, + opacity: [ + { value: 0, opacity: 0 }, + { value: 0.1, opacity: 0.8 }, + { value: 1, opacity: 0.8 }, + ], + }, + }; + + return [ + { + viewportOptions: { + viewportId: 'fusionAxial', + viewportType: 'volume', + orientation: 'axial', + toolGroupId: toolGroupIds.fusion, + initialImageOptions: { + preset: 'middle', // 'first', 'last', 'middle' + }, + syncGroups: [ + { + type: 'cameraPosition', + id: 'axialSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ctWLSync', + source: false, + target: true, + }, + { + type: 'voi', + id: 'fusionWLSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ptFusionWLSync', + source: false, + target: true, + options: { + syncInvertState: false, + }, + }, + { + type: 'hydrateseg', + id: 'sameFORId', + source: true, + target: true, + options: { + matchingRules: ['sameFOR'], + }, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + { + options: { ...getPTOptions(ptOptionsParams) }, + id: 'ptDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'fusionSagittal', + viewportType: 'volume', + orientation: 'sagittal', + toolGroupId: toolGroupIds.fusion, + initialImageOptions: { + preset: 'middle', // 'first', 'last', 'middle' + }, + syncGroups: [ + { + type: 'cameraPosition', + id: 'sagittalSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ctWLSync', + source: false, + target: true, + }, + { + type: 'voi', + id: 'fusionWLSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ptFusionWLSync', + source: false, + target: true, + options: { + syncInvertState: false, + }, + }, + { + type: 'hydrateseg', + id: 'sameFORId', + source: true, + target: true, + options: { + matchingRules: ['sameFOR'], + }, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + { + options: { ...getPTOptions(ptOptionsParams) }, + id: 'ptDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'fusionCoronal', + viewportType: 'volume', + orientation: 'coronal', + toolGroupId: toolGroupIds.fusion, + initialImageOptions: { + preset: 'middle', // 'first', 'last', 'middle' + }, + syncGroups: [ + { + type: 'cameraPosition', + id: 'coronalSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ctWLSync', + source: false, + target: true, + }, + { + type: 'voi', + id: 'fusionWLSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ptFusionWLSync', + source: false, + target: true, + options: { + syncInvertState: false, + }, + }, + { + type: 'hydrateseg', + id: 'sameFORId', + source: true, + target: true, + options: { + matchingRules: ['sameFOR'], + }, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + { + options: { ...getPTOptions(ptOptionsParams) }, + id: 'ptDisplaySet', + }, + ], + }, + ]; +} + +function getSeriesChartViewport() { + return { + viewportOptions: { + viewportId: 'seriesChart', + }, + displaySets: [ + { + id: 'chartDisplaySet', + options: { + // This dataset does not require the download of any instance since it is pre-computed locally, + // but interleaveTopToBottom.ts was not loading any series because it consider that all viewports + // are a Cornerstone viewport which is not true in this case and it waits for all viewports to + // have called interleaveTopToBottom(...). + skipLoading: true, + }, + }, + ], + }; +} + +function getCTViewports() { + return [ + { + viewportOptions: { + viewportId: 'ctAxial', + viewportType: 'volume', + orientation: 'axial', + toolGroupId: toolGroupIds.ct, + initialImageOptions: { + preset: 'middle', // 'first', 'last', 'middle' + }, + syncGroups: [ + { + type: 'cameraPosition', + id: 'axialSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ctWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'ctSagittal', + viewportType: 'volume', + orientation: 'sagittal', + toolGroupId: toolGroupIds.ct, + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [ + { + type: 'cameraPosition', + id: 'sagittalSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ctWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'ctCoronal', + viewportType: 'volume', + orientation: 'coronal', + toolGroupId: toolGroupIds.ct, + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [ + { + type: 'cameraPosition', + id: 'coronalSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ctWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + ], + }, + ]; +} + +const defaultProtocol = { + id: 'default4D', + locked: true, + // Don't store this hanging protocol as it applies to the currently active + // display set by default + // cacheId: null, + hasUpdatedPriorsInformation: false, + name: 'Default', + createdDate: '2023-01-01T00:00:00.000Z', + modifiedDate: '2023-01-01T00:00:00.000Z', + availableTo: {}, + editableBy: {}, + imageLoadStrategy: 'default', // "default" , "interleaveTopToBottom", "interleaveCenter" + protocolMatchingRules: [ + { + attribute: 'ModalitiesInStudy', + constraint: { + contains: ['CT', 'PT'], + }, + }, + ], + // -1 would be used to indicate active only, whereas other values are + // the number of required priors referenced - so 0 means active with + // 0 or more priors. + numberOfPriorsReferenced: -1, + displaySetSelectors: { + defaultDisplaySetId: { + // Unused currently + imageMatchingRules: [], + // Matches displaysets, NOT series + seriesMatchingRules: [ + // Try to match series with images by default, to prevent weird display + // on SEG/SR containing studies + { + attribute: 'numImageFrames', + constraint: { + greaterThan: { value: 0 }, + }, + }, + ], + // Can be used to select matching studies + // studyMatchingRules: [], + }, + ctDisplaySet: { + // Unused currently + imageMatchingRules: [], + // Matches displaysets, NOT series + seriesMatchingRules: [ + { + attribute: 'Modality', + constraint: { + equals: { + value: 'CT', + }, + }, + required: true, + }, + { + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + ], + // Can be used to select matching studies + // studyMatchingRules: [], + }, + ptDisplaySet: { + // Unused currently + imageMatchingRules: [], + // Matches displaysets, NOT series + seriesMatchingRules: [ + { + attribute: 'Modality', + constraint: { + equals: 'PT', + }, + required: true, + }, + { + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + { + attribute: 'SeriesDescription', + constraint: { + contains: 'Corrected', + }, + }, + { + weight: 2, + attribute: 'SeriesDescription', + constraint: { + doesNotContain: { + value: 'Uncorrected', + }, + }, + }, + + // Should we check if CorrectedImage contains ATTN? + // (0028,0051) (CorrectedImage): NORM\DTIM\ATTN\SCAT\RADL\DECY + ], + // Can be used to select matching studies + // studyMatchingRules: [], + }, + chartDisplaySet: { + // Unused currently + imageMatchingRules: [], + // Matches displaysets, NOT series + seriesMatchingRules: [ + { + attribute: 'Modality', + constraint: { + equals: { + value: 'CHT', + }, + }, + required: true, + }, + ], + }, + }, + stages: [ + { + id: 'dataPreparation', + name: 'Data Preparation', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 3, + }, + }, + viewports: [...getPTViewports()], + createdDate: '2023-01-01T00:00:00.000Z', + }, + + { + id: 'registration', + name: 'Registration', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 3, + columns: 3, + }, + }, + viewports: [...getFusionViewports(), ...getCTViewports(), ...getPTViewports()], + createdDate: '2023-01-01T00:00:00.000Z', + }, + + { + id: 'roiQuantification', + name: 'ROI Quantification', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 3, + }, + }, + viewports: [...getFusionViewports()], + createdDate: '2023-01-01T00:00:00.000Z', + }, + + { + id: 'kineticAnalysis', + name: 'Kinetic Analysis', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 2, + columns: 3, + layoutOptions: [ + { + x: 0, + y: 0, + width: 1 / 3, + height: 1 / 2, + }, + { + x: 1 / 3, + y: 0, + width: 1 / 3, + height: 1 / 2, + }, + { + x: 2 / 3, + y: 0, + width: 1 / 3, + height: 1 / 2, + }, + { + x: 0, + y: 1 / 2, + width: 1, + height: 1 / 2, + }, + ], + }, + }, + viewports: [...getFusionViewports(), getSeriesChartViewport()], + createdDate: '2023-01-01T00:00:00.000Z', + }, + ], +}; + +/** + * HangingProtocolModule should provide a list of hanging protocols that will be + * available in OHIF for Modes to use to decide on the structure of the viewports + * and also the series that hung in the viewports. Each hanging protocol is defined by + * { name, protocols}. Examples include the default hanging protocol provided by + * the default extension that shows 2x2 viewports. + */ + +function getHangingProtocolModule() { + return [ + { + name: defaultProtocol.id, + protocol: defaultProtocol, + }, + ]; +} + +export default getHangingProtocolModule; diff --git a/extensions/cornerstone-dynamic-volume/src/getPanelModule.tsx b/extensions/cornerstone-dynamic-volume/src/getPanelModule.tsx new file mode 100644 index 0000000..c4662df --- /dev/null +++ b/extensions/cornerstone-dynamic-volume/src/getPanelModule.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { DynamicDataPanel } from './panels'; +import { Toolbox } from '@ohif/ui-next'; +import { PanelSegmentation } from '@ohif/extension-cornerstone'; +import DynamicExport from './panels/DynamicExport'; + +function getPanelModule({ commandsManager, extensionManager, servicesManager, configuration }) { + const wrappedDynamicDataPanel = () => { + return ( + + ); + }; + + const wrappedDynamicSegmentation = () => { + return ( + <> + + + + + + ); + }; + + return [ + { + name: 'dynamic-volume', + iconName: 'tab-4d', + iconLabel: '4D Workflow', + label: '4D Workflow', + component: wrappedDynamicDataPanel, + }, + { + name: 'dynamic-segmentation', + iconName: 'tab-segmentation', + iconLabel: 'Segmentation', + label: 'Segmentation', + component: wrappedDynamicSegmentation, + }, + ]; +} + +export default getPanelModule; diff --git a/extensions/cornerstone-dynamic-volume/src/id.js b/extensions/cornerstone-dynamic-volume/src/id.js new file mode 100644 index 0000000..b2dfe18 --- /dev/null +++ b/extensions/cornerstone-dynamic-volume/src/id.js @@ -0,0 +1,6 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; +const SOPClassHandlerName = 'dynamic-volume'; + +export { id, SOPClassHandlerName }; diff --git a/extensions/cornerstone-dynamic-volume/src/index.ts b/extensions/cornerstone-dynamic-volume/src/index.ts new file mode 100644 index 0000000..625899e --- /dev/null +++ b/extensions/cornerstone-dynamic-volume/src/index.ts @@ -0,0 +1,57 @@ +import { id } from './id'; +import commandsModule from './commandsModule'; +import getPanelModule from './getPanelModule'; +import getHangingProtocolModule from './getHangingProtocolModule'; +import { cache } from '@cornerstonejs/core'; + +/** + * You can remove any of the following modules if you don't need them. + */ +const dynamicVolumeExtension = { + /** + * Only required property. Should be a unique value across all extensions. + * You ID can be anything you want, but it should be unique. + */ + id, + + /** + * Perform any pre-registration tasks here. This is called before the extension + * is registered. Usually we run tasks such as: configuring the libraries + * (e.g. cornerstone, cornerstoneTools, ...) or registering any services that + * this extension is providing. + */ + preRegistration: ({ servicesManager, commandsManager, configuration = {} }) => { + // TODO: look for the right fix + cache.setMaxCacheSize(5 * 1024 * 1024 * 1024); + }, + /** + * PanelModule should provide a list of panels that will be available in OHIF + * for Modes to consume and render. Each panel is defined by a {name, + * iconName, iconLabel, label, component} object. Example of a panel module + * is the StudyBrowserPanel that is provided by the default extension in OHIF. + */ + getPanelModule, + /** + * ViewportModule should provide a list of viewports that will be available in OHIF + * for Modes to consume and use in the viewports. Each viewport is defined by + * {name, component} object. Example of a viewport module is the CornerstoneViewport + * that is provided by the Cornerstone extension in OHIF. + */ + getHangingProtocolModule, + /** + * CommandsModule should provide a list of commands that will be available in OHIF + * for Modes to consume and use in the viewports. Each command is defined by + * an object of { actions, definitions, defaultContext } where actions is an + * object of functions, definitions is an object of available commands, their + * options, and defaultContext is the default context for the command to run against. + */ + getCommandsModule: ({ servicesManager, commandsManager, extensionManager }) => { + return commandsModule({ + servicesManager, + commandsManager, + extensionManager, + }); + }, +}; + +export { dynamicVolumeExtension as default }; diff --git a/extensions/cornerstone-dynamic-volume/src/panels/DynamicDataPanel.tsx b/extensions/cornerstone-dynamic-volume/src/panels/DynamicDataPanel.tsx new file mode 100644 index 0000000..f3f7a4d --- /dev/null +++ b/extensions/cornerstone-dynamic-volume/src/panels/DynamicDataPanel.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import PanelGenerateImage from './PanelGenerateImage'; + +function DynamicDataPanel({ servicesManager, commandsManager, tab }: withAppTypes) { + return ( + <> +
+ +
+ + ); +} + +export default DynamicDataPanel; diff --git a/extensions/cornerstone-dynamic-volume/src/panels/DynamicExport.tsx b/extensions/cornerstone-dynamic-volume/src/panels/DynamicExport.tsx new file mode 100644 index 0000000..9e8e4a3 --- /dev/null +++ b/extensions/cornerstone-dynamic-volume/src/panels/DynamicExport.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { Button, Icons } from '@ohif/ui-next'; +import { useSegmentations } from '@ohif/extension-cornerstone'; + +function DynamicExport({ commandsManager, servicesManager }: withAppTypes) { + const segmentations = useSegmentations({ servicesManager }); + + if (!segmentations?.length) { + return null; + } + + return ( +
+
+ +
+
+ +
+
+ ); +} + +export default DynamicExport; diff --git a/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx b/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx new file mode 100644 index 0000000..53aab39 --- /dev/null +++ b/extensions/cornerstone-dynamic-volume/src/panels/DynamicVolumeControls.tsx @@ -0,0 +1,224 @@ +import React, { useState } from 'react'; +import { Button, PanelSection, ButtonGroup, IconButton, InputNumber } from '@ohif/ui'; +import { Icons, Tooltip, TooltipTrigger, TooltipContent, Numeric } from '@ohif/ui-next'; +import { Enums } from '@cornerstonejs/core'; + +const controlClassNames = { + sizeClassName: 'w-[58px] h-[28px]', + arrowsDirection: 'horizontal', + labelPosition: 'bottom', +}; + +const Header = ({ title, tooltip }) => ( +
+ + + + + + + +
{tooltip}
+
+
+ {title} +
+); + +const DynamicVolumeControls = ({ + isPlaying, + onPlayPauseChange, + // fps + fps, + onFpsChange, + minFps, + maxFps, + // Frames + currentDimensionGroupNumber, + onDimensionGroupChange, + numDimensionGroups, + onGenerate, + onDoubleRangeChange, + rangeValues, + onDynamicClick, +}) => { + const [computedView, setComputedView] = useState(false); + const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM); + + return ( +
+ +
+
+ + + + +
+
+ +
+
+
+ Operation Buttons (SUM, AVERAGE, SUBTRACT): Select the mathematical operation to be + applied to the data set. +
Range Slider: Choose the numeric range of dimension groups within which the + operation will be performed. +
+ Generate Button: Execute the chosen operation on the specified range of data. +
+ } + /> + + + + + +
+ + + +
+ +
+ + + ); +}; + +export default DynamicVolumeControls; + +function DimensionGroupControls({ + isPlaying, + onPlayPauseChange, + fps, + minFps, + maxFps, + onFpsChange, + numDimensionGroups, + onDimensionGroupChange, + currentDimensionGroupNumber, + computedView, +}) { + const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play'); + + return ( +
+
+ Play/Pause Button: Begin or pause the animation of the 4D visualization.
+ Dimension Group Selector: Navigate through individual dimension groups of the 4D data.{' '} +
+ FPS (Frames Per Second) Selector: Adjust the playback speed of the animation. +
+ } + /> +
+ onPlayPauseChange(!isPlaying)} + > + + + + +
+ + ); +} diff --git a/extensions/cornerstone-dynamic-volume/src/panels/GenerateVolume.tsx b/extensions/cornerstone-dynamic-volume/src/panels/GenerateVolume.tsx new file mode 100644 index 0000000..516502a --- /dev/null +++ b/extensions/cornerstone-dynamic-volume/src/panels/GenerateVolume.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { InputDoubleRange } from '@ohif/ui'; +import { Select } from '@ohif/ui'; +import { Button } from '@ohif/ui'; +import PropTypes from 'prop-types'; + +const GenerateVolume = ({ + rangeValues, + handleSliderChange, + operationsUI, + options, + handleGenerateOptionsChange, + onGenerateImage, + returnTo4D, + displayingComputedVolume, +}) => { + return ( + <> +
+
Computed Image
+ + +
+ )} + + + {({ getRootProps, getInputProps }) => ( +
+ +
+ )} +
+ +
or drag images or folders here
+
(DICOM files supported)
+ + )} + + ); + }; + + return ( + <> + {dicomFileUploaderArr.length ? ( +
+ +
+ ) : ( +
{getDropZoneComponent()}
+ )} + + ); +} + +DicomUpload.propTypes = { + dataSource: PropTypes.object.isRequired, + onComplete: PropTypes.func.isRequired, + onStarted: PropTypes.func.isRequired, +}; + +export default DicomUpload; diff --git a/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx b/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx new file mode 100644 index 0000000..7f58172 --- /dev/null +++ b/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx @@ -0,0 +1,395 @@ +import React, { useCallback, useEffect, useRef, useState, ReactElement } from 'react'; +import PropTypes from 'prop-types'; +import { useSystem } from '@ohif/core'; +import { Button } from '@ohif/ui'; +import { Icons } from '@ohif/ui-next'; +import DicomFileUploader, { + EVENTS, + UploadStatus, + DicomFileUploaderProgressEvent, + UploadRejection, +} from '../../utils/DicomFileUploader'; +import DicomUploadProgressItem from './DicomUploadProgressItem'; +import classNames from 'classnames'; + +type DicomUploadProgressProps = { + dicomFileUploaderArr: DicomFileUploader[]; + onComplete: () => void; +}; + +const ONE_SECOND = 1000; +const ONE_MINUTE = ONE_SECOND * 60; +const ONE_HOUR = ONE_MINUTE * 60; + +// The base/initial interval time length used to calculate the +// rate of the upload and in turn estimate the +// the amount of time remaining for the upload. This is the length +// of the very first interval to get a reasonable estimate on screen in +// a reasonable amount of time. The length of each interval after the first +// is based on the upload rate calculated. Faster rates use this base interval +// length. Slower rates below UPLOAD_RATE_THRESHOLD get longer interval times +// to obtain more accurate upload rates. +const BASE_INTERVAL_TIME = 15000; + +// The upload rate threshold to determine the length of the interval to +// calculate the upload rate. +const UPLOAD_RATE_THRESHOLD = 75; + +const NO_WRAP_ELLIPSIS_CLASS_NAMES = 'text-ellipsis whitespace-nowrap overflow-hidden'; + +function DicomUploadProgress({ + dicomFileUploaderArr, + onComplete, +}: DicomUploadProgressProps): ReactElement { + const { servicesManager } = useSystem(); + + const ProgressLoadingBar = + servicesManager.services.customizationService.getCustomization('ui.progressLoadingBar'); + + const [totalUploadSize] = useState( + dicomFileUploaderArr.reduce((acc, fileUploader) => acc + fileUploader.getFileSize(), 0) + ); + + const currentUploadSizeRef = useRef(0); + + const uploadRateRef = useRef(0); + + const [timeRemaining, setTimeRemaining] = useState(null); + + const [percentComplete, setPercentComplete] = useState(0); + + const [numFilesCompleted, setNumFilesCompleted] = useState(0); + + const [numFails, setNumFails] = useState(0); + + const [showFailedOnly, setShowFailedOnly] = useState(false); + + const progressBarContainerRef = useRef(); + + /** + * The effect for measuring and setting the current upload rate. This is + * done by measuring the amount of data uploaded in a set interval time. + */ + useEffect(() => { + let timeoutId: NodeJS.Timeout; + + // The amount of data already uploaded at the start of the interval. + let intervalStartUploadSize = 0; + + // The starting time of the interval. + let intervalStartTime = Date.now(); + + const setUploadRateRef = () => { + const uploadSizeFromStartOfInterval = currentUploadSizeRef.current - intervalStartUploadSize; + + const now = Date.now(); + const timeSinceStartOfInterval = now - intervalStartTime; + + // Calculate and set the upload rate (ref) + uploadRateRef.current = uploadSizeFromStartOfInterval / timeSinceStartOfInterval; + + // Reset the interval starting values. + intervalStartUploadSize = currentUploadSizeRef.current; + intervalStartTime = now; + + // Only start a new interval if there is more to upload. + if (totalUploadSize - currentUploadSizeRef.current > 0) { + if (uploadRateRef.current >= UPLOAD_RATE_THRESHOLD) { + timeoutId = setTimeout(setUploadRateRef, BASE_INTERVAL_TIME); + } else { + // The current upload rate is relatively slow, so use a larger + // time interval to get a better upload rate estimate. + timeoutId = setTimeout(setUploadRateRef, BASE_INTERVAL_TIME * 2); + } + } + }; + + // The very first interval is just the base time interval length. + timeoutId = setTimeout(setUploadRateRef, BASE_INTERVAL_TIME); + + return () => { + clearTimeout(timeoutId); + }; + }, []); + + /** + * The effect for: updating the overall percentage complete; setting the + * estimated time remaining; updating the number of files uploaded; and + * detecting if any error has occurred. + */ + useEffect(() => { + let currentTimeRemaining = null; + + // For each uploader, listen for the progress percentage complete and + // add promise catch/finally callbacks to detect errors and count number + // of uploads complete. + const subscriptions = dicomFileUploaderArr.map(fileUploader => { + let currentFileUploadSize = 0; + + const updateProgress = (percentComplete: number) => { + const previousFileUploadSize = currentFileUploadSize; + + currentFileUploadSize = Math.round((percentComplete / 100) * fileUploader.getFileSize()); + + currentUploadSizeRef.current = Math.min( + totalUploadSize, + currentUploadSizeRef.current - previousFileUploadSize + currentFileUploadSize + ); + + setPercentComplete((currentUploadSizeRef.current / totalUploadSize) * 100); + + if (uploadRateRef.current !== 0) { + const uploadSizeRemaining = totalUploadSize - currentUploadSizeRef.current; + + const timeRemaining = Math.round(uploadSizeRemaining / uploadRateRef.current); + + if (currentTimeRemaining === null) { + currentTimeRemaining = timeRemaining; + setTimeRemaining(currentTimeRemaining); + return; + } + + // Do not show an increase in the time remaining by two seconds or minutes + // so as to prevent jumping the time remaining up and down constantly + // due to rounding, inaccuracies in the estimate and slight variations + // in upload rates over time. + if (timeRemaining < ONE_MINUTE) { + const currentSecondsRemaining = Math.ceil(currentTimeRemaining / ONE_SECOND); + const secondsRemaining = Math.ceil(timeRemaining / ONE_SECOND); + const delta = secondsRemaining - currentSecondsRemaining; + if (delta < 0 || delta > 2) { + currentTimeRemaining = timeRemaining; + setTimeRemaining(currentTimeRemaining); + } + return; + } + + if (timeRemaining < ONE_HOUR) { + const currentMinutesRemaining = Math.ceil(currentTimeRemaining / ONE_MINUTE); + const minutesRemaining = Math.ceil(timeRemaining / ONE_MINUTE); + const delta = minutesRemaining - currentMinutesRemaining; + if (delta < 0 || delta > 2) { + currentTimeRemaining = timeRemaining; + setTimeRemaining(currentTimeRemaining); + } + return; + } + + // Hours remaining... + currentTimeRemaining = timeRemaining; + setTimeRemaining(currentTimeRemaining); + } + }; + + const progressCallback = (progressEvent: DicomFileUploaderProgressEvent) => { + updateProgress(progressEvent.percentComplete); + }; + + // Use the uploader promise to flag any error and count the number of + // uploads completed. + fileUploader + .load() + .catch((rejection: UploadRejection) => { + if (rejection.status === UploadStatus.Failed) { + setNumFails(numFails => numFails + 1); + } + }) + .finally(() => { + // If any error occurred, the percent complete progress stops firing + // but this call to updateProgress nicely puts all finished uploads at 100%. + updateProgress(100); + setNumFilesCompleted(numCompleted => numCompleted + 1); + }); + + return fileUploader.subscribe(EVENTS.PROGRESS, progressCallback); + }); + return () => { + subscriptions.forEach(subscription => subscription.unsubscribe()); + }; + }, []); + + const cancelAllUploads = useCallback(async () => { + for (const dicomFileUploader of dicomFileUploaderArr) { + // Important: we need a non-blocking way to cancel every upload, + // otherwise the UI will freeze and the user will not be able + // to interact with the app and progress will not be updated. + const promise = new Promise((resolve, reject) => { + setTimeout(() => { + dicomFileUploader.cancel(); + resolve(); + }, 0); + }); + } + }, []); + + const getFormattedTimeRemaining = useCallback((): string => { + if (timeRemaining == null) { + return ''; + } + + if (timeRemaining < ONE_MINUTE) { + const secondsRemaining = Math.ceil(timeRemaining / ONE_SECOND); + return `${secondsRemaining} ${secondsRemaining === 1 ? 'second' : 'seconds'}`; + } + + if (timeRemaining < ONE_HOUR) { + const minutesRemaining = Math.ceil(timeRemaining / ONE_MINUTE); + return `${minutesRemaining} ${minutesRemaining === 1 ? 'minute' : 'minutes'}`; + } + + const hoursRemaining = Math.ceil(timeRemaining / ONE_HOUR); + return `${hoursRemaining} ${hoursRemaining === 1 ? 'hour' : 'hours'}`; + }, [timeRemaining]); + + const getPercentCompleteRounded = useCallback( + () => Math.min(100, Math.round(percentComplete)), + [percentComplete] + ); + + /** + * Determines if the progress bar should show the infinite animation or not. + * Show the infinite animation for progress less than 1% AND if less than + * one pixel of the progress bar would be displayed. + */ + const showInfiniteProgressBar = useCallback((): boolean => { + return ( + getPercentCompleteRounded() < 1 && + (progressBarContainerRef?.current?.offsetWidth ?? 0) * (percentComplete / 100) < 1 + ); + }, [getPercentCompleteRounded, percentComplete]); + + /** + * Gets the css style for the 'n of m' (files completed) text. The only css attribute + * of the style is width such that the 'n of m' is always a fixed width and thus + * as each file completes uploading the text on screen does not constantly shift + * left and right. + */ + const getNofMFilesStyle = useCallback(() => { + // the number of digits accounts for the digits being on each side of the ' of ' + const numDigits = 2 * dicomFileUploaderArr.length.toString().length; + // the number of digits + 2 spaces and 2 characters for ' of ' + const numChars = numDigits + 4; + return { width: `${numChars}ch` }; + }, []); + + const getNumCompletedAndTimeRemainingComponent = (): ReactElement => { + return ( +
+ {numFilesCompleted === dicomFileUploaderArr.length ? ( + <> + {`${dicomFileUploaderArr.length} ${ + dicomFileUploaderArr.length > 1 ? 'files' : 'file' + } completed.`} + + + ) : ( + <> + + {`${numFilesCompleted} of ${dicomFileUploaderArr.length}`}  + + {' files completed.'}  + + {timeRemaining ? `Less than ${getFormattedTimeRemaining()} remaining. ` : ''} + + + Cancel All Uploads + + + )} +
+ ); + }; + + const getShowFailedOnlyIconComponent = (): ReactElement => { + return ( +
+ {numFails > 0 && ( +
setShowFailedOnly(currentShowFailedOnly => !currentShowFailedOnly)}> + +
+ )} +
+ ); + }; + + const getPercentCompleteComponent = (): ReactElement => { + return ( +
+
+ {numFilesCompleted === dicomFileUploaderArr.length ? ( + <> +
+ {numFails > 0 + ? `Completed with ${numFails} ${numFails > 1 ? 'errors' : 'error'}!` + : 'Completed!'} +
+ {getShowFailedOnlyIconComponent()} + + ) : ( + <> +
+ +
+
+
{`${getPercentCompleteRounded()}%`}
+ {getShowFailedOnlyIconComponent()} +
+ + )} +
+
+ ); + }; + + return ( +
+ {getNumCompletedAndTimeRemainingComponent()} +
+ {getPercentCompleteComponent()} +
+ {dicomFileUploaderArr + .filter( + dicomFileUploader => + !showFailedOnly || dicomFileUploader.getStatus() === UploadStatus.Failed + ) + .map(dicomFileUploader => ( + + ))} +
+
+
+ ); +} + +DicomUploadProgress.propTypes = { + dicomFileUploaderArr: PropTypes.arrayOf(PropTypes.instanceOf(DicomFileUploader)).isRequired, + onComplete: PropTypes.func.isRequired, +}; + +export default DicomUploadProgress; diff --git a/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgressItem.tsx b/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgressItem.tsx new file mode 100644 index 0000000..d1673d4 --- /dev/null +++ b/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgressItem.tsx @@ -0,0 +1,108 @@ +import React, { ReactElement, memo, useCallback, useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import DicomFileUploader, { + DicomFileUploaderProgressEvent, + EVENTS, + UploadRejection, + UploadStatus, +} from '../../utils/DicomFileUploader'; +import { Icons } from '@ohif/ui-next'; + +type DicomUploadProgressItemProps = { + dicomFileUploader: DicomFileUploader; +}; + +// eslint-disable-next-line react/display-name +const DicomUploadProgressItem = memo( + ({ dicomFileUploader }: DicomUploadProgressItemProps): ReactElement => { + const [percentComplete, setPercentComplete] = useState(dicomFileUploader.getPercentComplete()); + const [failedReason, setFailedReason] = useState(''); + const [status, setStatus] = useState(dicomFileUploader.getStatus()); + + const isComplete = useCallback(() => { + return ( + status === UploadStatus.Failed || + status === UploadStatus.Cancelled || + status === UploadStatus.Success + ); + }, [status]); + + useEffect(() => { + const progressSubscription = dicomFileUploader.subscribe( + EVENTS.PROGRESS, + (dicomFileUploaderProgressEvent: DicomFileUploaderProgressEvent) => { + setPercentComplete(dicomFileUploaderProgressEvent.percentComplete); + } + ); + + dicomFileUploader + .load() + .catch((reason: UploadRejection) => { + setStatus(reason.status); + setFailedReason(reason.message ?? ''); + }) + .finally(() => setStatus(dicomFileUploader.getStatus())); + + return () => progressSubscription.unsubscribe(); + }, []); + + const cancelUpload = useCallback(() => { + dicomFileUploader.cancel(); + }, []); + + const getStatusIcon = (): ReactElement => { + switch (dicomFileUploader.getStatus()) { + case UploadStatus.Success: + return ( + + ); + case UploadStatus.InProgress: + return ; + case UploadStatus.Failed: + return ; + case UploadStatus.Cancelled: + return ; + default: + return <>; + } + }; + + return ( +
+
+
+
{getStatusIcon()}
+
+ {dicomFileUploader.getFileName()} +
+
+ {failedReason &&
{failedReason}
} +
+
+ {!isComplete() && ( + <> + {dicomFileUploader.getStatus() === UploadStatus.InProgress && ( +
{percentComplete}%
+ )} +
+ +
+ + )} +
+
+ ); + } +); + +DicomUploadProgressItem.propTypes = { + dicomFileUploader: PropTypes.instanceOf(DicomFileUploader).isRequired, +}; + +export default DicomUploadProgressItem; diff --git a/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx b/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx new file mode 100644 index 0000000..041dfa5 --- /dev/null +++ b/extensions/cornerstone/src/components/OHIFViewportActionCorners.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { useViewportActionCornersContext } from '../contextProviders/ViewportActionCornersProvider'; +import { useSystem } from '@ohif/core'; + +export type OHIFViewportActionCornersProps = { + viewportId: string; +}; + +function OHIFViewportActionCorners({ viewportId }: OHIFViewportActionCornersProps) { + const { servicesManager } = useSystem(); + const [viewportActionCornersState] = useViewportActionCornersContext(); + const ViewportActionCorners = + servicesManager.services.customizationService.getCustomization('ui.viewportActionCorner'); + if (!viewportActionCornersState[viewportId]) { + return null; + } + + return ( + + ); +} + +export default OHIFViewportActionCorners; diff --git a/extensions/cornerstone/src/components/StudySummaryFromMetadata.tsx b/extensions/cornerstone/src/components/StudySummaryFromMetadata.tsx new file mode 100644 index 0000000..60d8b94 --- /dev/null +++ b/extensions/cornerstone/src/components/StudySummaryFromMetadata.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { DicomMetadataStore, utils } from '@ohif/core'; +import { StudySummary } from '@ohif/ui-next'; + +const { formatDate } = utils; + +export function StudySummaryFromMetadata({ StudyInstanceUID }) { + if (!StudyInstanceUID) { + return null; + } + const studyMeta = DicomMetadataStore.getStudy(StudyInstanceUID); + if (!studyMeta?.series?.length) { + return null; + } + + const instanceMeta = studyMeta.series[0].instances[0]; + const { StudyDate, StudyDescription } = instanceMeta; + + return ( + + ); +} diff --git a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenu.tsx b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenu.tsx new file mode 100644 index 0000000..b247dbc --- /dev/null +++ b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenu.tsx @@ -0,0 +1,136 @@ +import React, { useEffect, useState } from 'react'; +import { Button, Icons, Separator } from '@ohif/ui-next'; +import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; + +function ViewportSegmentationMenu({ + viewportId, + servicesManager, +}: withAppTypes<{ viewportId: string }>) { + const { segmentationService } = servicesManager.services; + const [activeSegmentations, setActiveSegmentations] = useState([]); + const [availableSegmentations, setAvailableSegmentations] = useState([]); + + useEffect(() => { + const updateSegmentations = () => { + const active = segmentationService.getSegmentationRepresentations(viewportId); + setActiveSegmentations(active); + + const all = segmentationService.getSegmentations(); + const available = all.filter( + seg => !active.some(activeSeg => activeSeg.segmentationId === seg.segmentationId) + ); + setAvailableSegmentations(available); + }; + + updateSegmentations(); + + const subscriptions = [ + segmentationService.EVENTS.SEGMENTATION_MODIFIED, + segmentationService.EVENTS.SEGMENTATION_REMOVED, + segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, + ].map(event => segmentationService.subscribe(event, updateSegmentations)); + + return () => { + subscriptions.forEach(subscription => subscription.unsubscribe()); + }; + }, [segmentationService, viewportId]); + + const toggleSegmentationRepresentationVisibility = ( + segmentationId, + type = SegmentationRepresentations.Labelmap + ) => { + segmentationService.toggleSegmentationRepresentationVisibility(viewportId, { + segmentationId, + type, + }); + }; + + const addSegmentationToViewport = segmentationId => { + segmentationService.addSegmentationRepresentation(viewportId, { segmentationId }); + }; + + const removeSegmentationFromViewport = segmentationId => { + segmentationService.removeSegmentationRepresentations(viewportId, { + segmentationId, + }); + }; + + return ( +
+ Current Viewport +
    + {activeSegmentations.map(segmentation => ( +
  • + + {segmentation.label} + {segmentation.visible ? ( + + ) : ( + + )} +
  • + ))} +
+ {availableSegmentations.length > 0 && ( + <> + + Available +
    + {availableSegmentations.map(({ segmentationId, label }) => ( +
  • + + {label} +
  • + ))} +
+ + )} +
+ ); +} + +export default ViewportSegmentationMenu; diff --git a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenuWrapper.tsx b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenuWrapper.tsx new file mode 100644 index 0000000..76fb4e3 --- /dev/null +++ b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/ViewportSegmentationMenuWrapper.tsx @@ -0,0 +1,83 @@ +import React, { ReactNode, useEffect, useState } from 'react'; +import { Button, Icons, Popover, PopoverContent, PopoverTrigger } from '@ohif/ui-next'; +import ViewportSegmentationMenu from './ViewportSegmentationMenu'; +import classNames from 'classnames'; +import { useSegmentations } from '../../hooks/useSegmentations'; + +export function ViewportSegmentationMenuWrapper({ + viewportId, + displaySets, + servicesManager, + commandsManager, + location, +}: withAppTypes<{ + viewportId: string; + element: HTMLElement; +}>): ReactNode { + const { viewportActionCornersService, viewportGridService } = servicesManager.services; + + const segmentations = useSegmentations({ servicesManager }); + + const activeViewportId = viewportGridService.getActiveViewportId(); + const isActiveViewport = viewportId === activeViewportId; + + const { align, side } = getAlignAndSide(viewportActionCornersService, location); + + if (!segmentations?.length) { + return null; + } + + return ( + + + + + + + + + ); +} + +const getAlignAndSide = (viewportActionCornersService, location) => { + const ViewportActionCornersLocations = viewportActionCornersService.LOCATIONS; + + switch (location) { + case ViewportActionCornersLocations.topLeft: + return { align: 'start', side: 'bottom' }; + case ViewportActionCornersLocations.topRight: + return { align: 'end', side: 'bottom' }; + case ViewportActionCornersLocations.bottomLeft: + return { align: 'start', side: 'top' }; + case ViewportActionCornersLocations.bottomRight: + return { align: 'end', side: 'top' }; + default: + console.debug('Unknown location, defaulting to bottom-start'); + return { align: 'start', side: 'bottom' }; + } +}; diff --git a/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/index.tsx b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/index.tsx new file mode 100644 index 0000000..5933a82 --- /dev/null +++ b/extensions/cornerstone/src/components/ViewportDataOverlaySettingMenu/index.tsx @@ -0,0 +1,11 @@ +import React, { ReactNode } from 'react'; +import { ViewportSegmentationMenuWrapper } from './ViewportSegmentationMenuWrapper'; + +export function getViewportDataOverlaySettingsMenu( + props: withAppTypes<{ + viewportId: string; + element: HTMLElement; + }> +): ReactNode { + return ; +} diff --git a/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx b/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx new file mode 100644 index 0000000..3810d54 --- /dev/null +++ b/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx @@ -0,0 +1,235 @@ +import React, { useEffect, useCallback, useState, ReactElement, useMemo } from 'react'; +import PropTypes from 'prop-types'; +import debounce from 'lodash.debounce'; +import { PanelSection, WindowLevel } from '@ohif/ui'; +import { Enums, eventTarget } from '@cornerstonejs/core'; +import { useActiveViewportDisplaySets } from '@ohif/core'; +import { + getNodeOpacity, + isPetVolumeWithDefaultOpacity, + isVolumeWithConstantOpacity, + getWindowLevelsData, +} from './utils'; + +const { Events } = Enums; + +const ViewportWindowLevel = ({ + servicesManager, + viewportId, +}: withAppTypes<{ + viewportId: string; +}>): ReactElement => { + const { cornerstoneViewportService } = servicesManager.services; + const [windowLevels, setWindowLevels] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const displaySets = useActiveViewportDisplaySets({ servicesManager }); + + const getViewportsWithVolumeIds = useCallback( + (volumeIds: string[]) => { + const renderingEngine = cornerstoneViewportService.getRenderingEngine(); + const viewports = renderingEngine.getVolumeViewports(); + + return viewports.filter(vp => { + const viewportVolumeIds = vp.getActors().map(actor => actor.referencedId); + return ( + volumeIds.length === viewportVolumeIds.length && + volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId)) + ); + }); + }, + [cornerstoneViewportService] + ); + + const getVolumeOpacity = useCallback((viewport, volumeId) => { + const volumeActor = viewport.getActors().find(actor => actor.referencedId === volumeId)?.actor; + + if (isPetVolumeWithDefaultOpacity(volumeId, volumeActor)) { + return getNodeOpacity(volumeActor, 1); + } else if (isVolumeWithConstantOpacity(volumeActor)) { + return getNodeOpacity(volumeActor, 0); + } + + return undefined; + }, []); + + const updateViewportHistograms = useCallback(() => { + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); + + getWindowLevelsData(viewport, viewportInfo, getVolumeOpacity).then(data => { + setWindowLevels(data); + }); + }, [viewportId, cornerstoneViewportService, getVolumeOpacity]); + + const handleCornerstoneVOIModified = useCallback( + e => { + const { detail } = e; + const { volumeId, range } = detail; + const oldWindowLevel = windowLevels.find(wl => wl.volumeId === volumeId); + + if (!oldWindowLevel) { + return; + } + + const oldVOI = oldWindowLevel.voi; + const windowWidth = range.upper - range.lower; + const windowCenter = range.lower + windowWidth / 2; + + if (windowWidth === oldVOI.windowWidth && windowCenter === oldVOI.windowCenter) { + return; + } + + const newWindowLevel = { + ...oldWindowLevel, + voi: { + windowWidth, + windowCenter, + }, + }; + + setWindowLevels( + windowLevels.map(windowLevel => + windowLevel === oldWindowLevel ? newWindowLevel : windowLevel + ) + ); + }, + [windowLevels] + ); + + const debouncedHandleCornerstoneVOIModified = useMemo( + () => debounce(handleCornerstoneVOIModified, 100), + [handleCornerstoneVOIModified] + ); + + const handleVOIChange = useCallback( + (volumeId, voi) => { + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + + const newRange = { + lower: voi.windowCenter - voi.windowWidth / 2, + upper: voi.windowCenter + voi.windowWidth / 2, + }; + + viewport.setProperties({ voiRange: newRange }, volumeId); + viewport.render(); + }, + [cornerstoneViewportService, viewportId] + ); + + const handleOpacityChange = useCallback( + (viewportId, _volumeIndex, volumeId, opacity) => { + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + + if (!viewport) { + return; + } + + const viewportVolumeIds = viewport.getActors().map(actor => actor.referencedId); + const viewports = getViewportsWithVolumeIds(viewportVolumeIds); + + viewports.forEach(vp => { + vp.setProperties({ colormap: { opacity } }, volumeId); + vp.render(); + }); + }, + [getViewportsWithVolumeIds, cornerstoneViewportService] + ); + + // New function to handle image volume loading completion + const handleImageVolumeLoadingCompleted = useCallback(() => { + setIsLoading(false); + updateViewportHistograms(); + }, [updateViewportHistograms]); + + // Listen to cornerstone events and set up interval for histogram updates + useEffect(() => { + document.addEventListener(Events.VOI_MODIFIED, debouncedHandleCornerstoneVOIModified, true); + eventTarget.addEventListener( + Events.IMAGE_VOLUME_LOADING_COMPLETED, + handleImageVolumeLoadingCompleted + ); + + const intervalId = setInterval(() => { + if (isLoading) { + updateViewportHistograms(); + } + }, 1000); + + return () => { + document.removeEventListener( + Events.VOI_MODIFIED, + debouncedHandleCornerstoneVOIModified, + true + ); + eventTarget.removeEventListener( + Events.IMAGE_VOLUME_LOADING_COMPLETED, + handleImageVolumeLoadingCompleted + ); + clearInterval(intervalId); + }; + }, [ + updateViewportHistograms, + debouncedHandleCornerstoneVOIModified, + handleImageVolumeLoadingCompleted, + isLoading, + ]); + + // Create a memoized version of displaySet IDs for comparison + const displaySetIds = useMemo(() => { + return displaySets?.map(ds => ds.displaySetInstanceUID).sort() || []; + }, [displaySets]); + + useEffect(() => { + const { unsubscribe } = cornerstoneViewportService.subscribe( + cornerstoneViewportService.EVENTS.VIEWPORT_VOLUMES_CHANGED, + ({ viewportInfo }) => { + if (viewportInfo.viewportId === viewportId) { + updateViewportHistograms(); + } + } + ); + + // Only update if displaySets actually changed and are loaded + if (displaySetIds.length && !isLoading) { + updateViewportHistograms(); + } + + return () => { + unsubscribe(); + }; + }, [viewportId, cornerstoneViewportService, updateViewportHistograms, displaySetIds, isLoading]); + + return ( + + {windowLevels.map((windowLevel, i) => { + if (!windowLevel.histogram) { + return null; + } + + return ( + handleVOIChange(windowLevel.volumeId, voi)} + opacity={windowLevel.opacity} + onOpacityChange={opacity => + handleOpacityChange(windowLevel.viewportId, i, windowLevel.volumeId, opacity) + } + /> + ); + })} + + ); +}; + +ViewportWindowLevel.propTypes = { + servicesManager: PropTypes.object.isRequired, + viewportId: PropTypes.string.isRequired, +}; + +export default ViewportWindowLevel; diff --git a/extensions/cornerstone/src/components/ViewportWindowLevel/getViewportVolumeHistogram.ts b/extensions/cornerstone/src/components/ViewportWindowLevel/getViewportVolumeHistogram.ts new file mode 100644 index 0000000..f3911e4 --- /dev/null +++ b/extensions/cornerstone/src/components/ViewportWindowLevel/getViewportVolumeHistogram.ts @@ -0,0 +1,72 @@ +import { getWebWorkerManager } from '@cornerstonejs/core'; + +const workerManager = getWebWorkerManager(); + +const WorkerOptions = { + maxWorkerInstances: 1, + autoTerminateOnIdle: { + enabled: true, + idleTimeThreshold: 1000, + }, +}; + +// Register the task +const workerFn = () => { + return new Worker(new URL('./histogramWorker.js', import.meta.url), { + name: 'histogram-worker', // name used by the browser to name the worker + }); +}; + +const getViewportVolumeHistogram = async (viewport, volume, options?) => { + workerManager.registerWorker('histogram-worker', workerFn, WorkerOptions); + + const volumeImageData = viewport.getImageData(volume.volumeId); + + if (!volumeImageData) { + return undefined; + } + + let scalarData = volume.scalarData; + + if (volume.numTimePoints > 1) { + const targetTimePoint = volume.numTimePoints - 1; // or any other time point you need + scalarData = volume.voxelManager.getTimePointScalarData(targetTimePoint); + } else { + scalarData = volume.voxelManager.getCompleteScalarDataArray(); + } + + if (!scalarData?.length) { + return undefined; + } + + const { dimensions, origin, direction, spacing } = volume; + + const range = await workerManager.executeTask('histogram-worker', 'getRange', { + dimensions, + origin, + direction, + spacing, + scalarData, + }); + + const { minimum: min, maximum: max } = range; + + if (min === Infinity || max === -Infinity) { + return undefined; + } + + const calcHistOptions = { + numBins: 256, + min: Math.max(min, options?.min ?? min), + max: Math.min(max, options?.max ?? max), + }; + + const histogram = await workerManager.executeTask('histogram-worker', 'calcHistogram', { + data: scalarData, + options: calcHistOptions, + }); + + return histogram; +}; + +export { getViewportVolumeHistogram }; diff --git a/extensions/cornerstone/src/components/ViewportWindowLevel/histogramWorker.js b/extensions/cornerstone/src/components/ViewportWindowLevel/histogramWorker.js new file mode 100644 index 0000000..3d96cfe --- /dev/null +++ b/extensions/cornerstone/src/components/ViewportWindowLevel/histogramWorker.js @@ -0,0 +1,97 @@ +import { expose } from 'comlink'; +import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; +import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; + +/** + * This object simulates a heavy task by implementing a sleep function and a recursive Fibonacci function. + * It's used for testing or demonstrating purposes where a heavy or time-consuming task is needed. + */ +const obj = { + getRange: ({ dimensions, origin, direction, spacing, scalarData }) => { + const imageData = vtkImageData.newInstance(); + imageData.setDimensions(dimensions); + imageData.setOrigin(origin); + imageData.setDirection(direction); + imageData.setSpacing(spacing); + + const scalarArray = vtkDataArray.newInstance({ + name: 'Pixels', + numberOfComponents: 1, + values: scalarData, + }); + + imageData.getPointData().setScalars(scalarArray); + + imageData.modified(); + + const range = imageData.computeHistogram(imageData.getBounds()); + + return range; + }, + calcHistogram: ({ data, options }) => { + if (options === undefined) { + options = {}; + } + const histogram = { + numBins: options.numBins || 256, + range: { min: 0, max: 0 }, + bins: new Int32Array(1), + maxBin: 0, + maxBinValue: 0, + }; + + let minToUse = options.min; + let maxToUse = options.max; + + if (minToUse === undefined || maxToUse === undefined) { + let min = Infinity; + let max = -Infinity; + let index = data.length; + + while (index--) { + const value = data[index]; + if (value < min) { + min = value; + } + if (value > max) { + max = value; + } + } + + minToUse = min; + maxToUse = max; + } + + histogram.range = { min: minToUse, max: maxToUse }; + + const bins = new Int32Array(histogram.numBins); + const binScale = histogram.numBins / (maxToUse - minToUse); + + for (let index = 0; index < data.length; index++) { + const value = data[index]; + if (value < minToUse) { + continue; + } + if (value > maxToUse) { + continue; + } + const bin = Math.floor((value - minToUse) * binScale); + bins[bin] += 1; + } + + histogram.bins = bins; + histogram.maxBin = 0; + histogram.maxBinValue = 0; + + for (let bin = 0; bin < histogram.numBins; bin++) { + if (histogram.bins[bin] > histogram.maxBinValue) { + histogram.maxBin = bin; + histogram.maxBinValue = histogram.bins[bin]; + } + } + + return histogram; + }, +}; + +expose(obj); diff --git a/extensions/cornerstone/src/components/ViewportWindowLevel/index.js b/extensions/cornerstone/src/components/ViewportWindowLevel/index.js new file mode 100644 index 0000000..c24fa1d --- /dev/null +++ b/extensions/cornerstone/src/components/ViewportWindowLevel/index.js @@ -0,0 +1 @@ +export { default } from './ViewportWindowLevel'; diff --git a/extensions/cornerstone/src/components/ViewportWindowLevel/utils.ts b/extensions/cornerstone/src/components/ViewportWindowLevel/utils.ts new file mode 100644 index 0000000..8dcbf50 --- /dev/null +++ b/extensions/cornerstone/src/components/ViewportWindowLevel/utils.ts @@ -0,0 +1,153 @@ +import { cache as cs3DCache, Types } from '@cornerstonejs/core'; +import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'; +import { utilities as csUtils } from '@cornerstonejs/core'; +import { getViewportVolumeHistogram } from './getViewportVolumeHistogram'; + +/** + * Gets node opacity from volume actor + */ +export const getNodeOpacity = (volumeActor, nodeIndex) => { + const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0); + const nodeValue = []; + + volumeOpacity.getNodeValue(nodeIndex, nodeValue); + + return nodeValue[1]; +}; + +/** + * Checks if the opacity applied to the PET volume follows a specific pattern + */ +export const isPetVolumeWithDefaultOpacity = (volumeId: string, volumeActor) => { + const volume = cs3DCache.getVolume(volumeId); + + if (!volume || volume.metadata.Modality !== 'PT') { + return false; + } + + const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0); + + if (volumeOpacity.getSize() < 2) { + return false; + } + + const node1Value = []; + const node2Value = []; + + volumeOpacity.getNodeValue(0, node1Value); + volumeOpacity.getNodeValue(1, node2Value); + + if (node1Value[0] !== 0 || node1Value[1] !== 0 || node2Value[0] !== 0.1) { + return false; + } + + const expectedOpacity = node2Value[1]; + const opacitySize = volumeOpacity.getSize(); + const currentNodeValue = []; + + for (let i = 2; i < opacitySize; i++) { + volumeOpacity.getNodeValue(i, currentNodeValue); + if (currentNodeValue[1] !== expectedOpacity) { + return false; + } + } + + return true; +}; + +/** + * Checks if volume has constant opacity + */ +export const isVolumeWithConstantOpacity = volumeActor => { + const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0); + const opacitySize = volumeOpacity.getSize(); + const firstNodeValue = []; + + volumeOpacity.getNodeValue(0, firstNodeValue); + const firstNodeOpacity = firstNodeValue[1]; + + for (let i = 0; i < opacitySize; i++) { + const currentNodeValue = []; + volumeOpacity.getNodeValue(0, currentNodeValue); + if (currentNodeValue[1] !== firstNodeOpacity) { + return false; + } + } + + return true; +}; + +/** + * Gets window levels data for a viewport + */ +export const getWindowLevelsData = async ( + viewport: Types.IStackViewport | Types.IVolumeViewport, + viewportInfo: any, + getVolumeOpacity: (viewport: any, volumeId: string) => number | undefined +) => { + if (!viewport) { + return []; + } + + const volumeIds = (viewport as Types.IBaseVolumeViewport).getAllVolumeIds(); + const viewportProperties = viewport.getProperties(); + const { voiRange } = viewportProperties; + const viewportVoi = voiRange + ? { + windowWidth: voiRange.upper - voiRange.lower, + windowCenter: voiRange.lower + (voiRange.upper - voiRange.lower) / 2, + } + : undefined; + + const windowLevels = await Promise.all( + volumeIds.map(async (volumeId, volumeIndex) => { + const volume = cs3DCache.getVolume(volumeId); + + const opacity = getVolumeOpacity(viewport, volumeId); + const { metadata, scaling } = volume; + const modality = metadata.Modality; + + const options = { + min: modality === 'PT' ? 0.1 : -999, + max: modality === 'PT' ? 5 : 10000, + }; + + const histogram = await getViewportVolumeHistogram(viewport, volume, options); + + if (!histogram || histogram.range.min === histogram.range.max) { + return null; + } + + if (!viewportInfo.displaySetOptions || !viewportInfo.displaySetOptions[volumeIndex]) { + return null; + } + + const { voi: displaySetVOI, colormap: displaySetColormap } = + viewportInfo.displaySetOptions[volumeIndex]; + + let colormap; + if (displaySetColormap) { + colormap = + csUtils.colormap.getColormap(displaySetColormap.name) ?? + vtkColorMaps.getPresetByName(displaySetColormap.name); + } + + const voi = !volumeIndex ? (viewportVoi ?? displaySetVOI) : displaySetVOI; + + return { + viewportId: viewportInfo.viewportId, + modality, + volumeId, + volumeIndex, + voi, + histogram, + colormap, + step: scaling?.PT ? 0.05 : 1, + opacity, + showOpacitySlider: volumeIndex === 1 && opacity !== undefined, + }; + }) + ); + + return windowLevels.filter(Boolean); +}; diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/Colorbar.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/Colorbar.tsx new file mode 100644 index 0000000..298c2ae --- /dev/null +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/Colorbar.tsx @@ -0,0 +1,115 @@ +import React, { ReactElement, useCallback, useEffect, useState } from 'react'; +import { SwitchButton } from '@ohif/ui'; +import { StackViewport, VolumeViewport } from '@cornerstonejs/core'; +import { ColorbarProps } from '../../types/Colorbar'; +import { utilities } from '@cornerstonejs/core'; + +export function setViewportColorbar( + viewportId, + displaySets, + commandsManager, + servicesManager: AppTypes.ServicesManager, + colorbarOptions +) { + const { cornerstoneViewportService } = servicesManager.services; + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + + const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); + const backgroundColor = viewportInfo.getViewportOptions().background; + const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false; + + if (isLight) { + colorbarOptions.ticks = { + position: 'left', + style: { + font: '12px Arial', + color: '#000000', + maxNumTicks: 8, + tickSize: 5, + tickWidth: 1, + labelMargin: 3, + }, + }; + } + + const displaySetInstanceUIDs = []; + + if (viewport instanceof StackViewport) { + displaySetInstanceUIDs.push(viewportId); + } + + if (viewport instanceof VolumeViewport) { + displaySets.forEach(ds => { + displaySetInstanceUIDs.push(ds.displaySetInstanceUID); + }); + } + + commandsManager.run({ + commandName: 'toggleViewportColorbar', + commandOptions: { + viewportId, + options: colorbarOptions, + displaySetInstanceUIDs, + }, + context: 'CORNERSTONE', + }); +} + +export function Colorbar({ + viewportId, + displaySets, + commandsManager, + servicesManager, + colorbarProperties, +}: withAppTypes): ReactElement { + const { colorbarService } = servicesManager.services; + const { + width: colorbarWidth, + colorbarTickPosition, + colorbarContainerPosition, + colormaps, + colorbarInitialColormap, + } = colorbarProperties; + const [showColorbar, setShowColorbar] = useState(colorbarService.hasColorbar(viewportId)); + + const onSetColorbar = useCallback(() => { + setViewportColorbar(viewportId, displaySets, commandsManager, servicesManager, { + viewportId, + colormaps, + ticks: { + position: colorbarTickPosition, + }, + width: colorbarWidth, + position: colorbarContainerPosition, + activeColormapName: colorbarInitialColormap, + }); + }, [commandsManager]); + + useEffect(() => { + const updateColorbarState = () => { + setShowColorbar(colorbarService.hasColorbar(viewportId)); + }; + + const { unsubscribe } = colorbarService.subscribe( + colorbarService.EVENTS.STATE_CHANGED, + updateColorbarState + ); + + return () => { + unsubscribe(); + }; + }, [viewportId]); + + return ( +
+
+ { + onSetColorbar(); + }} + /> +
+ ); +} diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/Colormap.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/Colormap.tsx new file mode 100644 index 0000000..93c2ec4 --- /dev/null +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/Colormap.tsx @@ -0,0 +1,160 @@ +import React, { ReactElement, useCallback, useEffect, useRef, useState, useMemo } from 'react'; +import { AllInOneMenu, ButtonGroup, SwitchButton } from '@ohif/ui'; +import { StackViewport, Types } from '@cornerstonejs/core'; +import { ColormapProps } from '../../types/Colormap'; + +export function Colormap({ + colormaps, + viewportId, + displaySets, + commandsManager, + servicesManager, +}: ColormapProps): ReactElement { + const { cornerstoneViewportService } = servicesManager.services; + + const [activeDisplaySet, setActiveDisplaySet] = useState(displaySets[0]); + + const [showPreview, setShowPreview] = useState(false); + const [prePreviewColormap, setPrePreviewColormap] = useState(null); + + const showPreviewRef = useRef(showPreview); + showPreviewRef.current = showPreview; + const prePreviewColormapRef = useRef(prePreviewColormap); + prePreviewColormapRef.current = prePreviewColormap; + const activeDisplaySetRef = useRef(activeDisplaySet); + activeDisplaySetRef.current = activeDisplaySet; + + const onSetColorLUT = useCallback( + props => { + // TODO: Better way to check if it's a fusion + const oneOpacityColormaps = ['Grayscale', 'X Ray']; + const opacity = + displaySets.length > 1 && !oneOpacityColormaps.includes(props.colormap.name) ? 0.5 : 1; + commandsManager.run({ + commandName: 'setViewportColormap', + commandOptions: { + ...props, + opacity, + immediate: true, + }, + context: 'CORNERSTONE', + }); + }, + [commandsManager] + ); + + const getViewportColormap = (viewportId, displaySet) => { + const { displaySetInstanceUID } = displaySet; + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + if (viewport instanceof StackViewport) { + const { colormap } = viewport.getProperties(); + if (!colormap) { + return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0]; + } + return colormap; + } + const actorEntries = viewport.getActors(); + const actorEntry = actorEntries?.find(entry => + entry.referencedId.includes(displaySetInstanceUID) + ); + const { colormap } = (viewport as Types.IVolumeViewport).getProperties(actorEntry.referencedId); + if (!colormap) { + return colormaps.find(c => c.Name === 'Grayscale') || colormaps[0]; + } + return colormap; + }; + + const buttons = useMemo(() => { + return displaySets.map((displaySet, index) => ({ + children: displaySet.Modality, + key: index, + style: { + minWidth: `calc(100% / ${displaySets.length})`, + fontSize: '0.8rem', + textAlign: 'center', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, + })); + }, [displaySets]); + + useEffect(() => { + setActiveDisplaySet(displaySets[displaySets.length - 1]); + }, [displaySets]); + + return ( + <> + {buttons.length > 1 && ( +
+ { + setActiveDisplaySet(displaySets[index]); + setPrePreviewColormap(null); + }} + activeIndex={ + displaySets.findIndex( + ds => ds.displaySetInstanceUID === activeDisplaySetRef.current.displaySetInstanceUID + ) || 1 + } + className="w-[70%] text-[10px]" + > + {buttons.map(({ children, key, style }) => ( +
+ {children} +
+ ))} +
+
+ )} +
+ { + setShowPreview(checked); + }} + /> +
+ + + {colormaps.map((colormap, index) => ( + { + onSetColorLUT({ + viewportId, + colormap, + displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID, + }); + setPrePreviewColormap(null); + }} + onMouseEnter={() => { + if (showPreviewRef.current) { + setPrePreviewColormap(getViewportColormap(viewportId, activeDisplaySetRef.current)); + onSetColorLUT({ + viewportId, + colormap, + displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID, + }); + } + }} + onMouseLeave={() => { + if (showPreviewRef.current && prePreviewColormapRef.current) { + onSetColorLUT({ + viewportId, + colormap: prePreviewColormapRef.current, + displaySetInstanceUID: activeDisplaySetRef.current.displaySetInstanceUID, + }); + } + }} + > + ))} + + + ); +} diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeLighting.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeLighting.tsx new file mode 100644 index 0000000..a6c07dd --- /dev/null +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeLighting.tsx @@ -0,0 +1,142 @@ +import React, { ReactElement, useState, useEffect, useCallback } from 'react'; +import { VolumeLightingProps } from '../../types/ViewportPresets'; + +export function VolumeLighting({ + servicesManager, + commandsManager, + viewportId, + hasShade, +}: VolumeLightingProps): ReactElement { + const { cornerstoneViewportService } = servicesManager.services; + const [ambient, setAmbient] = useState(null); + const [diffuse, setDiffuse] = useState(null); + const [specular, setSpecular] = useState(null); + + const onAmbientChange = useCallback(() => { + commandsManager.runCommand('setVolumeLighting', { viewportId, options: { ambient } }); + }, [ambient, commandsManager, viewportId]); + + const onDiffuseChange = useCallback(() => { + commandsManager.runCommand('setVolumeLighting', { viewportId, options: { diffuse } }); + }, [diffuse, commandsManager, viewportId]); + + const onSpecularChange = useCallback(() => { + commandsManager.runCommand('setVolumeLighting', { viewportId, options: { specular } }); + }, [specular, commandsManager, viewportId]); + + const calculateBackground = value => { + const percentage = ((value - 0) / (1 - 0)) * 100; + return `linear-gradient(to right, #5acce6 0%, #5acce6 ${percentage}%, #3a3f99 ${percentage}%, #3a3f99 100%)`; + }; + + useEffect(() => { + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + const { actor } = viewport.getActors()[0]; + const ambient = actor.getProperty().getAmbient(); + const diffuse = actor.getProperty().getDiffuse(); + const specular = actor.getProperty().getSpecular(); + setAmbient(ambient); + setDiffuse(diffuse); + setSpecular(specular); + }, [viewportId, cornerstoneViewportService]); + const disableOption = hasShade ? '' : 'ohif-disabled !opacity-40'; + const disableSlider = !hasShade; + return ( + <> +
+ + {ambient !== null && ( + { + setAmbient(e.target.value); + onAmbientChange(); + }} + id="ambient" + disabled={disableSlider} + max={1} + min={0} + type="range" + step={0.1} + style={{ + background: calculateBackground(ambient), + '--thumb-inner-color': '#5acce6', + '--thumb-outer-color': '#090c29', + }} + /> + )} +
+
+ + {diffuse !== null && ( + { + setDiffuse(e.target.value); + onDiffuseChange(); + }} + disabled={disableSlider} + id="diffuse" + max={1} + min={0} + type="range" + step={0.1} + style={{ + background: calculateBackground(diffuse), + '--thumb-inner-color': '#5acce6', + '--thumb-outer-color': '#090c29', + }} + /> + )} +
+ +
+ + {specular !== null && ( + { + setSpecular(e.target.value); + onSpecularChange(); + }} + id="specular" + max={1} + min={0} + type="range" + step={0.1} + style={{ + background: calculateBackground(specular), + '--thumb-inner-color': '#5acce6', + '--thumb-outer-color': '#090c29', + }} + /> + )} +
+ + ); +} diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingOptions.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingOptions.tsx new file mode 100644 index 0000000..af39063 --- /dev/null +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingOptions.tsx @@ -0,0 +1,49 @@ +import React, { ReactElement, useState } from 'react'; +import { AllInOneMenu } from '@ohif/ui'; +import { VolumeRenderingOptionsProps } from '../../types/ViewportPresets'; +import { VolumeRenderingQuality } from './VolumeRenderingQuality'; +import { VolumeShift } from './VolumeShift'; +import { VolumeLighting } from './VolumeLighting'; +import { VolumeShade } from './VolumeShade'; +export function VolumeRenderingOptions({ + viewportId, + commandsManager, + volumeRenderingQualityRange, + servicesManager, +}: VolumeRenderingOptionsProps): ReactElement { + const [hasShade, setShade] = useState(false); + return ( + + + + +
+
LIGHTING
+
+
+
+ +
+ +
+ ); +} diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingPresets.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingPresets.tsx new file mode 100644 index 0000000..0949cac --- /dev/null +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingPresets.tsx @@ -0,0 +1,39 @@ +import { AllInOneMenu } from '@ohif/ui'; +import { Icons } from '@ohif/ui-next'; +import React, { ReactElement } from 'react'; +import { VolumeRenderingPresetsProps } from '../../types/ViewportPresets'; +import { VolumeRenderingPresetsContent } from './VolumeRenderingPresetsContent'; + +export function VolumeRenderingPresets({ + viewportId, + servicesManager, + commandsManager, + volumeRenderingPresets, +}: VolumeRenderingPresetsProps): ReactElement { + const { uiModalService } = servicesManager.services; + + const onClickPresets = () => { + uiModalService.show({ + content: VolumeRenderingPresetsContent, + title: 'Rendering Presets', + movable: true, + contentProps: { + onClose: uiModalService.hide, + presets: volumeRenderingPresets, + viewportId, + commandsManager, + }, + containerDimensions: 'h-[543px] w-[460px]', + contentDimensions: 'h-[493px] w-[460px] pl-[12px] pr-[12px]', + }); + }; + + return ( + } + rightIcon={} + onClick={onClickPresets} + /> + ); +} diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingPresetsContent.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingPresetsContent.tsx new file mode 100644 index 0000000..77853b4 --- /dev/null +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingPresetsContent.tsx @@ -0,0 +1,95 @@ +import { ButtonEnums } from '@ohif/ui'; +import { Icons } from '@ohif/ui-next'; +import React, { ReactElement, useState, useCallback } from 'react'; +import { Button, InputFilterText } from '@ohif/ui'; +import { ViewportPreset, VolumeRenderingPresetsContentProps } from '../../types/ViewportPresets'; + +export function VolumeRenderingPresetsContent({ + presets, + viewportId, + commandsManager, + onClose, +}: VolumeRenderingPresetsContentProps): ReactElement { + const [filteredPresets, setFilteredPresets] = useState(presets); + const [searchValue, setSearchValue] = useState(''); + const [selectedPreset, setSelectedPreset] = useState(null); + + const handleSearchChange = useCallback( + (value: string) => { + setSearchValue(value); + const filtered = value + ? presets.filter(preset => preset.name.toLowerCase().includes(value.toLowerCase())) + : presets; + setFilteredPresets(filtered); + }, + [presets] + ); + + const handleApply = useCallback( + props => { + commandsManager.runCommand('setViewportPreset', { + ...props, + }); + }, + [commandsManager] + ); + + const formatLabel = (label: string, maxChars: number) => { + return label.length > maxChars ? `${label.slice(0, maxChars)}...` : label; + }; + + return ( +
+
+
+
+ +
+
+
+
+ {filteredPresets.map((preset, index) => ( +
{ + setSelectedPreset(preset); + handleApply({ preset: preset.name, viewportId }); + }} + > + + +
+ ))} +
+
+
+
+
+ +
+
+
+ ); +} diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingQuality.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingQuality.tsx new file mode 100644 index 0000000..0186481 --- /dev/null +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeRenderingQuality.tsx @@ -0,0 +1,73 @@ +import React, { ReactElement, useCallback, useState, useEffect } from 'react'; +import { VolumeRenderingQualityProps } from '../../types/ViewportPresets'; + +export function VolumeRenderingQuality({ + volumeRenderingQualityRange, + commandsManager, + servicesManager, + viewportId, +}: VolumeRenderingQualityProps): ReactElement { + const { cornerstoneViewportService } = servicesManager.services; + const { min, max, step } = volumeRenderingQualityRange; + const [quality, setQuality] = useState(null); + + const onChange = useCallback( + (value: number) => { + commandsManager.runCommand('setVolumeRenderingQulaity', { + viewportId, + volumeQuality: value, + }); + setQuality(value); + }, + [commandsManager, viewportId] + ); + + const calculateBackground = value => { + const percentage = ((value - 0) / (1 - 0)) * 100; + return `linear-gradient(to right, #5acce6 0%, #5acce6 ${percentage}%, #3a3f99 ${percentage}%, #3a3f99 100%)`; + }; + + useEffect(() => { + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + const { actor } = viewport.getActors()[0]; + const mapper = actor.getMapper(); + const image = mapper.getInputData(); + const spacing = image.getSpacing(); + const sampleDistance = mapper.getSampleDistance(); + const averageSpacing = spacing.reduce((a, b) => a + b) / 3.0; + if (sampleDistance === averageSpacing) { + setQuality(1); + } else { + setQuality(Math.sqrt(averageSpacing / (sampleDistance * 0.5))); + } + }, [cornerstoneViewportService, viewportId]); + return ( + <> +
+ + {quality !== null && ( + onChange(parseInt(e.target.value, 10))} + style={{ + background: calculateBackground((quality - min) / (max - min)), + '--thumb-inner-color': '#5acce6', + '--thumb-outer-color': '#090c29', + }} + /> + )} +
+ + ); +} diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeShade.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeShade.tsx new file mode 100644 index 0000000..cc7cee7 --- /dev/null +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeShade.tsx @@ -0,0 +1,42 @@ +import React, { ReactElement, useCallback, useEffect, useState } from 'react'; +import { SwitchButton } from '@ohif/ui'; +import { VolumeShadeProps } from '../../types/ViewportPresets'; + +export function VolumeShade({ + commandsManager, + viewportId, + servicesManager, + onClickShade = bool => {}, +}: VolumeShadeProps): ReactElement { + const { cornerstoneViewportService } = servicesManager.services; + const [shade, setShade] = useState(true); + const [key, setKey] = useState(0); + + const onShadeChange = useCallback( + (checked: boolean) => { + commandsManager.runCommand('setVolumeLighting', { viewportId, options: { shade: checked } }); + }, + [commandsManager, viewportId] + ); + useEffect(() => { + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + const { actor } = viewport.getActors()[0]; + const shade = actor.getProperty().getShade(); + setShade(shade); + onClickShade(shade); + setKey(key + 1); + }, [viewportId, cornerstoneViewportService]); + + return ( + { + setShade(!shade); + onClickShade(!shade); + onShadeChange(!shade); + }} + /> + ); +} diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeShift.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeShift.tsx new file mode 100644 index 0000000..3531269 --- /dev/null +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/VolumeShift.tsx @@ -0,0 +1,93 @@ +import React, { ReactElement, useCallback, useEffect, useState, useRef } from 'react'; +import { VolumeShiftProps } from '../../types/ViewportPresets'; + +export function VolumeShift({ + viewportId, + commandsManager, + servicesManager, +}: VolumeShiftProps): ReactElement { + const { cornerstoneViewportService } = servicesManager.services; + const [minShift, setMinShift] = useState(null); + const [maxShift, setMaxShift] = useState(null); + const [shift, setShift] = useState( + cornerstoneViewportService.getCornerstoneViewport(viewportId)?.shiftedBy || 0 + ); + const [step, setStep] = useState(null); + const [isBlocking, setIsBlocking] = useState(false); + + const prevShiftRef = useRef(shift); + + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + const { actor } = viewport.getActors()[0]; + const ofun = actor.getProperty().getScalarOpacity(0); + + useEffect(() => { + if (isBlocking) { + return; + } + const range = ofun.getRange(); + + const transferFunctionWidth = range[1] - range[0]; + + const minShift = -transferFunctionWidth; + const maxShift = transferFunctionWidth; + + setMinShift(minShift); + setMaxShift(maxShift); + setStep(Math.pow(10, Math.floor(Math.log10(transferFunctionWidth / 500)))); + }, [cornerstoneViewportService, viewportId, actor, ofun, isBlocking]); + + const onChangeRange = useCallback( + newShift => { + const shiftDifference = newShift - prevShiftRef.current; + prevShiftRef.current = newShift; + viewport.shiftedBy = newShift; + commandsManager.runCommand('shiftVolumeOpacityPoints', { + viewportId, + shift: shiftDifference, + }); + }, + [commandsManager, viewportId, viewport] + ); + + const calculateBackground = value => { + const percentage = ((value - 0) / (1 - 0)) * 100; + return `linear-gradient(to right, #5acce6 0%, #5acce6 ${percentage}%, #3a3f99 ${percentage}%, #3a3f99 100%)`; + }; + + return ( + <> +
+ + {step !== null && ( + { + const shiftValue = parseInt(e.target.value, 10); + setShift(shiftValue); + onChangeRange(shiftValue); + }} + id="shift" + onMouseDown={() => setIsBlocking(true)} + onMouseUp={() => setIsBlocking(false)} + max={maxShift} + min={minShift} + type="range" + step={step} + style={{ + background: calculateBackground((shift - minShift) / (maxShift - minShift)), + '--thumb-inner-color': '#5acce6', + '--thumb-outer-color': '#090c29', + }} + /> + )} +
+ + ); +} diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevel.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevel.tsx new file mode 100644 index 0000000..eda52d5 --- /dev/null +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevel.tsx @@ -0,0 +1,57 @@ +import React, { ReactElement, useCallback } from 'react'; +import { AllInOneMenu } from '@ohif/ui'; +import { WindowLevelPreset } from '../../types/WindowLevel'; +import { CommandsManager } from '@ohif/core'; +import { useTranslation } from 'react-i18next'; + +export type WindowLevelProps = { + viewportId: string; + presets: Array>>; + commandsManager: CommandsManager; +}; + +export function WindowLevel({ + viewportId, + commandsManager, + presets, +}: WindowLevelProps): ReactElement { + const { t } = useTranslation('WindowLevelActionMenu'); + + const onSetWindowLevel = useCallback( + props => { + commandsManager.run({ + commandName: 'setViewportWindowLevel', + commandOptions: { + ...props, + viewportId, + }, + context: 'CORNERSTONE', + }); + }, + [commandsManager, viewportId] + ); + + return ( + + {presets.map((modalityPresets, modalityIndex) => ( + + {Object.entries(modalityPresets).map(([modality, presetsArray]) => ( + + + {t('Modality Presets', { modality })} + + {presetsArray.map((preset, index) => ( + onSetWindowLevel(preset)} + /> + ))} + + ))} + + ))} + + ); +} diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx new file mode 100644 index 0000000..65804c9 --- /dev/null +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/WindowLevelActionMenu.tsx @@ -0,0 +1,194 @@ +import React, { ReactElement, useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import classNames from 'classnames'; +import { AllInOneMenu } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; +import { Colormap } from './Colormap'; +import { Colorbar } from './Colorbar'; +import { setViewportColorbar } from './Colorbar'; +import { WindowLevelPreset } from '../../types/WindowLevel'; +import { ColorbarProperties } from '../../types/Colorbar'; +import { VolumeRenderingQualityRange } from '../../types/ViewportPresets'; +import { WindowLevel } from './WindowLevel'; +import { VolumeRenderingPresets } from './VolumeRenderingPresets'; +import { VolumeRenderingOptions } from './VolumeRenderingOptions'; +import { ViewportPreset } from '../../types/ViewportPresets'; +import { VolumeViewport3D } from '@cornerstonejs/core'; +import { utilities } from '@cornerstonejs/core'; + +export const nonWLModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE']; + +export type WindowLevelActionMenuProps = { + viewportId: string; + element: HTMLElement; + presets: Array>>; + colorbarProperties: ColorbarProperties; + displaySets: Array; + volumeRenderingPresets: Array; + volumeRenderingQualityRange: VolumeRenderingQualityRange; +}; + +export function WindowLevelActionMenu({ + viewportId, + element, + presets, + verticalDirection, + horizontalDirection, + commandsManager, + servicesManager, + colorbarProperties, + displaySets, + volumeRenderingPresets, + volumeRenderingQualityRange, +}: withAppTypes): ReactElement { + const { + colormaps, + colorbarContainerPosition, + colorbarInitialColormap, + colorbarTickPosition, + width: colorbarWidth, + } = colorbarProperties; + const { colorbarService, cornerstoneViewportService } = servicesManager.services; + const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + const backgroundColor = viewportInfo?.getViewportOptions().background; + const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false; + + const { t } = useTranslation('WindowLevelActionMenu'); + + const [viewportGrid] = useViewportGrid(); + const { activeViewportId } = viewportGrid; + + const [vpHeight, setVpHeight] = useState(element?.clientHeight); + const [menuKey, setMenuKey] = useState(0); + const [is3DVolume, setIs3DVolume] = useState(false); + + const onSetColorbar = useCallback(() => { + setViewportColorbar(viewportId, displaySets, commandsManager, servicesManager, { + colormaps, + ticks: { + position: colorbarTickPosition, + }, + width: colorbarWidth, + position: colorbarContainerPosition, + activeColormapName: colorbarInitialColormap, + }); + }, [commandsManager]); + + useEffect(() => { + const newVpHeight = element?.clientHeight; + if (vpHeight !== newVpHeight) { + setVpHeight(newVpHeight); + } + }, [element, vpHeight]); + + useEffect(() => { + if (!colorbarService.hasColorbar(viewportId)) { + return; + } + window.setTimeout(() => { + colorbarService.removeColorbar(viewportId); + onSetColorbar(); + }, 0); + }, [viewportId, displaySets, viewport]); + + useEffect(() => { + setMenuKey(menuKey + 1); + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + if (viewport instanceof VolumeViewport3D) { + setIs3DVolume(true); + } else { + setIs3DVolume(false); + } + }, [ + displaySets, + viewportId, + presets, + volumeRenderingQualityRange, + volumeRenderingPresets, + colorbarProperties, + activeViewportId, + viewportGrid, + ]); + + return ( + { + setVpHeight(element.clientHeight); + }} + menuKey={menuKey} + > + + {!is3DVolume && ( + !nonWLModalities.includes(ds.Modality))} + commandsManager={commandsManager} + servicesManager={servicesManager} + colorbarProperties={colorbarProperties} + /> + )} + + {colormaps && !is3DVolume && ( + + !nonWLModalities.includes(ds.Modality))} + commandsManager={commandsManager} + servicesManager={servicesManager} + /> + + )} + + {presets && presets.length > 0 && !is3DVolume && ( + + + + )} + + {volumeRenderingPresets && is3DVolume && ( + + )} + + {volumeRenderingQualityRange && is3DVolume && ( + + + + )} + + + ); +} diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/defaultWindowLevelPresets.ts b/extensions/cornerstone/src/components/WindowLevelActionMenu/defaultWindowLevelPresets.ts new file mode 100644 index 0000000..76e4d99 --- /dev/null +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/defaultWindowLevelPresets.ts @@ -0,0 +1,23 @@ +// The following are the default window level presets and can be further +// configured via the customization service. +const defaultWindowLevelPresets = { + CT: [ + { id: 'ct-soft-tissue', description: 'Soft tissue', window: '400', level: '40' }, + { id: 'ct-lung', description: 'Lung', window: '1500', level: '-600' }, + { id: 'ct-liver', description: 'Liver', window: '150', level: '90' }, + { id: 'ct-bone', description: 'Bone', window: '2500', level: '480' }, + { id: 'ct-brain', description: 'Brain', window: '80', level: '40' }, + ], + + PT: [ + { id: 'pt-default', description: 'Default', window: '5', level: '2.5' }, + { id: 'pt-suv-3', description: 'SUV', window: '0', level: '3' }, + { id: 'pt-suv-5', description: 'SUV', window: '0', level: '5' }, + { id: 'pt-suv-7', description: 'SUV', window: '0', level: '7' }, + { id: 'pt-suv-8', description: 'SUV', window: '0', level: '8' }, + { id: 'pt-suv-10', description: 'SUV', window: '0', level: '10' }, + { id: 'pt-suv-15', description: 'SUV', window: '0', level: '15' }, + ], +}; + +export default defaultWindowLevelPresets; diff --git a/extensions/cornerstone/src/components/WindowLevelActionMenu/getWindowLevelActionMenu.tsx b/extensions/cornerstone/src/components/WindowLevelActionMenu/getWindowLevelActionMenu.tsx new file mode 100644 index 0000000..ebd35e8 --- /dev/null +++ b/extensions/cornerstone/src/components/WindowLevelActionMenu/getWindowLevelActionMenu.tsx @@ -0,0 +1,55 @@ +import React, { ReactNode } from 'react'; +import { nonWLModalities } from './WindowLevelActionMenu'; + +export function getWindowLevelActionMenu({ + viewportId, + element, + displaySets, + servicesManager, + commandsManager, + verticalDirection, + horizontalDirection, +}: withAppTypes<{ + viewportId: string; + element: HTMLElement; + displaySets: AppTypes.DisplaySet[]; +}>): ReactNode { + const { customizationService } = servicesManager.services; + + const presets = customizationService.getCustomization('cornerstone.windowLevelPresets'); + const colorbarProperties = customizationService.getCustomization('cornerstone.colorbar'); + const { volumeRenderingPresets, volumeRenderingQualityRange } = + customizationService.getCustomization('cornerstone.3dVolumeRendering'); + const WindowLevelActionMenu = customizationService.getCustomization( + 'cornerstone.windowLevelActionMenu' + ); + const displaySetPresets = displaySets + .filter(displaySet => presets[displaySet.Modality]) + .map(displaySet => { + return { [displaySet.Modality]: presets[displaySet.Modality] }; + }); + + const modalities = displaySets + .map(displaySet => displaySet.Modality) + .filter(modality => !nonWLModalities.includes(modality)); + + if (modalities.length === 0) { + return null; + } + + return ( + + ); +} diff --git a/extensions/cornerstone/src/contextProviders/ViewportActionCornersProvider.tsx b/extensions/cornerstone/src/contextProviders/ViewportActionCornersProvider.tsx new file mode 100644 index 0000000..2866181 --- /dev/null +++ b/extensions/cornerstone/src/contextProviders/ViewportActionCornersProvider.tsx @@ -0,0 +1,180 @@ +import React, { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useReducer, +} from 'react'; +import PropTypes from 'prop-types'; + +import { Types, ViewportActionCornersLocations } from '@ohif/ui'; +import ViewportActionCornersService, { + ActionComponentInfo, +} from '../services/ViewportActionCornersService/ViewportActionCornersService'; + +interface StateComponentInfo extends Types.ViewportActionCornersComponentInfo { + indexPriority: number; +} + +type State = Record>>; + +const DEFAULT_STATE: State = { + // default here is the viewportId of the default viewport + default: { + [ViewportActionCornersLocations.topLeft]: [], + [ViewportActionCornersLocations.topRight]: [], + [ViewportActionCornersLocations.bottomLeft]: [], + [ViewportActionCornersLocations.bottomRight]: [], + }, + // [anotherViewportId]: { ..... } +}; + +export const ViewportActionCornersContext = createContext(DEFAULT_STATE); + +export function ViewportActionCornersProvider({ children, service }) { + const viewportActionCornersReducer = (state, action) => { + switch (action.type) { + case 'ADD_ACTION_COMPONENT': { + const { viewportId, id, component, location, indexPriority } = action.payload; + // Get the components at the specified location of the specified viewport. + let locationComponents = state?.[viewportId]?.[location] + ? [...state[viewportId][location]] + : []; + + // If the component (id) already exists at the location specified in the payload, + // then it must be replaced with the component in the payload so first + // remove it from that location. + const deletionIndex = locationComponents.findIndex(component => component.id === id); + if (deletionIndex !== -1) { + locationComponents = [ + ...locationComponents.slice(0, deletionIndex), + ...locationComponents.slice(deletionIndex + 1), + ]; + } + + // Insert the component from the payload but + // do not insert an undefined or null component. + if (component) { + let insertionIndex; + const isRightSide = + location === ViewportActionCornersLocations.topRight || + location === ViewportActionCornersLocations.bottomRight; + + if (indexPriority === undefined) { + // If no indexPriority is provided, add it to the appropriate end + insertionIndex = isRightSide ? 0 : locationComponents.length; + } else { + if (isRightSide) { + insertionIndex = locationComponents.findIndex( + component => indexPriority > component.indexPriority + ); + } else { + insertionIndex = locationComponents.findIndex( + component => indexPriority <= component.indexPriority + ); + } + if (insertionIndex === -1) { + // If no suitable position found, add to the appropriate end + insertionIndex = isRightSide ? 0 : locationComponents.length; + } + } + + const defaultPriority = isRightSide ? Number.MIN_SAFE_INTEGER : Number.MAX_SAFE_INTEGER; + + locationComponents = [ + ...locationComponents.slice(0, insertionIndex), + { + id, + component, + indexPriority: indexPriority ?? defaultPriority, + }, + ...locationComponents.slice(insertionIndex), + ]; + } + + return { + ...state, + [viewportId]: { + ...state[viewportId], + [location]: locationComponents, + }, + }; + } + case 'CLEAR_ACTION_COMPONENTS': { + const viewportId = action.payload; + const nextState = { ...state }; + delete nextState[viewportId]; + return nextState; + } + default: + return { ...state }; + } + }; + + const [viewportActionCornersState, dispatch] = useReducer( + viewportActionCornersReducer, + DEFAULT_STATE + ); + + const getState = useCallback(() => { + return viewportActionCornersState; + }, [viewportActionCornersState]); + + const addComponent = useCallback( + (actionComponentInfo: ActionComponentInfo) => { + dispatch({ type: 'ADD_ACTION_COMPONENT', payload: actionComponentInfo }); + }, + [dispatch] + ); + + const addComponents = useCallback( + (actionComponentInfos: Array) => { + actionComponentInfos.forEach(actionComponentInfo => + dispatch({ type: 'ADD_ACTION_COMPONENT', payload: actionComponentInfo }) + ); + }, + [dispatch] + ); + + const clear = useCallback( + (viewportId: string) => dispatch({ type: 'CLEAR_ACTION_COMPONENTS', payload: viewportId }), + [dispatch] + ); + + useEffect(() => { + if (service) { + service.setServiceImplementation({ + getState, + addComponent, + addComponents, + clear, + }); + } + }, [getState, service, addComponent, addComponents, clear]); + + const viewportCornerActions = { + getState, + addComponent: props => service.addComponent(props), + addComponents: props => service.addComponents(props), + clear: props => service.clear(props), + }; + + const contextValue = useMemo( + () => [viewportActionCornersState, viewportCornerActions], + [viewportActionCornersState, viewportCornerActions] + ); + + return ( + + {children} + + ); +} + +ViewportActionCornersProvider.propTypes = { + children: PropTypes.node, + service: PropTypes.instanceOf(ViewportActionCornersService).isRequired, +}; + +export const useViewportActionCornersContext = () => useContext(ViewportActionCornersContext); diff --git a/extensions/cornerstone/src/customizations/colorbarCustomization.ts b/extensions/cornerstone/src/customizations/colorbarCustomization.ts new file mode 100644 index 0000000..c5bd695 --- /dev/null +++ b/extensions/cornerstone/src/customizations/colorbarCustomization.ts @@ -0,0 +1,13 @@ +import { colormaps } from '../utils/colormaps'; + +const DefaultColormap = 'Grayscale'; + +export default { + 'cornerstone.colorbar': { + width: '16px', + colorbarTickPosition: 'left', + colormaps, + colorbarContainerPosition: 'right', + colorbarInitialColormap: DefaultColormap, + }, +}; diff --git a/extensions/cornerstone/src/customizations/layoutSelectorCustomization.ts b/extensions/cornerstone/src/customizations/layoutSelectorCustomization.ts new file mode 100644 index 0000000..94295cc --- /dev/null +++ b/extensions/cornerstone/src/customizations/layoutSelectorCustomization.ts @@ -0,0 +1,99 @@ +export default { + 'layoutSelector.advancedPresetGenerator': ({ + servicesManager, + }: { + servicesManager: AppTypes.ServicesManager; + }) => { + const _areSelectorsValid = ( + hp: AppTypes.HangingProtocol.Protocol, + displaySets: AppTypes.DisplaySet[], + hangingProtocolService: AppTypes.HangingProtocolService + ) => { + if (!hp.displaySetSelectors || Object.values(hp.displaySetSelectors).length === 0) { + return true; + } + + return hangingProtocolService.areRequiredSelectorsValid( + Object.values(hp.displaySetSelectors), + displaySets[0] + ); + }; + + const generateAdvancedPresets = ({ + servicesManager, + }: { + servicesManager: AppTypes.ServicesManager; + }) => { + const { hangingProtocolService, viewportGridService, displaySetService } = + servicesManager.services; + + const hangingProtocols = Array.from(hangingProtocolService.protocols.values()); + + const viewportId = viewportGridService.getActiveViewportId(); + + if (!viewportId) { + return []; + } + const displaySetInsaneUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); + + if (!displaySetInsaneUIDs) { + return []; + } + + const displaySets = displaySetInsaneUIDs.map(uid => + displaySetService.getDisplaySetByUID(uid) + ); + + return hangingProtocols + .map(hp => { + if (!hp.isPreset) { + return null; + } + + const areValid = _areSelectorsValid(hp, displaySets, hangingProtocolService); + + return { + icon: hp.icon, + title: hp.name, + commandOptions: { + protocolId: hp.id, + }, + disabled: !areValid, + }; + }) + .filter(preset => preset !== null); + }; + + return generateAdvancedPresets({ servicesManager }); + }, + 'layoutSelector.commonPresets': [ + { + icon: 'layout-common-1x1', + commandOptions: { + numRows: 1, + numCols: 1, + }, + }, + { + icon: 'layout-common-1x2', + commandOptions: { + numRows: 1, + numCols: 2, + }, + }, + { + icon: 'layout-common-2x2', + commandOptions: { + numRows: 2, + numCols: 2, + }, + }, + { + icon: 'layout-common-2x3', + commandOptions: { + numRows: 2, + numCols: 3, + }, + }, + ], +}; diff --git a/extensions/cornerstone/src/customizations/measurementsCustomization.ts b/extensions/cornerstone/src/customizations/measurementsCustomization.ts new file mode 100644 index 0000000..04815c9 --- /dev/null +++ b/extensions/cornerstone/src/customizations/measurementsCustomization.ts @@ -0,0 +1,124 @@ +export default { + 'cornerstone.measurements': { + Angle: { + displayText: [], + report: [], + }, + CobbAngle: { + displayText: [], + report: [], + }, + ArrowAnnotate: { + displayText: [], + report: [], + }, + RectangleROi: { + displayText: [], + report: [], + }, + CircleROI: { + displayText: [], + report: [], + }, + EllipticalROI: { + displayText: [], + report: [], + }, + Bidirectional: { + displayText: [], + report: [], + }, + Length: { + displayText: [], + report: [], + }, + LivewireContour: { + displayText: [], + report: [], + }, + SplineROI: { + displayText: [ + { + displayName: 'Areas', + value: 'area', + type: 'value', + }, + { + value: 'areaUnits', + for: ['area'], + type: 'unit', + }, + ], + report: [ + { + displayName: 'Area', + value: 'area', + type: 'value', + }, + { + displayName: 'Unit', + value: 'areaUnits', + type: 'value', + }, + ], + }, + PlanarFreehandROI: { + displayTextOpen: [ + { + displayName: 'Length', + value: 'length', + type: 'value', + }, + ], + displayText: [ + { + displayName: 'Mean', + value: 'mean', + type: 'value', + }, + { + displayName: 'Max', + value: 'max', + type: 'value', + }, + { + displayName: 'Area', + value: 'area', + type: 'value', + }, + { + value: 'pixelValueUnits', + for: ['mean', 'max'], + type: 'unit', + }, + { + value: 'areaUnits', + for: ['area'], + type: 'unit', + }, + ], + report: [ + { + displayName: 'Mean', + value: 'mean', + type: 'value', + }, + { + displayName: 'Max', + value: 'max', + type: 'value', + }, + { + displayName: 'Area', + value: 'area', + type: 'value', + }, + { + displayName: 'Unit', + value: 'unit', + type: 'value', + }, + ], + }, + }, +}; diff --git a/extensions/cornerstone/src/customizations/miscCustomization.ts b/extensions/cornerstone/src/customizations/miscCustomization.ts new file mode 100644 index 0000000..fe7d89b --- /dev/null +++ b/extensions/cornerstone/src/customizations/miscCustomization.ts @@ -0,0 +1,16 @@ +import { CinePlayer } from '@ohif/ui'; +import DicomUpload from '../components/DicomUpload/DicomUpload'; + +export default { + cinePlayer: CinePlayer, + autoCineModalities: ['OT', 'US'], + 'panelMeasurement.disableEditing': false, + onBeforeSRAddMeasurement: ({ measurement, StudyInstanceUID, SeriesInstanceUID }) => { + return measurement; + }, + onBeforeDicomStore: ({ dicomDict, measurementData, naturalizedReport }) => { + return dicomDict; + }, + dicomUploadComponent: DicomUpload, + codingValues: {}, +}; diff --git a/extensions/cornerstone/src/customizations/segmentationPanelCustomization.tsx b/extensions/cornerstone/src/customizations/segmentationPanelCustomization.tsx new file mode 100644 index 0000000..d0649f5 --- /dev/null +++ b/extensions/cornerstone/src/customizations/segmentationPanelCustomization.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + Icons, +} from '@ohif/ui-next'; + +export default function getSegmentationPanelCustomization({ commandsManager, servicesManager }) { + return { + 'panelSegmentation.customDropdownMenuContent': ({ + activeSegmentation, + onSegmentationAdd, + onSegmentationRemoveFromViewport, + onSegmentationEdit, + onSegmentationDelete, + allowExport, + storeSegmentation, + onSegmentationDownload, + onSegmentationDownloadRTSS, + t, + }) => ( + + onSegmentationAdd(activeSegmentation.segmentationId)}> + + {t('Create New Segmentation')} + + + {t('Manage Current Segmentation')} + onSegmentationRemoveFromViewport(activeSegmentation.segmentationId)} + > + + {t('Remove from Viewport')} + + onSegmentationEdit(activeSegmentation.segmentationId)}> + + {t('Rename')} + + + + + {t('Export & Download')} + + + + storeSegmentation(activeSegmentation.segmentationId)} + > + {t('Export DICOM SEG')} + + onSegmentationDownload(activeSegmentation.segmentationId)} + > + {t('Download DICOM SEG')} + + + + + + onSegmentationDelete(activeSegmentation.segmentationId)}> + + {t('Delete')} + + + ), + 'panelSegmentation.disableEditing': false, + 'panelSegmentation.showAddSegment': true, + 'panelSegmentation.onSegmentationAdd': () => { + const { viewportGridService } = servicesManager.services; + const viewportId = viewportGridService.getState().activeViewportId; + commandsManager.run('createLabelmapForViewport', { viewportId }); + }, + 'panelSegmentation.tableMode': 'collapsed', + 'panelSegmentation.readableText': { + lesionStats: 'Lesion Statistics', + minValue: 'Minimum Value', + maxValue: 'Maximum Value', + meanValue: 'Mean Value', + volume: 'Volume (ml)', + suvPeak: 'SUV Peak', + suvMax: 'Maximum SUV', + suvMaxIJK: 'SUV Max IJK', + lesionGlyoclysisStats: 'Lesion Glycolysis', + }, + }; +} diff --git a/extensions/cornerstone/src/customizations/viewportClickCommandsCustomization.ts b/extensions/cornerstone/src/customizations/viewportClickCommandsCustomization.ts new file mode 100644 index 0000000..1663819 --- /dev/null +++ b/extensions/cornerstone/src/customizations/viewportClickCommandsCustomization.ts @@ -0,0 +1,15 @@ +export default { + cornerstoneViewportClickCommands: { + doubleClick: ['toggleOneUp'], + button1: ['closeContextMenu'], + button3: [ + { + commandName: 'showCornerstoneContextMenu', + commandOptions: { + requireNearbyToolData: true, + menuId: 'measurementsContextMenu', + }, + }, + ], + }, +}; diff --git a/extensions/cornerstone/src/customizations/viewportOverlayCustomization.tsx b/extensions/cornerstone/src/customizations/viewportOverlayCustomization.tsx new file mode 100644 index 0000000..9d3e2e1 --- /dev/null +++ b/extensions/cornerstone/src/customizations/viewportOverlayCustomization.tsx @@ -0,0 +1,44 @@ +export default { + 'viewportOverlay.topLeft': [ + { + id: 'StudyDate', + inheritsFrom: 'ohif.overlayItem', + label: '', + title: 'Study date', + condition: ({ referenceInstance }) => referenceInstance?.StudyDate, + contentF: ({ referenceInstance, formatters: { formatDate } }) => + formatDate(referenceInstance.StudyDate), + }, + { + id: 'SeriesDescription', + inheritsFrom: 'ohif.overlayItem', + label: '', + title: 'Series description', + condition: ({ referenceInstance }) => { + return referenceInstance && referenceInstance.SeriesDescription; + }, + contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription, + }, + ], + 'viewportOverlay.topRight': [], + 'viewportOverlay.bottomLeft': [ + { + id: 'WindowLevel', + inheritsFrom: 'ohif.overlayItem.windowLevel', + }, + { + id: 'ZoomLevel', + inheritsFrom: 'ohif.overlayItem.zoomLevel', + condition: props => { + const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId); + return activeToolName === 'Zoom'; + }, + }, + ], + 'viewportOverlay.bottomRight': [ + { + id: 'InstanceNumber', + inheritsFrom: 'ohif.overlayItem.instanceNumber', + }, + ], +}; diff --git a/extensions/cornerstone/src/customizations/viewportToolsCustomization.ts b/extensions/cornerstone/src/customizations/viewportToolsCustomization.ts new file mode 100644 index 0000000..e8da3a1 --- /dev/null +++ b/extensions/cornerstone/src/customizations/viewportToolsCustomization.ts @@ -0,0 +1,33 @@ +import { Enums } from '@cornerstonejs/tools'; +import { toolNames } from '../initCornerstoneTools'; + +export default { + 'cornerstone.overlayViewportTools': { + active: [ + { + toolName: toolNames.WindowLevel, + bindings: [{ mouseButton: Enums.MouseBindings.Primary }], + }, + { + toolName: toolNames.Pan, + bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }], + }, + { + toolName: toolNames.Zoom, + bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], + }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, + ], + enabled: [ + { + toolName: toolNames.PlanarFreehandContourSegmentation, + configuration: { + displayOnePointAsCrosshairs: true, + }, + }, + ], + }, +}; diff --git a/extensions/cornerstone/src/customizations/volumeRenderingCustomization.ts b/extensions/cornerstone/src/customizations/volumeRenderingCustomization.ts new file mode 100644 index 0000000..51ea278 --- /dev/null +++ b/extensions/cornerstone/src/customizations/volumeRenderingCustomization.ts @@ -0,0 +1,14 @@ +import { CONSTANTS } from '@cornerstonejs/core'; + +const { VIEWPORT_PRESETS } = CONSTANTS; + +export default { + 'cornerstone.3dVolumeRendering': { + volumeRenderingPresets: VIEWPORT_PRESETS, + volumeRenderingQualityRange: { + min: 1, + max: 4, + step: 1, + }, + }, +}; diff --git a/extensions/cornerstone/src/customizations/windowLevelActionMenuCustomization.ts b/extensions/cornerstone/src/customizations/windowLevelActionMenuCustomization.ts new file mode 100644 index 0000000..ccbcca5 --- /dev/null +++ b/extensions/cornerstone/src/customizations/windowLevelActionMenuCustomization.ts @@ -0,0 +1,5 @@ +import { WindowLevelActionMenu } from '../components/WindowLevelActionMenu/WindowLevelActionMenu'; + +export default { + 'cornerstone.windowLevelActionMenu': WindowLevelActionMenu, +}; diff --git a/extensions/cornerstone/src/customizations/windowLevelPresetsCustomization.ts b/extensions/cornerstone/src/customizations/windowLevelPresetsCustomization.ts new file mode 100644 index 0000000..c9d7089 --- /dev/null +++ b/extensions/cornerstone/src/customizations/windowLevelPresetsCustomization.ts @@ -0,0 +1,5 @@ +import defaultWindowLevelPresets from '../components/WindowLevelActionMenu/defaultWindowLevelPresets'; + +export default { + 'cornerstone.windowLevelPresets': defaultWindowLevelPresets, +}; diff --git a/extensions/cornerstone/src/enums.ts b/extensions/cornerstone/src/enums.ts new file mode 100644 index 0000000..1c22851 --- /dev/null +++ b/extensions/cornerstone/src/enums.ts @@ -0,0 +1,9 @@ +export const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools'; +export const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1'; + +const Enums = { + CORNERSTONE_3D_TOOLS_SOURCE_NAME, + CORNERSTONE_3D_TOOLS_SOURCE_VERSION, +}; + +export default Enums; diff --git a/extensions/cornerstone/src/getCustomizationModule.tsx b/extensions/cornerstone/src/getCustomizationModule.tsx new file mode 100644 index 0000000..3e2a30d --- /dev/null +++ b/extensions/cornerstone/src/getCustomizationModule.tsx @@ -0,0 +1,34 @@ +import viewportOverlayCustomization from './customizations/viewportOverlayCustomization'; +import getSegmentationPanelCustomization from './customizations/segmentationPanelCustomization'; +import layoutSelectorCustomization from './customizations/layoutSelectorCustomization'; +import viewportToolsCustomization from './customizations/viewportToolsCustomization'; +import viewportClickCommandsCustomization from './customizations/viewportClickCommandsCustomization'; +import measurementsCustomization from './customizations/measurementsCustomization'; +import volumeRenderingCustomization from './customizations/volumeRenderingCustomization'; +import colorbarCustomization from './customizations/colorbarCustomization'; +import windowLevelPresetsCustomization from './customizations/windowLevelPresetsCustomization'; +import miscCustomization from './customizations/miscCustomization'; +import windowLevelActionMenuCustomization from './customizations/windowLevelActionMenuCustomization'; + +function getCustomizationModule({ commandsManager, servicesManager }) { + return [ + { + name: 'default', + value: { + ...viewportOverlayCustomization, + ...getSegmentationPanelCustomization({ commandsManager, servicesManager }), + ...layoutSelectorCustomization, + ...viewportToolsCustomization, + ...viewportClickCommandsCustomization, + ...measurementsCustomization, + ...volumeRenderingCustomization, + ...colorbarCustomization, + ...windowLevelPresetsCustomization, + ...miscCustomization, + ...windowLevelActionMenuCustomization, + }, + }, + ]; +} + +export default getCustomizationModule; diff --git a/extensions/cornerstone/src/getHangingProtocolModule.ts b/extensions/cornerstone/src/getHangingProtocolModule.ts new file mode 100644 index 0000000..6a1fc8a --- /dev/null +++ b/extensions/cornerstone/src/getHangingProtocolModule.ts @@ -0,0 +1,47 @@ +import { fourUp } from './hps/fourUp'; +import { main3D } from './hps/main3D'; +import { mpr } from './hps/mpr'; +import { mprAnd3DVolumeViewport } from './hps/mprAnd3DVolumeViewport'; +import { only3D } from './hps/only3D'; +import { primary3D } from './hps/primary3D'; +import { primaryAxial } from './hps/primaryAxial'; +import { frameView } from './hps/frameView'; + +function getHangingProtocolModule() { + return [ + { + name: mpr.id, + protocol: mpr, + }, + { + name: mprAnd3DVolumeViewport.id, + protocol: mprAnd3DVolumeViewport, + }, + { + name: fourUp.id, + protocol: fourUp, + }, + { + name: main3D.id, + protocol: main3D, + }, + { + name: primaryAxial.id, + protocol: primaryAxial, + }, + { + name: only3D.id, + protocol: only3D, + }, + { + name: primary3D.id, + protocol: primary3D, + }, + { + name: frameView.id, + protocol: frameView, + }, + ]; +} + +export default getHangingProtocolModule; diff --git a/extensions/cornerstone/src/getPanelModule.tsx b/extensions/cornerstone/src/getPanelModule.tsx new file mode 100644 index 0000000..20c3a43 --- /dev/null +++ b/extensions/cornerstone/src/getPanelModule.tsx @@ -0,0 +1,111 @@ +import React from 'react'; + +import { Toolbox } from '@ohif/ui-next'; +import PanelSegmentation from './panels/PanelSegmentation'; +import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel'; +import PanelMeasurement from './panels/PanelMeasurement'; + +const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: withAppTypes) => { + const wrappedPanelSegmentation = ({ configuration }) => { + return ( + + ); + }; + + const wrappedPanelSegmentationNoHeader = ({ configuration }) => { + return ( + + ); + }; + + const wrappedPanelSegmentationWithTools = ({ configuration }) => { + return ( + <> + + + + ); + }; + + const wrappedPanelMeasurement = ({ configuration }) => { + return ( + + ); + }; + + return [ + { + name: 'activeViewportWindowLevel', + component: () => { + return ; + }, + }, + { + name: 'panelMeasurement', + iconName: 'tab-linear', + iconLabel: 'Measure', + label: 'Measurement', + component: wrappedPanelMeasurement, + }, + { + name: 'panelSegmentation', + iconName: 'tab-segmentation', + iconLabel: 'Segmentation', + label: 'Segmentation', + component: wrappedPanelSegmentation, + }, + { + name: 'panelSegmentationNoHeader', + iconName: 'tab-segmentation', + iconLabel: 'Segmentation', + label: 'Segmentation', + component: wrappedPanelSegmentationNoHeader, + }, + { + name: 'panelSegmentationWithTools', + iconName: 'tab-segmentation', + iconLabel: 'Segmentation', + label: 'Segmentation', + component: wrappedPanelSegmentationWithTools, + }, + ]; +}; + +export default getPanelModule; diff --git a/extensions/cornerstone/src/getSopClassHandlerModule.js b/extensions/cornerstone/src/getSopClassHandlerModule.js new file mode 100644 index 0000000..f88dc66 --- /dev/null +++ b/extensions/cornerstone/src/getSopClassHandlerModule.js @@ -0,0 +1,170 @@ +import OHIF from '@ohif/core'; +import { utilities as csUtils, Enums as csEnums } from '@cornerstonejs/core'; +import dcmjs from 'dcmjs'; +import { dicomWebUtils } from '@ohif/extension-default'; + +const { MetadataModules } = csEnums; +const { utils } = OHIF; +const { denaturalizeDataset } = dcmjs.data.DicomMetaDictionary; +const { transferDenaturalizedDataset, fixMultiValueKeys } = dicomWebUtils; + +const SOP_CLASS_UIDS = { + VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.6', +}; + +const SOPClassHandlerId = + '@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler'; + +function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) { + // If the series has no instances, stop here + if (!instances || !instances.length) { + throw new Error('No instances were provided'); + } + + const instance = instances[0]; + + let singleFrameInstance = instance; + let currentFrames = +singleFrameInstance.NumberOfFrames || 1; + for (const instanceI of instances) { + const framesI = +instanceI.NumberOfFrames || 1; + if (framesI < currentFrames) { + singleFrameInstance = instanceI; + currentFrames = framesI; + } + } + let imageIdForThumbnail = null; + const dataSource = extensionManager.getActiveDataSource()[0]; + if (singleFrameInstance) { + if (currentFrames == 1) { + // Not all DICOM server implementations support thumbnail service, + // So if we have a single-frame image, we will prefer it. + imageIdForThumbnail = singleFrameInstance.imageId; + } + if (!imageIdForThumbnail) { + // use the thumbnail service provided by DICOM server + imageIdForThumbnail = dataSource.getImageIdsForInstance({ + instance: singleFrameInstance, + thumbnail: true, + }); + } + } + + const { + FrameOfReferenceUID, + SeriesDescription, + ContentDate, + ContentTime, + SeriesNumber, + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + SOPClassUID, + } = instance; + + instances = instances.map(inst => { + // NOTE: According to DICOM standard a series should have a FrameOfReferenceUID + // When the Microscopy file was built by certain tool from multiple image files, + // each instance's FrameOfReferenceUID is sometimes different. + // Even though this means the file was not well formatted DICOM VL Whole Slide Microscopy Image, + // the case is so often, so let's override this value manually here. + // + // https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.4.html#sect_C.7.4.1.1.1 + + inst.FrameOfReferenceUID = instance.FrameOfReferenceUID; + + return inst; + }); + + const othersFrameOfReferenceUID = instances + .filter(v => v) + .map(inst => inst.FrameOfReferenceUID) + .filter((value, index, array) => array.indexOf(value) === index); + if (othersFrameOfReferenceUID.length > 1) { + console.warn( + 'Expected FrameOfReferenceUID of difference instances within a series to be the same, found multiple different values', + othersFrameOfReferenceUID + ); + } + + const displaySet = { + plugin: 'microscopy', + Modality: 'SM', + viewportType: csEnums.ViewportType.WHOLE_SLIDE, + altImageText: 'Microscopy', + displaySetInstanceUID: utils.guid(), + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + FrameOfReferenceUID, + SOPClassHandlerId, + SOPClassUID, + SeriesDescription: SeriesDescription || 'Microscopy Data', + // Map ContentDate/Time to SeriesTime for series list sorting. + SeriesDate: ContentDate, + SeriesTime: ContentTime, + SeriesNumber, + firstInstance: singleFrameInstance, // top level instance in the image Pyramid + instance, + numImageFrames: 0, + numInstances: 1, + imageIdForThumbnail, // thumbnail image + others: instances, // all other level instances in the image Pyramid + instances, + othersFrameOfReferenceUID, + imageIds: instances.map(instance => instance.imageId), + }; + // The microscopy viewer directly accesses the metadata already loaded, and + // uses the DICOMweb client library directly for loading, so it has to be + // provided here. + const dicomWebClient = dataSource.retrieve.getWadoDicomWebClient?.(); + const instanceMap = new Map(); + instances.forEach(instance => instanceMap.set(instance.imageId, instance)); + if (dicomWebClient) { + const webClient = Object.create(dicomWebClient); + // This replaces just the dicom web metadata call with one which retrieves + // internally. + webClient.getDICOMwebMetadata = getDICOMwebMetadata.bind(webClient, instanceMap); + + csUtils.genericMetadataProvider.addRaw(displaySet.imageIds[0], { + type: MetadataModules.WADO_WEB_CLIENT, + metadata: webClient, + }); + } else { + // Might have some other way of getting the data in the future or internally? + // throw new Error('Unable to provide a DICOMWeb client library, microscopy will fail to view'); + } + + return [displaySet]; +} + +/** + * This method provides access to the internal DICOMweb metadata, used to avoid + * refetching the DICOMweb data. It gets assigned as a member function to the + * dicom web client. + */ +function getDICOMwebMetadata(instanceMap, imageId) { + const instance = instanceMap.get(imageId); + if (!instance) { + console.warn('Metadata not already found for', imageId, 'in', instanceMap); + return this.super.getDICOMwebMetadata(imageId); + } + return transferDenaturalizedDataset( + denaturalizeDataset(fixMultiValueKeys(instanceMap.get(imageId))) + ); +} + +export function getDicomMicroscopySopClassHandler({ servicesManager, extensionManager }) { + const getDisplaySetsFromSeries = instances => { + return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager); + }; + + return { + name: 'DicomMicroscopySopClassHandler', + sopClassUids: [SOP_CLASS_UIDS.VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE], + getDisplaySetsFromSeries, + }; +} + +export function getSopClassHandlerModule(params) { + return [getDicomMicroscopySopClassHandler(params)]; +} diff --git a/extensions/cornerstone/src/getToolbarModule.tsx b/extensions/cornerstone/src/getToolbarModule.tsx new file mode 100644 index 0000000..26e4aa7 --- /dev/null +++ b/extensions/cornerstone/src/getToolbarModule.tsx @@ -0,0 +1,299 @@ +import { Enums } from '@cornerstonejs/tools'; +import { utils } from '@ohif/ui-next'; + +const getDisabledState = (disabledText?: string) => ({ + disabled: true, + disabledText: disabledText ?? 'Not available on the current viewport', +}); + +export default function getToolbarModule({ commandsManager, servicesManager }: withAppTypes) { + const { + toolGroupService, + toolbarService, + syncGroupService, + cornerstoneViewportService, + hangingProtocolService, + displaySetService, + viewportGridService, + } = servicesManager.services; + + return [ + // functions/helpers to be used by the toolbar buttons to decide if they should + // enabled or not + { + name: 'evaluate.viewport.supported', + evaluate: ({ viewportId, unsupportedViewportTypes, disabledText }) => { + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + + if (viewport && unsupportedViewportTypes?.includes(viewport.type)) { + return getDisabledState(disabledText); + } + + return undefined; + }, + }, + { + name: 'evaluate.modality.supported', + evaluate: ({ viewportId, unsupportedModalities, supportedModalities, disabledText }) => { + const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); + + if (!displaySetUIDs?.length) { + return; + } + + const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID); + + // Check for unsupported modalities (exclusion) + if (unsupportedModalities?.length) { + const hasUnsupportedModality = displaySets.some(displaySet => + unsupportedModalities.includes(displaySet?.Modality) + ); + + if (hasUnsupportedModality) { + return getDisabledState(disabledText); + } + } + + // Check for supported modalities (inclusion) + if (supportedModalities?.length) { + const hasAnySupportedModality = displaySets.some(displaySet => + supportedModalities.includes(displaySet?.Modality) + ); + + if (!hasAnySupportedModality) { + return getDisabledState(disabledText || 'Tool not available for this modality'); + } + } + }, + }, + { + name: 'evaluate.cornerstoneTool', + evaluate: ({ viewportId, button, toolNames, disabledText }) => { + const toolGroup = toolGroupService.getToolGroupForViewport(viewportId); + + if (!toolGroup) { + return; + } + + const toolName = toolbarService.getToolNameForButton(button); + + if (!toolGroup || (!toolGroup.hasTool(toolName) && !toolNames)) { + return getDisabledState(disabledText); + } + + const isPrimaryActive = toolNames + ? toolNames.includes(toolGroup.getActivePrimaryMouseButtonTool()) + : toolGroup.getActivePrimaryMouseButtonTool() === toolName; + + return { + disabled: false, + // Todo: isActive right now is used for nested buttons where the primary + // button needs to be fully rounded (vs partial rounded) when active + // otherwise it does not have any other use + isActive: isPrimaryActive, + }; + }, + }, + { + name: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList', + evaluate: ({ viewportId, button, itemId }) => { + const { items } = button.props; + + const toolGroup = toolGroupService.getToolGroupForViewport(viewportId); + + if (!toolGroup) { + return { + primary: button.props.primary, + items, + }; + } + + const activeToolName = toolGroup.getActivePrimaryMouseButtonTool(); + + // check if the active toolName is part of the items then we need + // to move it to the primary button + const activeToolIndex = items.findIndex(item => { + const toolName = toolbarService.getToolNameForButton(item); + return toolName === activeToolName; + }); + + // if there is an active tool in the items dropdown bound to the primary mouse/touch + // we should show that no matter what + if (activeToolIndex > -1) { + return { + primary: items[activeToolIndex], + items, + }; + } + + if (!itemId) { + return { + primary: button.props.primary, + items, + }; + } + + // other wise we can move the clicked tool to the primary button + const clickedItemProps = items.find(item => item.id === itemId || item.itemId === itemId); + + return { + primary: clickedItemProps, + items, + }; + }, + }, + { + name: 'evaluate.action', + evaluate: () => { + return { + disabled: false, + }; + }, + }, + { + name: 'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled', + evaluate: ({ viewportId, button, disabledText }) => + _evaluateToggle({ + viewportId, + button, + toolbarService, + disabledText, + offModes: [Enums.ToolModes.Disabled], + toolGroupService, + }), + }, + { + name: 'evaluate.cornerstoneTool.toggle', + evaluate: ({ viewportId, button, disabledText }) => + _evaluateToggle({ + viewportId, + button, + toolbarService, + disabledText, + offModes: [Enums.ToolModes.Disabled, Enums.ToolModes.Passive], + toolGroupService, + }), + }, + { + name: 'evaluate.cornerstone.synchronizer', + evaluate: ({ viewportId, button }) => { + let synchronizers = syncGroupService.getSynchronizersForViewport(viewportId); + + if (!synchronizers?.length) { + return { + className: utils.getToggledClassName(false), + }; + } + + const isArray = Array.isArray(button.commands); + + const synchronizerType = isArray + ? button.commands?.[0].commandOptions.type + : button.commands?.commandOptions.type; + + synchronizers = syncGroupService.getSynchronizersOfType(synchronizerType); + + if (!synchronizers?.length) { + return { + className: utils.getToggledClassName(false), + }; + } + + // Todo: we need a better way to find the synchronizers based on their + // type, but for now we just check the first one and see if it is + // enabled + const synchronizer = synchronizers[0]; + + const isEnabled = synchronizer?._enabled; + + return { + className: utils.getToggledClassName(isEnabled), + }; + }, + }, + { + name: 'evaluate.viewportProperties.toggle', + evaluate: ({ viewportId, button }) => { + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + + if (!viewport || viewport.isDisabled) { + return; + } + + const propId = button.id; + + const properties = viewport.getProperties(); + const camera = viewport.getCamera(); + + const prop = camera?.[propId] || properties?.[propId]; + + if (!prop) { + return { + disabled: false, + }; + } + + const isToggled = prop; + + return { + className: utils.getToggledClassName(isToggled), + }; + }, + }, + { + name: 'evaluate.mpr', + evaluate: ({ viewportId, disabledText = 'Selected viewport is not reconstructable' }) => { + const { protocol } = hangingProtocolService.getActiveProtocol(); + + const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); + + if (!displaySetUIDs?.length) { + return; + } + + const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID); + + const areReconstructable = displaySets.every(displaySet => { + return displaySet?.isReconstructable; + }); + + if (!areReconstructable) { + return getDisabledState(disabledText); + } + + const isMpr = protocol?.id === 'mpr'; + + return { + disabled: false, + className: utils.getToggledClassName(isMpr), + }; + }, + }, + ]; +} + +function _evaluateToggle({ + viewportId, + toolbarService, + button, + disabledText, + offModes, + toolGroupService, +}) { + const toolGroup = toolGroupService.getToolGroupForViewport(viewportId); + + if (!toolGroup) { + return; + } + const toolName = toolbarService.getToolNameForButton(button); + + if (!toolGroup.hasTool(toolName)) { + return getDisabledState(disabledText); + } + + const isOff = offModes.includes(toolGroup.getToolOptions(toolName).mode); + + return { + className: utils.getToggledClassName(!isOff), + }; +} diff --git a/extensions/cornerstone/src/hooks/useActiveViewportSegmentationRepresentations.ts b/extensions/cornerstone/src/hooks/useActiveViewportSegmentationRepresentations.ts new file mode 100644 index 0000000..9468393 --- /dev/null +++ b/extensions/cornerstone/src/hooks/useActiveViewportSegmentationRepresentations.ts @@ -0,0 +1,216 @@ +import { useState, useEffect } from 'react'; +import debounce from 'lodash.debounce'; +import { roundNumber } from '@ohif/core/src/utils'; +import { + SegmentationData, + SegmentationRepresentation, +} from '../services/SegmentationService/SegmentationService'; + +const excludedModalities = ['SM', 'OT', 'DOC', 'ECG']; + +function mapSegmentationToDisplay(segmentation, customizationService) { + const { label, segments } = segmentation; + + // Get the readable text mapping once + const readableTextMap = customizationService.getCustomization('panelSegmentation.readableText'); + + // Helper function to recursively map cachedStats to readable display text + function mapStatsToDisplay(stats, indent = 0) { + const primary = []; + const indentation = ' '.repeat(indent); + + for (const key in stats) { + if (Object.prototype.hasOwnProperty.call(stats, key)) { + const value = stats[key]; + const readableText = readableTextMap?.[key]; + + if (!readableText) { + continue; + } + + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + // Add empty row before category (except for the first category) + if (primary.length > 0) { + primary.push(''); + } + // Add category title + primary.push(`${indentation}${readableText}`); + // Recursively handle nested objects + primary.push(...mapStatsToDisplay(value, indent + 1)); + } else { + // For non-nested values, don't add empty rows + primary.push(`${indentation}${readableText}: ${roundNumber(value, 2)}`); + } + } + } + + return primary; + } + + // Get customization for display text mapping + const displayTextMapper = segment => { + const defaultDisplay = { + primary: [], + secondary: [], + }; + + // If the segment has cachedStats, map it to readable text + if (segment.cachedStats) { + const primary = mapStatsToDisplay(segment.cachedStats); + defaultDisplay.primary = primary; + } + + return defaultDisplay; + }; + + const updatedSegments = {}; + + Object.entries(segments).forEach(([segmentIndex, segment]) => { + updatedSegments[segmentIndex] = { + ...segment, + displayText: displayTextMapper(segment), + }; + }); + + // Map the segments and apply the display text mapper + return { + ...segmentation, + label, + segments: updatedSegments, + }; +} + +/** + * Represents the combination of segmentation data and its representation in a viewport. + */ +type ViewportSegmentationRepresentation = { + segmentationsWithRepresentations: { + representation: SegmentationRepresentation; + segmentation: SegmentationData; + }[]; + disabled: boolean; +}; + +/** + * Custom hook that provides segmentation data and their representations for the active viewport. + * @param options - The options object. + * @param options.servicesManager - The services manager object. + * @param options.subscribeToDataModified - Whether to subscribe to segmentation data modifications. + * @param options.debounceTime - Debounce time in milliseconds for updates. + * @returns An array of segmentation data and their representations for the active viewport. + */ +export function useActiveViewportSegmentationRepresentations({ + servicesManager, + subscribeToDataModified = false, + debounceTime = 0, +}: withAppTypes<{ debounceTime?: number }>): ViewportSegmentationRepresentation { + const { segmentationService, viewportGridService, customizationService, displaySetService } = + servicesManager.services; + + const [segmentationsWithRepresentations, setSegmentationsWithRepresentations] = + useState({ + segmentationsWithRepresentations: [], + disabled: false, + }); + + useEffect(() => { + const update = () => { + const viewportId = viewportGridService.getActiveViewportId(); + const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId); + + if (!displaySetUIDs?.length) { + return; + } + + const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]); + + if (!displaySet) { + return; + } + + if (excludedModalities.includes(displaySet.Modality)) { + setSegmentationsWithRepresentations(prev => ({ + segmentationsWithRepresentations: [], + disabled: true, + })); + return; + } + + const segmentations = segmentationService.getSegmentations(); + + if (!segmentations?.length) { + setSegmentationsWithRepresentations(prev => ({ + segmentationsWithRepresentations: [], + disabled: false, + })); + return; + } + + const representations = segmentationService.getSegmentationRepresentations(viewportId); + + const newSegmentationsWithRepresentations = representations.map(representation => { + const segmentation = segmentationService.getSegmentation(representation.segmentationId); + const mappedSegmentation = mapSegmentationToDisplay(segmentation, customizationService); + return { + representation, + segmentation: mappedSegmentation, + }; + }); + + setSegmentationsWithRepresentations({ + segmentationsWithRepresentations: newSegmentationsWithRepresentations, + disabled: false, + }); + }; + + const debouncedUpdate = + debounceTime > 0 ? debounce(update, debounceTime, { leading: true, trailing: true }) : update; + + update(); + + const subscriptions = [ + segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_MODIFIED, + debouncedUpdate + ), + segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_REMOVED, + debouncedUpdate + ), + segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, + debouncedUpdate + ), + viewportGridService.subscribe( + viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED, + debouncedUpdate + ), + viewportGridService.subscribe(viewportGridService.EVENTS.GRID_STATE_CHANGED, debouncedUpdate), + ]; + + if (subscribeToDataModified) { + subscriptions.push( + segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED, + debouncedUpdate + ) + ); + } + + return () => { + subscriptions.forEach(subscription => subscription.unsubscribe()); + if (debounceTime > 0) { + debouncedUpdate.cancel(); + } + }; + }, [ + segmentationService, + viewportGridService, + customizationService, + displaySetService, + debounceTime, + subscribeToDataModified, + ]); + + return segmentationsWithRepresentations; +} diff --git a/extensions/cornerstone/src/hooks/useMeasurements.ts b/extensions/cornerstone/src/hooks/useMeasurements.ts new file mode 100644 index 0000000..e764453 --- /dev/null +++ b/extensions/cornerstone/src/hooks/useMeasurements.ts @@ -0,0 +1,100 @@ +import { useState, useEffect } from 'react'; +import debounce from 'lodash.debounce'; + +function mapMeasurementToDisplay(measurement, displaySetService) { + const { referenceSeriesUID } = measurement; + + const displaySets = displaySetService.getDisplaySetsForSeries(referenceSeriesUID); + + if (!displaySets[0]?.instances) { + throw new Error('The tracked measurements panel should only be tracking "stack" displaySets.'); + } + + const { findingSites, finding, label: baseLabel, displayText: baseDisplayText } = measurement; + + const firstSite = findingSites?.[0]; + const label = baseLabel || finding?.text || firstSite?.text || '(empty)'; + + // Initialize displayText with the structure used in Length.ts and CobbAngle.ts + const displayText = { + primary: [], + secondary: baseDisplayText?.secondary || [], + }; + + // Add baseDisplayText to primary if it exists + if (baseDisplayText) { + displayText.primary.push(...baseDisplayText.primary); + } + + // Add finding sites to primary + if (findingSites) { + findingSites.forEach(site => { + if (site?.text && site.text !== label) { + displayText.primary.push(site.text); + } + }); + } + + // Add finding to primary if it's different from the label + if (finding && finding.text && finding.text !== label) { + displayText.primary.push(finding.text); + } + + return { + ...measurement, + displayText, + label, + }; +} + +/** + * A custom hook that provides mapped measurements based on the given services and filters. + * + * @param {Object} servicesManager - The services manager object. + * @param {Object} options - The options for filtering and mapping measurements. + * @param {Function} options.measurementFilter - Optional function to filter measurements. + * @param {Object} options.valueTypes - The value types for mapping measurements. + * @returns {Array} An array of mapped and filtered measurements. + */ +export function useMeasurements(servicesManager, { measurementFilter }) { + const { measurementService, displaySetService } = servicesManager.services; + const [displayMeasurements, setDisplayMeasurements] = useState([]); + + useEffect(() => { + const updateDisplayMeasurements = () => { + let measurements = measurementService.getMeasurements(measurementFilter); + const mappedMeasurements = measurements.map(m => + mapMeasurementToDisplay(m, displaySetService) + ); + setDisplayMeasurements(prevMeasurements => { + if (JSON.stringify(prevMeasurements) !== JSON.stringify(mappedMeasurements)) { + return mappedMeasurements; + } + return prevMeasurements; + }); + }; + + const debouncedUpdate = debounce(updateDisplayMeasurements, 100); + + updateDisplayMeasurements(); + + const events = [ + measurementService.EVENTS.MEASUREMENT_ADDED, + measurementService.EVENTS.RAW_MEASUREMENT_ADDED, + measurementService.EVENTS.MEASUREMENT_UPDATED, + measurementService.EVENTS.MEASUREMENT_REMOVED, + measurementService.EVENTS.MEASUREMENTS_CLEARED, + ]; + + const subscriptions = events.map( + evt => measurementService.subscribe(evt, debouncedUpdate).unsubscribe + ); + + return () => { + subscriptions.forEach(unsub => unsub()); + debouncedUpdate.cancel(); + }; + }, [measurementService, measurementFilter, displaySetService]); + + return displayMeasurements; +} diff --git a/extensions/cornerstone/src/hooks/useSegmentations.ts b/extensions/cornerstone/src/hooks/useSegmentations.ts new file mode 100644 index 0000000..e32dae4 --- /dev/null +++ b/extensions/cornerstone/src/hooks/useSegmentations.ts @@ -0,0 +1,145 @@ +import { useState, useEffect } from 'react'; +import debounce from 'lodash.debounce'; +import { roundNumber } from '@ohif/core/src/utils'; +import { SegmentationData } from '../services/SegmentationService/SegmentationService'; + +function mapSegmentationToDisplay(segmentation, customizationService) { + const { label, segments } = segmentation; + + // Get the readable text mapping once + const readableTextMap = customizationService.getCustomization('panelSegmentation.readableText'); + + // Helper function to recursively map cachedStats to readable display text + function mapStatsToDisplay(stats, indent = 0) { + const primary = []; + const indentation = ' '.repeat(indent); + + for (const key in stats) { + if (Object.prototype.hasOwnProperty.call(stats, key)) { + const value = stats[key]; + const readableText = readableTextMap?.[key]; + + if (!readableText) { + continue; + } + + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + // Add empty row before category (except for the first category) + if (primary.length > 0) { + primary.push(''); + } + // Add category title + primary.push(`${indentation}${readableText}`); + // Recursively handle nested objects + primary.push(...mapStatsToDisplay(value, indent + 1)); + } else { + // For non-nested values, don't add empty rows + primary.push(`${indentation}${readableText}: ${roundNumber(value, 2)}`); + } + } + } + + return primary; + } + + // Get customization for display text mapping + const displayTextMapper = segment => { + const defaultDisplay = { + primary: [], + secondary: [], + }; + + // If the segment has cachedStats, map it to readable text + if (segment.cachedStats) { + const primary = mapStatsToDisplay(segment.cachedStats); + defaultDisplay.primary = primary; + } + + return defaultDisplay; + }; + + const updatedSegments = {}; + + Object.entries(segments).forEach(([segmentIndex, segment]) => { + updatedSegments[segmentIndex] = { + ...segment, + displayText: displayTextMapper(segment), + }; + }); + + // Map the segments and apply the display text mapper + return { + ...segmentation, + label, + segments: updatedSegments, + }; +} + +/** + * Custom hook that provides segmentation data. + * @param options - The options object. + * @param options.servicesManager - The services manager object. + * @param options.subscribeToDataModified - Whether to subscribe to segmentation data modifications. + * @param options.debounceTime - Debounce time in milliseconds for updates. + * @returns An array of segmentation data. + */ +export function useSegmentations({ + servicesManager, + subscribeToDataModified = false, + debounceTime = 0, +}: withAppTypes<{ debounceTime?: number }>): SegmentationData[] { + const { segmentationService, customizationService } = servicesManager.services; + + const [segmentations, setSegmentations] = useState([]); + + useEffect(() => { + const update = () => { + const segmentations = segmentationService.getSegmentations(); + + if (!segmentations?.length) { + setSegmentations([]); + return; + } + + const mappedSegmentations = segmentations.map(segmentation => + mapSegmentationToDisplay(segmentation, customizationService) + ); + + setSegmentations(mappedSegmentations); + }; + + const debouncedUpdate = + debounceTime > 0 ? debounce(update, debounceTime, { leading: true, trailing: true }) : update; + + update(); + + const subscriptions = [ + segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_MODIFIED, + debouncedUpdate + ), + segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_REMOVED, + debouncedUpdate + ), + ]; + + if (subscribeToDataModified) { + subscriptions.push( + segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED, + debouncedUpdate + ) + ); + } + + return () => { + subscriptions.forEach(subscription => subscription.unsubscribe()); + if (debounceTime > 0) { + debouncedUpdate.cancel(); + } + }; + }, [segmentationService, customizationService, debounceTime, subscribeToDataModified]); + + return segmentations; +} diff --git a/extensions/cornerstone/src/hps/fourUp.ts b/extensions/cornerstone/src/hps/fourUp.ts new file mode 100644 index 0000000..9a72b04 --- /dev/null +++ b/extensions/cornerstone/src/hps/fourUp.ts @@ -0,0 +1,117 @@ +import { HYDRATE_SEG_SYNC_GROUP, VOI_SYNC_GROUP } from './mpr'; + +export const fourUp = { + id: 'fourUp', + locked: true, + name: '3D four up', + icon: 'layout-advanced-3d-four-up', + isPreset: true, + createdDate: '2023-03-15T10:29:44.894Z', + modifiedDate: '2023-03-15T10:29:44.894Z', + availableTo: {}, + editableBy: {}, + protocolMatchingRules: [], + imageLoadStrategy: 'interleaveCenter', + displaySetSelectors: { + activeDisplaySet: { + seriesMatchingRules: [ + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + ], + }, + }, + stages: [ + { + id: 'fourUpStage', + name: 'fourUp', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 2, + columns: 2, + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: 'axial', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'volume3d', + viewportType: 'volume3d', + orientation: 'coronal', + customViewportProps: { + hideOverlays: true, + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + options: { + displayPreset: { + CT: 'CT-Bone', + MR: 'MR-Default', + default: 'CT-Bone', + }, + }, + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: 'coronal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: 'sagittal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + }, + ], + }, + ], + }, + ], +}; diff --git a/extensions/cornerstone/src/hps/frameView.ts b/extensions/cornerstone/src/hps/frameView.ts new file mode 100644 index 0000000..e699f68 --- /dev/null +++ b/extensions/cornerstone/src/hps/frameView.ts @@ -0,0 +1,1582 @@ +import { Types } from '@ohif/core'; + +const frameView: Types.HangingProtocol.Protocol = { + id: '@ohif/frameView', + description: 'Frame view for the active series', + name: 'Frame View', + icon: 'tool-stack-scroll', + isPreset: true, + toolGroupIds: ['default'], + protocolMatchingRules: [], + displaySetSelectors: { + defaultDisplaySetId: { + seriesMatchingRules: [ + { + attribute: 'numImageFrames', + constraint: { + greaterThan: { value: 16 }, + }, + required: true, + }, + { + attribute: 'isDisplaySetFromUrl', + weight: 20, + constraint: { + equals: true, + }, + }, + ], + }, + }, + defaultViewport: { + viewportOptions: { + viewportType: 'stack', + toolGroupId: 'default', + allowUnmatchedView: true, + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + matchedDisplaySetsIndex: -1, + }, + ], + }, + stages: [ + { + name: 'frameView', + id: '4x4', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 4, + columns: 4, + }, + }, + viewports: [ + { + viewportOptions: { + viewportId: 'custom_R0_C0', + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 0, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + viewportId: 'custom_R0_C1', + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 1, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + viewportId: 'custom_R0_C2', + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 2, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + viewportId: 'custom_R0_C3', + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 3, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + viewportId: 'custom_R1_C0', + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 4, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + viewportId: 'custom_R1_C1', + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 5, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + viewportId: 'custom_R1_C2', + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 6, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + viewportId: 'custom_R1_C3', + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 7, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + viewportId: 'custom_R2_C0', + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 8, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + viewportId: 'custom_R2_C1', + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 9, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + viewportId: 'custom_R2_C2', + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 10, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + viewportId: 'custom_R2_C3', + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 11, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + viewportId: 'custom_R3_C0', + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 12, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + viewportId: 'custom_R3_C1', + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 13, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + viewportId: 'custom_R3_C2', + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 14, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + viewportId: 'custom_R3_C3', + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 15, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + ], + }, + { + name: 'frameView', + id: '3x3', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 3, + columns: 3, + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 0, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 1, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 2, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 3, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 4, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 5, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 6, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 7, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 8, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + ], + }, + { + name: 'frameView', + id: '3x2', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 2, + columns: 3, + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 0, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 1, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 2, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 3, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 4, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 5, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + ], + }, + { + name: 'frameView', + id: '2x2', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 2, + columns: 2, + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 0, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 1, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 2, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 3, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + ], + }, + { + name: 'frameView', + id: '1x3', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 3, + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 0, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 1, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 2, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + ], + }, + { + name: 'frameView', + id: '1x2', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 2, + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 0, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + syncGroups: [ + { + type: 'zoompan', + id: 'zoompansync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'wlsync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'frameview', + id: 'frameViewSync', + source: true, + target: true, + options: { + viewportIndex: 1, + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + ], + }, + ], +}; + +export { frameView }; diff --git a/extensions/cornerstone/src/hps/main3D.ts b/extensions/cornerstone/src/hps/main3D.ts new file mode 100644 index 0000000..1842d5b --- /dev/null +++ b/extensions/cornerstone/src/hps/main3D.ts @@ -0,0 +1,142 @@ +import { HYDRATE_SEG_SYNC_GROUP, VOI_SYNC_GROUP } from './mpr'; + +export const main3D = { + id: 'main3D', + locked: true, + name: '3D main', + icon: 'layout-advanced-3d-main', + isPreset: true, + createdDate: '2023-03-15T10:29:44.894Z', + modifiedDate: '2023-03-15T10:29:44.894Z', + availableTo: {}, + editableBy: {}, + protocolMatchingRules: [], + imageLoadStrategy: 'interleaveCenter', + displaySetSelectors: { + activeDisplaySet: { + seriesMatchingRules: [ + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + ], + }, + }, + stages: [ + { + id: 'main3DStage', + name: 'main3D', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 2, + columns: 3, + layoutOptions: [ + { + x: 0, + y: 0, + width: 1, + height: 1 / 2, + }, + { + x: 0, + y: 1 / 2, + width: 1 / 3, + height: 1 / 2, + }, + { + x: 1 / 3, + y: 1 / 2, + width: 1 / 3, + height: 1 / 2, + }, + { + x: 2 / 3, + y: 1 / 2, + width: 1 / 3, + height: 1 / 2, + }, + ], + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'volume3d', + viewportType: 'volume3d', + orientation: 'coronal', + customViewportProps: { + hideOverlays: true, + }, + }, + displaySets: [ + { + id: 'activeDisplaySet', + options: { + displayPreset: { + CT: 'CT-Bone', + MR: 'MR-Default', + default: 'CT-Bone', + }, + }, + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: 'axial', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: 'coronal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: 'sagittal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + }, + ], + }, + ], + }, + ], +}; diff --git a/extensions/cornerstone/src/hps/mpr.ts b/extensions/cornerstone/src/hps/mpr.ts new file mode 100644 index 0000000..331855a --- /dev/null +++ b/extensions/cornerstone/src/hps/mpr.ts @@ -0,0 +1,138 @@ +import { Types } from '@ohif/core'; + +export const VOI_SYNC_GROUP = { + type: 'voi', + id: 'mpr', + source: true, + target: true, + options: { + syncColormap: true, + }, +}; + +export const HYDRATE_SEG_SYNC_GROUP = { + type: 'hydrateseg', + id: 'sameFORId', + source: true, + target: true, + options: { + matchingRules: ['sameFOR'], + }, +}; + +export const mpr: Types.HangingProtocol.Protocol = { + id: 'mpr', + name: 'MPR', + locked: true, + icon: 'layout-advanced-mpr', + isPreset: true, + createdDate: '2021-02-23', + modifiedDate: '2023-08-15', + availableTo: {}, + editableBy: {}, + numberOfPriorsReferenced: 0, + protocolMatchingRules: [], + imageLoadStrategy: 'nth', + callbacks: {}, + displaySetSelectors: { + activeDisplaySet: { + seriesMatchingRules: [ + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + ], + }, + }, + stages: [ + { + name: 'MPR 1x3', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 3, + layoutOptions: [ + { + x: 0, + y: 0, + width: 1 / 3, + height: 1, + }, + { + x: 1 / 3, + y: 0, + width: 1 / 3, + height: 1, + }, + { + x: 2 / 3, + y: 0, + width: 1 / 3, + height: 1, + }, + ], + }, + }, + viewports: [ + { + viewportOptions: { + viewportId: 'mpr-axial', + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: 'axial', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'mpr-sagittal', + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: 'sagittal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'mpr-coronal', + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: 'coronal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + }, + ], + }, + ], + }, + ], +}; diff --git a/extensions/cornerstone/src/hps/mprAnd3DVolumeViewport.ts b/extensions/cornerstone/src/hps/mprAnd3DVolumeViewport.ts new file mode 100644 index 0000000..002600f --- /dev/null +++ b/extensions/cornerstone/src/hps/mprAnd3DVolumeViewport.ts @@ -0,0 +1,124 @@ +import { HYDRATE_SEG_SYNC_GROUP, VOI_SYNC_GROUP } from './mpr'; + +export const mprAnd3DVolumeViewport = { + id: 'mprAnd3DVolumeViewport', + locked: true, + name: 'mpr', + createdDate: '2023-03-15T10:29:44.894Z', + modifiedDate: '2023-03-15T10:29:44.894Z', + availableTo: {}, + editableBy: {}, + protocolMatchingRules: [], + imageLoadStrategy: 'interleaveCenter', + displaySetSelectors: { + activeDisplaySet: { + seriesMatchingRules: [ + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + { + attribute: 'Modality', + constraint: { + equals: { + value: 'CT', + }, + }, + required: true, + }, + ], + }, + }, + stages: [ + { + id: 'mpr3Stage', + name: 'mpr', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 2, + columns: 2, + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: 'axial', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'volume3d', + viewportType: 'volume3d', + orientation: 'coronal', + customViewportProps: { + hideOverlays: true, + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + options: { + displayPreset: { + CT: 'CT-Bone', + MR: 'MR-Default', + default: 'CT-Bone', + }, + }, + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: 'coronal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: 'sagittal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + }, + ], + }, + ], + }, + ], +}; diff --git a/extensions/cornerstone/src/hps/only3D.ts b/extensions/cornerstone/src/hps/only3D.ts new file mode 100644 index 0000000..6e5782e --- /dev/null +++ b/extensions/cornerstone/src/hps/only3D.ts @@ -0,0 +1,69 @@ +import { HYDRATE_SEG_SYNC_GROUP, VOI_SYNC_GROUP } from './mpr'; + +export const only3D = { + id: 'only3D', + locked: true, + name: '3D only', + icon: 'layout-advanced-3d-only', + isPreset: true, + createdDate: '2023-03-15T10:29:44.894Z', + modifiedDate: '2023-03-15T10:29:44.894Z', + availableTo: {}, + editableBy: {}, + protocolMatchingRules: [], + imageLoadStrategy: 'interleaveCenter', + displaySetSelectors: { + activeDisplaySet: { + seriesMatchingRules: [ + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + ], + }, + }, + stages: [ + { + id: 'only3DStage', + name: 'only3D', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 1, + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'volume3d', + viewportType: 'volume3d', + orientation: 'coronal', + customViewportProps: { + hideOverlays: true, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + }, + displaySets: [ + { + id: 'activeDisplaySet', + options: { + displayPreset: { + CT: 'CT-Bone', + MR: 'MR-Default', + default: 'CT-Bone', + }, + }, + }, + ], + }, + ], + }, + ], +}; diff --git a/extensions/cornerstone/src/hps/primary3D.ts b/extensions/cornerstone/src/hps/primary3D.ts new file mode 100644 index 0000000..70dfecc --- /dev/null +++ b/extensions/cornerstone/src/hps/primary3D.ts @@ -0,0 +1,142 @@ +import { HYDRATE_SEG_SYNC_GROUP, VOI_SYNC_GROUP } from './mpr'; + +export const primary3D = { + id: 'primary3D', + locked: true, + name: '3D primary', + icon: 'layout-advanced-3d-primary', + isPreset: true, + createdDate: '2023-03-15T10:29:44.894Z', + modifiedDate: '2023-03-15T10:29:44.894Z', + availableTo: {}, + editableBy: {}, + protocolMatchingRules: [], + imageLoadStrategy: 'interleaveCenter', + displaySetSelectors: { + activeDisplaySet: { + seriesMatchingRules: [ + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + ], + }, + }, + stages: [ + { + id: 'primary3DStage', + name: 'primary3D', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 3, + columns: 3, + layoutOptions: [ + { + x: 0, + y: 0, + width: 2 / 3, + height: 1, + }, + { + x: 2 / 3, + y: 0, + width: 1 / 3, + height: 1 / 3, + }, + { + x: 2 / 3, + y: 1 / 3, + width: 1 / 3, + height: 1 / 3, + }, + { + x: 2 / 3, + y: 2 / 3, + width: 1 / 3, + height: 1 / 3, + }, + ], + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'volume3d', + viewportType: 'volume3d', + orientation: 'coronal', + customViewportProps: { + hideOverlays: true, + }, + }, + displaySets: [ + { + id: 'activeDisplaySet', + options: { + displayPreset: { + CT: 'CT-Bone', + MR: 'MR-Default', + default: 'CT-Bone', + }, + }, + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: 'axial', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: 'coronal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: 'sagittal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + }, + ], + }, + ], + }, + ], +}; diff --git a/extensions/cornerstone/src/hps/primaryAxial.ts b/extensions/cornerstone/src/hps/primaryAxial.ts new file mode 100644 index 0000000..2d240b5 --- /dev/null +++ b/extensions/cornerstone/src/hps/primaryAxial.ts @@ -0,0 +1,114 @@ +import { HYDRATE_SEG_SYNC_GROUP, VOI_SYNC_GROUP } from './mpr'; + +export const primaryAxial = { + id: 'primaryAxial', + locked: true, + name: 'Axial Primary', + icon: 'layout-advanced-axial-primary', + isPreset: true, + createdDate: '2023-03-15T10:29:44.894Z', + modifiedDate: '2023-03-15T10:29:44.894Z', + availableTo: {}, + editableBy: {}, + protocolMatchingRules: [], + imageLoadStrategy: 'interleaveCenter', + displaySetSelectors: { + activeDisplaySet: { + seriesMatchingRules: [ + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + ], + }, + }, + stages: [ + { + id: 'primaryAxialStage', + name: 'primaryAxial', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 2, + columns: 3, + layoutOptions: [ + { + x: 0, + y: 0, + width: 2 / 3, + height: 1, + }, + { + x: 2 / 3, + y: 0, + width: 1 / 3, + height: 1 / 2, + }, + { + x: 2 / 3, + y: 1 / 2, + width: 1 / 3, + height: 1 / 2, + }, + ], + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: 'axial', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: 'sagittal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: 'coronal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'activeDisplaySet', + }, + ], + }, + ], + }, + ], +}; diff --git a/extensions/cornerstone/src/id.js b/extensions/cornerstone/src/id.js new file mode 100644 index 0000000..ebe5acd --- /dev/null +++ b/extensions/cornerstone/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx new file mode 100644 index 0000000..3af9177 --- /dev/null +++ b/extensions/cornerstone/src/index.tsx @@ -0,0 +1,260 @@ +import React from 'react'; +import * as cornerstone from '@cornerstonejs/core'; +import * as cornerstoneTools from '@cornerstonejs/tools'; +import { + Enums as cs3DEnums, + imageLoadPoolManager, + imageRetrievalPoolManager, +} from '@cornerstonejs/core'; +import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools'; +import { Types } from '@ohif/core'; +import Enums from './enums'; + +import init from './init'; +import getCustomizationModule from './getCustomizationModule'; +import getCommandsModule from './commandsModule'; +import getHangingProtocolModule from './getHangingProtocolModule'; +import getToolbarModule from './getToolbarModule'; +import ToolGroupService from './services/ToolGroupService'; +import SyncGroupService from './services/SyncGroupService'; +import SegmentationService from './services/SegmentationService'; +import CornerstoneCacheService from './services/CornerstoneCacheService'; +import CornerstoneViewportService from './services/ViewportService/CornerstoneViewportService'; +import ColorbarService from './services/ColorbarService'; +import * as CornerstoneExtensionTypes from './types'; + +import { toolNames } from './initCornerstoneTools'; +import { getEnabledElement, reset as enabledElementReset, setEnabledElement } from './state'; +import dicomLoaderService from './utils/dicomLoaderService'; +import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement'; + +import { id } from './id'; +import { measurementMappingUtils } from './utils/measurementServiceMappings'; +import type { PublicViewportOptions } from './services/ViewportService/Viewport'; +import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool'; +import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService'; +import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider'; +import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes'; +import { findNearbyToolData } from './utils/findNearbyToolData'; +import { createFrameViewSynchronizer } from './synchronizers/frameViewSynchronizer'; +import { getSopClassHandlerModule } from './getSopClassHandlerModule'; +import { getDynamicVolumeInfo } from '@cornerstonejs/core/utilities'; +import { + useLutPresentationStore, + usePositionPresentationStore, + useSegmentationPresentationStore, + useSynchronizersStore, +} from './stores'; +import { useToggleOneUpViewportGridStore } from '@ohif/extension-default'; +import { useActiveViewportSegmentationRepresentations } from './hooks/useActiveViewportSegmentationRepresentations'; +import { useMeasurements } from './hooks/useMeasurements'; +import getPanelModule from './getPanelModule'; +import PanelSegmentation from './panels/PanelSegmentation'; +import PanelMeasurement from './panels/PanelMeasurement'; +import DicomUpload from './components/DicomUpload/DicomUpload'; +import { useSegmentations } from './hooks/useSegmentations'; +import { StudySummaryFromMetadata } from './components/StudySummaryFromMetadata'; +import utils from './utils'; + +const { imageRetrieveMetadataProvider } = cornerstone.utilities; + +const Component = React.lazy(() => { + return import(/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport'); +}); + +const OHIFCornerstoneViewport = props => { + return ( + Loading...}> + + + ); +}; + +const stackRetrieveOptions = { + retrieveOptions: { + single: { + streaming: true, + decodeLevel: 1, + }, + }, +}; + +/** + * + */ +const cornerstoneExtension: Types.Extensions.Extension = { + /** + * Only required property. Should be a unique value across all extensions. + */ + id, + + onModeEnter: ({ servicesManager }: withAppTypes): void => { + const { cornerstoneViewportService, toolbarService, segmentationService } = + servicesManager.services; + toolbarService.registerEventForToolbarUpdate(cornerstoneViewportService, [ + cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED, + ]); + + toolbarService.registerEventForToolbarUpdate(segmentationService, [ + segmentationService.EVENTS.SEGMENTATION_REMOVED, + segmentationService.EVENTS.SEGMENTATION_MODIFIED, + ]); + + toolbarService.registerEventForToolbarUpdate(cornerstone.eventTarget, [ + cornerstoneTools.Enums.Events.TOOL_ACTIVATED, + ]); + + // Configure the interleaved/HTJ2K loader + imageRetrieveMetadataProvider.clear(); + // The default volume interleaved options are to interleave the + // image retrieve, but don't perform progressive loading per image + // This interleaves images and replicates them for low-resolution depth volume + // reconstruction, which progressively improves + imageRetrieveMetadataProvider.add( + 'volume', + cornerstone.ProgressiveRetrieveImages.interleavedRetrieveStages + ); + // The default stack loading option is to progressive load HTJ2K images + // There are other possible options, but these need more thought about + // how to define them. + imageRetrieveMetadataProvider.add('stack', stackRetrieveOptions); + }, + getPanelModule, + onModeExit: ({ servicesManager }: withAppTypes): void => { + const { cineService, segmentationService } = servicesManager.services; + // Empty out the image load and retrieval pools to prevent memory leaks + // on the mode exits + Object.values(cs3DEnums.RequestType).forEach(type => { + imageLoadPoolManager.clearRequestStack(type); + imageRetrievalPoolManager.clearRequestStack(type); + }); + + cineService.setIsCineEnabled(false); + + enabledElementReset(); + + useLutPresentationStore.getState().clearLutPresentationStore(); + usePositionPresentationStore.getState().clearPositionPresentationStore(); + useSynchronizersStore.getState().clearSynchronizersStore(); + useToggleOneUpViewportGridStore.getState().clearToggleOneUpViewportGridStore(); + useSegmentationPresentationStore.getState().clearSegmentationPresentationStore(); + segmentationService.removeAllSegmentations(); + }, + + /** + * Register the Cornerstone 3D services and set them up for use. + * + * @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools` + */ + preRegistration: function (props: Types.Extensions.ExtensionParams): Promise { + const { servicesManager, serviceProvidersManager } = props; + servicesManager.registerService(CornerstoneViewportService.REGISTRATION); + servicesManager.registerService(ToolGroupService.REGISTRATION); + servicesManager.registerService(SyncGroupService.REGISTRATION); + servicesManager.registerService(SegmentationService.REGISTRATION); + servicesManager.registerService(CornerstoneCacheService.REGISTRATION); + servicesManager.registerService(ViewportActionCornersService.REGISTRATION); + servicesManager.registerService(ColorbarService.REGISTRATION); + + serviceProvidersManager.registerProvider( + ViewportActionCornersService.REGISTRATION.name, + ViewportActionCornersProvider + ); + + const { syncGroupService } = servicesManager.services; + syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer); + + return init.call(this, props); + }, + getToolbarModule, + getHangingProtocolModule, + getViewportModule({ servicesManager, commandsManager }) { + const ExtendedOHIFCornerstoneViewport = props => { + // const onNewImageHandler = jumpData => { + // commandsManager.runCommand('jumpToImage', jumpData); + // }; + const { toolbarService } = servicesManager.services; + + return ( + + ); + }; + + return [ + { + name: 'cornerstone', + component: ExtendedOHIFCornerstoneViewport, + }, + ]; + }, + getCommandsModule, + getCustomizationModule, + getUtilityModule({ servicesManager }) { + return [ + { + name: 'common', + exports: { + getCornerstoneLibraries: () => { + return { cornerstone, cornerstoneTools }; + }, + getEnabledElement, + dicomLoaderService, + }, + }, + { + name: 'core', + exports: { + Enums: cs3DEnums, + }, + }, + { + name: 'tools', + exports: { + toolNames, + Enums: cs3DToolsEnums, + }, + }, + { + name: 'volumeLoader', + exports: { + getDynamicVolumeInfo, + }, + }, + ]; + }, + getSopClassHandlerModule, +}; + +export type { PublicViewportOptions }; +export { + measurementMappingUtils, + CornerstoneExtensionTypes as Types, + toolNames, + getActiveViewportEnabledElement, + setEnabledElement, + findNearbyToolData, + getEnabledElement, + ImageOverlayViewerTool, + getSOPInstanceAttributes, + dicomLoaderService, + // Export all stores + useLutPresentationStore, + usePositionPresentationStore, + useSegmentationPresentationStore, + useSynchronizersStore, + Enums, + useMeasurements, + useActiveViewportSegmentationRepresentations, + useSegmentations, + PanelSegmentation, + PanelMeasurement, + DicomUpload, + StudySummaryFromMetadata, + utils, +}; +export default cornerstoneExtension; diff --git a/extensions/cornerstone/src/init.tsx b/extensions/cornerstone/src/init.tsx new file mode 100644 index 0000000..5658441 --- /dev/null +++ b/extensions/cornerstone/src/init.tsx @@ -0,0 +1,368 @@ +import OHIF, { errorHandler } from '@ohif/core'; +import React from 'react'; + +import * as cornerstone from '@cornerstonejs/core'; +import * as cornerstoneTools from '@cornerstonejs/tools'; +import { + init as cs3DInit, + eventTarget, + EVENTS, + metaData, + volumeLoader, + imageLoadPoolManager, + getEnabledElement, + Settings, + utilities as csUtilities, +} from '@cornerstonejs/core'; +import { + cornerstoneStreamingImageVolumeLoader, + cornerstoneStreamingDynamicImageVolumeLoader, +} from '@cornerstonejs/core/loaders'; + +import RequestTypes from '@cornerstonejs/core/enums/RequestType'; + +import initWADOImageLoader from './initWADOImageLoader'; +import initCornerstoneTools from './initCornerstoneTools'; + +import { connectToolsToMeasurementService } from './initMeasurementService'; +import initCineService from './initCineService'; +import initStudyPrefetcherService from './initStudyPrefetcherService'; +import interleaveCenterLoader from './utils/interleaveCenterLoader'; +import nthLoader from './utils/nthLoader'; +import interleaveTopToBottom from './utils/interleaveTopToBottom'; +import initContextMenu from './initContextMenu'; +import initDoubleClick from './initDoubleClick'; +import initViewTiming from './utils/initViewTiming'; +import { colormaps } from './utils/colormaps'; +import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; +import { useLutPresentationStore } from './stores/useLutPresentationStore'; +import { usePositionPresentationStore } from './stores/usePositionPresentationStore'; +import { useSegmentationPresentationStore } from './stores/useSegmentationPresentationStore'; +import { imageRetrieveMetadataProvider } from '@cornerstonejs/core/utilities'; + +const { registerColormap } = csUtilities.colormap; + +// TODO: Cypress tests are currently grabbing this from the window? +(window as any).cornerstone = cornerstone; +(window as any).cornerstoneTools = cornerstoneTools; +/** + * + */ +export default async function init({ + servicesManager, + commandsManager, + extensionManager, + appConfig, +}: withAppTypes): Promise { + // Note: this should run first before initializing the cornerstone + // DO NOT CHANGE THE ORDER + + await cs3DInit({ + peerImport: appConfig.peerImport, + }); + + // For debugging e2e tests that are failing on CI + cornerstone.setUseCPURendering(Boolean(appConfig.useCPURendering)); + + cornerstone.setConfiguration({ + ...cornerstone.getConfiguration(), + rendering: { + ...cornerstone.getConfiguration().rendering, + strictZSpacingForVolumeViewport: appConfig.strictZSpacingForVolumeViewport, + }, + }); + + // For debugging large datasets, otherwise prefer the defaults + const { maxCacheSize } = appConfig; + if (maxCacheSize) { + cornerstone.cache.setMaxCacheSize(maxCacheSize); + } + + initCornerstoneTools(); + + Settings.getRuntimeSettings().set('useCursors', Boolean(appConfig.useCursors)); + + const { + userAuthenticationService, + customizationService, + uiModalService, + uiNotificationService, + cornerstoneViewportService, + hangingProtocolService, + viewportGridService, + } = servicesManager.services; + + window.services = servicesManager.services; + window.extensionManager = extensionManager; + window.commandsManager = commandsManager; + + if (appConfig.showCPUFallbackMessage && cornerstone.getShouldUseCPURendering()) { + _showCPURenderingModal(uiModalService, hangingProtocolService); + } + const { getPresentationId: getLutPresentationId } = useLutPresentationStore.getState(); + + const { getPresentationId: getSegmentationPresentationId } = + useSegmentationPresentationStore.getState(); + + const { getPresentationId: getPositionPresentationId } = usePositionPresentationStore.getState(); + + // register presentation id providers + viewportGridService.addPresentationIdProvider( + 'positionPresentationId', + getPositionPresentationId + ); + viewportGridService.addPresentationIdProvider('lutPresentationId', getLutPresentationId); + viewportGridService.addPresentationIdProvider( + 'segmentationPresentationId', + getSegmentationPresentationId + ); + + cornerstoneTools.segmentation.config.style.setStyle( + { type: SegmentationRepresentations.Contour }, + { + renderFill: false, + } + ); + + const metadataProvider = OHIF.classes.MetadataProvider; + + volumeLoader.registerVolumeLoader( + 'cornerstoneStreamingImageVolume', + cornerstoneStreamingImageVolumeLoader + ); + + volumeLoader.registerVolumeLoader( + 'cornerstoneStreamingDynamicImageVolume', + cornerstoneStreamingDynamicImageVolumeLoader + ); + + // Register strategies using the wrapper + const imageLoadStrategies = { + interleaveCenter: interleaveCenterLoader, + interleaveTopToBottom: interleaveTopToBottom, + nth: nthLoader, + }; + + Object.entries(imageLoadStrategies).forEach(([name, strategyFn]) => { + hangingProtocolService.registerImageLoadStrategy( + name, + createMetadataWrappedStrategy(strategyFn) + ); + }); + + // ... existing code ... + + // add metadata providers + metaData.addProvider( + csUtilities.calibratedPixelSpacingMetadataProvider.get.bind( + csUtilities.calibratedPixelSpacingMetadataProvider + ) + ); // this provider is required for Calibration tool + metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999); + + // These are set reasonably low to allow for interleaved retrieves and slower + // connections. + imageLoadPoolManager.maxNumRequests = { + [RequestTypes.Interaction]: appConfig?.maxNumRequests?.interaction || 10, + [RequestTypes.Thumbnail]: appConfig?.maxNumRequests?.thumbnail || 5, + [RequestTypes.Prefetch]: appConfig?.maxNumRequests?.prefetch || 5, + [RequestTypes.Compute]: appConfig?.maxNumRequests?.compute || 10, + }; + + initWADOImageLoader(userAuthenticationService, appConfig, extensionManager); + + /* Measurement Service */ + this.measurementServiceSource = connectToolsToMeasurementService(servicesManager); + + initCineService(servicesManager); + initStudyPrefetcherService(servicesManager); + + // When a custom image load is performed, update the relevant viewports + hangingProtocolService.subscribe( + hangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED, + volumeInputArrayMap => { + const { lutPresentationStore } = useLutPresentationStore.getState(); + const { segmentationPresentationStore } = useSegmentationPresentationStore.getState(); + const { positionPresentationStore } = usePositionPresentationStore.getState(); + + for (const entry of volumeInputArrayMap.entries()) { + const [viewportId, volumeInputArray] = entry; + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + + const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId); + + const { presentationIds } = ohifViewport.getViewportOptions(); + + const presentations = { + positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId], + lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId], + segmentationPresentation: + segmentationPresentationStore[presentationIds?.segmentationPresentationId], + }; + + cornerstoneViewportService.setVolumesForViewport(viewport, volumeInputArray, presentations); + } + } + ); + + // resize the cornerstone viewport service when the grid size changes + // IMPORTANT: this should happen outside of the OHIFCornerstoneViewport + // since it will trigger a rerender of each viewport and each resizing + // the offscreen canvas which would result in a performance hit, this should + // done only once per grid resize here. Doing it once here, allows us to reduce + // the refreshRage(in ms) to 10 from 50. I tried with even 1 or 5 ms it worked fine + viewportGridService.subscribe(viewportGridService.EVENTS.GRID_SIZE_CHANGED, () => { + cornerstoneViewportService.resize(true); + }); + + initContextMenu({ + cornerstoneViewportService, + customizationService, + commandsManager, + }); + + initDoubleClick({ + customizationService, + commandsManager, + }); + + /** + * Runs error handler for failed requests. + * @param event + */ + const imageLoadFailedHandler = ({ detail }) => { + const handler = errorHandler.getHTTPErrorHandler(); + handler(detail.error); + }; + + eventTarget.addEventListener(EVENTS.IMAGE_LOAD_FAILED, imageLoadFailedHandler); + eventTarget.addEventListener(EVENTS.IMAGE_LOAD_ERROR, imageLoadFailedHandler); + + function elementEnabledHandler(evt) { + const { element } = evt.detail; + + element.addEventListener(EVENTS.CAMERA_RESET, evt => { + const { element } = evt.detail; + const enabledElement = getEnabledElement(element); + if (!enabledElement) { + return; + } + const { viewportId } = enabledElement; + commandsManager.runCommand('resetCrosshairs', { viewportId }); + }); + + initViewTiming({ element }); + } + + eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null)); + + colormaps.forEach(registerColormap); + + // Event listener + eventTarget.addEventListenerDebounced( + EVENTS.ERROR_EVENT, + ({ detail }) => { + uiNotificationService.show({ + title: detail.type, + message: detail.message, + type: 'error', + }); + }, + 100 + ); + + // Call this function when initializing + initializeWebWorkerProgressHandler(servicesManager.services.uiNotificationService); +} + +function initializeWebWorkerProgressHandler(uiNotificationService) { + const activeToasts = new Map(); + + eventTarget.addEventListener(EVENTS.WEB_WORKER_PROGRESS, ({ detail }) => { + const { progress, type, id } = detail; + + const cacheKey = `${type}-${id}`; + if (progress === 0 && !activeToasts.has(cacheKey)) { + const progressPromise = new Promise((resolve, reject) => { + activeToasts.set(cacheKey, { resolve, reject }); + }); + + uiNotificationService.show({ + id: cacheKey, + title: `${type}`, + message: `${type}: ${progress}%`, + autoClose: false, + promise: progressPromise, + promiseMessages: { + loading: `Computing...`, + success: `Completed successfully`, + error: 'Web Worker failed', + }, + }); + } else { + if (progress === 100) { + const { resolve } = activeToasts.get(cacheKey); + resolve({ progress, type }); + activeToasts.delete(cacheKey); + } + } + }); +} + +/** + * Creates a wrapped image load strategy with metadata handling + * @param strategyFn - The image loading strategy function to wrap + * @returns A wrapped strategy function that handles metadata configuration + */ +const createMetadataWrappedStrategy = (strategyFn: (args: any) => any) => { + return (args: any) => { + const clonedConfig = imageRetrieveMetadataProvider.clone(); + imageRetrieveMetadataProvider.clear(); + + try { + const result = strategyFn(args); + return result; + } finally { + // Ensure metadata is always restored, even if there's an error + setTimeout(() => { + imageRetrieveMetadataProvider.restore(clonedConfig); + }, 10); + } + }; +}; + +function CPUModal() { + return ( +
+

+ Your computer does not have enough GPU power to support the default GPU rendering mode. OHIF + has switched to CPU rendering mode. Please note that CPU rendering does not support all + features such as Volume Rendering, Multiplanar Reconstruction, and Segmentation Overlays. +

+
+ ); +} + +function _showCPURenderingModal(uiModalService, hangingProtocolService) { + const callback = progress => { + if (progress === 100) { + uiModalService.show({ + content: CPUModal, + title: 'OHIF Fell Back to CPU Rendering', + }); + + return true; + } + }; + + const { unsubscribe } = hangingProtocolService.subscribe( + hangingProtocolService.EVENTS.PROTOCOL_CHANGED, + () => { + const done = callback(100); + + if (done) { + unsubscribe(); + } + } + ); +} diff --git a/extensions/cornerstone/src/initCineService.ts b/extensions/cornerstone/src/initCineService.ts new file mode 100644 index 0000000..60843b3 --- /dev/null +++ b/extensions/cornerstone/src/initCineService.ts @@ -0,0 +1,70 @@ +import { cache, Types } from '@cornerstonejs/core'; +import { utilities } from '@cornerstonejs/tools'; + +function _getVolumeFromViewport(viewport: Types.IBaseVolumeViewport) { + const volumeIds = viewport.getAllVolumeIds(); + const volumes = volumeIds.map(id => cache.getVolume(id)); + const dynamicVolume = volumes.find(volume => volume.isDynamicVolume()); + + return dynamicVolume ?? volumes[0]; +} + +/** + * Return all viewports that needs to be synchronized with the source + * viewport passed as parameter when cine is updated. + * @param servicesManager ServiceManager + * @param srcViewportIndex Source viewport index + * @returns array with viewport information. + */ +function _getSyncedViewports(servicesManager: AppTypes.ServicesManager, srcViewportId) { + const { viewportGridService, cornerstoneViewportService } = servicesManager.services; + + const { viewports: viewportsStates } = viewportGridService.getState(); + const srcViewportState = viewportsStates.get(srcViewportId); + + if (srcViewportState?.viewportOptions?.viewportType !== 'volume') { + return []; + } + + const srcViewport = cornerstoneViewportService.getCornerstoneViewport(srcViewportId); + + const srcVolume = srcViewport ? _getVolumeFromViewport(srcViewport) : null; + + if (!srcVolume?.isDynamicVolume()) { + return []; + } + + const { volumeId: srcVolumeId } = srcVolume; + + return Array.from(viewportsStates.values()) + .filter(({ viewportId }) => { + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + + return viewportId !== srcViewportId && viewport?.hasVolumeId?.(srcVolumeId); + }) + .map(({ viewportId }) => ({ viewportId })); +} + +function initCineService(servicesManager: AppTypes.ServicesManager) { + const { cineService } = servicesManager.services; + + const getSyncedViewports = viewportId => { + return _getSyncedViewports(servicesManager, viewportId); + }; + + const playClip = (element, playClipOptions) => { + return utilities.cine.playClip(element, playClipOptions); + }; + + const stopClip = (element, stopClipOptions) => { + return utilities.cine.stopClip(element, stopClipOptions); + }; + + cineService.setServiceImplementation({ + getSyncedViewports, + playClip, + stopClip, + }); +} + +export default initCineService; diff --git a/extensions/cornerstone/src/initContextMenu.ts b/extensions/cornerstone/src/initContextMenu.ts new file mode 100644 index 0000000..c5ea90f --- /dev/null +++ b/extensions/cornerstone/src/initContextMenu.ts @@ -0,0 +1,94 @@ +import { eventTarget, EVENTS } from '@cornerstonejs/core'; +import { Enums } from '@cornerstonejs/tools'; +import { setEnabledElement } from './state'; +import { findNearbyToolData } from './utils/findNearbyToolData'; + +const cs3DToolsEvents = Enums.Events; + +/** + * Generates a name, consisting of: + * * alt when the alt key is down + * * ctrl when the cctrl key is down + * * shift when the shift key is down + * * 'button' followed by the button number (1 left, 3 right etc) + */ +function getEventName(evt) { + const button = evt.detail.event.which; + const nameArr = []; + if (evt.detail.event.altKey) { + nameArr.push('alt'); + } + if (evt.detail.event.ctrlKey) { + nameArr.push('ctrl'); + } + if (evt.detail.event.shiftKey) { + nameArr.push('shift'); + } + nameArr.push('button'); + nameArr.push(button); + return nameArr.join(''); +} + +function initContextMenu({ + cornerstoneViewportService, + customizationService, + commandsManager, +}): void { + /* + * Run the commands associated with the given button press, + * defaults on button1 and button2 + */ + const cornerstoneViewportHandleEvent = (name, evt) => { + const customizations = customizationService.getCustomization( + 'cornerstoneViewportClickCommands' + ); + + const toRun = customizations[name]; + + if (!toRun) { + return; + } + + // only find nearbyToolData if required, for the click (which closes the context menu + // we don't need to find nearbyToolData) + let nearbyToolData = null; + if (toRun.some(command => command.commandOptions?.requireNearbyToolData)) { + nearbyToolData = findNearbyToolData(commandsManager, evt); + } + + const options = { + nearbyToolData, + event: evt, + }; + commandsManager.run(toRun, options); + }; + + const cornerstoneViewportHandleClick = evt => { + const name = getEventName(evt); + cornerstoneViewportHandleEvent(name, evt); + }; + + function elementEnabledHandler(evt) { + const { viewportId, element } = evt.detail; + const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); + if (!viewportInfo) { + return; + } + // TODO check update upstream + setEnabledElement(viewportId, element); + + element.addEventListener(cs3DToolsEvents.MOUSE_CLICK, cornerstoneViewportHandleClick); + } + + function elementDisabledHandler(evt) { + const { element } = evt.detail; + + element.removeEventListener(cs3DToolsEvents.MOUSE_CLICK, cornerstoneViewportHandleClick); + } + + eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null)); + + eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null)); +} + +export default initContextMenu; diff --git a/extensions/cornerstone/src/initCornerstoneTools.js b/extensions/cornerstone/src/initCornerstoneTools.js new file mode 100644 index 0000000..dd1aedc --- /dev/null +++ b/extensions/cornerstone/src/initCornerstoneTools.js @@ -0,0 +1,142 @@ +import { + PanTool, + WindowLevelTool, + StackScrollTool, + VolumeRotateTool, + ZoomTool, + MIPJumpToClickTool, + LengthTool, + RectangleROITool, + RectangleROIThresholdTool, + EllipticalROITool, + CircleROITool, + BidirectionalTool, + ArrowAnnotateTool, + DragProbeTool, + ProbeTool, + AngleTool, + CobbAngleTool, + MagnifyTool, + CrosshairsTool, + RectangleScissorsTool, + SphereScissorsTool, + CircleScissorsTool, + BrushTool, + PaintFillTool, + init, + addTool, + annotation, + ReferenceLinesTool, + TrackballRotateTool, + AdvancedMagnifyTool, + UltrasoundDirectionalTool, + PlanarFreehandROITool, + PlanarFreehandContourSegmentationTool, + SplineROITool, + LivewireContourTool, + OrientationMarkerTool, + WindowLevelRegionTool, +} from '@cornerstonejs/tools'; + +import CalibrationLineTool from './tools/CalibrationLineTool'; +import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool'; + +export default function initCornerstoneTools(configuration = {}) { + CrosshairsTool.isAnnotation = false; + ReferenceLinesTool.isAnnotation = false; + AdvancedMagnifyTool.isAnnotation = false; + PlanarFreehandContourSegmentationTool.isAnnotation = false; + + init(configuration); + addTool(PanTool); + addTool(WindowLevelTool); + addTool(StackScrollTool); + addTool(VolumeRotateTool); + addTool(ZoomTool); + addTool(ProbeTool); + addTool(MIPJumpToClickTool); + addTool(LengthTool); + addTool(RectangleROITool); + addTool(RectangleROIThresholdTool); + addTool(EllipticalROITool); + addTool(CircleROITool); + addTool(BidirectionalTool); + addTool(ArrowAnnotateTool); + addTool(DragProbeTool); + addTool(AngleTool); + addTool(CobbAngleTool); + addTool(MagnifyTool); + addTool(CrosshairsTool); + addTool(RectangleScissorsTool); + addTool(SphereScissorsTool); + addTool(CircleScissorsTool); + addTool(BrushTool); + addTool(PaintFillTool); + addTool(ReferenceLinesTool); + addTool(CalibrationLineTool); + addTool(TrackballRotateTool); + addTool(ImageOverlayViewerTool); + addTool(AdvancedMagnifyTool); + addTool(UltrasoundDirectionalTool); + addTool(PlanarFreehandROITool); + addTool(SplineROITool); + addTool(LivewireContourTool); + addTool(OrientationMarkerTool); + addTool(WindowLevelRegionTool); + addTool(PlanarFreehandContourSegmentationTool); + + // Modify annotation tools to use dashed lines on SR + const annotationStyle = { + textBoxFontSize: '15px', + lineWidth: '1.5', + }; + + const defaultStyles = annotation.config.style.getDefaultToolStyles(); + annotation.config.style.setDefaultToolStyles({ + global: { + ...defaultStyles.global, + ...annotationStyle, + }, + }); +} + +const toolNames = { + Pan: PanTool.toolName, + ArrowAnnotate: ArrowAnnotateTool.toolName, + WindowLevel: WindowLevelTool.toolName, + StackScroll: StackScrollTool.toolName, + Zoom: ZoomTool.toolName, + VolumeRotate: VolumeRotateTool.toolName, + MipJumpToClick: MIPJumpToClickTool.toolName, + Length: LengthTool.toolName, + DragProbe: DragProbeTool.toolName, + Probe: ProbeTool.toolName, + RectangleROI: RectangleROITool.toolName, + RectangleROIThreshold: RectangleROIThresholdTool.toolName, + EllipticalROI: EllipticalROITool.toolName, + CircleROI: CircleROITool.toolName, + Bidirectional: BidirectionalTool.toolName, + Angle: AngleTool.toolName, + CobbAngle: CobbAngleTool.toolName, + Magnify: MagnifyTool.toolName, + Crosshairs: CrosshairsTool.toolName, + Brush: BrushTool.toolName, + PaintFill: PaintFillTool.toolName, + ReferenceLines: ReferenceLinesTool.toolName, + CalibrationLine: CalibrationLineTool.toolName, + TrackballRotateTool: TrackballRotateTool.toolName, + CircleScissors: CircleScissorsTool.toolName, + RectangleScissors: RectangleScissorsTool.toolName, + SphereScissors: SphereScissorsTool.toolName, + ImageOverlayViewer: ImageOverlayViewerTool.toolName, + AdvancedMagnify: AdvancedMagnifyTool.toolName, + UltrasoundDirectional: UltrasoundDirectionalTool.toolName, + SplineROI: SplineROITool.toolName, + LivewireContour: LivewireContourTool.toolName, + PlanarFreehandROI: PlanarFreehandROITool.toolName, + OrientationMarker: OrientationMarkerTool.toolName, + WindowLevelRegion: WindowLevelRegionTool.toolName, + PlanarFreehandContourSegmentation: PlanarFreehandContourSegmentationTool.toolName, +}; + +export { toolNames }; diff --git a/extensions/cornerstone/src/initDoubleClick.ts b/extensions/cornerstone/src/initDoubleClick.ts new file mode 100644 index 0000000..2448b69 --- /dev/null +++ b/extensions/cornerstone/src/initDoubleClick.ts @@ -0,0 +1,82 @@ +import { eventTarget, EVENTS } from '@cornerstonejs/core'; +import { Enums } from '@cornerstonejs/tools'; +import { CommandsManager, CustomizationService } from '@ohif/core'; +import { findNearbyToolData } from './utils/findNearbyToolData'; + +const cs3DToolsEvents = Enums.Events; + +/** + * Generates a double click event name, consisting of: + * * alt when the alt key is down + * * ctrl when the cctrl key is down + * * shift when the shift key is down + * * 'doubleClick' + */ +function getDoubleClickEventName(evt: CustomEvent) { + const nameArr = []; + if (evt.detail.event.altKey) { + nameArr.push('alt'); + } + if (evt.detail.event.ctrlKey) { + nameArr.push('ctrl'); + } + if (evt.detail.event.shiftKey) { + nameArr.push('shift'); + } + nameArr.push('doubleClick'); + return nameArr.join(''); +} + +export type initDoubleClickArgs = { + customizationService: CustomizationService; + commandsManager: CommandsManager; +}; + +function initDoubleClick({ customizationService, commandsManager }: initDoubleClickArgs): void { + const cornerstoneViewportHandleDoubleClick = (evt: CustomEvent) => { + // Do not allow double click on a tool. + const nearbyToolData = findNearbyToolData(commandsManager, evt); + if (nearbyToolData) { + return; + } + + const eventName = getDoubleClickEventName(evt); + + // Allows for the customization of the double click on a viewport. + const customizations = customizationService.getCustomization( + 'cornerstoneViewportClickCommands' + ); + + const toRun = customizations[eventName]; + + if (!toRun) { + return; + } + + commandsManager.run(toRun); + }; + + function elementEnabledHandler(evt: CustomEvent) { + const { element } = evt.detail; + + element.addEventListener( + cs3DToolsEvents.MOUSE_DOUBLE_CLICK, + cornerstoneViewportHandleDoubleClick + ); + } + + function elementDisabledHandler(evt: CustomEvent) { + const { element } = evt.detail; + + element.removeEventListener( + cs3DToolsEvents.MOUSE_DOUBLE_CLICK, + cornerstoneViewportHandleDoubleClick + ); + } + + eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null)); + + eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null)); +} + +export default initDoubleClick; diff --git a/extensions/cornerstone/src/initMeasurementService.ts b/extensions/cornerstone/src/initMeasurementService.ts new file mode 100644 index 0000000..f0d8633 --- /dev/null +++ b/extensions/cornerstone/src/initMeasurementService.ts @@ -0,0 +1,462 @@ +import { eventTarget, Types } from '@cornerstonejs/core'; +import { Enums, annotation } from '@cornerstonejs/tools'; +import { DicomMetadataStore } from '@ohif/core'; + +import * as CSExtensionEnums from './enums'; +import { toolNames } from './initCornerstoneTools'; +import { onCompletedCalibrationLine } from './tools/CalibrationLineTool'; +import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory'; +import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes'; +import { triggerAnnotationRenderForViewportIds } from '@cornerstonejs/tools/utilities'; + +const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums; +const { removeAnnotation } = annotation.state; +const csToolsEvents = Enums.Events; + +const initMeasurementService = ( + measurementService, + displaySetService, + cornerstoneViewportService, + customizationService +) => { + /* Initialization */ + const { + Length, + Bidirectional, + EllipticalROI, + CircleROI, + ArrowAnnotate, + Angle, + CobbAngle, + RectangleROI, + PlanarFreehandROI, + SplineROI, + LivewireContour, + Probe, + UltrasoundDirectional, + } = measurementServiceMappingsFactory( + measurementService, + displaySetService, + cornerstoneViewportService, + customizationService + ); + const csTools3DVer1MeasurementSource = measurementService.createSource( + CORNERSTONE_3D_TOOLS_SOURCE_NAME, + CORNERSTONE_3D_TOOLS_SOURCE_VERSION + ); + + /* Mappings */ + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'Length', + Length.matchingCriteria, + Length.toAnnotation, + Length.toMeasurement + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'Crosshairs', + Length.matchingCriteria, + () => { + return null; + }, + () => { + return null; + } + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'Bidirectional', + Bidirectional.matchingCriteria, + Bidirectional.toAnnotation, + Bidirectional.toMeasurement + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'EllipticalROI', + EllipticalROI.matchingCriteria, + EllipticalROI.toAnnotation, + EllipticalROI.toMeasurement + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'CircleROI', + CircleROI.matchingCriteria, + CircleROI.toAnnotation, + CircleROI.toMeasurement + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'ArrowAnnotate', + ArrowAnnotate.matchingCriteria, + ArrowAnnotate.toAnnotation, + ArrowAnnotate.toMeasurement + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'CobbAngle', + CobbAngle.matchingCriteria, + CobbAngle.toAnnotation, + CobbAngle.toMeasurement + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'Angle', + Angle.matchingCriteria, + Angle.toAnnotation, + Angle.toMeasurement + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'RectangleROI', + RectangleROI.matchingCriteria, + RectangleROI.toAnnotation, + RectangleROI.toMeasurement + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'PlanarFreehandROI', + PlanarFreehandROI.matchingCriteria, + PlanarFreehandROI.toAnnotation, + PlanarFreehandROI.toMeasurement + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'SplineROI', + SplineROI.matchingCriteria, + SplineROI.toAnnotation, + SplineROI.toMeasurement + ); + + // On the UI side, the Calibration Line tool will work almost the same as the + // Length tool + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'CalibrationLine', + Length.matchingCriteria, + Length.toAnnotation, + Length.toMeasurement + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'LivewireContour', + LivewireContour.matchingCriteria, + LivewireContour.toAnnotation, + LivewireContour.toMeasurement + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'Probe', + Probe.matchingCriteria, + Probe.toAnnotation, + Probe.toMeasurement + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'UltrasoundDirectionalTool', + UltrasoundDirectional.matchingCriteria, + UltrasoundDirectional.toAnnotation, + UltrasoundDirectional.toMeasurement + ); + + return csTools3DVer1MeasurementSource; +}; + +const connectToolsToMeasurementService = (servicesManager: AppTypes.ServicesManager) => { + const { + measurementService, + displaySetService, + cornerstoneViewportService, + customizationService, + } = servicesManager.services; + const csTools3DVer1MeasurementSource = initMeasurementService( + measurementService, + displaySetService, + cornerstoneViewportService, + customizationService + ); + connectMeasurementServiceToTools(measurementService, cornerstoneViewportService); + const { annotationToMeasurement, remove } = csTools3DVer1MeasurementSource; + + // + function addMeasurement(csToolsEvent) { + try { + const annotationAddedEventDetail = csToolsEvent.detail; + const { + annotation: { metadata, annotationUID }, + } = annotationAddedEventDetail; + const { toolName } = metadata; + + if (csToolsEvent.type === completedEvt && toolName === toolNames.CalibrationLine) { + // show modal to input the measurement (mm) + onCompletedCalibrationLine(servicesManager, csToolsEvent) + .then( + () => { + console.log('Calibration applied.'); + }, + () => true + ) + .finally(() => { + // we don't need the calibration line lingering around, remove the + // annotation from the display + removeAnnotation(annotationUID); + removeMeasurement(csToolsEvent); + // this will ensure redrawing of annotations + cornerstoneViewportService.resize(); + }); + } else { + // To force the measurementUID be the same as the annotationUID + // Todo: this should be changed when a measurement can include multiple annotations + // in the future + annotationAddedEventDetail.uid = annotationUID; + annotationToMeasurement(toolName, annotationAddedEventDetail); + } + } catch (error) { + console.warn('Failed to add measurement:', error); + } + } + + function updateMeasurement(csToolsEvent) { + try { + const annotationModifiedEventDetail = csToolsEvent.detail; + + const { + annotation: { metadata, annotationUID }, + } = annotationModifiedEventDetail; + + // If the measurement hasn't been added, don't modify it + const measurement = measurementService.getMeasurement(annotationUID); + + if (!measurement) { + return; + } + const { toolName } = metadata; + + annotationModifiedEventDetail.uid = annotationUID; + // Passing true to indicate this is an update and NOT a annotation (start) completion. + annotationToMeasurement(toolName, annotationModifiedEventDetail, true); + } catch (error) { + console.warn('Failed to update measurement:', error); + } + } + + function selectMeasurement(csToolsEvent) { + try { + const annotationSelectionEventDetail = csToolsEvent.detail; + + const { added: addedSelectedAnnotationUIDs, removed: removedSelectedAnnotationUIDs } = + annotationSelectionEventDetail; + + if (removedSelectedAnnotationUIDs) { + removedSelectedAnnotationUIDs.forEach(annotationUID => + measurementService.setMeasurementSelected(annotationUID, false) + ); + } + + if (addedSelectedAnnotationUIDs) { + addedSelectedAnnotationUIDs.forEach(annotationUID => + measurementService.setMeasurementSelected(annotationUID, true) + ); + } + } catch (error) { + console.warn('Failed to select/unselect measurements:', error); + } + } + + /** + * When csTools fires a removed event, remove the same measurement + * from the measurement service + * + * @param {*} csToolsEvent + */ + function removeMeasurement(csToolsEvent) { + try { + const annotationRemovedEventDetail = csToolsEvent.detail; + const { + annotation: { annotationUID }, + } = annotationRemovedEventDetail; + const measurement = measurementService.getMeasurement(annotationUID); + if (measurement) { + remove(annotationUID, annotationRemovedEventDetail); + } + } catch (error) { + console.warn('Failed to remove measurement:', error); + } + } + + // on display sets added, check if there are any measurements in measurement service that need to be + // put into cornerstone tools + const addedEvt = csToolsEvents.ANNOTATION_ADDED; + const completedEvt = csToolsEvents.ANNOTATION_COMPLETED; + const updatedEvt = csToolsEvents.ANNOTATION_MODIFIED; + const removedEvt = csToolsEvents.ANNOTATION_REMOVED; + const selectionEvt = csToolsEvents.ANNOTATION_SELECTION_CHANGE; + + eventTarget.addEventListener(addedEvt, addMeasurement); + eventTarget.addEventListener(completedEvt, addMeasurement); + eventTarget.addEventListener(updatedEvt, updateMeasurement); + eventTarget.addEventListener(removedEvt, removeMeasurement); + eventTarget.addEventListener(selectionEvt, selectMeasurement); + + return csTools3DVer1MeasurementSource; +}; + +const connectMeasurementServiceToTools = (measurementService, cornerstoneViewportService) => { + const { MEASUREMENT_REMOVED, MEASUREMENTS_CLEARED, MEASUREMENT_UPDATED, RAW_MEASUREMENT_ADDED } = + measurementService.EVENTS; + + measurementService.subscribe(MEASUREMENTS_CLEARED, ({ measurements }) => { + if (!Object.keys(measurements).length) { + return; + } + + for (const measurement of Object.values(measurements)) { + const { uid, source } = measurement; + if (source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) { + continue; + } + removeAnnotation(uid); + } + + // trigger a render + cornerstoneViewportService.getRenderingEngine().render(); + }); + + measurementService.subscribe( + MEASUREMENT_UPDATED, + ({ source, measurement, notYetUpdatedAtSource }) => { + if (source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) { + return; + } + + if (notYetUpdatedAtSource === false) { + // This event was fired by cornerstone telling the measurement service to sync. + // Already in sync. + return; + } + + const { uid, label, isLocked, isVisible } = measurement; + const sourceAnnotation = annotation.state.getAnnotation(uid); + const { data, metadata } = sourceAnnotation; + + if (!data) { + return; + } + + if (data.label !== label) { + data.label = label; + } + + if (metadata.toolName === 'ArrowAnnotate') { + data.text = label; + } + + // update the isLocked state + annotation.locking.setAnnotationLocked(uid, isLocked); + + // update the isVisible state + annotation.visibility.setAnnotationVisibility(uid, isVisible); + + // annotation.config.style.setAnnotationStyles(uid, { + // color: `rgb(${color[0]}, ${color[1]}, ${color[2]})`, + // }); + + // I don't like this but will fix later + const renderingEngine = + cornerstoneViewportService.getRenderingEngine() as Types.IRenderingEngine; + // Note: We could do a better job by triggering the render on the + // viewport itself, but the removeAnnotation does not include that info... + const viewportIds = renderingEngine.getViewports().map(viewport => viewport.id); + triggerAnnotationRenderForViewportIds(viewportIds); + } + ); + + measurementService.subscribe( + RAW_MEASUREMENT_ADDED, + ({ source, measurement, data, dataSource }) => { + if (source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) { + return; + } + + const { referenceSeriesUID, referenceStudyUID, SOPInstanceUID } = measurement; + + const instance = DicomMetadataStore.getInstance( + referenceStudyUID, + referenceSeriesUID, + SOPInstanceUID + ); + + let imageId; + let frameNumber = 1; + + if (measurement?.metadata?.referencedImageId) { + imageId = measurement.metadata.referencedImageId; + frameNumber = getSOPInstanceAttributes(measurement.metadata.referencedImageId).frameNumber; + } else { + imageId = dataSource.getImageIdsForInstance({ instance }); + } + + /** + * This annotation is used by the cornerstone viewport. + * This is not the read-only annotation rendered by the SR viewport. + */ + const annotationManager = annotation.state.getAnnotationManager(); + annotationManager.addAnnotation({ + annotationUID: measurement.uid, + highlighted: false, + isLocked: false, + invalidated: false, + metadata: { + toolName: measurement.toolName, + FrameOfReferenceUID: measurement.FrameOfReferenceUID, + referencedImageId: imageId, + }, + data: { + /** + * Don't remove this destructuring of data here. + * This is used to pass annotation specific data forward e.g. contour + */ + ...(data.annotation.data || {}), + text: data.annotation.data.text, + handles: { ...data.annotation.data.handles }, + cachedStats: { ...data.annotation.data.cachedStats }, + label: data.annotation.data.label, + frameNumber, + }, + }); + } + ); + + measurementService.subscribe( + MEASUREMENT_REMOVED, + ({ source, measurement: removedMeasurementId }) => { + if (source?.name && source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) { + return; + } + removeAnnotation(removedMeasurementId); + const renderingEngine = cornerstoneViewportService.getRenderingEngine(); + // Note: We could do a better job by triggering the render on the + // viewport itself, but the removeAnnotation does not include that info... + renderingEngine.render(); + } + ); +}; + +export { + initMeasurementService, + connectToolsToMeasurementService, + connectMeasurementServiceToTools, +}; diff --git a/extensions/cornerstone/src/initStudyPrefetcherService.ts b/extensions/cornerstone/src/initStudyPrefetcherService.ts new file mode 100644 index 0000000..78020f4 --- /dev/null +++ b/extensions/cornerstone/src/initStudyPrefetcherService.ts @@ -0,0 +1,33 @@ +import { cache, imageLoadPoolManager, imageLoader, Enums, eventTarget, EVENTS as csEvents } from '@cornerstonejs/core'; + +function initStudyPrefetcherService(servicesManager: AppTypes.ServicesManager) { + const { studyPrefetcherService } = servicesManager.services; + + studyPrefetcherService.requestType = Enums.RequestType.Prefetch; + studyPrefetcherService.imageLoadPoolManager = imageLoadPoolManager; + studyPrefetcherService.imageLoader = imageLoader; + + studyPrefetcherService.cache = { + isImageCached(imageId: string): boolean { + return !!cache.getImageLoadObject(imageId); + } + } + + studyPrefetcherService.imageLoadEventsManager = { + addEventListeners(onImageLoaded, onImageLoadFailed) { + eventTarget.addEventListener(csEvents.IMAGE_LOADED, onImageLoaded); + eventTarget.addEventListener(csEvents.IMAGE_LOAD_FAILED, onImageLoadFailed); + + return [ + { + unsubscribe: () => eventTarget.removeEventListener(csEvents.IMAGE_LOADED, onImageLoaded) + }, + { + unsubscribe: () => eventTarget.removeEventListener(csEvents.IMAGE_LOAD_FAILED, onImageLoadFailed) + }, + ] + } + } +} + +export default initStudyPrefetcherService; diff --git a/extensions/cornerstone/src/initWADOImageLoader.js b/extensions/cornerstone/src/initWADOImageLoader.js new file mode 100644 index 0000000..9f9922f --- /dev/null +++ b/extensions/cornerstone/src/initWADOImageLoader.js @@ -0,0 +1,56 @@ +import { volumeLoader } from '@cornerstonejs/core'; +import { + cornerstoneStreamingImageVolumeLoader, + cornerstoneStreamingDynamicImageVolumeLoader, +} from '@cornerstonejs/core/loaders'; +import dicomImageLoader from '@cornerstonejs/dicom-image-loader'; +import { errorHandler, utils } from '@ohif/core'; + +const { registerVolumeLoader } = volumeLoader; + +export default function initWADOImageLoader( + userAuthenticationService, + appConfig, + extensionManager +) { + registerVolumeLoader('cornerstoneStreamingImageVolume', cornerstoneStreamingImageVolumeLoader); + + registerVolumeLoader( + 'cornerstoneStreamingDynamicImageVolume', + cornerstoneStreamingDynamicImageVolumeLoader + ); + + dicomImageLoader.init({ + maxWebWorkers: Math.min( + Math.max(navigator.hardwareConcurrency - 1, 1), + appConfig.maxNumberOfWebWorkers + ), + beforeSend: function (xhr) { + //TODO should be removed in the future and request emitted by DicomWebDataSource + const sourceConfig = extensionManager.getActiveDataSource()?.[0].getConfig() ?? {}; + const headers = userAuthenticationService.getAuthorizationHeader(); + const acceptHeader = utils.generateAcceptHeader( + sourceConfig.acceptHeader, + sourceConfig.requestTransferSyntaxUID, + sourceConfig.omitQuotationForMultipartRequest + ); + + const xhrRequestHeaders = { + Accept: acceptHeader, + }; + + if (headers) { + Object.assign(xhrRequestHeaders, headers); + } + + return xhrRequestHeaders; + }, + errorInterceptor: error => { + errorHandler.getHTTPErrorHandler(error); + }, + }); +} + +export function destroy() { + console.debug('Destroying WADO Image Loader'); +} diff --git a/extensions/cornerstone/src/panels/PanelMeasurement.tsx b/extensions/cornerstone/src/panels/PanelMeasurement.tsx new file mode 100644 index 0000000..c7871d9 --- /dev/null +++ b/extensions/cornerstone/src/panels/PanelMeasurement.tsx @@ -0,0 +1,107 @@ +import React, { useEffect, useRef } from 'react'; +import { utils } from '@ohif/core'; +import { MeasurementTable } from '@ohif/ui-next'; +import debounce from 'lodash.debounce'; +import { useMeasurements } from '../hooks/useMeasurements'; + +const { filterAdditionalFindings: filterAdditionalFinding, filterAny } = utils.MeasurementFilters; + +export type withAppAndFilters = withAppTypes & { + measurementFilter: (item) => boolean; +}; + +export default function PanelMeasurement({ + servicesManager, + commandsManager, + customHeader, + measurementFilter = filterAny, +}: withAppAndFilters): React.ReactNode { + const measurementsPanelRef = useRef(null); + + const { measurementService } = servicesManager.services; + + const displayMeasurements = useMeasurements(servicesManager, { + measurementFilter, + }); + + useEffect(() => { + if (displayMeasurements.length > 0) { + debounce(() => { + measurementsPanelRef.current.scrollTop = measurementsPanelRef.current.scrollHeight; + }, 300)(); + } + }, [displayMeasurements.length]); + + const bindCommand = (name: string | string[], options?) => { + return (uid: string) => { + commandsManager.run(name, { ...options, uid }); + }; + }; + + const jumpToImage = bindCommand('jumpToMeasurement', { displayMeasurements }); + const removeMeasurement = bindCommand('removeMeasurement'); + const renameMeasurement = bindCommand(['jumpToMeasurement', 'renameMeasurement'], { + displayMeasurements, + }); + const toggleLockMeasurement = bindCommand('toggleLockMeasurement'); + const toggleVisibilityMeasurement = bindCommand('toggleVisibilityMeasurement'); + + const additionalFilter = filterAdditionalFinding(measurementService); + + const measurements = displayMeasurements.filter( + item => !additionalFilter(item) && measurementFilter(item) + ); + const additionalFindings = displayMeasurements.filter( + item => additionalFilter(item) && measurementFilter(item) + ); + + const onArgs = { + onClick: jumpToImage, + onDelete: removeMeasurement, + onToggleVisibility: toggleVisibilityMeasurement, + onToggleLocked: toggleLockMeasurement, + onRename: renameMeasurement, + }; + + return ( + <> +
+ + + {customHeader && ( + <> + {typeof customHeader === 'function' + ? customHeader({ + additionalFindings, + measurements, + }) + : customHeader} + + )} + + + + {additionalFindings.length > 0 && ( + + + + )} +
+ + ); +} diff --git a/extensions/cornerstone/src/panels/PanelSegmentation.tsx b/extensions/cornerstone/src/panels/PanelSegmentation.tsx new file mode 100644 index 0000000..bb5def0 --- /dev/null +++ b/extensions/cornerstone/src/panels/PanelSegmentation.tsx @@ -0,0 +1,231 @@ +import React from 'react'; +import { SegmentationTable } from '@ohif/ui-next'; +import { useActiveViewportSegmentationRepresentations } from '../hooks/useActiveViewportSegmentationRepresentations'; +import { metaData } from '@cornerstonejs/core'; + +export default function PanelSegmentation({ + servicesManager, + commandsManager, + children, +}: withAppTypes) { + const { customizationService, displaySetService } = servicesManager.services; + + const { segmentationsWithRepresentations, disabled } = + useActiveViewportSegmentationRepresentations({ + servicesManager, + }); + + const handlers = { + onSegmentationClick: (segmentationId: string) => { + commandsManager.run('setActiveSegmentation', { segmentationId }); + }, + + onSegmentAdd: segmentationId => { + commandsManager.run('addSegment', { segmentationId }); + }, + + onSegmentClick: (segmentationId, segmentIndex) => { + commandsManager.run('setActiveSegmentAndCenter', { segmentationId, segmentIndex }); + }, + + onSegmentEdit: (segmentationId, segmentIndex) => { + commandsManager.run('editSegmentLabel', { segmentationId, segmentIndex }); + }, + + onSegmentationEdit: segmentationId => { + commandsManager.run('editSegmentationLabel', { segmentationId }); + }, + + onSegmentColorClick: (segmentationId, segmentIndex) => { + commandsManager.run('editSegmentColor', { segmentationId, segmentIndex }); + }, + + onSegmentDelete: (segmentationId, segmentIndex) => { + commandsManager.run('deleteSegment', { segmentationId, segmentIndex }); + }, + + onToggleSegmentVisibility: (segmentationId, segmentIndex, type) => { + commandsManager.run('toggleSegmentVisibility', { segmentationId, segmentIndex, type }); + }, + + onToggleSegmentLock: (segmentationId, segmentIndex) => { + commandsManager.run('toggleSegmentLock', { segmentationId, segmentIndex }); + }, + + onToggleSegmentationRepresentationVisibility: (segmentationId, type) => { + commandsManager.run('toggleSegmentationVisibility', { segmentationId, type }); + }, + + onSegmentationDownload: segmentationId => { + commandsManager.run('downloadSegmentation', { segmentationId }); + }, + + storeSegmentation: async segmentationId => { + commandsManager.run({ + commandName: 'storeSegmentation', + commandOptions: { segmentationId }, + context: 'CORNERSTONE', + }); + }, + + onSegmentationDownloadRTSS: segmentationId => { + commandsManager.run('downloadRTSS', { segmentationId }); + }, + + setStyle: (segmentationId, type, key, value) => { + commandsManager.run('setSegmentationStyle', { segmentationId, type, key, value }); + }, + + toggleRenderInactiveSegmentations: () => { + commandsManager.run('toggleRenderInactiveSegmentations'); + }, + + onSegmentationRemoveFromViewport: segmentationId => { + commandsManager.run('removeSegmentationFromViewport', { segmentationId }); + }, + + onSegmentationDelete: segmentationId => { + commandsManager.run('deleteSegmentation', { segmentationId }); + }, + + setFillAlpha: ({ type }, value) => { + commandsManager.run('setFillAlpha', { type, value }); + }, + + setOutlineWidth: ({ type }, value) => { + commandsManager.run('setOutlineWidth', { type, value }); + }, + + setRenderFill: ({ type }, value) => { + commandsManager.run('setRenderFill', { type, value }); + }, + + setRenderOutline: ({ type }, value) => { + commandsManager.run('setRenderOutline', { type, value }); + }, + + setFillAlphaInactive: ({ type }, value) => { + commandsManager.run('setFillAlphaInactive', { type, value }); + }, + + getRenderInactiveSegmentations: () => { + return commandsManager.run('getRenderInactiveSegmentations'); + }, + }; + + const segmentationTableMode = customizationService.getCustomization( + 'panelSegmentation.tableMode' + ); + + // custom onSegmentationAdd if provided + const onSegmentationAdd = customizationService.getCustomization( + 'panelSegmentation.onSegmentationAdd' + ); + + const disableEditing = customizationService.getCustomization('panelSegmentation.disableEditing'); + const showAddSegment = customizationService.getCustomization('panelSegmentation.showAddSegment'); + const CustomDropdownMenuContent = customizationService.getCustomization( + 'panelSegmentation.customDropdownMenuContent' + ); + + const exportOptions = segmentationsWithRepresentations.map(({ segmentation }) => { + const { representationData, segmentationId } = segmentation; + const { Labelmap } = representationData; + + if (!Labelmap) { + return { + segmentationId, + isExportable: true, + }; + } + + const referencedImageIds = Labelmap.referencedImageIds; + const firstImageId = referencedImageIds[0]; + + const instance = metaData.get('instance', firstImageId); + + if (!instance) { + return { + segmentationId, + isExportable: false, + }; + } + + const SOPInstanceUID = instance.SOPInstanceUID || instance.SopInstanceUID; + const SeriesInstanceUID = instance.SeriesInstanceUID; + + const displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + + const isExportable = displaySet?.isReconstructable; + + return { + segmentationId, + isExportable, + }; + }); + + return ( + <> + + {children} + + + + {segmentationTableMode === 'collapsed' ? ( + + + + + + + + ) : ( + + + + + {/* */} + + + )} + + + ); +} diff --git a/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts b/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts new file mode 100644 index 0000000..9c62ff4 --- /dev/null +++ b/extensions/cornerstone/src/services/ColorbarService/ColorbarService.ts @@ -0,0 +1,284 @@ +import { PubSubService } from '@ohif/core'; +import { RENDERING_ENGINE_ID } from '../ViewportService/constants'; +import { StackViewport, VolumeViewport, getRenderingEngine } from '@cornerstonejs/core'; +import { utilities } from '@cornerstonejs/tools'; +import { ColorbarOptions, ChangeTypes } from '../../types/Colorbar'; +const { ViewportColorbar } = utilities.voi.colorbar; + +export default class ColorbarService extends PubSubService { + static EVENTS = { + STATE_CHANGED: 'event::ColorbarService:stateChanged', + }; + + static defaultStyles = { + position: 'absolute', + boxSizing: 'border-box', + border: 'solid 1px #555', + cursor: 'initial', + }; + + static positionStyles = { + left: { left: '5%' }, + right: { right: '5%' }, + top: { top: '5%' }, + bottom: { bottom: '5%' }, + }; + + static defaultTickStyles = { + position: 'left', + style: { + font: '12px Arial', + color: '#fff', + maxNumTicks: 8, + tickSize: 5, + tickWidth: 1, + labelMargin: 3, + }, + }; + + public static REGISTRATION = { + name: 'colorbarService', + create: () => { + return new ColorbarService(); + }, + }; + colorbars = {}; + + constructor() { + super(ColorbarService.EVENTS); + } + + /** + * Gets the volume ID for a given identifier by searching through the viewport's volume IDs. + * @param viewport - The viewport instance to search volumes in + * @param searchId - The identifier to search for within volume IDs + * @returns The matching volume ID if found, null otherwise + */ + private getVolumeIdForIdentifier(viewport, searchId: string): string | null { + const volumeIds = viewport.getAllVolumeIds?.() || []; + return volumeIds.length > 0 ? volumeIds.find(id => id.includes(searchId)) || null : null; + } + + /** + * Adds a colorbar to a specific viewport identified by `viewportId`, using the provided `displaySetInstanceUIDs` and `options`. + * This method sets up the colorbar, associates it with the viewport, and applies initial configurations based on the provided options. + * + * @param viewportId The identifier for the viewport where the colorbar will be added. + * @param displaySetInstanceUIDs An array of display set instance UIDs to associate with the colorbar. + * @param options Configuration options for the colorbar, including position, colormaps, active colormap name, ticks, and width. + */ + public addColorbar(viewportId, displaySetInstanceUIDs, options = {} as ColorbarOptions) { + const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID); + const viewport = renderingEngine.getViewport(viewportId); + + if (!viewport) { + return; + } + + const { element } = viewport; + const actorEntries = viewport.getActors(); + + if (!actorEntries || actorEntries.length === 0) { + return; + } + + const { position, width: thickness, activeColormapName, colormaps } = options; + + const numContainers = displaySetInstanceUIDs.length; + + const containers = this.createContainers( + numContainers, + element, + position, + thickness, + viewportId + ); + + displaySetInstanceUIDs.forEach((displaySetInstanceUID, index) => { + const volumeId = this.getVolumeIdForIdentifier(viewport, displaySetInstanceUID); + const properties = viewport?.getProperties(volumeId); + const colormap = properties?.colormap; + if (activeColormapName && !colormap) { + this.setViewportColormap( + viewportId, + displaySetInstanceUID, + colormaps[activeColormapName], + true + ); + } + + const colorbarContainer = containers[index]; + + const colorbar = new ViewportColorbar({ + id: `ctColorbar-${viewportId}-${index}`, + element, + colormaps: options.colormaps || {}, + // if there's an existing colormap set, we use it, otherwise we use the activeColormapName, otherwise, grayscale + activeColormapName: colormap?.name || options?.activeColormapName || 'Grayscale', + container: colorbarContainer, + ticks: { + ...ColorbarService.defaultTickStyles, + ...options.ticks, + }, + volumeId: viewport instanceof VolumeViewport ? volumeId : undefined, + }); + if (this.colorbars[viewportId]) { + this.colorbars[viewportId].push({ colorbar, container: colorbarContainer }); + } else { + this.colorbars[viewportId] = [{ colorbar, container: colorbarContainer }]; + } + }); + + this._broadcastEvent(ColorbarService.EVENTS.STATE_CHANGED, { + viewportId, + changeType: ChangeTypes.Added, + }); + } + + /** + * Removes the colorbar associated with a given viewport ID. This involves cleaning up any created DOM elements and internal references. + * + * @param viewportId The identifier for the viewport from which the colorbar will be removed. + */ + public removeColorbar(viewportId) { + const colorbarInfo = this.colorbars[viewportId]; + if (!colorbarInfo) { + return; + } + + colorbarInfo.forEach(({ colorbar, container }) => { + container.parentNode.removeChild(container); + }); + + delete this.colorbars[viewportId]; + + this._broadcastEvent(ColorbarService.EVENTS.STATE_CHANGED, { + viewportId, + changeType: ChangeTypes.Removed, + }); + } + + /** + * Checks whether a colorbar is associated with a given viewport ID. + * + * @param viewportId The identifier for the viewport to check. + * @returns `true` if a colorbar exists for the specified viewport, otherwise `false`. + */ + public hasColorbar(viewportId) { + return this.colorbars[viewportId] ? true : false; + } + + /** + * Retrieves the current state of colorbars, including all active colorbars and their configurations. + * + * @returns An object representing the current state of all colorbars managed by this service. + */ + public getState() { + return this.colorbars; + } + + /** + * Retrieves colorbar information for a specific viewport ID. + * + * @param viewportId The identifier for the viewport to retrieve colorbar information for. + * @returns The colorbar information associated with the specified viewport, if available. + */ + public getViewportColorbar(viewportId) { + return this.colorbars[viewportId]; + } + + /** + * Handles the cleanup and removal of all colorbars from the viewports. This is typically called + * when exiting the mode or context in which the colorbars are used, ensuring that no DOM + * elements or references are left behind. + */ + public onModeExit() { + const viewportIds = Object.keys(this.colorbars); + viewportIds.forEach(viewportId => { + this.removeColorbar(viewportId); + }); + } + + /** + * Sets the colormap for a viewport. This function is used internally to update the colormap the viewport + * + * @param viewportId The identifier of the viewport to update. + * @param displaySetInstanceUID The display set instance UID associated with the viewport. + * @param colormap The colormap object to set on the viewport. + * @param immediate A boolean indicating whether the viewport should be re-rendered immediately after setting the colormap. + */ + private setViewportColormap(viewportId, displaySetInstanceUID, colormap, immediate = false) { + const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID); + const viewport = renderingEngine.getViewport(viewportId); + const actorEntries = viewport?.getActors(); + if (!viewport || !actorEntries || actorEntries.length === 0) { + return; + } + const setViewportProperties = (viewport, uid) => { + const volumeId = this.getVolumeIdForIdentifier(viewport, uid); + viewport.setProperties({ colormap }, volumeId); + }; + + if (viewport instanceof StackViewport) { + setViewportProperties(viewport, viewportId); + } + + if (viewport instanceof VolumeViewport) { + setViewportProperties(viewport, displaySetInstanceUID); + } + + if (immediate) { + viewport.render(); + } + } + + /** + * Creates the container elements for colorbars based on the specified parameters. This function dynamically + * generates and styles DOM elements to host the colorbars, positioning them according to the specified options. + * + * @param numContainers The number of containers to create, typically corresponding to the number of colorbars. + * @param element The DOM element within which the colorbar containers will be placed. + * @param position The position of the colorbar containers (e.g., 'top', 'bottom', 'left', 'right'). + * @param thickness The thickness of the colorbar containers, affecting their width or height depending on their position. + * @param viewportId The identifier of the viewport for which the containers are being created. + * @returns An array of the created container DOM elements. + */ + private createContainers(numContainers, element, position, thickness, viewportId) { + const containers = []; + const dimensions = { + 1: 50, + 2: 33, + }; + const dimension = dimensions[numContainers] || 50 / numContainers; + + Array.from({ length: numContainers }).forEach((_, i) => { + const colorbarContainer = document.createElement('div'); + colorbarContainer.id = `ctColorbarContainer-${viewportId}-${i + 1}`; + + Object.assign(colorbarContainer.style, ColorbarService.defaultStyles); + + if (['top', 'bottom'].includes(position)) { + Object.assign(colorbarContainer.style, { + width: `${dimension}%`, + height: thickness || '2.5%', + left: `${(i + 1) * dimension}%`, + transform: 'translateX(-50%)', + ...ColorbarService.positionStyles[position], + }); + } else if (['left', 'right'].includes(position)) { + Object.assign(colorbarContainer.style, { + height: `${dimension}%`, + width: thickness || '2.5%', + top: `${(i + 1) * dimension}%`, + transform: 'translateY(-50%)', + ...ColorbarService.positionStyles[position], + }); + } + + element.appendChild(colorbarContainer); + containers.push(colorbarContainer); + }); + + return containers; + } +} diff --git a/extensions/cornerstone/src/services/ColorbarService/index.ts b/extensions/cornerstone/src/services/ColorbarService/index.ts new file mode 100644 index 0000000..32bd650 --- /dev/null +++ b/extensions/cornerstone/src/services/ColorbarService/index.ts @@ -0,0 +1,2 @@ +import ColorbarService from './ColorbarService'; +export default ColorbarService; diff --git a/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts new file mode 100644 index 0000000..b185f6c --- /dev/null +++ b/extensions/cornerstone/src/services/CornerstoneCacheService/CornerstoneCacheService.ts @@ -0,0 +1,324 @@ +import { Types } from '@ohif/core'; +import { cache as cs3DCache, Enums, volumeLoader } from '@cornerstonejs/core'; + +import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType'; +import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; + +const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume'; + +class CornerstoneCacheService { + static REGISTRATION = { + name: 'cornerstoneCacheService', + altName: 'CornerstoneCacheService', + create: ({ servicesManager }: Types.Extensions.ExtensionParams): CornerstoneCacheService => { + return new CornerstoneCacheService(servicesManager); + }, + }; + + stackImageIds: Map = new Map(); + volumeImageIds: Map = new Map(); + readonly servicesManager: AppTypes.ServicesManager; + + constructor(servicesManager: AppTypes.ServicesManager) { + this.servicesManager = servicesManager; + } + + public getCacheSize() { + return cs3DCache.getCacheSize(); + } + + public getCacheFreeSpace() { + return cs3DCache.getBytesAvailable(); + } + + public async createViewportData( + displaySets: Types.DisplaySet[], + viewportOptions: AppTypes.ViewportGrid.GridViewportOptions, + dataSource: unknown, + initialImageIndex?: number + ): Promise { + const viewportType = viewportOptions.viewportType as string; + + const cs3DViewportType = getCornerstoneViewportType(viewportType, displaySets); + let viewportData: StackViewportData | VolumeViewportData; + + if ( + cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC || + cs3DViewportType === Enums.ViewportType.VOLUME_3D + ) { + viewportData = await this._getVolumeViewportData(dataSource, displaySets, cs3DViewportType); + } else if (cs3DViewportType === Enums.ViewportType.STACK) { + // Everything else looks like a stack + viewportData = await this._getStackViewportData( + dataSource, + displaySets, + initialImageIndex, + cs3DViewportType + ); + } else { + viewportData = await this._getOtherViewportData( + dataSource, + displaySets, + initialImageIndex, + cs3DViewportType + ); + } + + viewportData.viewportType = cs3DViewportType; + + return viewportData; + } + + public async invalidateViewportData( + viewportData: VolumeViewportData | StackViewportData, + invalidatedDisplaySetInstanceUID: string, + dataSource, + displaySetService + ): Promise { + if (viewportData.viewportType === Enums.ViewportType.STACK) { + const displaySet = displaySetService.getDisplaySetByUID(invalidatedDisplaySetInstanceUID); + const imageIds = this._getCornerstoneStackImageIds(displaySet, dataSource); + + // remove images from the cache to be able to re-load them + imageIds.forEach(imageId => { + if (cs3DCache.getImageLoadObject(imageId)) { + cs3DCache.removeImageLoadObject(imageId); + } + }); + + return { + viewportType: Enums.ViewportType.STACK, + data: { + StudyInstanceUID: displaySet.StudyInstanceUID, + displaySetInstanceUID: invalidatedDisplaySetInstanceUID, + imageIds, + }, + }; + } + + // Todo: grab the volume and get the id from the viewport itself + const volumeId = `${VOLUME_LOADER_SCHEME}:${invalidatedDisplaySetInstanceUID}`; + + const volume = cs3DCache.getVolume(volumeId); + + if (volume) { + if (volume.imageIds) { + // also for each imageId in the volume, remove the imageId from the cache + // since that will hold the old metadata as well + + volume.imageIds.forEach(imageId => { + if (cs3DCache.getImageLoadObject(imageId)) { + cs3DCache.removeImageLoadObject(imageId); + } + }); + } + + // this shouldn't be via removeVolumeLoadObject, since that will + // remove the texture as well, but here we really just need a remove + // from registry so that we load it again + cs3DCache._volumeCache.delete(volumeId); + this.volumeImageIds.delete(volumeId); + } + + const displaySets = viewportData.data.map(({ displaySetInstanceUID }) => + displaySetService.getDisplaySetByUID(displaySetInstanceUID) + ); + + const newViewportData = await this._getVolumeViewportData( + dataSource, + displaySets, + viewportData.viewportType + ); + + return newViewportData; + } + + private async _getOtherViewportData( + dataSource, + displaySets, + _initialImageIndex, + viewportType: Enums.ViewportType + ): Promise { + // TODO - handle overlays and secondary display sets, but for now assume + // the 1st display set is the one of interest + const [displaySet] = displaySets; + if (!displaySet.imageIds) { + displaySet.imagesIds = this._getCornerstoneStackImageIds(displaySet, dataSource); + } + const { imageIds: data, viewportType: dsViewportType } = displaySet; + return { + viewportType: dsViewportType || viewportType, + data: displaySets, + }; + } + + private async _getStackViewportData( + dataSource, + displaySets, + initialImageIndex, + viewportType: Enums.ViewportType + ): Promise { + const { uiNotificationService } = this.servicesManager.services; + const overlayDisplaySets = displaySets.filter(ds => ds.isOverlayDisplaySet); + for (const overlayDisplaySet of overlayDisplaySets) { + if (overlayDisplaySet.load && overlayDisplaySet.load instanceof Function) { + const { userAuthenticationService } = this.servicesManager.services; + const headers = userAuthenticationService.getAuthorizationHeader(); + try { + await overlayDisplaySet.load({ headers }); + } catch (e) { + uiNotificationService.show({ + title: 'Error loading displaySet', + message: e.message, + type: 'error', + }); + console.error(e); + } + } + } + + // Ensuring the first non-overlay `displaySet` is always the primary one + const StackViewportData = []; + for (const displaySet of displaySets) { + const { displaySetInstanceUID, StudyInstanceUID, isCompositeStack } = displaySet; + + if (displaySet.load && displaySet.load instanceof Function) { + const { userAuthenticationService } = this.servicesManager.services; + const headers = userAuthenticationService.getAuthorizationHeader(); + try { + await displaySet.load({ headers }); + } catch (e) { + uiNotificationService.show({ + title: 'Error loading displaySet', + message: e.message, + type: 'error', + }); + console.error(e); + } + } + + let stackImageIds = this.stackImageIds.get(displaySet.displaySetInstanceUID); + + if (!stackImageIds) { + stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource); + // assign imageIds to the displaySet + displaySet.imageIds = stackImageIds; + this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds); + } + + StackViewportData.push({ + StudyInstanceUID, + displaySetInstanceUID, + isCompositeStack, + imageIds: stackImageIds, + initialImageIndex, + }); + } + + return { + viewportType, + data: StackViewportData, + }; + } + + private async _getVolumeViewportData( + dataSource, + displaySets, + viewportType: Enums.ViewportType + ): Promise { + // Todo: Check the cache for multiple scenarios to see if we need to + // decache the volume data from other viewports or not + + const volumeData = []; + + for (const displaySet of displaySets) { + const { Modality } = displaySet; + const isParametricMap = Modality === 'PMAP'; + const isSeg = Modality === 'SEG'; + + // Don't create volumes for the displaySets that have custom load + // function (e.g., SEG, RT, since they rely on the reference volumes + // and they take care of their own loading after they are created in their + // getSOPClassHandler method + + if (displaySet.load && displaySet.load instanceof Function) { + const { userAuthenticationService } = this.servicesManager.services; + const headers = userAuthenticationService.getAuthorizationHeader(); + + try { + await displaySet.load({ headers }); + } catch (e) { + const { uiNotificationService } = this.servicesManager.services; + uiNotificationService.show({ + title: 'Error loading displaySet', + message: e.message, + type: 'error', + }); + console.error(e); + } + + // Parametric maps have a `load` method but it should not be loaded in the + // same way as SEG and RTSTRUCT but like a normal volume + if (!isParametricMap) { + volumeData.push({ + studyInstanceUID: displaySet.StudyInstanceUID, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + }); + + // Todo: do some cache check and empty the cache if needed + continue; + } + } + + const volumeLoaderSchema = displaySet.volumeLoaderSchema ?? VOLUME_LOADER_SCHEME; + const volumeId = `${volumeLoaderSchema}:${displaySet.displaySetInstanceUID}`; + let volumeImageIds = this.volumeImageIds.get(displaySet.displaySetInstanceUID); + let volume = cs3DCache.getVolume(volumeId); + + // Parametric maps do not have image ids but they already have volume data + // therefore a new volume should not be created. + if (!isParametricMap && !isSeg && (!volumeImageIds || !volume)) { + volumeImageIds = this._getCornerstoneVolumeImageIds(displaySet, dataSource); + + volume = await volumeLoader.createAndCacheVolume(volumeId, { + imageIds: volumeImageIds, + }); + + this.volumeImageIds.set(displaySet.displaySetInstanceUID, volumeImageIds); + + // Add imageIds to the displaySet for volumes + displaySet.imageIds = volumeImageIds; + } + + volumeData.push({ + StudyInstanceUID: displaySet.StudyInstanceUID, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + volume, + volumeId, + imageIds: volumeImageIds, + isDynamicVolume: displaySet.isDynamicVolume, + }); + } + + return { + viewportType, + data: volumeData, + }; + } + + private _getCornerstoneStackImageIds(displaySet, dataSource): string[] { + return dataSource.getImageIdsForDisplaySet(displaySet); + } + + private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] { + if (displaySet.imageIds) { + return displaySet.imageIds; + } + + const stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource); + + return stackImageIds; + } +} + +export default CornerstoneCacheService; diff --git a/extensions/cornerstone/src/services/CornerstoneCacheService/index.js b/extensions/cornerstone/src/services/CornerstoneCacheService/index.js new file mode 100644 index 0000000..39b5a1e --- /dev/null +++ b/extensions/cornerstone/src/services/CornerstoneCacheService/index.js @@ -0,0 +1,3 @@ +import CornerstoneCacheService from './CornerstoneCacheService'; + +export default CornerstoneCacheService; diff --git a/extensions/cornerstone/src/services/SegmentationService/RTSTRUCT/mapROIContoursToRTStructData.ts b/extensions/cornerstone/src/services/SegmentationService/RTSTRUCT/mapROIContoursToRTStructData.ts new file mode 100644 index 0000000..b613074 --- /dev/null +++ b/extensions/cornerstone/src/services/SegmentationService/RTSTRUCT/mapROIContoursToRTStructData.ts @@ -0,0 +1,33 @@ +/** + * Maps a DICOM RT Struct ROI Contour to a RTStruct data that can be used + * in Segmentation Service + * + * @param structureSet - A DICOM RT Struct ROI Contour + * @param rtDisplaySetUID - A CornerstoneTools DisplaySet UID + * @returns An array of object that includes data, id, segmentIndex, color + * and geometry Id + */ +export function mapROIContoursToRTStructData(structureSet: unknown, rtDisplaySetUID: unknown) { + return structureSet.ROIContours.map(({ contourPoints, ROINumber, ROIName, colorArray }) => { + const data = contourPoints.map(({ points, ...rest }) => { + const newPoints = points.map(({ x, y, z }) => { + return [x, y, z]; + }); + + return { + ...rest, + points: newPoints, + }; + }); + + const id = ROIName || ROINumber; + + return { + data, + id, + segmentIndex: ROINumber, + color: colorArray, + geometryId: `${rtDisplaySetUID}:${id}:segmentIndex-${ROINumber}`, + }; + }); +} diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts new file mode 100644 index 0000000..0541e6c --- /dev/null +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -0,0 +1,1803 @@ +import { + cache, + Enums as csEnums, + eventTarget, + geometryLoader, + getEnabledElementByViewportId, + imageLoader, + Types as csTypes, + utilities as csUtils, + metaData, +} from '@cornerstonejs/core'; +import { + Enums as csToolsEnums, + segmentation as cstSegmentation, + Types as cstTypes, +} from '@cornerstonejs/tools'; +import { PubSubService, Types as OHIFTypes } from '@ohif/core'; +import i18n from '@ohif/i18n'; +import { easeInOutBell, easeInOutBellRelative } from '../../utils/transitions'; +import { mapROIContoursToRTStructData } from './RTSTRUCT/mapROIContoursToRTStructData'; +import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; +import { addColorLUT } from '@cornerstonejs/tools/segmentation/addColorLUT'; +import { getNextColorLUTIndex } from '@cornerstonejs/tools/segmentation/getNextColorLUTIndex'; +import { Segment } from '@cornerstonejs/tools/types/SegmentationStateTypes'; +import { ContourStyle, LabelmapStyle, SurfaceStyle } from '@cornerstonejs/tools/types'; +import { ViewportType } from '@cornerstonejs/core/enums'; +import { SegmentationPresentation, SegmentationPresentationItem } from '../../types/Presentation'; +import { updateLabelmapSegmentationImageReferences } from '@cornerstonejs/tools/segmentation/updateLabelmapSegmentationImageReferences'; +import { triggerSegmentationRepresentationModified } from '@cornerstonejs/tools/segmentation/triggerSegmentationEvents'; +import { convertStackToVolumeLabelmap } from '@cornerstonejs/tools/segmentation/helpers/convertStackToVolumeLabelmap'; +import { getLabelmapImageIds } from '@cornerstonejs/tools/segmentation'; + +const LABELMAP = csToolsEnums.SegmentationRepresentations.Labelmap; +const CONTOUR = csToolsEnums.SegmentationRepresentations.Contour; + +export type SegmentRepresentation = { + segmentIndex: number; + color: csTypes.Color; + opacity: number; + visible: boolean; +}; + +export type SegmentationData = cstTypes.Segmentation; + +export type SegmentationRepresentation = cstTypes.SegmentationRepresentation & { + viewportId: string; + id: string; + label: string; + styles: cstTypes.RepresentationStyle; + segments: { + [key: number]: SegmentRepresentation; + }; +}; + +export type SegmentationInfo = { + segmentation: SegmentationData; + representation?: SegmentationRepresentation; +}; + +const EVENTS = { + SEGMENTATION_MODIFIED: 'event::segmentation_modified', + // fired when the segmentation is added + SEGMENTATION_ADDED: 'event::segmentation_added', + // + SEGMENTATION_DATA_MODIFIED: 'event::segmentation_data_modified', + // fired when the segmentation is removed + SEGMENTATION_REMOVED: 'event::segmentation_removed', + // + // fired when segmentation representation is added + SEGMENTATION_REPRESENTATION_MODIFIED: 'event::segmentation_representation_modified', + // fired when segmentation representation is removed + SEGMENTATION_REPRESENTATION_REMOVED: 'event::segmentation_representation_removed', + // + // LOADING EVENTS + // fired when the active segment is loaded in SEG or RTSTRUCT + SEGMENT_LOADING_COMPLETE: 'event::segment_loading_complete', + // loading completed for all segments + SEGMENTATION_LOADING_COMPLETE: 'event::segmentation_loading_complete', +}; + +const VALUE_TYPES = {}; + +const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume'; + +class SegmentationService extends PubSubService { + static REGISTRATION = { + name: 'segmentationService', + altName: 'SegmentationService', + create: ({ servicesManager }: OHIFTypes.Extensions.ExtensionParams): SegmentationService => { + return new SegmentationService({ servicesManager }); + }, + }; + + private _segmentationIdToColorLUTIndexMap: Map; + readonly servicesManager: AppTypes.ServicesManager; + highlightIntervalId = null; + readonly EVENTS = EVENTS; + + constructor({ servicesManager }) { + super(EVENTS); + + this._segmentationIdToColorLUTIndexMap = new Map(); + + this.servicesManager = servicesManager; + } + + public onModeEnter(): void { + this._initSegmentationService(); + } + + public onModeExit(): void { + this.destroy(); + } + + /** + * Retrieves a segmentation by its ID. + * + * @param segmentationId - The unique identifier of the segmentation to retrieve. + * @returns The segmentation object if found, or undefined if not found. + * + * @remarks + * This method directly accesses the cornerstone tools segmentation state to fetch + * the segmentation data. It's useful when you need to access specific properties + * or perform operations on a particular segmentation. + */ + public getSegmentation(segmentationId: string): cstTypes.Segmentation | undefined { + return cstSegmentation.state.getSegmentation(segmentationId); + } + + /** + * Retrieves all segmentations from the cornerstone tools segmentation state. + * + * @returns An array of all segmentations currently stored in the state + * + * @remarks + * This is a convenience method that directly accesses the cornerstone tools + * segmentation state to get all available segmentations. It returns the raw + * segmentation objects without any additional processing or filtering. + */ + public getSegmentations(): cstTypes.Segmentation[] | [] { + return cstSegmentation.state.getSegmentations(); + } + + public getPresentation(viewportId: string): SegmentationPresentation { + const segmentationPresentations: SegmentationPresentation = []; + const segmentationsMap = new Map(); + + const representations = this.getSegmentationRepresentations(viewportId); + for (const representation of representations) { + const { segmentationId } = representation; + + if (!representation) { + continue; + } + + const { type } = representation; + + segmentationsMap.set(segmentationId, { + segmentationId, + type, + hydrated: true, + config: representation.config || {}, + }); + } + + // Check inside the removedDisplaySetAndRepresentationMaps to see if any of the representations are not hydrated + // const hydrationMap = this._segmentationRepresentationHydrationMaps.get(presentationId); + + // if (hydrationMap) { + // hydrationMap.forEach(rep => { + // segmentationsMap.set(rep.segmentationId, { + // segmentationId: rep.segmentationId, + // type: rep.type, + // hydrated: rep.hydrated, + // config: rep.config || {}, + // }); + // }); + // } + + // // Convert the Map to an array + segmentationPresentations.push(...segmentationsMap.values()); + + return segmentationPresentations; + } + + public getRepresentationsForSegmentation( + segmentationId: string + ): { viewportId: string; representations: any[] }[] { + const representations = + cstSegmentation.state.getSegmentationRepresentationsBySegmentationId(segmentationId); + + return representations; + } + + /** + * Retrieves segmentation representations (labelmap, contour, surface) based on specified criteria. + * + * @param viewportId - The ID of the viewport. + * @param specifier - An object containing optional `segmentationId` and `type` to filter the representations. + * @returns An array of `SegmentationRepresentation` matching the criteria, or an empty array if none are found. + * + * @remarks + * This method filters the segmentation representations according to the provided `specifier`: + * - **No `segmentationId` or `type` provided**: Returns all representations associated with the given `viewportId`. + * - **Only `segmentationId` provided**: Returns all representations with that `segmentationId`, regardless of `viewportId`. + * - **Only `type` provided**: Returns all representations of that `type` associated with the given `viewportId`. + * - **Both `segmentationId` and `type` provided**: Returns representations matching both criteria, regardless of `viewportId`. + */ + public getSegmentationRepresentations( + viewportId: string, + specifier: { + segmentationId?: string; + type?: SegmentationRepresentations; + } = {} + ): SegmentationRepresentation[] { + // Get all representations for the viewportId + const representations = cstSegmentation.state.getSegmentationRepresentations( + viewportId, + specifier + ); + + // Map to our SegmentationRepresentation type + const ohifRepresentations = representations.map(repr => + this._toOHIFSegmentationRepresentation(viewportId, repr) + ); + + return ohifRepresentations; + } + + public destroy = () => { + eventTarget.removeEventListener( + csToolsEnums.Events.SEGMENTATION_MODIFIED, + this._onSegmentationModifiedFromSource + ); + + eventTarget.removeEventListener( + csToolsEnums.Events.SEGMENTATION_REMOVED, + this._onSegmentationModifiedFromSource + ); + + eventTarget.removeEventListener( + csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED, + this._onSegmentationDataModifiedFromSource + ); + + eventTarget.removeEventListener( + csToolsEnums.Events.SEGMENTATION_REPRESENTATION_ADDED, + this._onSegmentationModifiedFromSource + ); + + eventTarget.removeEventListener( + csToolsEnums.Events.SEGMENTATION_ADDED, + this._onSegmentationAddedFromSource + ); + + this.listeners = {}; + }; + + public async addSegmentationRepresentation( + viewportId: string, + { + segmentationId, + type, + suppressEvents = false, + }: { + segmentationId: string; + type?: csToolsEnums.SegmentationRepresentations; + suppressEvents?: boolean; + } + ): Promise { + const segmentation = this.getSegmentation(segmentationId); + const csViewport = this.getAndValidateViewport(viewportId); + const colorLUTIndex = this._segmentationIdToColorLUTIndexMap.get(segmentationId); + + const defaultRepresentationType = csToolsEnums.SegmentationRepresentations.Labelmap; + let representationTypeToUse = type || defaultRepresentationType; + let isConverted = false; + + if (type === csToolsEnums.SegmentationRepresentations.Labelmap) { + const { isVolumeViewport, isVolumeSegmentation } = this.determineViewportAndSegmentationType( + csViewport, + segmentation + ); + + ({ representationTypeToUse, isConverted } = await this.handleViewportConversion( + isVolumeViewport, + isVolumeSegmentation, + csViewport, + segmentation, + viewportId, + segmentationId, + representationTypeToUse + )); + } + + await this._addSegmentationRepresentation( + viewportId, + segmentationId, + representationTypeToUse, + colorLUTIndex, + isConverted + ); + + if (!suppressEvents) { + this._broadcastEvent(this.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, { segmentationId }); + } + } + + /** + * Creates an labelmap segmentation for a given display set + * + * @param displaySet - The display set to create the segmentation for. + * @param options - Optional parameters for creating the segmentation. + * @param options.segmentationId - Custom segmentation ID. If not provided, a UUID will be generated. + * @param options.FrameOfReferenceUID - Frame of reference UID for the segmentation. + * @param options.label - Label for the segmentation. + * @returns A promise that resolves to the created segmentation ID. + */ + public async createLabelmapForDisplaySet( + displaySet: AppTypes.DisplaySet, + options?: { + segmentationId?: string; + segments?: { [segmentIndex: number]: Partial }; + FrameOfReferenceUID?: string; + label?: string; + } + ): Promise { + // Todo: random does not makes sense, make this better, like + // labelmap 1, 2, 3 etc + const segmentationId = options?.segmentationId ?? `${csUtils.uuidv4()}`; + + const isDynamicVolume = displaySet.isDynamicVolume; + + let referenceImageIds = displaySet.imageIds; + if (isDynamicVolume) { + // get the middle timepoint for referenceImageIds + const timePoints = displaySet.dynamicVolumeInfo.timePoints; + const middleTimePoint = timePoints[Math.floor(timePoints.length / 2)]; + referenceImageIds = middleTimePoint; + } + + const derivedImages = await imageLoader.createAndCacheDerivedLabelmapImages(referenceImageIds); + + const segs = this.getSegmentations(); + const label = options.label || `Segmentation ${segs.length + 1}`; + + const segImageIds = derivedImages.map(image => image.imageId); + + const segmentationPublicInput: cstTypes.SegmentationPublicInput = { + segmentationId, + representation: { + type: LABELMAP, + data: { + imageIds: segImageIds, + referencedVolumeId: this._getVolumeIdForDisplaySet(displaySet), + referencedImageIds: referenceImageIds, + }, + }, + config: { + label, + segments: + options.segments && Object.keys(options.segments).length > 0 + ? options.segments + : { + 1: { + label: `${i18n.t('Segment')} 1`, + active: true, + }, + }, + cachedStats: { + info: `S${displaySet.SeriesNumber}: ${displaySet.SeriesDescription}`, + }, + }, + }; + + this.addOrUpdateSegmentation(segmentationPublicInput); + return segmentationId; + } + + public async createSegmentationForSEGDisplaySet( + segDisplaySet, + options: { + segmentationId?: string; + type: SegmentationRepresentations; + } = { + type: LABELMAP, + } + ): Promise { + const { type } = options; + let { segmentationId } = options; + const { labelmapBufferArray } = segDisplaySet; + + if (type !== LABELMAP) { + throw new Error('Only labelmap type is supported for SEG display sets right now'); + } + + if (!labelmapBufferArray) { + throw new Error('SEG reading failed'); + } + + segmentationId = segmentationId ?? segDisplaySet.displaySetInstanceUID; + const referencedDisplaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID; + const referencedDisplaySet = this.servicesManager.services.displaySetService.getDisplaySetByUID( + referencedDisplaySetInstanceUID + ); + + const images = referencedDisplaySet.instances; + + if (!images.length) { + throw new Error('No instances were provided for the referenced display set of the SEG'); + } + + const imageIds = images.map(image => image.imageId); + + const derivedSegmentationImages = await imageLoader.createAndCacheDerivedLabelmapImages( + imageIds as string[] + ); + + segDisplaySet.images = derivedSegmentationImages.map(image => ({ + ...image, + ...metaData.get('instance', image.referencedImageId), + })); + + const segmentsInfo = segDisplaySet.segMetadata.data; + + const segments: { [segmentIndex: string]: cstTypes.Segment } = {}; + const colorLUT = []; + + segmentsInfo.forEach((segmentInfo, index) => { + if (index === 0) { + colorLUT.push([0, 0, 0, 0]); + return; + } + + const { + SegmentedPropertyCategoryCodeSequence, + SegmentNumber, + SegmentLabel, + SegmentAlgorithmType, + SegmentAlgorithmName, + SegmentedPropertyTypeCodeSequence, + rgba, + } = segmentInfo; + + colorLUT.push(rgba); + + const segmentIndex = Number(SegmentNumber); + + const centroid = segDisplaySet.centroids?.get(index); + const imageCentroidXYZ = centroid?.image || { x: 0, y: 0, z: 0 }; + const worldCentroidXYZ = centroid?.world || { x: 0, y: 0, z: 0 }; + + segments[segmentIndex] = { + segmentIndex, + label: SegmentLabel || `Segment ${SegmentNumber}`, + locked: false, + active: false, + cachedStats: { + center: { + image: [imageCentroidXYZ.x, imageCentroidXYZ.y, imageCentroidXYZ.z], + world: [worldCentroidXYZ.x, worldCentroidXYZ.y, worldCentroidXYZ.z], + }, + modifiedTime: segDisplaySet.SeriesDate, + category: SegmentedPropertyCategoryCodeSequence + ? SegmentedPropertyCategoryCodeSequence.CodeMeaning + : '', + type: SegmentedPropertyTypeCodeSequence + ? SegmentedPropertyTypeCodeSequence.CodeMeaning + : '', + algorithmType: SegmentAlgorithmType, + algorithmName: SegmentAlgorithmName, + }, + }; + }); + + // get next color lut index + const colorLUTIndex = getNextColorLUTIndex(); + addColorLUT(colorLUT, colorLUTIndex); + this._segmentationIdToColorLUTIndexMap.set(segmentationId, colorLUTIndex); + + // now we need to chop the volume array into chunks and set the scalar data for each derived segmentation image + const volumeScalarData = new Uint8Array(labelmapBufferArray[0]); + + // We should parse the segmentation as separate slices to support overlapping segments. + // This parsing should occur in the CornerstoneJS library adapters. + // For now, we use the volume returned from the library and chop it here. + let firstSegmentedSliceImageId = null; + for (let i = 0; i < derivedSegmentationImages.length; i++) { + const voxelManager = derivedSegmentationImages[i] + .voxelManager as csTypes.IVoxelManager; + const scalarData = voxelManager.getScalarData(); + const sliceData = volumeScalarData.slice(i * scalarData.length, (i + 1) * scalarData.length); + scalarData.set(sliceData); + voxelManager.setScalarData(scalarData); + + // Check if this slice has any non-zero voxels and we haven't found one yet + if (!firstSegmentedSliceImageId && sliceData.some(value => value !== 0)) { + firstSegmentedSliceImageId = derivedSegmentationImages[i].referencedImageId; + } + } + + // assign the first non zero voxel image id to the segDisplaySet + segDisplaySet.firstSegmentedSliceImageId = firstSegmentedSliceImageId; + + this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, { + segmentationId, + segDisplaySet, + }); + + const seg: cstTypes.SegmentationPublicInput = { + segmentationId, + representation: { + type: LABELMAP, + data: { + imageIds: derivedSegmentationImages.map(image => image.imageId), + referencedVolumeId: this._getVolumeIdForDisplaySet(referencedDisplaySet), + referencedImageIds: imageIds as string[], + }, + }, + config: { + label: segDisplaySet.SeriesDescription, + segments, + }, + }; + + segDisplaySet.isLoaded = true; + + this.addOrUpdateSegmentation(seg); + + return segmentationId; + } + + public async createSegmentationForRTDisplaySet( + rtDisplaySet, + options: { + segmentationId?: string; + type: SegmentationRepresentations; + } = { + type: CONTOUR, + } + ): Promise { + const { type } = options; + let { segmentationId } = options; + + // Currently, only contour representation is supported for RT display + if (type !== CONTOUR) { + throw new Error('Only contour type is supported for RT display sets right now'); + } + + // Assign segmentationId if not provided + segmentationId = segmentationId ?? rtDisplaySet.displaySetInstanceUID; + const { structureSet } = rtDisplaySet; + + if (!structureSet) { + throw new Error( + 'To create the contours from RT displaySet, the displaySet should be loaded first. You can perform rtDisplaySet.load() before calling this method.' + ); + } + + const rtDisplaySetUID = rtDisplaySet.displaySetInstanceUID; + const referencedDisplaySet = this.servicesManager.services.displaySetService.getDisplaySetByUID( + rtDisplaySet.referencedDisplaySetInstanceUID + ); + + const referencedImageIdsWithGeometry = Array.from(structureSet.ReferencedSOPInstanceUIDsSet); + + const referencedImageIds = referencedDisplaySet.instances.map(image => image.imageId); + // find the first image id that contains a referenced SOP instance UID + const firstSegmentedSliceImageId = referencedImageIds.find(imageId => + referencedImageIdsWithGeometry.some(referencedId => imageId.includes(referencedId)) + ); + + rtDisplaySet.firstSegmentedSliceImageId = firstSegmentedSliceImageId; + // Map ROI contours to RT Struct Data + const allRTStructData = mapROIContoursToRTStructData(structureSet, rtDisplaySetUID); + + // Sort by segmentIndex for consistency + allRTStructData.sort((a, b) => a.segmentIndex - b.segmentIndex); + + const geometryIds = allRTStructData.map(({ geometryId }) => geometryId); + + // Initialize SegmentationPublicInput similar to SEG function + const segmentation: cstTypes.SegmentationPublicInput = { + segmentationId, + representation: { + type: CONTOUR, + data: { + geometryIds, + }, + }, + config: { + label: rtDisplaySet.SeriesDescription, + }, + }; + + if (!structureSet.ROIContours?.length) { + throw new Error( + 'The structureSet does not contain any ROIContours. Please ensure the structureSet is loaded first.' + ); + } + + const segments: { [segmentIndex: string]: cstTypes.Segment } = {}; + let segmentsCachedStats = {}; + + // Process each segment similarly to the SEG function + for (const rtStructData of allRTStructData) { + const { data, id, color, segmentIndex, geometryId } = rtStructData; + + try { + const geometry = await geometryLoader.createAndCacheGeometry(geometryId, { + geometryData: { + data, + id, + color, + frameOfReferenceUID: structureSet.frameOfReferenceUID, + segmentIndex, + }, + type: csEnums.GeometryType.CONTOUR, + }); + + const contourSet = geometry.data as csTypes.IContourSet; + const centroid = contourSet.centroid; + + segmentsCachedStats = { + center: { world: centroid }, + modifiedTime: rtDisplaySet.SeriesDate, // Using SeriesDate as modifiedTime + }; + + segments[segmentIndex] = { + label: id, + segmentIndex, + cachedStats: segmentsCachedStats, + locked: false, + active: false, + }; + + // Broadcast segment loading progress + const numInitialized = Object.keys(segmentsCachedStats).length; + const percentComplete = Math.round((numInitialized / allRTStructData.length) * 100); + this._broadcastEvent(EVENTS.SEGMENT_LOADING_COMPLETE, { + percentComplete, + numSegments: allRTStructData.length, + }); + } catch (e) { + console.warn(`Error initializing contour for segment ${segmentIndex}:`, e); + continue; // Continue processing other segments even if one fails + } + } + + // Assign processed segments to segmentation config + segmentation.config.segments = segments; + + // Broadcast segmentation loading complete event + this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, { + segmentationId, + rtDisplaySet, + }); + + // Mark the RT display set as loaded + rtDisplaySet.isLoaded = true; + + // Add or update the segmentation in the state + this.addOrUpdateSegmentation(segmentation); + + return segmentationId; + } + + /** + * Adds or updates a segmentation in the state + * @param segmentationId - The ID of the segmentation to add or update + * @param data - The data to add or update the segmentation with + * + * @remarks + * This method handles the addition or update of a segmentation in the state. + * If the segmentation already exists, it updates the existing segmentation. + * If the segmentation does not exist, it adds a new segmentation. + */ + public addOrUpdateSegmentation( + data: cstTypes.SegmentationPublicInput | Partial + ) { + const segmentationId = data.segmentationId; + const existingSegmentation = cstSegmentation.state.getSegmentation(segmentationId); + + if (existingSegmentation) { + // Update the existing segmentation + this.updateSegmentationInSource(segmentationId, data as Partial); + } else { + // Add a new segmentation + this.addSegmentationToSource(data as cstTypes.SegmentationPublicInput); + } + } + + public setActiveSegmentation(viewportId: string, segmentationId: string): void { + cstSegmentation.activeSegmentation.setActiveSegmentation(viewportId, segmentationId); + } + + /** + * Gets the active segmentation for a viewport + * @param viewportId - The ID of the viewport to get the active segmentation for + * @returns The active segmentation object, or null if no segmentation is active + * + * @remarks + * This method retrieves the currently active segmentation for the specified viewport. + * The active segmentation is the one that is currently selected for editing operations. + * Returns null if no segmentation is active in the viewport. + */ + public getActiveSegmentation(viewportId: string): cstTypes.Segmentation | null { + return cstSegmentation.activeSegmentation.getActiveSegmentation(viewportId); + } + + /** + * Gets the active segment from the active segmentation in a viewport + * @param viewportId - The ID of the viewport to get the active segment from + * @returns The active segment object, or undefined if no segment is active + * + * @remarks + * This method retrieves the currently active segment from the active segmentation + * in the specified viewport. The active segment is the one that is currently + * selected for editing operations. Returns undefined if no segment is active or + * if there is no active segmentation. + */ + public getActiveSegment(viewportId: string): cstTypes.Segment | undefined { + const activeSegmentation = this.getActiveSegmentation(viewportId); + + if (!activeSegmentation) { + return; + } + + const { segments } = activeSegmentation; + + let activeSegment; + for (const segment of Object.values(segments)) { + if (segment.active) { + activeSegment = segment; + break; + } + } + + return activeSegment; + } + + public hasCustomStyles(specifier: { + viewportId: string; + segmentationId: string; + type: SegmentationRepresentations; + }): boolean { + return cstSegmentation.config.style.hasCustomStyle(specifier); + } + + public getStyle = (specifier: { + viewportId: string; + segmentationId: string; + type: SegmentationRepresentations; + segmentIndex?: number; + }) => { + const style = cstSegmentation.config.style.getStyle(specifier); + + return style; + }; + + public setStyle = ( + specifier: { + type: SegmentationRepresentations; + viewportId?: string; + segmentationId?: string; + segmentIndex?: number; + }, + style: LabelmapStyle | ContourStyle | SurfaceStyle + ) => { + cstSegmentation.config.style.setStyle(specifier, style); + }; + + public resetToGlobalStyle = () => { + cstSegmentation.config.style.resetToGlobalStyle(); + }; + + /** + * Adds a new segment to the specified segmentation. + * @param segmentationId - The ID of the segmentation to add the segment to. + * @param viewportId: The ID of the viewport to add the segment to, it is used to get the representation, if it is not + * provided, the first available representation for the segmentationId will be used. + * @param config - An object containing the configuration options for the new segment. + * - segmentIndex: (optional) The index of the segment to add. If not provided, the next available index will be used. + * - properties: (optional) An object containing the properties of the new segment. + * - label: (optional) The label of the new segment. If not provided, a default label will be used. + * - color: (optional) The color of the new segment in RGB format. If not provided, a default color will be used. + * - visibility: (optional) Whether the new segment should be visible. If not provided, the segment will be visible by default. + * - isLocked: (optional) Whether the new segment should be locked for editing. If not provided, the segment will not be locked by default. + * - active: (optional) Whether the new segment should be the active segment to be edited. If not provided, the segment will not be active by default. + */ + public addSegment( + segmentationId: string, + config: { + segmentIndex?: number; + label?: string; + isLocked?: boolean; + active?: boolean; + color?: csTypes.Color; // Add color type + visibility?: boolean; // Add visibility option + } = {} + ): void { + if (config?.segmentIndex === 0) { + throw new Error(i18n.t('Segment') + ' index 0 is reserved for "no label"'); + } + + const csSegmentation = this.getCornerstoneSegmentation(segmentationId); + + let segmentIndex = config.segmentIndex; + if (!segmentIndex) { + // grab the next available segment index based on the object keys, + // so basically get the highest segment index value + 1 + const segmentKeys = Object.keys(csSegmentation.segments); + segmentIndex = segmentKeys.length === 0 ? 1 : Math.max(...segmentKeys.map(Number)) + 1; + } + + // update the segmentation + if (!config.label) { + config.label = `${i18n.t('Segment')} ${segmentIndex}`; + } + + const currentSegments = csSegmentation.segments; + + cstSegmentation.updateSegmentations([ + { + segmentationId, + payload: { + segments: { + ...currentSegments, + [segmentIndex]: { + ...currentSegments[segmentIndex], + segmentIndex, + cachedStats: {}, + locked: false, + ...config, + }, + }, + }, + }, + ]); + + this.setActiveSegment(segmentationId, segmentIndex); + + // Apply additional configurations + if (config.isLocked !== undefined) { + this._setSegmentLockedStatus(segmentationId, segmentIndex, config.isLocked); + } + + // Get all viewports that have this segmentation + const viewportIds = this.getViewportIdsWithSegmentation(segmentationId); + + viewportIds.forEach(viewportId => { + // Set color if provided + if (config.color !== undefined) { + this.setSegmentColor(viewportId, segmentationId, segmentIndex, config.color); + } + + // Set visibility if provided + if (config.visibility !== undefined) { + this.setSegmentVisibility(viewportId, segmentationId, segmentIndex, config.visibility); + } + }); + } + + /** + * Removes a segment from a segmentation and updates the active segment index if necessary. + * + * @param segmentationId - The ID of the segmentation containing the segment to remove. + * @param segmentIndex - The index of the segment to remove. + * + * @remarks + * This method performs the following actions: + * 1. Clears the segment value in the Cornerstone segmentation. + * 2. Updates all related segmentation representations to remove the segment. + * 3. If the removed segment was the active segment, it updates the active segment index. + * + */ + public removeSegment(segmentationId: string, segmentIndex: number): void { + cstSegmentation.removeSegment(segmentationId, segmentIndex); + } + + public setSegmentVisibility( + viewportId: string, + segmentationId: string, + segmentIndex: number, + isVisible: boolean, + type?: SegmentationRepresentations + ): void { + this._setSegmentVisibility(viewportId, segmentationId, segmentIndex, isVisible, type); + } + + /** + * Sets the locked status of a segment in a segmentation. + * + * @param segmentationId - The ID of the segmentation containing the segment. + * @param segmentIndex - The index of the segment to set the locked status for. + * @param isLocked - The new locked status of the segment. + * + * @remarks + * This method updates the locked status of a specific segment within a segmentation. + * A locked segment cannot be modified or edited. + */ + public setSegmentLocked(segmentationId: string, segmentIndex: number, isLocked: boolean): void { + this._setSegmentLockedStatus(segmentationId, segmentIndex, isLocked); + } + + /** + * Toggles the locked state of a segment in a segmentation. + * @param segmentationId - The ID of the segmentation. + * @param segmentIndex - The index of the segment to toggle. + */ + public toggleSegmentLocked(segmentationId: string, segmentIndex: number): void { + const isLocked = cstSegmentation.segmentLocking.isSegmentIndexLocked( + segmentationId, + segmentIndex + ); + this._setSegmentLockedStatus(segmentationId, segmentIndex, !isLocked); + } + + public toggleSegmentVisibility( + viewportId: string, + segmentationId: string, + segmentIndex: number, + type: SegmentationRepresentations + ): void { + const isVisible = cstSegmentation.config.visibility.getSegmentIndexVisibility( + viewportId, + { + segmentationId, + type, + }, + segmentIndex + ); + this._setSegmentVisibility(viewportId, segmentationId, segmentIndex, !isVisible, type); + } + + /** + * Sets the color of a specific segment in a segmentation. + * + * @param viewportId - The ID of the viewport containing the segmentation + * @param segmentationId - The ID of the segmentation containing the segment + * @param segmentIndex - The index of the segment to set the color for + * @param color - The new color to apply to the segment as an array of RGBA values + * + * @remarks + * This method updates the color of a specific segment within a segmentation. + * The color parameter should be an array of 4 numbers representing RGBA values. + */ + public setSegmentColor( + viewportId: string, + segmentationId: string, + segmentIndex: number, + color: csTypes.Color + ): void { + cstSegmentation.config.color.setSegmentIndexColor( + viewportId, + segmentationId, + segmentIndex, + color + ); + } + + /** + * Gets the current color of a specific segment in a segmentation. + * + * @param viewportId - The ID of the viewport containing the segmentation + * @param segmentationId - The ID of the segmentation containing the segment + * @param segmentIndex - The index of the segment to get the color for + * @returns An array of 4 numbers representing the RGBA color values of the segment + * + * @remarks + * This method retrieves the current color of a specific segment within a segmentation. + * The returned color is an array of 4 numbers representing RGBA values. + */ + public getSegmentColor(viewportId: string, segmentationId: string, segmentIndex: number) { + return cstSegmentation.config.color.getSegmentIndexColor( + viewportId, + segmentationId, + segmentIndex + ); + } + + /** + * Gets the labelmap volume for a segmentation + * @param segmentationId - The ID of the segmentation to get the labelmap volume for + * @returns The labelmap volume for the segmentation, or null if not found + * + * @remarks + * This method retrieves the labelmap volume data for a specific segmentation. + * The labelmap volume contains the actual segmentation data in the form of a 3D volume. + * Returns null if the segmentation does not have valid labelmap volume data. + */ + public getLabelmapVolume(segmentationId: string) { + const csSegmentation = cstSegmentation.state.getSegmentation(segmentationId); + const labelmapData = csSegmentation.representationData[ + SegmentationRepresentations.Labelmap + ] as cstTypes.LabelmapToolOperationDataVolume; + + if (!labelmapData || !labelmapData.volumeId) { + return null; + } + + const { volumeId } = labelmapData; + const labelmapVolume = cache.getVolume(volumeId); + + return labelmapVolume; + } + + /** + * Sets the label for a specific segment in a segmentation + * @param segmentationId - The ID of the segmentation containing the segment + * @param segmentIndex - The index of the segment to set the label for + * @param label - The new label to apply to the segment + * + * @remarks + * This method updates the text label of a specific segment within a segmentation. + * The label is used to identify and describe the segment in the UI. + */ + public setSegmentLabel(segmentationId: string, segmentIndex: number, label: string) { + this._setSegmentLabel(segmentationId, segmentIndex, label); + } + + /** + * Sets the active segment for a segmentation + * @param segmentationId - The ID of the segmentation containing the segment + * @param segmentIndex - The index of the segment to set as active + * + * @remarks + * This method updates which segment is considered "active" within a segmentation. + * The active segment is typically highlighted and available for editing operations. + */ + public setActiveSegment(segmentationId: string, segmentIndex: number): void { + this._setActiveSegment(segmentationId, segmentIndex); + } + + /** + * Controls whether inactive segmentations should be rendered in a viewport + * @param viewportId - The ID of the viewport to update + * @param renderInactive - Whether inactive segmentations should be rendered + * + * @remarks + * This method configures if segmentations that are not currently active + * should still be visible in the specified viewport. This can be useful + * for comparing or viewing multiple segmentations simultaneously. + */ + public setRenderInactiveSegmentations(viewportId: string, renderInactive: boolean): void { + cstSegmentation.config.style.setRenderInactiveSegmentations(viewportId, renderInactive); + } + + /** + * Gets whether inactive segmentations are being rendered for a viewport + * @param viewportId - The ID of the viewport to check + * @returns boolean indicating if inactive segmentations are rendered + * + * @remarks + * This method retrieves the current rendering state for inactive segmentations + * in the specified viewport. Returns true if inactive segmentations are visible. + */ + public getRenderInactiveSegmentations(viewportId: string): boolean { + return cstSegmentation.config.style.getRenderInactiveSegmentations(viewportId); + } + + /** + * Toggles the visibility of a segmentation in the state, and broadcasts the event. + * Note: this method does not update the segmentation state in the source. It only + * updates the state, and there should be separate listeners for that. + * @param ids segmentation ids + */ + public toggleSegmentationRepresentationVisibility = ( + viewportId: string, + { segmentationId, type }: { segmentationId: string; type: SegmentationRepresentations } + ): void => { + this._toggleSegmentationRepresentationVisibility(viewportId, segmentationId, type); + }; + + public getViewportIdsWithSegmentation = (segmentationId: string): string[] => { + const viewportIds = cstSegmentation.state.getViewportIdsWithSegmentation(segmentationId); + return viewportIds; + }; + + /** + * Clears segmentation representations from the viewport. + * Unlike removeSegmentationRepresentations, this doesn't update + * removed display set and representation maps. + * We track removed segmentations manually to avoid re-adding them + * when the display set is added again. + * @param viewportId - The viewport ID to clear segmentation representations from. + */ + public clearSegmentationRepresentations(viewportId: string): void { + this.removeSegmentationRepresentations(viewportId); + } + + /** + * Completely removes a segmentation from the state + * @param segmentationId - The ID of the segmentation to remove. + */ + public remove(segmentationId: string): void { + cstSegmentation.state.removeSegmentation(segmentationId); + } + + public removeAllSegmentations(): void { + cstSegmentation.state.removeAllSegmentations(); + } + + /** + * It removes the segmentation representations from the viewport. + * @param viewportId - The viewport id to remove the segmentation representations from. + * @param specifier - The specifier to remove the segmentation representations. + * + * @remarks + * If no specifier is provided, all segmentation representations for the viewport are removed. + * If a segmentationId specifier is provided, only the segmentation representation with the specified segmentationId and type are removed. + * If a type specifier is provided, only the segmentation representation with the specified type are removed. + * If both a segmentationId and type specifier are provided, only the segmentation representation with the specified segmentationId and type are removed. + */ + public removeSegmentationRepresentations( + viewportId: string, + specifier: { + segmentationId?: string; + type?: SegmentationRepresentations; + } = {} + ): void { + cstSegmentation.removeSegmentationRepresentations(viewportId, specifier); + } + + public jumpToSegmentCenter( + segmentationId: string, + segmentIndex: number, + viewportId?: string, + highlightAlpha = 0.9, + highlightSegment = true, + animationLength = 750, + highlightHideOthers = false, + highlightFunctionType = 'ease-in-out' // todo: make animation functions configurable from outside + ): void { + const center = this._getSegmentCenter(segmentationId, segmentIndex); + + if (!center) { + return; + } + + const { world } = center as { world: csTypes.Point3 }; + + // need to find which viewports are displaying the segmentation + const viewportIds = viewportId + ? [viewportId] + : this.getViewportIdsWithSegmentation(segmentationId); + + viewportIds.forEach(viewportId => { + const { viewport } = getEnabledElementByViewportId(viewportId); + viewport.jumpToWorld(world); + + highlightSegment && + this.highlightSegment( + segmentationId, + segmentIndex, + viewportId, + highlightAlpha, + animationLength, + highlightHideOthers + ); + }); + } + + public highlightSegment( + segmentationId: string, + segmentIndex: number, + viewportId?: string, + alpha = 0.9, + animationLength = 750, + hideOthers = true, + highlightFunctionType = 'ease-in-out' + ): void { + if (this.highlightIntervalId) { + clearInterval(this.highlightIntervalId); + } + + const csSegmentation = this.getCornerstoneSegmentation(segmentationId); + + const viewportIds = viewportId + ? [viewportId] + : this.getViewportIdsWithSegmentation(segmentationId); + + viewportIds.forEach(viewportId => { + const segmentationRepresentation = this.getSegmentationRepresentations(viewportId, { + segmentationId, + }); + + const representation = segmentationRepresentation[0]; + const { type } = representation; + const segments = csSegmentation.segments; + + const highlightFn = + type === LABELMAP ? this._highlightLabelmap.bind(this) : this._highlightContour.bind(this); + + const adjustedAlpha = type === LABELMAP ? alpha : 1 - alpha; + + highlightFn( + segmentIndex, + adjustedAlpha, + hideOthers, + segments, + viewportId, + animationLength, + representation + ); + }); + } + + private getAndValidateViewport(viewportId: string) { + const csViewport = + this.servicesManager.services.cornerstoneViewportService.getCornerstoneViewport(viewportId); + if (!csViewport) { + throw new Error(`Viewport with id ${viewportId} not found.`); + } + return csViewport; + } + + /** + * Sets the visibility of a segmentation representation. + * + * @param viewportId - The ID of the viewport. + * @param segmentationId - The ID of the segmentation. + * @param isVisible - The new visibility state. + */ + private _setSegmentationRepresentationVisibility( + viewportId: string, + segmentationId: string, + type: SegmentationRepresentations, + isVisible: boolean + ): void { + const representations = this.getSegmentationRepresentations(viewportId, { + segmentationId, + type, + }); + const representation = representations[0]; + + if (!representation) { + console.debug( + 'No segmentation representation found for the given viewportId and segmentationId' + ); + return; + } + + cstSegmentation.config.visibility.setSegmentationRepresentationVisibility( + viewportId, + { + segmentationId, + type, + }, + isVisible + ); + } + + private determineViewportAndSegmentationType(csViewport, segmentation) { + const isVolumeViewport = + csViewport.type === ViewportType.ORTHOGRAPHIC || csViewport.type === ViewportType.VOLUME_3D; + const isVolumeSegmentation = 'volumeId' in segmentation.representationData[LABELMAP]; + return { isVolumeViewport, isVolumeSegmentation }; + } + + private async handleViewportConversion( + isVolumeViewport: boolean, + isVolumeSegmentation: boolean, + csViewport: csTypes.IViewport, + segmentation: cstTypes.Segmentation, + viewportId: string, + segmentationId: string, + representationType: csToolsEnums.SegmentationRepresentations + ) { + let representationTypeToUse = representationType; + let isConverted = false; + + const handler = isVolumeViewport ? this.handleVolumeViewportCase : this.handleStackViewportCase; + + ({ representationTypeToUse, isConverted } = await handler.apply(this, [ + csViewport, + segmentation, + isVolumeSegmentation, + viewportId, + segmentationId, + ])); + + return { representationTypeToUse, isConverted }; + } + + private async handleVolumeViewportCase(csViewport, segmentation, isVolumeSegmentation) { + if (csViewport.type === ViewportType.VOLUME_3D) { + return { representationTypeToUse: SegmentationRepresentations.Surface, isConverted: false }; + } else { + await this.handleVolumeViewport( + csViewport as csTypes.IVolumeViewport, + segmentation, + isVolumeSegmentation + ); + return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted: false }; + } + } + + private async handleStackViewportCase( + csViewport: csTypes.IViewport, + segmentation: cstTypes.Segmentation, + isVolumeSegmentation: boolean, + viewportId: string, + segmentationId: string + ): Promise<{ representationTypeToUse: SegmentationRepresentations; isConverted: boolean }> { + if (isVolumeSegmentation) { + const isConverted = await this.convertStackToVolumeViewport(csViewport); + return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted }; + } + + if (updateLabelmapSegmentationImageReferences(viewportId, segmentationId)) { + return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted: false }; + } + + const isConverted = await this.attemptStackToVolumeConversion( + csViewport as csTypes.IStackViewport, + segmentation, + viewportId, + segmentationId + ); + + return { representationTypeToUse: SegmentationRepresentations.Labelmap, isConverted }; + } + + private async _addSegmentationRepresentation( + viewportId: string, + segmentationId: string, + representationType: csToolsEnums.SegmentationRepresentations, + colorLUTIndex: number, + isConverted: boolean + ): Promise { + const representation = { + type: representationType, + segmentationId, + config: { colorLUTOrIndex: colorLUTIndex }, + }; + + const addRepresentation = () => + cstSegmentation.addSegmentationRepresentations(viewportId, [representation]); + + if (isConverted) { + const { viewportGridService } = this.servicesManager.services; + await new Promise(resolve => { + const { unsubscribe } = viewportGridService.subscribe( + viewportGridService.EVENTS.GRID_STATE_CHANGED, + () => { + addRepresentation(); + unsubscribe(); + resolve(); + } + ); + }); + } else { + addRepresentation(); + } + } + private async handleVolumeViewport( + viewport: csTypes.IVolumeViewport, + segmentation: SegmentationData, + isVolumeSegmentation: boolean + ): Promise { + if (isVolumeSegmentation) { + return; // Volume Labelmap on Volume Viewport is natively supported + } + + const frameOfReferenceUID = viewport.getFrameOfReferenceUID(); + const imageIds = getLabelmapImageIds(segmentation.segmentationId); + const segImage = cache.getImage(imageIds[0]); + + if (segImage?.FrameOfReferenceUID === frameOfReferenceUID) { + await convertStackToVolumeLabelmap(segmentation); + } + } + + private async convertStackToVolumeViewport(viewport: csTypes.IViewport): Promise { + const { viewportGridService, cornerstoneViewportService } = this.servicesManager.services; + const state = viewportGridService.getState(); + const gridViewport = state.viewports.get(viewport.id); + + const prevViewPresentation = viewport.getViewPresentation(); + const prevViewReference = viewport.getViewReference(); + const stackViewport = cornerstoneViewportService.getCornerstoneViewport(viewport.id); + const { element } = stackViewport; + + const volumeViewportNewVolumeHandler = () => { + const volumeViewport = cornerstoneViewportService.getCornerstoneViewport(viewport.id); + volumeViewport.setViewPresentation(prevViewPresentation); + volumeViewport.setViewReference(prevViewReference); + volumeViewport.render(); + + element.removeEventListener( + csEnums.Events.VOLUME_VIEWPORT_NEW_VOLUME, + volumeViewportNewVolumeHandler + ); + }; + + element.addEventListener( + csEnums.Events.VOLUME_VIEWPORT_NEW_VOLUME, + volumeViewportNewVolumeHandler + ); + + viewportGridService.setDisplaySetsForViewport({ + viewportId: viewport.id, + displaySetInstanceUIDs: gridViewport.displaySetInstanceUIDs, + viewportOptions: { + ...gridViewport.viewportOptions, + viewportType: ViewportType.ORTHOGRAPHIC, + }, + }); + + return true; + } + + private async attemptStackToVolumeConversion( + viewport: csTypes.IStackViewport, + segmentation: SegmentationData, + viewportId: string, + segmentationId: string + ): Promise { + const imageIds = getLabelmapImageIds(segmentation.segmentationId); + const frameOfReferenceUID = viewport.getFrameOfReferenceUID(); + const segImage = cache.getImage(imageIds[0]); + + if ( + segImage?.FrameOfReferenceUID && + frameOfReferenceUID && + segImage.FrameOfReferenceUID === frameOfReferenceUID + ) { + const isConverted = await this.convertStackToVolumeViewport(viewport); + triggerSegmentationRepresentationModified( + viewportId, + segmentationId, + SegmentationRepresentations.Labelmap + ); + + return isConverted; + } + } + + private addSegmentationToSource(segmentationPublicInput: cstTypes.SegmentationPublicInput) { + cstSegmentation.addSegmentations([segmentationPublicInput]); + } + + private updateSegmentationInSource( + segmentationId: string, + payload: Partial + ) { + cstSegmentation.updateSegmentations([{ segmentationId, payload }]); + } + + private _toOHIFSegmentationRepresentation( + viewportId: string, + csRepresentation: cstTypes.SegmentationRepresentation + ): SegmentationRepresentation { + const { segmentationId, type, active, visible } = csRepresentation; + const { colorLUTIndex } = csRepresentation; + + const segmentsRepresentations: { [segmentIndex: number]: SegmentRepresentation } = {}; + + const segmentation = cstSegmentation.state.getSegmentation(segmentationId); + + if (!segmentation) { + throw new Error(`Segmentation with ID ${segmentationId} not found.`); + } + + const segmentIds = Object.keys(segmentation.segments); + + for (const segmentId of segmentIds) { + const segmentIndex = parseInt(segmentId, 10); + + const color = cstSegmentation.config.color.getSegmentIndexColor( + viewportId, + segmentationId, + segmentIndex + ); + + const isVisible = cstSegmentation.config.visibility.getSegmentIndexVisibility( + viewportId, + { + segmentationId, + type, + }, + segmentIndex + ); + + segmentsRepresentations[segmentIndex] = { + color, + segmentIndex, + opacity: color[3], + visible: isVisible, + }; + } + + const styles = cstSegmentation.config.style.getStyle({ + viewportId, + segmentationId, + type, + }); + + const id = `${segmentationId}-${type}-${viewportId}`; + + return { + id: id, + segmentationId, + label: segmentation.label, + active, + type, + visible, + segments: segmentsRepresentations, + styles, + viewportId, + colorLUTIndex, + config: {}, + }; + } + + private _initSegmentationService() { + eventTarget.addEventListener( + csToolsEnums.Events.SEGMENTATION_MODIFIED, + this._onSegmentationModifiedFromSource + ); + + eventTarget.addEventListener( + csToolsEnums.Events.SEGMENTATION_REMOVED, + this._onSegmentationModifiedFromSource + ); + + eventTarget.addEventListener( + csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED, + this._onSegmentationDataModifiedFromSource + ); + + eventTarget.addEventListener( + csToolsEnums.Events.SEGMENTATION_REPRESENTATION_MODIFIED, + this._onSegmentationRepresentationModifiedFromSource + ); + + eventTarget.addEventListener( + csToolsEnums.Events.SEGMENTATION_REPRESENTATION_ADDED, + this._onSegmentationRepresentationModifiedFromSource + ); + + eventTarget.addEventListener( + csToolsEnums.Events.SEGMENTATION_REPRESENTATION_REMOVED, + this._onSegmentationRepresentationModifiedFromSource + ); + + eventTarget.addEventListener( + csToolsEnums.Events.SEGMENTATION_ADDED, + this._onSegmentationAddedFromSource + ); + } + + private getCornerstoneSegmentation(segmentationId: string) { + return cstSegmentation.state.getSegmentation(segmentationId); + } + + private _highlightLabelmap( + segmentIndex: number, + alpha: number, + hideOthers: boolean, + segments: Segment[], + viewportId: string, + animationLength: number, + representation: cstTypes.SegmentationRepresentation + ) { + const { segmentationId } = representation; + const newSegmentSpecificConfig = { + fillAlpha: alpha, + }; + + if (hideOthers) { + throw new Error('hideOthers is not working right now'); + for (let i = 0; i < segments.length; i++) { + if (i !== segmentIndex) { + newSegmentSpecificConfig[i] = { + fillAlpha: 0, + }; + } + } + } + + const { fillAlpha } = this.getStyle({ + viewportId, + segmentationId, + type: LABELMAP, + segmentIndex, + }) as cstTypes.LabelmapStyle; + + let startTime: number = null; + const animation = (timestamp: number) => { + if (startTime === null) { + startTime = timestamp; + } + + const elapsed = timestamp - startTime; + const progress = Math.min(elapsed / animationLength, 1); + + cstSegmentation.config.style.setStyle( + { + segmentationId, + segmentIndex, + type: LABELMAP, + }, + { + fillAlpha: easeInOutBell(progress, fillAlpha), + } + ); + + if (progress < 1) { + requestAnimationFrame(animation); + } else { + cstSegmentation.config.style.setStyle( + { + segmentationId, + segmentIndex, + type: LABELMAP, + }, + {} + ); + } + }; + + requestAnimationFrame(animation); + } + + private _highlightContour( + segmentIndex: number, + alpha: number, + hideOthers: boolean, + segments: Segment[], + viewportId: string, + animationLength: number, + representation: cstTypes.SegmentationRepresentation + ) { + const { segmentationId } = representation; + const startTime = performance.now(); + + const prevStyle = cstSegmentation.config.style.getStyle({ + type: CONTOUR, + }) as ContourStyle; + + const prevOutlineWidth = prevStyle.outlineWidth; + // make this configurable + const baseline = Math.max(prevOutlineWidth * 3.5, 5); + + const animate = (currentTime: number) => { + const progress = (currentTime - startTime) / animationLength; + if (progress >= 1) { + cstSegmentation.config.style.resetToGlobalStyle(); + return; + } + + const reversedProgress = easeInOutBellRelative(progress, baseline, prevOutlineWidth); + + cstSegmentation.config.style.setStyle( + { + segmentationId, + segmentIndex, + type: CONTOUR, + }, + { + outlineWidth: reversedProgress, + } + ); + + requestAnimationFrame(animate); + }; + + requestAnimationFrame(animate); + } + + private _toggleSegmentationRepresentationVisibility = ( + viewportId: string, + segmentationId: string, + type: SegmentationRepresentations + ): void => { + const representations = this.getSegmentationRepresentations(viewportId, { + segmentationId, + type, + }); + const representation = representations[0]; + + const segmentsHidden = cstSegmentation.config.visibility.getHiddenSegmentIndices(viewportId, { + segmentationId, + type: representation.type, + }); + + const currentVisibility = segmentsHidden.size === 0; + this._setSegmentationRepresentationVisibility( + viewportId, + segmentationId, + representation.type, + !currentVisibility + ); + }; + + private _setActiveSegment(segmentationId: string, segmentIndex: number) { + cstSegmentation.segmentIndex.setActiveSegmentIndex(segmentationId, segmentIndex); + } + + private _getVolumeIdForDisplaySet(displaySet) { + const volumeLoaderSchema = displaySet.volumeLoaderSchema ?? VOLUME_LOADER_SCHEME; + + return `${volumeLoaderSchema}:${displaySet.displaySetInstanceUID}`; + } + + private _getSegmentCenter(segmentationId, segmentIndex) { + const segmentation = this.getSegmentation(segmentationId); + + if (!segmentation) { + return; + } + + const { segments } = segmentation; + + const { cachedStats } = segments[segmentIndex]; + + if (!cachedStats) { + return; + } + + const { center } = cachedStats; + + return center; + } + + private _setSegmentLockedStatus(segmentationId: string, segmentIndex: number, isLocked: boolean) { + cstSegmentation.segmentLocking.setSegmentIndexLocked(segmentationId, segmentIndex, isLocked); + } + + private _setSegmentVisibility( + viewportId: string, + segmentationId: string, + segmentIndex: number, + isVisible: boolean, + type?: SegmentationRepresentations + ) { + cstSegmentation.config.visibility.setSegmentIndexVisibility( + viewportId, + { + segmentationId, + type, + }, + segmentIndex, + isVisible + ); + } + + private _setSegmentLabel(segmentationId: string, segmentIndex: number, segmentLabel: string) { + const segmentation = this.getCornerstoneSegmentation(segmentationId); + const { segments } = segmentation; + + segments[segmentIndex].label = segmentLabel; + + cstSegmentation.updateSegmentations([ + { + segmentationId, + payload: { + segments, + }, + }, + ]); + } + + private _onSegmentationDataModifiedFromSource = evt => { + const { segmentationId } = evt.detail; + this._broadcastEvent(this.EVENTS.SEGMENTATION_DATA_MODIFIED, { + segmentationId, + }); + }; + + private _onSegmentationRepresentationModifiedFromSource = evt => { + const { segmentationId, viewportId } = evt.detail; + this._broadcastEvent(this.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, { + segmentationId, + viewportId, + }); + }; + + private _onSegmentationModifiedFromSource = ( + evt: cstTypes.EventTypes.SegmentationModifiedEventType + ) => { + const { segmentationId } = evt.detail; + + this._broadcastEvent(this.EVENTS.SEGMENTATION_MODIFIED, { + segmentationId, + }); + }; + + private _onSegmentationAddedFromSource = ( + evt: cstTypes.EventTypes.SegmentationAddedEventType + ) => { + const { segmentationId } = evt.detail; + + this._broadcastEvent(this.EVENTS.SEGMENTATION_ADDED, { + segmentationId, + }); + }; +} + +export default SegmentationService; +export { EVENTS, VALUE_TYPES }; diff --git a/extensions/cornerstone/src/services/SegmentationService/index.ts b/extensions/cornerstone/src/services/SegmentationService/index.ts new file mode 100644 index 0000000..848c25e --- /dev/null +++ b/extensions/cornerstone/src/services/SegmentationService/index.ts @@ -0,0 +1,3 @@ +import SegmentationService from './SegmentationService'; + +export default SegmentationService; diff --git a/extensions/cornerstone/src/services/SyncGroupService/SyncGroupService.ts b/extensions/cornerstone/src/services/SyncGroupService/SyncGroupService.ts new file mode 100644 index 0000000..5a32367 --- /dev/null +++ b/extensions/cornerstone/src/services/SyncGroupService/SyncGroupService.ts @@ -0,0 +1,264 @@ +import { synchronizers, SynchronizerManager, Synchronizer } from '@cornerstonejs/tools'; +import { getRenderingEngines, utilities } from '@cornerstonejs/core'; + +import { pubSubServiceInterface, Types } from '@ohif/core'; +import createHydrateSegmentationSynchronizer from './createHydrateSegmentationSynchronizer'; + +const EVENTS = { + TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated', +}; + +/** + * @params options - are an optional set of options associated with the first + * sync group declared. + */ +export type SyncCreator = (id: string, options?: Record) => Synchronizer; + +export type SyncGroup = { + type: string; + id?: string; + // Source and target default to true if not specified + source?: boolean; + target?: boolean; + options?: Record; +}; + +const POSITION = 'cameraposition'; +const VOI = 'voi'; +const ZOOMPAN = 'zoompan'; +const STACKIMAGE = 'stackimage'; +const IMAGE_SLICE = 'imageslice'; +const HYDRATE_SEG = 'hydrateseg'; + +const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup => + typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup; + +export default class SyncGroupService { + static REGISTRATION = { + name: 'syncGroupService', + altName: 'SyncGroupService', + create: ({ servicesManager }: Types.Extensions.ExtensionParams): SyncGroupService => { + return new SyncGroupService(servicesManager); + }, + }; + + servicesManager: AppTypes.ServicesManager; + listeners: { [key: string]: (...args: any[]) => void } = {}; + EVENTS: { [key: string]: string }; + synchronizerCreators: Record = { + [POSITION]: synchronizers.createCameraPositionSynchronizer, + [VOI]: synchronizers.createVOISynchronizer, + [ZOOMPAN]: synchronizers.createZoomPanSynchronizer, + // todo: remove stack image since it is legacy now and the image_slice + // handles both stack and volume viewports + [STACKIMAGE]: synchronizers.createImageSliceSynchronizer, + [IMAGE_SLICE]: synchronizers.createImageSliceSynchronizer, + [HYDRATE_SEG]: createHydrateSegmentationSynchronizer, + }; + + synchronizersByType: { [key: string]: Synchronizer[] } = {}; + + constructor(servicesManager: AppTypes.ServicesManager) { + this.servicesManager = servicesManager; + this.listeners = {}; + this.EVENTS = EVENTS; + // + Object.assign(this, pubSubServiceInterface); + } + + private _createSynchronizer(type: string, id: string, options): Synchronizer | undefined { + // Initialize if not already done + this.synchronizersByType[type] = this.synchronizersByType[type] || []; + const syncCreator = this.synchronizerCreators[type.toLowerCase()]; + + if (syncCreator) { + // Pass the servicesManager along with other parameters + const synchronizer = syncCreator(id, { ...options, servicesManager: this.servicesManager }); + + if (synchronizer) { + this.synchronizersByType[type].push(synchronizer); + return synchronizer; + } + } else { + console.debug(`Unknown synchronizer type: ${type}, id: ${id}`); + } + } + + public getSyncCreatorForType(type: string): SyncCreator { + return this.synchronizerCreators[type.toLowerCase()]; + } + + /** + * Creates a synchronizer type. + * @param type is the type of the synchronizer to create + * @param creator + */ + public addSynchronizerType(type: string, creator: SyncCreator): void { + this.synchronizerCreators[type.toLowerCase()] = creator; + } + + public getSynchronizer(id: string): Synchronizer | void { + return SynchronizerManager.getSynchronizer(id); + } + + /** + * Registers a custom synchronizer. + * @param id - The id of the synchronizer. + * @param createFunction - The function that creates the synchronizer. + */ + public registerCustomSynchronizer(id: string, createFunction: SyncCreator): void { + this.synchronizerCreators[id] = createFunction; + } + + /** + * Retrieves an array of synchronizers of a specific type. + * @param type - The type of synchronizers to retrieve. + * @returns An array of synchronizers of the specified type. + */ + public getSynchronizersOfType(type: string): Synchronizer[] { + return this.synchronizersByType[type]; + } + + protected _getOrCreateSynchronizer( + type: string, + id: string, + options: Record + ): Synchronizer | undefined { + let synchronizer = SynchronizerManager.getSynchronizer(id); + + if (!synchronizer) { + synchronizer = this._createSynchronizer(type, id, options); + } + return synchronizer; + } + + public addViewportToSyncGroup( + viewportId: string, + renderingEngineId: string, + syncGroups?: SyncGroup | string | SyncGroup[] | string[] + ): void { + if (!syncGroups) { + return; + } + + const syncGroupsArray = Array.isArray(syncGroups) ? syncGroups : [syncGroups]; + + syncGroupsArray.forEach(syncGroup => { + const syncGroupObj = asSyncGroup(syncGroup); + const { type, target = true, source = true, options = {}, id = type } = syncGroupObj; + + const synchronizer = this._getOrCreateSynchronizer(type, id, options); + + if (!synchronizer) { + return; + } + + synchronizer.setOptions(viewportId, options); + + const viewportInfo = { viewportId, renderingEngineId }; + if (target && source) { + synchronizer.add(viewportInfo); + return; + } else if (source) { + synchronizer.addSource(viewportInfo); + } else if (target) { + synchronizer.addTarget(viewportInfo); + } + }); + } + + public destroy(): void { + SynchronizerManager.destroy(); + } + + public getSynchronizersForViewport(viewportId: string): Synchronizer[] { + const renderingEngine = + getRenderingEngines().find(re => { + return re.getViewports().find(vp => vp.id === viewportId); + }) || getRenderingEngines()[0]; + + const synchronizers = SynchronizerManager.getAllSynchronizers(); + return synchronizers.filter( + s => + s.hasSourceViewport(renderingEngine.id, viewportId) || + s.hasTargetViewport(renderingEngine.id, viewportId) + ); + } + + public removeViewportFromSyncGroup( + viewportId: string, + renderingEngineId: string, + syncGroupId?: string + ): void { + const synchronizers = SynchronizerManager.getAllSynchronizers(); + + const filteredSynchronizers = syncGroupId + ? synchronizers.filter(s => s.id === syncGroupId) + : synchronizers; + + filteredSynchronizers.forEach(synchronizer => { + if (!synchronizer) { + return; + } + + // Only image slice synchronizer register spatial registration + if (this.isImageSliceSyncronizer(synchronizer)) { + this.unRegisterSpatialRegistration(synchronizer); + } + + synchronizer.remove({ + viewportId, + renderingEngineId, + }); + + // check if any viewport is left in any of the sync groups, if not, delete that sync group + const sourceViewports = synchronizer.getSourceViewports(); + const targetViewports = synchronizer.getTargetViewports(); + + if (!sourceViewports.length && !targetViewports.length) { + SynchronizerManager.destroySynchronizer(synchronizer.id); + } + }); + } + /** + * Clean up the spatial registration metadata created by synchronizer + * This is needed to be able to re-sync images slices if needed + * @param synchronizer + */ + unRegisterSpatialRegistration(synchronizer: Synchronizer) { + const sourceViewports = synchronizer.getSourceViewports().map(vp => vp.viewportId); + const targetViewports = synchronizer.getTargetViewports().map(vp => vp.viewportId); + + // Create an array of pair of viewports to remove from spatialRegistrationMetadataProvider + // All sourceViewports combined with all targetViewports + const toUnregister = sourceViewports + .map((sourceViewportId: string) => { + return targetViewports.map(targetViewportId => [targetViewportId, sourceViewportId]); + }) + .reduce((acc, c) => acc.concat(c), []); + + toUnregister.forEach(viewportIdPair => { + utilities.spatialRegistrationMetadataProvider.add(viewportIdPair, undefined); + }); + } + /** + * Check if the synchronizer type is IMAGE_SLICE + * Need to convert to lowercase here because the types are lowercase + * e.g: synchronizerCreators + * @param synchronizer + */ + isImageSliceSyncronizer(synchronizer: Synchronizer) { + return this.getSynchronizerType(synchronizer).toLowerCase() === IMAGE_SLICE; + } + /** + * Returns the syncronizer type + * @param synchronizer + */ + getSynchronizerType(synchronizer: Synchronizer): string { + const synchronizerTypes = Object.keys(this.synchronizersByType); + const syncType = synchronizerTypes.find(syncType => + this.getSynchronizersOfType(syncType).includes(synchronizer) + ); + return syncType; + } +} diff --git a/extensions/cornerstone/src/services/SyncGroupService/createHydrateSegmentationSynchronizer.ts b/extensions/cornerstone/src/services/SyncGroupService/createHydrateSegmentationSynchronizer.ts new file mode 100644 index 0000000..561b95d --- /dev/null +++ b/extensions/cornerstone/src/services/SyncGroupService/createHydrateSegmentationSynchronizer.ts @@ -0,0 +1,79 @@ +import { Types, getEnabledElementByViewportId } from '@cornerstonejs/core'; +import { + SynchronizerManager, + Synchronizer, + Enums, + Types as ToolsTypes, +} from '@cornerstonejs/tools'; + +const { createSynchronizer } = SynchronizerManager; +const { SEGMENTATION_REPRESENTATION_ADDED } = Enums.Events; + +export default function createHydrateSegmentationSynchronizer( + synchronizerName: string, + { servicesManager, ...options }: { servicesManager: AppTypes.ServicesManager; options } +): Synchronizer { + const stackImageSynchronizer = createSynchronizer( + synchronizerName, + SEGMENTATION_REPRESENTATION_ADDED, + (synchronizerInstance, sourceViewport, targetViewport, sourceEvent) => + segmentationRepresentationModifiedCallback( + synchronizerInstance, + sourceViewport, + targetViewport, + sourceEvent, + { servicesManager, options } + ), + { + eventSource: 'eventTarget', + } + ); + + return stackImageSynchronizer; +} + +const segmentationRepresentationModifiedCallback = async ( + synchronizerInstance: Synchronizer, + sourceViewport: Types.IViewportId, + targetViewport: Types.IViewportId, + sourceEvent: Event, + { servicesManager, options }: { servicesManager: AppTypes.ServicesManager; options: unknown } +) => { + const event = sourceEvent as ToolsTypes.EventTypes.SegmentationRepresentationModifiedEventType; + + const { segmentationId, viewportId } = event.detail; + const { segmentationService, hangingProtocolService } = servicesManager.services; + + const targetViewportId = targetViewport.viewportId; + + const { viewport } = getEnabledElementByViewportId(targetViewportId); + + const targetFrameOfReferenceUID = viewport.getFrameOfReferenceUID(); + + if (!targetFrameOfReferenceUID) { + console.debug('No frame of reference UID found for the target viewport'); + return; + } + + const targetViewportRepresentation = segmentationService.getSegmentationRepresentations( + targetViewportId, + { segmentationId } + ); + + if (targetViewportRepresentation.length > 0) { + return; + } + + // whatever type the source viewport has, we need to add that to the target viewport + const sourceViewportRepresentation = segmentationService.getSegmentationRepresentations( + sourceViewport.viewportId, + { segmentationId } + ); + + const type = sourceViewportRepresentation[0].type; + + await segmentationService.addSegmentationRepresentation(targetViewportId, { + segmentationId, + type, + }); +}; diff --git a/extensions/cornerstone/src/services/SyncGroupService/index.js b/extensions/cornerstone/src/services/SyncGroupService/index.js new file mode 100644 index 0000000..1e0a218 --- /dev/null +++ b/extensions/cornerstone/src/services/SyncGroupService/index.js @@ -0,0 +1,3 @@ +import SyncGroupService from './SyncGroupService'; + +export default SyncGroupService; diff --git a/extensions/cornerstone/src/services/ToolGroupService/ToolGroupService.ts b/extensions/cornerstone/src/services/ToolGroupService/ToolGroupService.ts new file mode 100644 index 0000000..298f286 --- /dev/null +++ b/extensions/cornerstone/src/services/ToolGroupService/ToolGroupService.ts @@ -0,0 +1,321 @@ +import { ToolGroupManager, Enums, Types } from '@cornerstonejs/tools'; +import { eventTarget } from '@cornerstonejs/core'; + +import { Types as OhifTypes, pubSubServiceInterface } from '@ohif/core'; +import getActiveViewportEnabledElement from '../../utils/getActiveViewportEnabledElement'; + +const EVENTS = { + VIEWPORT_ADDED: 'event::cornerstone::toolgroupservice:viewportadded', + TOOLGROUP_CREATED: 'event::cornerstone::toolgroupservice:toolgroupcreated', + TOOL_ACTIVATED: 'event::cornerstone::toolgroupservice:toolactivated', + PRIMARY_TOOL_ACTIVATED: 'event::cornerstone::toolgroupservice:primarytoolactivated', +}; + +type Tool = { + toolName: string; + bindings?: typeof Enums.MouseBindings | Enums.KeyboardBindings; +}; + +type Tools = { + active: Tool[]; + passive?: Tool[]; + enabled?: Tool[]; + disabled?: Tool[]; +}; + +export default class ToolGroupService { + public static REGISTRATION = { + name: 'toolGroupService', + altName: 'ToolGroupService', + create: ({ servicesManager }: OhifTypes.Extensions.ExtensionParams): ToolGroupService => { + return new ToolGroupService(servicesManager); + }, + }; + + servicesManager: AppTypes.ServicesManager; + cornerstoneViewportService: any; + viewportGridService: any; + uiNotificationService: any; + private toolGroupIds: Set = new Set(); + /** + * Service-specific + */ + listeners: { [key: string]: Function[] }; + EVENTS: { [key: string]: string }; + + constructor(servicesManager: AppTypes.ServicesManager) { + const { cornerstoneViewportService, viewportGridService, uiNotificationService } = + servicesManager.services; + this.cornerstoneViewportService = cornerstoneViewportService; + this.viewportGridService = viewportGridService; + this.uiNotificationService = uiNotificationService; + this.listeners = {}; + this.EVENTS = EVENTS; + Object.assign(this, pubSubServiceInterface); + + this._init(); + } + + onModeExit() { + this.destroy(); + } + + private _init() { + eventTarget.addEventListener(Enums.Events.TOOL_ACTIVATED, this._onToolActivated); + } + + /** + * Retrieves a tool group from the ToolGroupManager by tool group ID. + * If no tool group ID is provided, it retrieves the tool group of the active viewport. + * @param toolGroupId - Optional ID of the tool group to retrieve. + * @returns The tool group or undefined if it is not found. + */ + public getToolGroup(toolGroupId?: string): Types.IToolGroup | void { + let toolGroupIdToUse = toolGroupId; + + if (!toolGroupIdToUse) { + // Use the active viewport's tool group if no tool group id is provided + const enabledElement = getActiveViewportEnabledElement(this.viewportGridService); + + if (!enabledElement) { + return; + } + + const { renderingEngineId, viewportId } = enabledElement; + const toolGroup = ToolGroupManager.getToolGroupForViewport(viewportId, renderingEngineId); + + if (!toolGroup) { + console.warn( + 'No tool group found for viewportId:', + viewportId, + 'and renderingEngineId:', + renderingEngineId + ); + return; + } + + toolGroupIdToUse = toolGroup.id; + } + + const toolGroup = ToolGroupManager.getToolGroup(toolGroupIdToUse); + return toolGroup; + } + + public getToolGroupIds(): string[] { + return Array.from(this.toolGroupIds); + } + + public getToolGroupForViewport(viewportId: string): Types.IToolGroup | void { + const renderingEngine = this.cornerstoneViewportService.getRenderingEngine(); + return ToolGroupManager.getToolGroupForViewport(viewportId, renderingEngine.id); + } + + public getActiveToolForViewport(viewportId: string): string { + const toolGroup = this.getToolGroupForViewport(viewportId); + if (!toolGroup) { + return; + } + + return toolGroup.getActivePrimaryMouseButtonTool(); + } + + public destroy(): void { + ToolGroupManager.destroy(); + this.toolGroupIds = new Set(); + + eventTarget.removeEventListener(Enums.Events.TOOL_ACTIVATED, this._onToolActivated); + } + + public destroyToolGroup(toolGroupId: string): void { + ToolGroupManager.destroyToolGroup(toolGroupId); + this.toolGroupIds.delete(toolGroupId); + } + + public removeViewportFromToolGroup( + viewportId: string, + renderingEngineId: string, + deleteToolGroupIfEmpty?: boolean + ): void { + const toolGroup = ToolGroupManager.getToolGroupForViewport(viewportId, renderingEngineId); + + if (!toolGroup) { + return; + } + + toolGroup.removeViewports(renderingEngineId, viewportId); + + const viewportIds = toolGroup.getViewportIds(); + + if (viewportIds.length === 0 && deleteToolGroupIfEmpty) { + ToolGroupManager.destroyToolGroup(toolGroup.id); + } + } + + public addViewportToToolGroup( + viewportId: string, + renderingEngineId: string, + toolGroupId?: string + ): void { + if (!toolGroupId) { + // If toolGroupId is not provided, add the viewport to all toolGroups + const toolGroups = ToolGroupManager.getAllToolGroups(); + toolGroups.forEach(toolGroup => { + toolGroup.addViewport(viewportId, renderingEngineId); + }); + } else { + let toolGroup = ToolGroupManager.getToolGroup(toolGroupId); + if (!toolGroup) { + toolGroup = this.createToolGroup(toolGroupId); + } + + toolGroup.addViewport(viewportId, renderingEngineId); + } + + this._broadcastEvent(EVENTS.VIEWPORT_ADDED, { + viewportId, + toolGroupId, + }); + } + + public createToolGroup(toolGroupId: string): Types.IToolGroup { + if (this.getToolGroup(toolGroupId)) { + throw new Error(`ToolGroup ${toolGroupId} already exists`); + } + + // if the toolGroup doesn't exist, create it + const toolGroup = ToolGroupManager.createToolGroup(toolGroupId); + this.toolGroupIds.add(toolGroupId); + + this._broadcastEvent(EVENTS.TOOLGROUP_CREATED, { + toolGroupId, + }); + + return toolGroup; + } + + public addToolsToToolGroup(toolGroupId: string, tools: Array, configs: any = {}): void { + const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); + // this.changeConfigurationIfNecessary(toolGroup, volumeId); + this._addTools(toolGroup, tools, configs); + this._setToolsMode(toolGroup, tools); + } + + public createToolGroupAndAddTools(toolGroupId: string, tools: Array): Types.IToolGroup { + const toolGroup = this.createToolGroup(toolGroupId); + this.addToolsToToolGroup(toolGroupId, tools); + return toolGroup; + } + /** + * Get the tool's configuration based on the tool name and tool group id + * @param toolGroupId - The id of the tool group that the tool instance belongs to. + * @param toolName - The name of the tool + * @returns The configuration of the tool. + */ + public getToolConfiguration(toolGroupId: string, toolName: string) { + const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); + if (!toolGroup) { + return null; + } + + const tool = toolGroup.getToolInstance(toolName); + if (!tool) { + return null; + } + + return tool.configuration; + } + + /** + * Set the tool instance configuration. This will update the tool instance configuration + * on the toolGroup + * @param toolGroupId - The id of the tool group that the tool instance belongs to. + * @param toolName - The name of the tool + * @param config - The configuration object that you want to set. + */ + public setToolConfiguration(toolGroupId, toolName, config) { + const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); + const toolInstance = toolGroup.getToolInstance(toolName); + toolInstance.configuration = config; + } + + public getActivePrimaryMouseButtonTool(toolGroupId?: string): string { + return this.getToolGroup(toolGroupId)?.getActivePrimaryMouseButtonTool(); + } + + private _setToolsMode(toolGroup, tools) { + const { active, passive, enabled, disabled } = tools; + + if (active) { + active.forEach(({ toolName, bindings }) => { + toolGroup.setToolActive(toolName, { bindings }); + }); + } + + if (passive) { + passive.forEach(({ toolName }) => { + toolGroup.setToolPassive(toolName); + }); + } + + if (enabled) { + enabled.forEach(({ toolName }) => { + toolGroup.setToolEnabled(toolName); + }); + } + + if (disabled) { + disabled.forEach(({ toolName }) => { + toolGroup.setToolDisabled(toolName); + }); + } + } + + private _addTools(toolGroup, tools) { + const addTools = tools => { + tools.forEach(({ toolName, parentTool, configuration }) => { + if (parentTool) { + toolGroup.addToolInstance(toolName, parentTool, { + ...configuration, + }); + } else { + toolGroup.addTool(toolName, { ...configuration }); + } + }); + }; + + if (tools.active) { + addTools(tools.active); + } + + if (tools.passive) { + addTools(tools.passive); + } + + if (tools.enabled) { + addTools(tools.enabled); + } + + if (tools.disabled) { + addTools(tools.disabled); + } + } + + private _onToolActivated = (evt: Types.EventTypes.ToolActivatedEventType) => { + const { toolGroupId, toolName, toolBindingsOptions } = evt.detail; + const isPrimaryTool = toolBindingsOptions.bindings?.some( + binding => binding.mouseButton === Enums.MouseBindings.Primary + ); + + const callbackProps = { + toolGroupId, + toolName, + toolBindingsOptions, + }; + + this._broadcastEvent(EVENTS.TOOL_ACTIVATED, callbackProps); + + if (isPrimaryTool) { + this._broadcastEvent(EVENTS.PRIMARY_TOOL_ACTIVATED, callbackProps); + } + }; +} diff --git a/extensions/cornerstone/src/services/ToolGroupService/index.js b/extensions/cornerstone/src/services/ToolGroupService/index.js new file mode 100644 index 0000000..c45c146 --- /dev/null +++ b/extensions/cornerstone/src/services/ToolGroupService/index.js @@ -0,0 +1,3 @@ +import ToolGroupService from './ToolGroupService'; + +export default ToolGroupService; diff --git a/extensions/cornerstone/src/services/ViewportActionCornersService/ViewportActionCornersService.ts b/extensions/cornerstone/src/services/ViewportActionCornersService/ViewportActionCornersService.ts new file mode 100644 index 0000000..7c67761 --- /dev/null +++ b/extensions/cornerstone/src/services/ViewportActionCornersService/ViewportActionCornersService.ts @@ -0,0 +1,71 @@ +import { PubSubService } from '@ohif/core'; +import { ViewportActionCornersLocations } from '@ohif/ui'; +import { ReactNode } from 'react'; + +export type ActionComponentInfo = { + viewportId: string; + id: string; + component: ReactNode; + location: ViewportActionCornersLocations; + indexPriority?: number; +}; + +class ViewportActionCornersService extends PubSubService { + public static readonly EVENTS = {}; + public static readonly LOCATIONS = ViewportActionCornersLocations; + + public static REGISTRATION = { + name: 'viewportActionCornersService', + altName: 'ViewportActionCornersService', + create: ({ configuration = {} }) => { + return new ViewportActionCornersService(); + }, + }; + + serviceImplementation = {}; + + public LOCATIONS = ViewportActionCornersService.LOCATIONS; + + constructor() { + super(ViewportActionCornersService.EVENTS); + this.serviceImplementation = {}; + } + + public setServiceImplementation({ + getState: getStateImplementation, + addComponent: addComponentImplementation, + addComponents: addComponentsImplementation, + clear: clearComponentsImplementation, + }): void { + if (getStateImplementation) { + this.serviceImplementation._getState = getStateImplementation; + } + if (addComponentImplementation) { + this.serviceImplementation._addComponent = addComponentImplementation; + } + if (addComponentsImplementation) { + this.serviceImplementation._addComponents = addComponentsImplementation; + } + if (clearComponentsImplementation) { + this.serviceImplementation._clear = clearComponentsImplementation; + } + } + + public getState() { + return this.serviceImplementation._getState(); + } + + public addComponent(component: ActionComponentInfo) { + this.serviceImplementation._addComponent(component); + } + + public addComponents(components: Array) { + this.serviceImplementation._addComponents(components); + } + + public clear(viewportId: string) { + this.serviceImplementation._clear(viewportId); + } +} + +export default ViewportActionCornersService; diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts new file mode 100644 index 0000000..82f7fbb --- /dev/null +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -0,0 +1,1182 @@ +import { PubSubService } from '@ohif/core'; +import { Types as OhifTypes } from '@ohif/core'; +import { + RenderingEngine, + StackViewport, + Types, + getRenderingEngine, + utilities as csUtils, + VolumeViewport, + VolumeViewport3D, + cache, + Enums as csEnums, + BaseVolumeViewport, +} from '@cornerstonejs/core'; + +import { utilities as csToolsUtils, Enums as csToolsEnums } from '@cornerstonejs/tools'; +import { IViewportService } from './IViewportService'; +import { RENDERING_ENGINE_ID } from './constants'; +import ViewportInfo, { DisplaySetOptions, PublicViewportOptions } from './Viewport'; +import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; +import { + LutPresentation, + PositionPresentation, + Presentations, + SegmentationPresentation, + SegmentationPresentationItem, +} from '../../types/Presentation'; + +import JumpPresets from '../../utils/JumpPresets'; +import { ViewportProperties } from '@cornerstonejs/core/types'; +import { useLutPresentationStore } from '../../stores/useLutPresentationStore'; +import { usePositionPresentationStore } from '../../stores/usePositionPresentationStore'; +import { useSynchronizersStore } from '../../stores/useSynchronizersStore'; +import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore'; + +const EVENTS = { + VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged', + VIEWPORT_VOLUMES_CHANGED: 'event::cornerstoneViewportService:viewportVolumesChanged', +}; + +export const WITH_NAVIGATION = { withNavigation: true, withOrientation: true }; + +/** + * Handles cornerstone viewport logic including enabling, disabling, and + * updating the viewport. + */ +class CornerstoneViewportService extends PubSubService implements IViewportService { + static REGISTRATION = { + name: 'cornerstoneViewportService', + altName: 'CornerstoneViewportService', + create: ({ + servicesManager, + }: OhifTypes.Extensions.ExtensionParams): CornerstoneViewportService => { + return new CornerstoneViewportService(servicesManager); + }, + }; + + renderingEngine: Types.IRenderingEngine | null; + viewportsById: Map = new Map(); + viewportGridResizeObserver: ResizeObserver | null; + viewportsDisplaySets: Map = new Map(); + beforeResizePositionPresentations: Map = new Map(); + + // Some configs + enableResizeDetector: true; + resizeRefreshRateMs: 200; + resizeRefreshMode: 'debounce'; + servicesManager: AppTypes.ServicesManager = null; + + resizeQueue = []; + viewportResizeTimer = null; + gridResizeDelay = 50; + gridResizeTimeOut = null; + + constructor(servicesManager: AppTypes.ServicesManager) { + super(EVENTS); + this.renderingEngine = null; + this.viewportGridResizeObserver = null; + this.servicesManager = servicesManager; + } + hangingProtocolService: unknown; + viewportsInfo: unknown; + sceneVolumeInputs: unknown; + viewportDivElements: unknown; + ViewportPropertiesMap: unknown; + volumeUIDs: unknown; + displaySetsNeedRerendering: unknown; + viewportDisplaySets: unknown; + + /** + * Adds the HTML element to the viewportService + * @param {*} viewportId + * @param {*} elementRef + */ + public enableViewport(viewportId: string, elementRef: HTMLDivElement): void { + const viewportInfo = new ViewportInfo(viewportId); + viewportInfo.setElement(elementRef); + this.viewportsById.set(viewportId, viewportInfo); + } + + public getViewportIds(): string[] { + return Array.from(this.viewportsById.keys()); + } + + /** + * It retrieves the renderingEngine if it does exist, or creates one otherwise + * @returns {RenderingEngine} rendering engine + */ + public getRenderingEngine() { + // get renderingEngine from cache if it exists + const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID); + + if (renderingEngine) { + this.renderingEngine = renderingEngine; + return this.renderingEngine; + } + + if (!renderingEngine || renderingEngine.hasBeenDestroyed) { + this.renderingEngine = new RenderingEngine(RENDERING_ENGINE_ID); + } + + return this.renderingEngine; + } + + /** + * It triggers the resize on the rendering engine, and renders the viewports + * + * @param isGridResize - if the resize is triggered by a grid resize + * this is used to avoid double resize of the viewports since if the + * grid is resized, all viewports will be resized so there is no need + * to resize them individually which will get triggered by their + * individual resize observers + */ + public resize(isGridResize = false) { + // https://stackoverflow.com/a/26279685 + // This resize() call, among other things, rerenders the viewports. But when the entire viewer is + // display: none'd, it makes the size of all hidden elements 0, including the viewport canvas and its containers. + // Even if the viewer is later displayed again, trying to render when the size is 0 permanently "breaks" the + // viewport, making it fully black even after the size is normal again. So just ignore resize events when hidden: + const areViewportsHidden = Array.from(this.viewportsById.values()).every(viewportInfo => { + const element = viewportInfo.getElement(); + + return element.clientWidth === 0 && element.clientHeight === 0; + }); + if (areViewportsHidden) { + console.warn('Ignoring resize when viewports have size 0'); + return; + } + + // if there is a grid resize happening, it means the viewport grid + // has been manipulated (e.g., panels closed, added, etc.) and we need + // to resize all viewports, so we will add a timeout here to make sure + // we don't double resize the viewports when viewports in the grid are + // resized individually + if (isGridResize) { + this.performResize(); + this.resetGridResizeTimeout(); + this.resizeQueue = []; + clearTimeout(this.viewportResizeTimer); + } else { + this.enqueueViewportResizeRequest(); + } + } + + /** + * Removes the viewport from cornerstone, and destroys the rendering engine + */ + public destroy() { + this._removeResizeObserver(); + this.viewportGridResizeObserver = null; + try { + this.renderingEngine?.destroy?.(); + } catch (e) { + console.warn('Rendering engine not destroyed', e); + } + this.viewportsDisplaySets.clear(); + this.renderingEngine = null; + cache.purgeCache(); + } + + /** + * Disables the viewport inside the renderingEngine, if no viewport is left + * it destroys the renderingEngine. + * + * This is called when the element goes away entirely - with new viewportId's + * created for every new viewport, this will be called whenever the set of + * viewports is changed, but NOT when the viewport position changes only. + * + * @param viewportId - The viewportId to disable + */ + public disableElement(viewportId: string): void { + this.renderingEngine?.disableElement(viewportId); + + // clean up + this.viewportsById.delete(viewportId); + this.viewportsDisplaySets.delete(viewportId); + } + + /** + * Sets the presentations for a given viewport. Presentations is an object + * that can define the lut or position for a viewport. + * + * @param viewportId - The ID of the viewport. + * @param presentations - The presentations to apply to the viewport. + * @param viewportInfo - Contains a view reference for immediate application + */ + public setPresentations(viewportId: string, presentations: Presentations): void { + const viewport = this.getCornerstoneViewport(viewportId) as + | Types.IStackViewport + | Types.IVolumeViewport; + + if (!viewport || !presentations) { + return; + } + + const { lutPresentation, positionPresentation, segmentationPresentation } = presentations; + + // Always set the segmentation presentation first, since there might be some + // lutpresentation states that need to be set on the segmentation + // Todo: i think we should even await this + this._setSegmentationPresentation(viewport, segmentationPresentation); + + this._setLutPresentation(viewport, lutPresentation); + this._setPositionPresentation(viewport, { ...positionPresentation, viewportId }); + } + + /** + * Stores the presentation state for a given viewport inside the + * each store. This is used to persist the presentation state + * across different scenarios e.g., when the viewport is changing the + * display set, or when the viewport is moving to a different layout. + * + * @param viewportId The ID of the viewport. + */ + public storePresentation({ viewportId }) { + const presentationIds = this.getPresentationIds(viewportId); + const { syncGroupService } = this.servicesManager.services; + const synchronizers = syncGroupService.getSynchronizersForViewport(viewportId); + + if (!presentationIds || Object.keys(presentationIds).length === 0) { + return null; + } + + const { lutPresentationId, positionPresentationId, segmentationPresentationId } = + presentationIds; + + const positionPresentation = this._getPositionPresentation(viewportId); + const lutPresentation = this._getLutPresentation(viewportId); + const segmentationPresentation = this._getSegmentationPresentation(viewportId); + + const { setLutPresentation } = useLutPresentationStore.getState(); + const { setPositionPresentation } = usePositionPresentationStore.getState(); + const { setSynchronizers } = useSynchronizersStore.getState(); + const { setSegmentationPresentation } = useSegmentationPresentationStore.getState(); + + if (lutPresentationId) { + setLutPresentation(lutPresentationId, lutPresentation); + } + + if (positionPresentationId) { + setPositionPresentation(positionPresentationId, positionPresentation); + } + + if (segmentationPresentationId) { + setSegmentationPresentation(segmentationPresentationId, segmentationPresentation); + } + + if (synchronizers?.length) { + setSynchronizers( + viewportId, + synchronizers.map(synchronizer => ({ + id: synchronizer.id, + sourceViewports: [...synchronizer.getSourceViewports()], + targetViewports: [...synchronizer.getTargetViewports()], + })) + ); + } + } + + /** + * Retrieves the presentations for a given viewport. + * @param viewportId - The ID of the viewport. + * @returns The presentations for the viewport. + */ + public getPresentations(viewportId: string): Presentations { + const positionPresentation = this._getPositionPresentation(viewportId); + const lutPresentation = this._getLutPresentation(viewportId); + const segmentationPresentation = this._getSegmentationPresentation(viewportId); + + return { + positionPresentation, + lutPresentation, + segmentationPresentation, + }; + } + + private getPresentationIds(viewportId: string): AppTypes.PresentationIds | null { + const viewportInfo = this.viewportsById.get(viewportId); + if (!viewportInfo) { + return null; + } + + return viewportInfo.getPresentationIds(); + } + + private _getPositionPresentation(viewportId: string): PositionPresentation { + const csViewport = this.getCornerstoneViewport(viewportId); + if (!csViewport) { + return; + } + + const viewportInfo = this.viewportsById.get(viewportId); + + return { + viewportType: viewportInfo.getViewportType(), + viewReference: csViewport instanceof VolumeViewport3D ? null : csViewport.getViewReference(), + viewPresentation: csViewport.getViewPresentation({ pan: true, zoom: true }), + viewportId, + }; + } + + private _getLutPresentation(viewportId: string): LutPresentation { + const csViewport = this.getCornerstoneViewport(viewportId) as + | Types.IStackViewport + | Types.IVolumeViewport; + + if (!csViewport) { + return; + } + + const cleanProperties = properties => { + if (properties?.isComputedVOI) { + delete properties?.voiRange; + delete properties?.VOILUTFunction; + } + return properties; + }; + + const properties = + csViewport instanceof BaseVolumeViewport + ? new Map() + : cleanProperties(csViewport.getProperties()); + + if (properties instanceof Map) { + const volumeIds = (csViewport as Types.IBaseVolumeViewport).getAllVolumeIds(); + volumeIds?.forEach(volumeId => { + const csProps = cleanProperties(csViewport.getProperties(volumeId)); + properties.set(volumeId, csProps); + }); + } + + const viewportInfo = this.viewportsById.get(viewportId); + + return { + viewportType: viewportInfo.getViewportType(), + properties, + }; + } + + private _getSegmentationPresentation(viewportId: string): SegmentationPresentation { + const { segmentationService } = this.servicesManager.services; + + const presentation = segmentationService.getPresentation(viewportId); + return presentation; + } + + /** + * Sets the viewport data for a viewport. + * @param viewportId - The ID of the viewport to set the data for. + * @param viewportData - The viewport data to set. + * @param publicViewportOptions - The public viewport options. + * @param publicDisplaySetOptions - The public display set options. + * @param presentations - The presentations to set. + */ + public setViewportData( + viewportId: string, + viewportData: StackViewportData | VolumeViewportData, + publicViewportOptions: PublicViewportOptions, + publicDisplaySetOptions: DisplaySetOptions[], + presentations?: Presentations + ): void { + const renderingEngine = this.getRenderingEngine(); + + // if not valid viewportData then return early + if (viewportData.viewportType === csEnums.ViewportType.STACK) { + // check if imageIds is valid + if (!viewportData.data[0].imageIds?.length) { + return; + } + } + + // This is the old viewportInfo, which may have old options but we might be + // using its viewport (same viewportId as the new viewportInfo) + const viewportInfo = this.viewportsById.get(viewportId); + + // We should store the presentation for the current viewport since we can't only + // rely to store it WHEN the viewport is disabled since we might keep around the + // same viewport/element and just change the viewportData for it (drag and drop etc.) + // the disableElement storePresentation handle would not be called in this case + // and we would lose the presentation. + this.storePresentation({ viewportId: viewportInfo.getViewportId() }); + + // Todo: i don't like this here, move it + this.servicesManager.services.segmentationService.clearSegmentationRepresentations( + viewportInfo.getViewportId() + ); + + if (!viewportInfo) { + throw new Error('element is not enabled for the given viewportId'); + } + + // override the viewportOptions and displaySetOptions with the public ones + // since those are the newly set ones, we set them here so that it handles defaults + const displaySetOptions = viewportInfo.setPublicDisplaySetOptions(publicDisplaySetOptions); + // Specify an over-ride for the viewport type, even though it is in the public + // viewport options, because the one in the viewportData is a requirement based on the + // type of data being displayed. + const viewportOptions = viewportInfo.setPublicViewportOptions( + publicViewportOptions, + viewportData.viewportType + ); + + const element = viewportInfo.getElement(); + const type = viewportInfo.getViewportType(); + const background = viewportInfo.getBackground(); + const orientation = viewportInfo.getOrientation(); + const displayArea = viewportInfo.getDisplayArea(); + + const viewportInput: Types.PublicViewportInput = { + viewportId, + element, + type, + defaultOptions: { + background, + orientation, + displayArea, + }, + }; + + // Rendering Engine Id set should happen before enabling the element + // since there are callbacks that depend on the renderingEngine id + // Todo: however, this is a limitation which means that we can't change + // the rendering engine id for a given viewport which might be a super edge + // case + viewportInfo.setRenderingEngineId(renderingEngine.id); + + // Todo: this is not optimal at all, we are re-enabling the already enabled + // element which is not what we want. But enabledElement as part of the + // renderingEngine is designed to be used like this. This will trigger + // ENABLED_ELEMENT again and again, which will run onEnableElement callbacks + renderingEngine.enableElement(viewportInput); + + viewportInfo.setViewportOptions(viewportOptions); + viewportInfo.setDisplaySetOptions(displaySetOptions); + viewportInfo.setViewportData(viewportData); + viewportInfo.setViewportId(viewportId); + + this.viewportsById.set(viewportId, viewportInfo); + + const viewport = renderingEngine.getViewport(viewportId); + const displaySetPromise = this._setDisplaySets( + viewport, + viewportData, + viewportInfo, + presentations + ); + + // The broadcast event here ensures that listeners have a valid, up to date + // viewport to access. Doing it too early can result in exceptions or + // invalid data. + displaySetPromise.then(() => { + this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, { + viewportData, + viewportId, + }); + }); + } + + /** + * Retrieves the Cornerstone viewport with the specified ID. + * + * @param viewportId - The ID of the viewport. + * @returns The Cornerstone viewport object if found, otherwise null. + */ + public getCornerstoneViewport(viewportId: string): Types.IViewport | null { + const viewportInfo = this.getViewportInfo(viewportId); + + if (!viewportInfo || !this.renderingEngine || this.renderingEngine.hasBeenDestroyed) { + return null; + } + + const viewport = this.renderingEngine.getViewport(viewportId); + + return viewport; + } + + /** + * Retrieves the viewport information for a given viewport ID. The viewport information + * is the OHIF construct that holds different options and data for a given viewport and + * is different from the cornerstone viewport. + * + * @param viewportId The ID of the viewport. + * @returns The viewport information. + */ + public getViewportInfo(viewportId: string): ViewportInfo { + return this.viewportsById.get(viewportId); + } + + /** + * Looks through the viewports to see if the specified measurement can be + * displayed in one of the viewports. This function tries to get a "best fit" + * viewport to display the image in where it matches, in order: + * * Active viewport that can be navigated to the given image without orientation change + * * Other viewport that can be navigated to the given image without orientation change + * * Active viewport that can change orientation to display the image + * * Other viewport that can change orientation to display the image + * + * It returns `null` otherwise, indicating that a viewport needs display set/type + * changes in order to display the image. + * + * Notes: + * * If the display set is displayed in multiple viewports all needing orientation change, + * then the active one or first one listed will be modified. This can create unexpected + * behaviour for MPR views. + * * If the image is contained in multiple display sets, then the first one + * found will be navigated (active first, followed by first found) + * + * @param measurement - The measurement that is desired to view. + * @param activeViewportId - the index that was active at the time the jump + * was initiated. + * @return the viewportId that the measurement should be displayed in. + */ + public getViewportIdToJump(activeViewportId: string, metadata): string { + // First check if the active viewport can just be navigated to show the given item + const activeViewport = this.getCornerstoneViewport(activeViewportId); + if (activeViewport.isReferenceViewable(metadata, { withNavigation: true })) { + return activeViewportId; + } + + // Next, see if any viewport could be navigated to show the given item, + // without considering orientation changes. + for (const id of this.viewportsById.keys()) { + const viewport = this.getCornerstoneViewport(id); + if (viewport?.isReferenceViewable(metadata, { withNavigation: true })) { + return id; + } + } + + // No viewport is in the right display set/orientation to show this, so see if + // the active viewport could change orientations to show this + if ( + activeViewport.isReferenceViewable(metadata, { withNavigation: true, withOrientation: true }) + ) { + return activeViewportId; + } + + // See if any viewport could show this with an orientation change + for (const id of this.viewportsById.keys()) { + const viewport = this.getCornerstoneViewport(id); + if ( + viewport?.isReferenceViewable(metadata, { withNavigation: true, withOrientation: true }) + ) { + return id; + } + } + + // No luck, need to update the viewport itself + return null; + } + + /** + * Sets the image data for the given viewport. + */ + private async _setOtherViewport( + viewport: Types.IStackViewport, + viewportData: StackViewportData, + viewportInfo: ViewportInfo, + _presentations: Presentations = {} + ): Promise { + const [displaySet] = viewportData.data; + return viewport.setDataIds(displaySet.imageIds, { + groupId: displaySet.displaySetInstanceUID, + viewReference: viewportInfo.getViewReference(), + }); + } + + private async _setStackViewport( + viewport: Types.IStackViewport, + viewportData: StackViewportData, + viewportInfo: ViewportInfo, + presentations: Presentations = {} + ): Promise { + const displaySetOptions = viewportInfo.getDisplaySetOptions(); + + const displaySetInstanceUIDs = viewportData.data.map(data => data.displaySetInstanceUID); + + // based on the cache service construct always the first one is the non-overlay + // and the rest are overlays + + this.viewportsDisplaySets.set(viewport.id, [...displaySetInstanceUIDs]); + + const { initialImageIndex, imageIds } = viewportData.data[0]; + + // Use the slice index from any provided view reference, as the view reference + // is being used to navigate to the initial view position for measurement + // navigation and other navigation forcing specific views. + let initialImageIndexToUse = + presentations?.positionPresentation?.initialImageIndex ?? initialImageIndex; + + const { rotation, flipHorizontal, displayArea } = viewportInfo.getViewportOptions(); + + const properties = { ...presentations.lutPresentation?.properties }; + if (!presentations.lutPresentation?.properties) { + const { voi, voiInverted, colormap } = displaySetOptions[0]; + if (voi && (voi.windowWidth || voi.windowCenter)) { + const { lower, upper } = csUtils.windowLevel.toLowHighRange( + voi.windowWidth, + voi.windowCenter + ); + properties.voiRange = { lower, upper }; + } + + properties.invert = voiInverted ?? properties.invert; + properties.colormap = colormap ?? properties.colormap; + } + + viewport.element.addEventListener(csEnums.Events.VIEWPORT_NEW_IMAGE_SET, evt => { + const { element } = evt.detail; + + if (element !== viewport.element) { + return; + } + + csToolsUtils.stackContextPrefetch.enable(element); + }); + + let imageIdsToSet = imageIds; + const overlayProcessingResult = this._processExtraDisplaySetsForViewport(viewport); + imageIdsToSet = overlayProcessingResult?.imageIds ?? imageIdsToSet; + + const referencedImageId = presentations?.positionPresentation?.viewReference?.referencedImageId; + if (referencedImageId) { + initialImageIndexToUse = imageIdsToSet.indexOf(referencedImageId); + } + + if (initialImageIndexToUse === undefined || initialImageIndexToUse === null) { + initialImageIndexToUse = this._getInitialImageIndexForViewport(viewportInfo, imageIds) || 0; + } + + return viewport.setStack(imageIdsToSet, initialImageIndexToUse).then(() => { + viewport.setProperties({ ...properties }); + this.setPresentations(viewport.id, presentations, viewportInfo); + + if (overlayProcessingResult?.addOverlayFn) { + overlayProcessingResult.addOverlayFn(); + } + + if (displayArea) { + viewport.setDisplayArea(displayArea); + } + if (rotation) { + viewport.setProperties({ rotation }); + } + if (flipHorizontal) { + viewport.setCamera({ flipHorizontal: true }); + } + }); + } + + private _getInitialImageIndexForViewport( + viewportInfo: ViewportInfo, + imageIds?: string[] + ): number { + const initialImageOptions = viewportInfo.getInitialImageOptions(); + if (!initialImageOptions) { + return; + } + const { index, preset } = initialImageOptions; + const viewportType = viewportInfo.getViewportType(); + + let numberOfSlices; + if (viewportType === csEnums.ViewportType.STACK) { + numberOfSlices = imageIds.length; + } else if (viewportType === csEnums.ViewportType.ORTHOGRAPHIC) { + const viewport = this.getCornerstoneViewport(viewportInfo.getViewportId()); + const imageSliceData = csUtils.getImageSliceDataForVolumeViewport(viewport); + + if (!imageSliceData) { + return; + } + + ({ numberOfSlices } = imageSliceData); + } else { + return; + } + + return this._getInitialImageIndex(numberOfSlices, index, preset); + } + + _getInitialImageIndex(numberOfSlices: number, imageIndex?: number, preset?: JumpPresets): number { + const lastSliceIndex = numberOfSlices - 1; + + if (imageIndex !== undefined) { + return csUtils.clip(imageIndex, 0, lastSliceIndex); + } + + if (preset === JumpPresets.First) { + return 0; + } + + if (preset === JumpPresets.Last) { + return lastSliceIndex; + } + + if (preset === JumpPresets.Middle) { + // Note: this is a simple but yet very important formula. + // since viewport reset works with the middle slice + // if the below formula is not correct, on a viewport reset + // it will jump to a different slice than the middle one which + // was the initial slice, and we have some tools such as Crosshairs + // which rely on a relative camera modifications and those will break. + return lastSliceIndex % 2 === 0 ? lastSliceIndex / 2 : (lastSliceIndex + 1) / 2; + } + + return 0; + } + + async _setVolumeViewport( + viewport: Types.IVolumeViewport, + viewportData: VolumeViewportData, + viewportInfo: ViewportInfo, + presentations: Presentations = {} + ): Promise { + // TODO: We need to overhaul the way data sources work so requests can be made + // async. I think we should follow the image loader pattern which is async and + // has a cache behind it. + // The problem is that to set this volume, we need the metadata, but the request is + // already in-flight, and the promise is not cached, so we have no way to wait for + // it and know when it has fully arrived. + // loadStudyMetadata(StudyInstanceUID) => Promise([instances for study]) + // loadSeriesMetadata(StudyInstanceUID, SeriesInstanceUID) => Promise([instances for series]) + // If you call loadStudyMetadata and it's not in the DicomMetadataStore cache, it should fire + // a request through the data source? + // (This call may or may not create sub-requests for series metadata) + const volumeInputArray = []; + const displaySetOptionsArray = viewportInfo.getDisplaySetOptions(); + const { hangingProtocolService } = this.servicesManager.services; + + const volumeToLoad = []; + const displaySetInstanceUIDs = []; + + for (const [index, data] of viewportData.data.entries()) { + const { volume, imageIds, displaySetInstanceUID } = data; + + displaySetInstanceUIDs.push(displaySetInstanceUID); + + if (!volume) { + console.log('Volume display set not found'); + continue; + } + + volumeToLoad.push(volume); + + const displaySetOptions = displaySetOptionsArray[index]; + const { volumeId } = volume; + volumeInputArray.push({ + imageIds, + volumeId, + blendMode: displaySetOptions.blendMode, + slabThickness: this._getSlabThickness(displaySetOptions, volumeId), + }); + } + + this.viewportsDisplaySets.set(viewport.id, displaySetInstanceUIDs); + + const volumesNotLoaded = volumeToLoad.filter(volume => !volume.loadStatus?.loaded); + if (volumesNotLoaded.length) { + if (hangingProtocolService.getShouldPerformCustomImageLoad()) { + // delegate the volume loading to the hanging protocol service if it has a custom image load strategy + return hangingProtocolService.runImageLoadStrategy({ + viewportId: viewport.id, + volumeInputArray, + }); + } + + volumesNotLoaded.forEach(volume => { + if (!volume.loadStatus?.loading && volume.load instanceof Function) { + volume.load(); + } + }); + } + + // It's crucial not to return here because the volume may be loaded, + // but the viewport also needs to set the volume. + // if (!volumesNotLoaded.length) { + // return; + // } + + // This returns the async continuation only + return this.setVolumesForViewport(viewport, volumeInputArray, presentations); + } + + public async setVolumesForViewport(viewport, volumeInputArray, presentations) { + const { displaySetService, viewportGridService } = this.servicesManager.services; + + const viewportInfo = this.getViewportInfo(viewport.id); + const displaySetOptions = viewportInfo.getDisplaySetOptions(); + const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewport.id); + const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]); + const displaySetModality = displaySet?.Modality; + // Todo: use presentations states + const volumesProperties = volumeInputArray.map((volumeInput, index) => { + const { volumeId } = volumeInput; + const displaySetOption = displaySetOptions[index]; + const { voi, voiInverted, colormap, displayPreset } = displaySetOption; + const properties = {} as ViewportProperties; + + if (voi && (voi.windowWidth || voi.windowCenter)) { + const { lower, upper } = csUtils.windowLevel.toLowHighRange( + voi.windowWidth, + voi.windowCenter + ); + properties.voiRange = { lower, upper }; + } + + if (voiInverted !== undefined) { + properties.invert = voiInverted; + } + + if (colormap !== undefined) { + properties.colormap = colormap; + } + + if (displayPreset !== undefined) { + properties.preset = displayPreset[displaySetModality] || displayPreset.default; + } + + return { properties, volumeId }; + }); + + // For SEG and RT viewports + const { addOverlayFn } = this._processExtraDisplaySetsForViewport(viewport) || {}; + + await viewport.setVolumes(volumeInputArray); + + if (addOverlayFn) { + addOverlayFn(); + } + + volumesProperties.forEach(({ properties, volumeId }) => { + viewport.setProperties(properties, volumeId); + }); + + this.setPresentations(viewport.id, presentations, viewportInfo); + + const imageIndex = this._getInitialImageIndexForViewport(viewportInfo); + + if (imageIndex !== undefined) { + csUtils.jumpToSlice(viewport.element, { + imageIndex, + }); + } + + viewport.render(); + + this._broadcastEvent(this.EVENTS.VIEWPORT_VOLUMES_CHANGED, { + viewportInfo, + }); + } + + private _processExtraDisplaySetsForViewport( + viewport: Types.IStackViewport | Types.IVolumeViewport + ) { + const { displaySetService } = this.servicesManager.services; + + // load any secondary displaySets + const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id); + + // Can be SEG or RTSTRUCT for now but not PMAP + const segOrRTSOverlayDisplaySet = displaySetInstanceUIDs + .map(displaySetService.getDisplaySetByUID) + .find( + displaySet => + displaySet?.isOverlayDisplaySet && ['SEG', 'RTSTRUCT'].includes(displaySet.Modality) + ); + + // if it is only the overlay displaySet, then we need to get the reference + // displaySet imageIds and set them as the imageIds for the viewport, + // here we can do some logic if the reference is missing + // then find the most similar match of displaySet instead + if (!segOrRTSOverlayDisplaySet) { + return; + } + + const referenceDisplaySet = displaySetService.getDisplaySetByUID( + segOrRTSOverlayDisplaySet.referencedDisplaySetInstanceUID + ); + const imageIds = referenceDisplaySet.images.map(image => image.imageId); + + return { + imageIds, + addOverlayFn: () => + this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport), + }; + } + + private addOverlayRepresentationForDisplaySet( + displaySet: OhifTypes.DisplaySet, + viewport: Types.IViewport + ) { + const { segmentationService } = this.servicesManager.services; + const segmentationId = displaySet.displaySetInstanceUID; + + const representationType = + displaySet.Modality === 'SEG' + ? csToolsEnums.SegmentationRepresentations.Labelmap + : csToolsEnums.SegmentationRepresentations.Contour; + + segmentationService.addSegmentationRepresentation(viewport.id, { + segmentationId, + type: representationType, + }); + + // store the segmentation presentation id in the viewport info + this.storePresentation({ viewportId: viewport.id }); + } + + // Todo: keepCamera is an interim solution until we have a better solution for + // keeping the camera position when the viewport data is changed + public updateViewport(viewportId: string, viewportData, keepCamera = false) { + const viewportInfo = this.getViewportInfo(viewportId); + const viewport = this.getCornerstoneViewport(viewportId); + const viewportCamera = viewport.getCamera(); + + let displaySetPromise; + + if (viewport instanceof VolumeViewport || viewport instanceof VolumeViewport3D) { + displaySetPromise = this._setVolumeViewport(viewport, viewportData, viewportInfo).then(() => { + if (keepCamera) { + viewport.setCamera(viewportCamera); + viewport.render(); + } + }); + } + + if (viewport instanceof StackViewport) { + displaySetPromise = this._setStackViewport(viewport, viewportData, viewportInfo); + } + + displaySetPromise.then(() => { + this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, { + viewportData, + viewportId, + }); + }); + } + + _setDisplaySets( + viewport: Types.IViewport, + viewportData: StackViewportData | VolumeViewportData, + viewportInfo: ViewportInfo, + presentations: Presentations = {} + ): Promise { + if (viewport instanceof StackViewport) { + return this._setStackViewport( + viewport, + viewportData as StackViewportData, + viewportInfo, + presentations + ); + } + + if ([VolumeViewport, VolumeViewport3D].some(type => viewport instanceof type)) { + return this._setVolumeViewport( + viewport as Types.IVolumeViewport, + viewportData as VolumeViewportData, + viewportInfo, + presentations + ); + } + + return this._setOtherViewport( + viewport, + viewportData as StackViewportData, + viewportInfo, + presentations + ); + } + + /** + * Removes the resize observer from the viewport element + */ + _removeResizeObserver() { + if (this.viewportGridResizeObserver) { + this.viewportGridResizeObserver.disconnect(); + } + } + + _getSlabThickness(displaySetOptions, volumeId) { + const { blendMode } = displaySetOptions; + if (blendMode === undefined || displaySetOptions.slabThickness === undefined) { + return; + } + + // if there is a slabThickness set as a number then use it + if (typeof displaySetOptions.slabThickness === 'number') { + return displaySetOptions.slabThickness; + } + + if (displaySetOptions.slabThickness.toLowerCase() === 'fullvolume') { + // calculate the slab thickness based on the volume dimensions + const imageVolume = cache.getVolume(volumeId); + + const { dimensions, spacing } = imageVolume; + const slabThickness = Math.sqrt( + Math.pow(dimensions[0] * spacing[0], 2) + + Math.pow(dimensions[1] * spacing[1], 2) + + Math.pow(dimensions[2] * spacing[2], 2) + ); + + return slabThickness; + } + } + + _getFrameOfReferenceUID(displaySetInstanceUID) { + const { displaySetService } = this.servicesManager.services; + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + + if (!displaySet) { + return; + } + + if (displaySet.frameOfReferenceUID) { + return displaySet.frameOfReferenceUID; + } + + if (displaySet.Modality === 'SEG') { + const { instance } = displaySet; + return instance.FrameOfReferenceUID; + } + + if (displaySet.Modality === 'RTSTRUCT') { + const { instance } = displaySet; + return instance.ReferencedFrameOfReferenceSequence.FrameOfReferenceUID; + } + + const { images } = displaySet; + if (images && images.length) { + return images[0].FrameOfReferenceUID; + } + } + + private enqueueViewportResizeRequest() { + this.resizeQueue.push(false); // false indicates viewport resize + + clearTimeout(this.viewportResizeTimer); + this.viewportResizeTimer = setTimeout(() => { + this.processViewportResizeQueue(); + }, this.gridResizeDelay); + } + + private processViewportResizeQueue() { + const isGridResizeInQueue = this.resizeQueue.some(isGridResize => isGridResize); + if (this.resizeQueue.length > 0 && !isGridResizeInQueue && !this.gridResizeTimeOut) { + this.performResize(); + } + + // Clear the queue after processing viewport resizes + this.resizeQueue = []; + } + + private performResize() { + const isImmediate = false; + + try { + const viewports = this.getRenderingEngine().getViewports(); + + // Store the current position presentations for each viewport. + viewports.forEach(({ id: viewportId }) => { + const presentation = this._getPositionPresentation(viewportId); + + // During a resize, the slice index should remain unchanged. This is a temporary fix for + // a larger issue regarding the definition of slice index with slab thickness. + // We need to revisit this to make it more robust and understandable. + delete presentation.viewReference?.sliceIndex; + this.beforeResizePositionPresentations.set(viewportId, presentation); + }); + + // Resize the rendering engine and render. + const renderingEngine = this.renderingEngine; + renderingEngine.resize(isImmediate); + renderingEngine.render(); + + // Reset the camera for all viewports using position presentation to maintain relative size/position + // which means only those viewports that have a zoom level of 1. + this.beforeResizePositionPresentations.forEach((positionPresentation, viewportId) => { + this.setPresentations(viewportId, { + positionPresentation, + }); + }); + + // Resize and render the rendering engine again. + renderingEngine.resize(isImmediate); + renderingEngine.render(); + } catch (e) { + // This can happen if the resize is too close to navigation or shutdown + console.warn('Caught resize exception', e); + } + } + + private resetGridResizeTimeout() { + clearTimeout(this.gridResizeTimeOut); + this.gridResizeTimeOut = setTimeout(() => { + this.gridResizeTimeOut = null; + }, this.gridResizeDelay); + } + + private _setLutPresentation( + viewport: Types.IStackViewport | Types.IVolumeViewport, + lutPresentation: LutPresentation + ): void { + if (!lutPresentation) { + return; + } + + const { properties } = lutPresentation; + if (viewport instanceof BaseVolumeViewport) { + if (properties instanceof Map) { + properties.forEach((propertiesEntry, volumeId) => { + viewport.setProperties(propertiesEntry, volumeId); + }); + } else { + viewport.setProperties(properties); + } + } else { + viewport.setProperties(properties); + } + } + + private _setPositionPresentation( + viewport: Types.IStackViewport | Types.IVolumeViewport, + positionPresentation: PositionPresentation + ): void { + const viewRef = positionPresentation?.viewReference; + if (viewRef) { + if (viewport.isReferenceViewable(viewRef, WITH_NAVIGATION)) { + viewport.setViewReference(viewRef); + } else { + console.warn('Unable to apply reference viewable', viewRef); + } + } + + const viewPresentation = positionPresentation?.viewPresentation; + if (viewPresentation) { + viewport.setViewPresentation(viewPresentation); + } + } + + private _setSegmentationPresentation( + viewport: Types.IStackViewport | Types.IVolumeViewport, + segmentationPresentation: SegmentationPresentation + ): void { + if (!segmentationPresentation) { + return; + } + + const { segmentationService } = this.servicesManager.services; + + segmentationPresentation.forEach((presentationItem: SegmentationPresentationItem) => { + const { segmentationId, type, hydrated } = presentationItem; + + if (hydrated) { + segmentationService.addSegmentationRepresentation(viewport.id, { + segmentationId, + type, + }); + } + }); + } +} + +export default CornerstoneViewportService; diff --git a/extensions/cornerstone/src/services/ViewportService/IViewportService.ts b/extensions/cornerstone/src/services/ViewportService/IViewportService.ts new file mode 100644 index 0000000..46d4acd --- /dev/null +++ b/extensions/cornerstone/src/services/ViewportService/IViewportService.ts @@ -0,0 +1,65 @@ +import { Types } from '@cornerstonejs/core'; +import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; +import { DisplaySetOptions, PublicViewportOptions } from './Viewport'; +import { Presentations } from '../../types/Presentation'; + +/** + * Handles cornerstone viewport logic including enabling, disabling, and + * updating the viewport. + */ +export interface IViewportService { + servicesManager: AppTypes.ServicesManager; + hangingProtocolService: unknown; + renderingEngine: unknown; + viewportGridResizeObserver: unknown; + viewportsInfo: unknown; + sceneVolumeInputs: unknown; + viewportDivElements: unknown; + ViewportPropertiesMap: unknown; + volumeUIDs: unknown; + listeners: { [key: string]: {} }; + displaySetsNeedRerendering: unknown; + viewportDisplaySets: unknown; + EVENTS: { [key: string]: string }; + _broadcastEvent: unknown; + /** + * Adds the HTML element to the viewportService + * @param {*} elementRef + */ + enableViewport(viewportId: string, elementRef: HTMLDivElement): void; + /** + * It retrieves the renderingEngine if it does exist, or creates one otherwise + * @returns {RenderingEngine} rendering engine + */ + getRenderingEngine(): Types.IRenderingEngine; + /** + * It creates a resize observer for the viewport element, and observes + * the element for resizing events + * @param {*} elementRef + */ + resize(isGridResize: boolean): void; + /** + * Removes the viewport from cornerstone, and destroys the rendering engine + */ + destroy(): void; + /** + * Disables the viewport inside the renderingEngine, if no viewport is left + * it destroys the renderingEngine. + * @param viewportId + */ + disableElement(viewportId: string): void; + /** + * Uses the renderingEngine to enable the element for the given viewport index + * and sets the displaySet data to the viewport + * @param {*} displaySet + * @param {*} dataSource + * @returns + */ + setViewportData( + viewportId: string, + viewportData: StackViewportData | VolumeViewportData, + publicViewportOptions: PublicViewportOptions, + publicDisplaySetOptions: DisplaySetOptions[], + presentations?: Presentations + ): void; +} diff --git a/extensions/cornerstone/src/services/ViewportService/Viewport.ts b/extensions/cornerstone/src/services/ViewportService/Viewport.ts new file mode 100644 index 0000000..091c7f7 --- /dev/null +++ b/extensions/cornerstone/src/services/ViewportService/Viewport.ts @@ -0,0 +1,361 @@ +import { + Types, + Enums, + getEnabledElementByViewportId, + VolumeViewport, + utilities, +} from '@cornerstonejs/core'; +import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; +import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode'; +import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation'; +import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType'; +import JumpPresets from '../../utils/JumpPresets'; +import { SyncGroup } from '../SyncGroupService/SyncGroupService'; + +export type InitialImageOptions = { + index?: number; + preset?: JumpPresets; + useOnce?: boolean; +}; + +export type ViewportOptions = { + id?: string; + viewportType: Enums.ViewportType; + toolGroupId: string; + viewportId: string; + // Presentation ID to store/load presentation state from + presentationIds?: AppTypes.PresentationIds; + orientation?: Enums.OrientationAxis; + background?: Types.Point3; + displayArea?: Types.DisplayArea; + syncGroups?: SyncGroup[]; + initialImageOptions?: InitialImageOptions; + rotation?: number; + flipHorizontal?: boolean; + viewReference?: Types.ViewReference; + customViewportProps?: Record; + /* + * Allows drag and drop of display sets not matching viewport options, but + * doesn't show them initially. Displays initially blank if no required match + */ + allowUnmatchedView?: boolean; +}; + +export type PublicViewportOptions = { + id?: string; + viewportType?: string; + toolGroupId?: string; + presentationIds?: string[]; + viewportId?: string; + orientation?: Enums.OrientationAxis; + background?: Types.Point3; + displayArea?: Types.DisplayArea; + syncGroups?: SyncGroup[]; + rotation?: number; + flipHorizontal?: boolean; + initialImageOptions?: InitialImageOptions; + customViewportProps?: Record; + allowUnmatchedView?: boolean; +}; + +export type DisplaySetSelector = { + id?: string; + options?: PublicDisplaySetOptions; +}; + +export type PublicDisplaySetOptions = { + /** The display set options can have an id in order to distinguish + * it from other similar items. + */ + id?: string; + voi?: VOI; + voiInverted?: boolean; + blendMode?: string; + slabThickness?: number; + colormap?: string; + displayPreset?: string; +}; + +export type DisplaySetOptions = { + id?: string; + voi?: VOI; + voiInverted: boolean; + blendMode?: Enums.BlendModes; + slabThickness?: number; + colormap?: { name: string; opacity?: number }; + displayPreset?: string; +}; + +type VOI = { + windowWidth: number; + windowCenter: number; +}; + +export type DisplaySet = { + displaySetInstanceUID: string; +}; + +const STACK = 'stack'; +const DEFAULT_TOOLGROUP_ID = 'default'; + +// Return true if the data contains the given display set UID OR the imageId +// if it is a composite object. +const dataContains = ({ data, displaySetUID, imageId, viewport }): boolean => { + if (imageId && data.isCompositeStack && data.imageIds) { + return !!data.imageIds.find(dataId => dataId === imageId); + } + + if (imageId && (data.volumeId || viewport instanceof VolumeViewport)) { + const isAcquisition = !!viewport.getCurrentImageId(); + + if (!isAcquisition) { + return false; + } + + const imageURI = utilities.imageIdToURI(imageId); + const hasImageId = viewport.hasImageURI(imageURI); + + if (hasImageId) { + return true; + } + } + + if (data.displaySetInstanceUID === displaySetUID) { + return true; + } + + return false; +}; + +class ViewportInfo { + private viewportId = ''; + private element: HTMLDivElement; + private viewportOptions: ViewportOptions; + private displaySetOptions: Array; + private viewportData: StackViewportData | VolumeViewportData; + private renderingEngineId: string; + private viewReference: Types.ViewReference; + + constructor(viewportId: string) { + this.viewportId = viewportId; + this.setPublicViewportOptions({}); + this.setPublicDisplaySetOptions([{}]); + } + + /** + * Return true if the viewport contains the given display set UID, + * OR if it is a composite stack and contains the given imageId + */ + public contains(displaySetUID: string, imageId: string): boolean { + if (!this.viewportData?.data) { + return false; + } + + const { viewport } = getEnabledElementByViewportId(this.viewportId) || {}; + + if (this.viewportData.data.length) { + return !!this.viewportData.data.find(data => + dataContains({ data, displaySetUID, imageId, viewport }) + ); + } + + return dataContains({ + data: this.viewportData.data, + displaySetUID, + imageId, + viewport, + }); + } + + public destroy = (): void => { + this.element = null; + this.viewportData = null; + this.viewportOptions = null; + this.displaySetOptions = null; + }; + + public setRenderingEngineId(renderingEngineId: string): void { + this.renderingEngineId = renderingEngineId; + } + + public getRenderingEngineId(): string { + return this.renderingEngineId; + } + + public setViewportId(viewportId: string): void { + this.viewportId = viewportId; + } + + public setElement(element: HTMLDivElement): void { + this.element = element; + } + + public setViewportData(viewportData: StackViewportData | VolumeViewportData): void { + this.viewportData = viewportData; + } + + public getViewportData(): StackViewportData | VolumeViewportData { + return this.viewportData; + } + + public getElement(): HTMLDivElement { + return this.element; + } + + public getViewportId(): string { + return this.viewportId; + } + + public getViewReference(): Types.ViewReference { + return this.viewportOptions?.viewReference; + } + + public setPublicDisplaySetOptions( + publicDisplaySetOptions: PublicDisplaySetOptions[] | DisplaySetSelector[] + ): Array { + // map the displaySetOptions and check if they are undefined then set them to default values + const displaySetOptions = this.mapDisplaySetOptions(publicDisplaySetOptions); + + this.setDisplaySetOptions(displaySetOptions); + + return this.displaySetOptions; + } + + public hasDisplaySet(displaySetInstanceUID: string): boolean { + // Todo: currently this does not work for non image & referenceImage displaySets. + // Since SEG and other derived displaySets are loaded in a different way, and not + // via cornerstoneViewportService + let viewportData = this.getViewportData(); + + if ( + viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC || + viewportData.viewportType === Enums.ViewportType.VOLUME_3D + ) { + viewportData = viewportData as VolumeViewportData; + return viewportData.data.some( + ({ displaySetInstanceUID: dsUID }) => dsUID === displaySetInstanceUID + ); + } + + viewportData = viewportData as StackViewportData; + return viewportData.data.displaySetInstanceUID === displaySetInstanceUID; + } + + /** + * + * @param viewportOptionsEntry - the base values for the options + * @param viewportTypeDisplaySet - allows overriding the viewport type + */ + public setPublicViewportOptions( + viewportOptionsEntry: PublicViewportOptions, + viewportTypeDisplaySet?: string + ): ViewportOptions { + const ohifViewportType = viewportTypeDisplaySet || viewportOptionsEntry.viewportType || STACK; + const { presentationIds } = viewportOptionsEntry; + let { toolGroupId = DEFAULT_TOOLGROUP_ID } = viewportOptionsEntry; + // Just assign the orientation for any viewport type and let the viewport deal with it + const orientation = getCornerstoneOrientation(viewportOptionsEntry.orientation); + + const viewportType = getCornerstoneViewportType(ohifViewportType); + + if (!toolGroupId) { + toolGroupId = DEFAULT_TOOLGROUP_ID; + } + + this.setViewportOptions({ + ...viewportOptionsEntry, + viewportId: this.viewportId, + viewportType: viewportType as Enums.ViewportType, + orientation, + toolGroupId, + presentationIds, + }); + + return this.viewportOptions; + } + + public setViewportOptions(viewportOptions: ViewportOptions): void { + this.viewportOptions = viewportOptions; + } + + public getViewportOptions(): ViewportOptions { + return this.viewportOptions; + } + + public getPresentationIds(): AppTypes.PresentationIds | null { + const { presentationIds } = this.viewportOptions; + return presentationIds; + } + + public setDisplaySetOptions(displaySetOptions: Array): void { + this.displaySetOptions = displaySetOptions; + } + + public getSyncGroups(): SyncGroup[] { + this.viewportOptions.syncGroups ||= []; + return this.viewportOptions.syncGroups; + } + + public getDisplaySetOptions(): Array { + return this.displaySetOptions; + } + + public getViewportType(): Enums.ViewportType { + return this.viewportOptions.viewportType || Enums.ViewportType.STACK; + } + + public getToolGroupId(): string { + return this.viewportOptions.toolGroupId; + } + + public getBackground(): Types.Point3 { + return this.viewportOptions.background || [0, 0, 0]; + } + + public getOrientation(): Enums.OrientationAxis { + return this.viewportOptions.orientation; + } + + public getDisplayArea(): Types.DisplayArea { + return this.viewportOptions.displayArea; + } + + public getInitialImageOptions(): InitialImageOptions { + return this.viewportOptions.initialImageOptions; + } + + // Handle incoming public display set options or a display set select + // with a contained options. + private mapDisplaySetOptions( + options: PublicDisplaySetOptions[] | DisplaySetSelector[] = [{}] + ): Array { + const displaySetOptions: Array = []; + + options.forEach(item => { + let option = item?.options || item; + if (!option) { + option = { + blendMode: undefined, + slabThickness: undefined, + colormap: undefined, + voi: {}, + voiInverted: false, + }; + } + const blendMode = getCornerstoneBlendMode(option.blendMode); + + displaySetOptions.push({ + voi: option.voi, + voiInverted: option.voiInverted, + colormap: option.colormap, + slabThickness: option.slabThickness, + blendMode, + displayPreset: option.displayPreset, + }); + }); + + return displaySetOptions; + } +} + +export default ViewportInfo; diff --git a/extensions/cornerstone/src/services/ViewportService/constants.ts b/extensions/cornerstone/src/services/ViewportService/constants.ts new file mode 100644 index 0000000..3945c32 --- /dev/null +++ b/extensions/cornerstone/src/services/ViewportService/constants.ts @@ -0,0 +1,3 @@ +const RENDERING_ENGINE_ID = 'OHIFCornerstoneRenderingEngine'; + +export { RENDERING_ENGINE_ID }; diff --git a/extensions/cornerstone/src/state.ts b/extensions/cornerstone/src/state.ts new file mode 100644 index 0000000..76afcdb --- /dev/null +++ b/extensions/cornerstone/src/state.ts @@ -0,0 +1,34 @@ +const state = { + // The `defaultContext` of an extension's commandsModule + DEFAULT_CONTEXT: 'CORNERSTONE', + enabledElements: {}, +}; + +/** + * Sets the enabled element `dom` reference for an active viewport. + * @param {HTMLElement} dom Active viewport element. + * @return void + */ +const setEnabledElement = (viewportId: string, element: HTMLElement, context?: string): void => { + const targetContext = context || state.DEFAULT_CONTEXT; + + state.enabledElements[viewportId] = { + element, + context: targetContext, + }; +}; + +/** + * Grabs the enabled element `dom` reference of an active viewport. + * + * @return {HTMLElement} Active viewport element. + */ +const getEnabledElement = viewportId => { + return state.enabledElements[viewportId]; +}; + +const reset = () => { + state.enabledElements = {}; +}; + +export { setEnabledElement, getEnabledElement, reset }; diff --git a/extensions/cornerstone/src/stores/index.ts b/extensions/cornerstone/src/stores/index.ts new file mode 100644 index 0000000..f834c82 --- /dev/null +++ b/extensions/cornerstone/src/stores/index.ts @@ -0,0 +1,4 @@ +export { useLutPresentationStore } from './useLutPresentationStore'; +export { usePositionPresentationStore } from './usePositionPresentationStore'; +export { useSegmentationPresentationStore } from './useSegmentationPresentationStore'; +export { useSynchronizersStore } from './useSynchronizersStore'; diff --git a/extensions/cornerstone/src/stores/presentationUtils.ts b/extensions/cornerstone/src/stores/presentationUtils.ts new file mode 100644 index 0000000..bdc4fdf --- /dev/null +++ b/extensions/cornerstone/src/stores/presentationUtils.ts @@ -0,0 +1,42 @@ +const JOIN_STR = '&'; + +// The default lut presentation id if none defined +const DEFAULT_STR = 'default'; + +// This code finds the first unique index to add to the presentation id so that +// two viewports containing the same display set in the same type of viewport +// can have different presentation information. This allows comparison of +// a single display set in two or more viewports, when the user has simply +// dragged and dropped the view in twice. For example, it allows displaying +// bone, brain and soft tissue views of a single display set, and to still +// remember the specific changes to each viewport. +const addUniqueIndex = ( + arr, + key, + viewports: AppTypes.ViewportGrid.Viewports, + isUpdatingSameViewport +) => { + arr.push(0); + + // If we are updating the viewport, we should not increment the index + if (isUpdatingSameViewport) { + return; + } + + // The 128 is just a value that is larger than how many viewports we + // display at once, used as an upper bound on how many unique presentation + // ID's might exist for a single display set at once. + for (let displayInstance = 0; displayInstance < 128; displayInstance++) { + arr[arr.length - 1] = displayInstance; + const testId = arr.join(JOIN_STR); + if ( + !Array.from(viewports.values()).find( + viewport => viewport.viewportOptions?.presentationIds?.[key] === testId + ) + ) { + break; + } + } +}; + +export { addUniqueIndex, DEFAULT_STR, JOIN_STR }; diff --git a/extensions/cornerstone/src/stores/useLutPresentationStore.ts b/extensions/cornerstone/src/stores/useLutPresentationStore.ts new file mode 100644 index 0000000..0f0e29b --- /dev/null +++ b/extensions/cornerstone/src/stores/useLutPresentationStore.ts @@ -0,0 +1,166 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { LutPresentation } from '../types/Presentation'; +import { addUniqueIndex, DEFAULT_STR, JOIN_STR } from './presentationUtils'; + +/** + * Identifier for the LUT Presentation store type. + */ +const PRESENTATION_TYPE_ID = 'lutPresentationId'; + +/** + * Flag to enable or disable debug mode for the store. + * Set to `true` to enable zustand devtools. + */ +const DEBUG_STORE = false; + +/** + * Represents the state and actions for managing LUT presentations. + */ +type LutPresentationState = { + /** + * Type identifier for the store. + */ + type: string; + + /** + * Stores LUT presentations indexed by their presentation ID. + */ + lutPresentationStore: Record; + + /** + * Sets the LUT presentation for a given key. + * + * @param key - The key identifying the LUT presentation. + * @param value - The `LutPresentation` to associate with the key. + */ + setLutPresentation: (key: string, value: LutPresentation) => void; + + /** + * Clears all LUT presentations from the store. + */ + clearLutPresentationStore: () => void; + + /** + * Retrieves the presentation ID based on the provided parameters. + * + * @param id - The presentation ID to check. + * @param options - Configuration options. + * @param options.viewport - The current viewport in grid + * @param options.viewports - All available viewports in grid + * @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated. + * @returns The presentation ID or undefined. + */ + getPresentationId: ( + id: string, + options: { + viewport: AppTypes.ViewportGrid.Viewport; + viewports: AppTypes.ViewportGrid.Viewports; + isUpdatingSameViewport: boolean; + } + ) => string | undefined; +}; + +/** + * Generates a presentation ID for LUT based on the viewport configuration. + * + * @param id - The ID to check. + * @param options - Configuration options. + * @param options.viewport - The current viewport. + * @param options.viewports - All available viewports. + * @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated. + * @returns The LUT presentation ID or undefined. + */ +const getLutPresentationId = ( + id: string, + { + viewport, + viewports, + isUpdatingSameViewport, + }: { + viewport: AppTypes.ViewportGrid.Viewport; + viewports: AppTypes.ViewportGrid.Viewports; + isUpdatingSameViewport: boolean; + } +): string | undefined => { + if (id !== PRESENTATION_TYPE_ID) { + return; + } + + const getLutId = (ds): string => { + if (!ds || !ds.options) { + return DEFAULT_STR; + } + if (ds.options.id) { + return ds.options.id; + } + const arr = Object.entries(ds.options).map(([key, val]) => `${key}=${val}`); + if (!arr.length) { + return DEFAULT_STR; + } + return arr.join(JOIN_STR); + }; + + if (!viewport || !viewport.viewportOptions || !viewport.displaySetInstanceUIDs?.length) { + return; + } + + const { displaySetOptions, displaySetInstanceUIDs } = viewport; + const lutId = getLutId(displaySetOptions[0]); + const lutPresentationArr = [lutId]; + + for (const uid of displaySetInstanceUIDs) { + lutPresentationArr.push(uid); + } + + addUniqueIndex(lutPresentationArr, PRESENTATION_TYPE_ID, viewports, isUpdatingSameViewport); + + return lutPresentationArr.join(JOIN_STR); +}; + +/** + * Creates the LUT Presentation store. + * + * @param set - The zustand set function. + * @returns The LUT Presentation store state and actions. + */ +const createLutPresentationStore = (set): LutPresentationState => ({ + type: PRESENTATION_TYPE_ID, + lutPresentationStore: {}, + + /** + * Sets the LUT presentation for a given key. + */ + setLutPresentation: (key, value) => + set( + state => ({ + lutPresentationStore: { + ...state.lutPresentationStore, + [key]: value, + }, + }), + false, + 'setLutPresentation' + ), + + /** + * Clears all LUT presentations from the store. + */ + clearLutPresentationStore: () => + set({ lutPresentationStore: {} }, false, 'clearLutPresentationStore'), + + /** + * Retrieves the presentation ID based on the provided parameters. + */ + getPresentationId: getLutPresentationId, +}); + +/** + * Zustand store for managing LUT presentations. + * Applies devtools middleware when DEBUG_STORE is enabled. + */ +export const useLutPresentationStore = create()( + DEBUG_STORE + ? devtools(createLutPresentationStore, { name: 'LutPresentationStore' }) + : createLutPresentationStore +); diff --git a/extensions/cornerstone/src/stores/usePositionPresentationStore.ts b/extensions/cornerstone/src/stores/usePositionPresentationStore.ts new file mode 100644 index 0000000..ac6d94c --- /dev/null +++ b/extensions/cornerstone/src/stores/usePositionPresentationStore.ts @@ -0,0 +1,172 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { PositionPresentation } from '../types/Presentation'; +import { addUniqueIndex, JOIN_STR } from './presentationUtils'; + +const PRESENTATION_TYPE_ID = 'positionPresentationId'; +const DEBUG_STORE = false; + +/** + * Represents the state and actions for managing position presentations. + */ +type PositionPresentationState = { + /** + * Type identifier for the store. + */ + type: string; + + /** + * Stores position presentations indexed by their presentation ID. + */ + positionPresentationStore: Record; + + /** + * Sets the position presentation for a given key. + * + * @param key - The key identifying the position presentation. + * @param value - The `PositionPresentation` to associate with the key. + */ + setPositionPresentation: (key: string, value: PositionPresentation) => void; + + /** + * Clears all position presentations from the store. + */ + clearPositionPresentationStore: () => void; + + /** + * Retrieves the presentation ID based on the provided parameters. + * + * @param id - The ID to check. + * @param options - Configuration options. + * @param options.viewport - The current viewport. + * @param options.viewports - All available viewports. + * @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated. + * @returns The position presentation ID or undefined. + */ + getPresentationId: ( + id: string, + options: { + viewport: any; + viewports: any; + isUpdatingSameViewport: boolean; + } + ) => string | undefined; + + getPositionPresentationId: ( + viewport: any, + viewports?: any, + isUpdatingSameViewport?: boolean + ) => string | undefined; +}; + +/** + * Generates a position presentation ID based on the viewport configuration. + * + * @param id - The ID to check. + * @param options - Configuration options. + * @param options.viewport - The current viewport. + * @param options.viewports - All available viewports. + * @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated. + * @returns The position presentation ID or undefined. + */ +const getPresentationId = ( + id: string, + { + viewport, + viewports, + isUpdatingSameViewport, + }: { + viewport: any; + viewports: any; + isUpdatingSameViewport: boolean; + } +): string | undefined => { + if (id !== PRESENTATION_TYPE_ID) { + return; + } + + if (!viewport?.viewportOptions || !viewport.displaySetInstanceUIDs?.length) { + return; + } + + return getPositionPresentationId(viewport, viewports, isUpdatingSameViewport); +}; + +function getPositionPresentationId(viewport, viewports, isUpdatingSameViewport) { + const { viewportOptions = {}, displaySetInstanceUIDs = [], displaySetOptions = [] } = viewport; + const { id: viewportOptionId, orientation } = viewportOptions; + + const positionPresentationArr = [orientation || 'acquisition']; + if (viewportOptionId) { + positionPresentationArr.push(viewportOptionId); + } + + if (displaySetOptions?.some(ds => ds.options?.blendMode || ds.options?.displayPreset)) { + positionPresentationArr.push(`custom`); + } + + for (const uid of displaySetInstanceUIDs) { + positionPresentationArr.push(uid); + } + + if (viewports && viewports.length && isUpdatingSameViewport !== undefined) { + addUniqueIndex( + positionPresentationArr, + PRESENTATION_TYPE_ID, + viewports, + isUpdatingSameViewport + ); + } else { + positionPresentationArr.push(0); + } + + return positionPresentationArr.join(JOIN_STR); +} + +/** + * Creates the Position Presentation store. + * + * @param set - The zustand set function. + * @returns The Position Presentation store state and actions. + */ +const createPositionPresentationStore = set => ({ + type: PRESENTATION_TYPE_ID, + positionPresentationStore: {}, + + /** + * Sets the position presentation for a given key. + */ + setPositionPresentation: (key, value) => + set( + state => ({ + positionPresentationStore: { + ...state.positionPresentationStore, + [key]: value, + }, + }), + false, + 'setPositionPresentation' + ), + + /** + * Clears all position presentations from the store. + */ + clearPositionPresentationStore: () => + set({ positionPresentationStore: {} }, false, 'clearPositionPresentationStore'), + + /** + * Retrieves the presentation ID based on the provided parameters. + */ + getPresentationId, + getPositionPresentationId: getPositionPresentationId, +}); + +/** + * Zustand store for managing position presentations. + * Applies devtools middleware when DEBUG_STORE is enabled. + */ +export const usePositionPresentationStore = create()( + DEBUG_STORE + ? devtools(createPositionPresentationStore, { name: 'PositionPresentationStore' }) + : createPositionPresentationStore +); diff --git a/extensions/cornerstone/src/stores/useSegmentationPresentationStore.ts b/extensions/cornerstone/src/stores/useSegmentationPresentationStore.ts new file mode 100644 index 0000000..8bdbb9e --- /dev/null +++ b/extensions/cornerstone/src/stores/useSegmentationPresentationStore.ts @@ -0,0 +1,256 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { SegmentationPresentation, SegmentationPresentationItem } from '../types/Presentation'; +import { JOIN_STR } from './presentationUtils'; +import { getViewportOrientationFromImageOrientationPatient } from '../utils/getViewportOrientationFromImageOrientationPatient'; + +const PRESENTATION_TYPE_ID = 'segmentationPresentationId'; +const DEBUG_STORE = false; + +/** + * The keys are the presentationId. + */ +type SegmentationPresentationStore = { + /** + * Type identifier for the store. + */ + type: string; + + /** + * Stores segmentation presentations indexed by their presentation ID. + */ + segmentationPresentationStore: Record; + + /** + * Sets the segmentation presentation for a given segmentation ID. + * + * @param presentationId - The presentation ID. + * @param value - The `SegmentationPresentation` to associate with the ID. + */ + setSegmentationPresentation: (presentationId: string, value: SegmentationPresentation) => void; + + /** + * Clears all segmentation presentations from the store. + */ + clearSegmentationPresentationStore: () => void; + + /** + * Retrieves the presentation ID based on the provided parameters. + * + * @param id - The ID to check. + * @param options - Configuration options. + * @param options.viewport - The current viewport. + * @param options.viewports - All available viewports. + * @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated. + * @param options.servicesManager - The services manager instance. + * @returns The segmentation presentation ID or undefined. + */ + getPresentationId: ( + id: string, + options: { + viewport: AppTypes.ViewportGrid.Viewport; + viewports: AppTypes.ViewportGrid.Viewports; + isUpdatingSameViewport: boolean; + servicesManager: AppTypes.ServicesManager; + } + ) => string | undefined; + + /** + * Adds a new segmentation presentation state. + * + * @param presentationId - The presentation ID. + * @param segmentationPresentation - The `SegmentationPresentation` to add. + * @param servicesManager - The services manager instance. + */ + addSegmentationPresentationItem: ( + presentationId: string, + segmentationPresentationItem: SegmentationPresentationItem + ) => void; + + /** + * Gets the current segmentation presentation ID. + * + * @param params - Parameters for retrieving the segmentation presentation ID. + * @param params.viewport - The current viewport. + * @param params.servicesManager - The services manager instance. + * @returns The current segmentation presentation ID. + */ + getSegmentationPresentationId: ({ + viewport, + servicesManager, + }: { + viewport: AppTypes.ViewportGrid.Viewport; + servicesManager: AppTypes.ServicesManager; + }) => string; +}; + +/** + * Generates a segmentation presentation ID based on the viewport configuration. + * + * @param id - The ID to check. + * @param options - Configuration options. + * @param options.viewport - The current viewport. + * @param options.viewports - All available viewports. + * @param options.isUpdatingSameViewport - Indicates if the same viewport is being updated. + * @param options.servicesManager - The services manager instance. + * @returns The segmentation presentation ID or undefined. + */ +const getPresentationId = ( + id: string, + { + viewport, + viewports, + isUpdatingSameViewport, + servicesManager, + }: { + viewport: AppTypes.ViewportGrid.Viewport; + viewports: AppTypes.ViewportGrid.Viewports; + isUpdatingSameViewport: boolean; + servicesManager: AppTypes.ServicesManager; + } +): string | undefined => { + if (id !== PRESENTATION_TYPE_ID) { + return; + } + + return _getSegmentationPresentationId({ viewport, servicesManager }); +}; + +/** + * Helper function to generate the segmentation presentation ID. + * + * @param params - Parameters for generating the segmentation presentation ID. + * @param params.viewport - The current viewport. + * @param params.servicesManager - The services manager instance. + * @returns The segmentation presentation ID or undefined. + */ +const _getSegmentationPresentationId = ({ + viewport, + servicesManager, +}: { + viewport: AppTypes.ViewportGrid.Viewport; + servicesManager: AppTypes.ServicesManager; +}) => { + if (!viewport?.viewportOptions || !viewport.displaySetInstanceUIDs?.length) { + return; + } + + const { displaySetInstanceUIDs, viewportOptions } = viewport; + + let orientation = viewportOptions.orientation; + + if (!orientation) { + // Calculate orientation from the viewport sample image + const displaySet = servicesManager.services.displaySetService.getDisplaySetByUID( + displaySetInstanceUIDs[0] + ); + const sampleImage = displaySet.images?.[0]; + const imageOrientationPatient = sampleImage?.ImageOrientationPatient; + + orientation = getViewportOrientationFromImageOrientationPatient(imageOrientationPatient); + } + + const segmentationPresentationArr = []; + + segmentationPresentationArr.push(...displaySetInstanceUIDs); + + // Uncomment if unique indexing is needed + // addUniqueIndex( + // segmentationPresentationArr, + // 'segmentationPresentationId', + // viewports, + // isUpdatingSameViewport + // ); + + return segmentationPresentationArr.join(JOIN_STR); +}; + +/** + * Creates the Segmentation Presentation store. + * + * @param set - The zustand set function. + * @returns The Segmentation Presentation store state and actions. + */ +const createSegmentationPresentationStore = set => ({ + type: PRESENTATION_TYPE_ID, + segmentationPresentationStore: {}, + + /** + * Clears all segmentation presentations from the store. + */ + clearSegmentationPresentationStore: () => + set({ segmentationPresentationStore: {} }, false, 'clearSegmentationPresentationStore'), + + /** + * Adds a new segmentation presentation item to the store. + * + * segmentationPresentationItem: { + * segmentationId: string; + * type: SegmentationRepresentations; + * hydrated: boolean | null; + * config?: unknown; + * } + */ + addSegmentationPresentationItem: ( + presentationId: string, + segmentationPresentationItem: SegmentationPresentationItem + ) => + set( + state => ({ + segmentationPresentationStore: { + ...state.segmentationPresentationStore, + [presentationId]: [ + ...(state.segmentationPresentationStore[presentationId] || []), + segmentationPresentationItem, + ], + }, + }), + false, + 'addSegmentationPresentationItem' + ), + + /** + * Sets the segmentation presentation for a given presentation ID. A segmentation + * presentation is an array of SegmentationPresentationItem. + * + * segmentationPresentationItem: { + * segmentationId: string; + * type: SegmentationRepresentations; + * hydrated: boolean | null; + * config?: unknown; + * } + * + * segmentationPresentation: SegmentationPresentationItem[] + */ + setSegmentationPresentation: (presentationId: string, values: SegmentationPresentation) => + set( + state => ({ + segmentationPresentationStore: { + ...state.segmentationPresentationStore, + [presentationId]: values, + }, + }), + false, + 'setSegmentationPresentation' + ), + + /** + * Retrieves the presentation ID based on the provided parameters. + */ + getPresentationId, + + /** + * Retrieves the current segmentation presentation ID. + */ + getSegmentationPresentationId: _getSegmentationPresentationId, +}); + +/** + * Zustand store for managing segmentation presentations. + * Applies devtools middleware when DEBUG_STORE is enabled. + */ +export const useSegmentationPresentationStore = create()( + DEBUG_STORE + ? devtools(createSegmentationPresentationStore, { name: 'Segmentation Presentation Store' }) + : createSegmentationPresentationStore +); diff --git a/extensions/cornerstone/src/stores/useSynchronizersStore.ts b/extensions/cornerstone/src/stores/useSynchronizersStore.ts new file mode 100644 index 0000000..612a0c9 --- /dev/null +++ b/extensions/cornerstone/src/stores/useSynchronizersStore.ts @@ -0,0 +1,84 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +/** + * Identifier for the synchronizers store type. + */ +const PRESENTATION_TYPE_ID = 'synchronizersStoreId'; + +/** + * Flag to enable or disable debug mode for the store. + * Set to `true` to enable zustand devtools. + */ +const DEBUG_STORE = false; + +/** + * Information about a single synchronizer. + */ +type SynchronizerInfo = { + id: string; + type: string; + sourceViewports: Array<{ viewportId: string; renderingEngineId: string }>; + targetViewports: Array<{ viewportId: string; renderingEngineId: string }>; +}; + +/** + * State shape for the Synchronizers store. + */ +type SynchronizersState = { + /** + * Stores synchronizer information indexed by a unique key. + */ + synchronizersStore: Record; + + /** + * Sets the synchronizers for a specific viewport. + * + * @param viewportId - The ID of the viewport. + * @param synchronizers - An array of SynchronizerInfo. + */ + setSynchronizers: (viewportId: string, synchronizers: SynchronizerInfo[]) => void; + + /** + * Clears the entire synchronizers store. + */ + clearSynchronizersStore: () => void; +}; + +/** + * Creates the Synchronizers store. + * + * @param set - The zustand set function. + * @returns The synchronizers store state and actions. + */ +const createSynchronizersStore = (set): SynchronizersState => ({ + synchronizersStore: {}, + type: PRESENTATION_TYPE_ID, + + setSynchronizers: (viewportId: string, synchronizers: SynchronizerInfo[]) => { + set( + state => ({ + synchronizersStore: { + ...state.synchronizersStore, + [viewportId]: synchronizers, + }, + }), + false, + 'setSynchronizers' + ); + }, + + clearSynchronizersStore: () => { + set({ synchronizersStore: {} }, false, 'clearSynchronizersStore'); + }, +}); + +/** + * Zustand store for managing synchronizers. + * Applies devtools middleware when DEBUG_STORE is enabled. + */ +export const useSynchronizersStore = create()( + DEBUG_STORE + ? devtools(createSynchronizersStore, { name: 'SynchronizersStore' }) + : createSynchronizersStore +); diff --git a/extensions/cornerstone/src/synchronizers/frameViewSynchronizer.ts b/extensions/cornerstone/src/synchronizers/frameViewSynchronizer.ts new file mode 100644 index 0000000..3242656 --- /dev/null +++ b/extensions/cornerstone/src/synchronizers/frameViewSynchronizer.ts @@ -0,0 +1,51 @@ +import { SynchronizerManager, Synchronizer } from '@cornerstonejs/tools'; +import { EVENTS, getRenderingEngine, type Types, utilities } from '@cornerstonejs/core'; + +const frameViewSyncCallback = ( + synchronizerInstance: Synchronizer, + sourceViewport: Types.IViewportId, + targetViewport: Types.IViewportId +) => { + const renderingEngine = getRenderingEngine(targetViewport.renderingEngineId); + if (!renderingEngine) { + throw new Error(`No RenderingEngine for Id: ${targetViewport.renderingEngineId}`); + } + const sViewport = renderingEngine.getViewport(sourceViewport.viewportId) as Types.IStackViewport; + + const { viewportIndex: targetViewportIndex } = synchronizerInstance.getOptions( + targetViewport.viewportId + ); + + const { viewportIndex: sourceViewportIndex } = synchronizerInstance.getOptions( + sourceViewport.viewportId + ); + + if (targetViewportIndex === undefined || sourceViewportIndex === undefined) { + throw new Error('No viewportIndex provided'); + } + + const tViewport = renderingEngine.getViewport(targetViewport.viewportId) as Types.IStackViewport; + + const sourceSliceIndex = sViewport.getSliceIndex(); + const sliceDifference = Number(targetViewportIndex) - Number(sourceViewportIndex); + const targetSliceIndex = sourceSliceIndex + sliceDifference; + + if (targetSliceIndex === tViewport.getSliceIndex()) { + return; + } + + utilities.jumpToSlice(tViewport.element, { + imageIndex: targetSliceIndex, + }); +}; + +const createFrameViewSynchronizer = (synchronizerName: string): Synchronizer => { + const synchronizer = SynchronizerManager.createSynchronizer( + synchronizerName, + EVENTS.CAMERA_MODIFIED, + frameViewSyncCallback + ); + return synchronizer; +}; + +export { createFrameViewSynchronizer }; diff --git a/extensions/cornerstone/src/tools/CalibrationLineTool.ts b/extensions/cornerstone/src/tools/CalibrationLineTool.ts new file mode 100644 index 0000000..2c4f46d --- /dev/null +++ b/extensions/cornerstone/src/tools/CalibrationLineTool.ts @@ -0,0 +1,118 @@ +import { LengthTool, utilities } from '@cornerstonejs/tools'; +import { callInputDialog } from '@ohif/extension-default'; +import getActiveViewportEnabledElement from '../utils/getActiveViewportEnabledElement'; + +const { calibrateImageSpacing } = utilities; + +/** + * Calibration Line tool works almost the same as the + */ +class CalibrationLineTool extends LengthTool { + static toolName = 'CalibrationLine'; + + _renderingViewport: any; + _lengthToolRenderAnnotation = this.renderAnnotation; + + renderAnnotation = (enabledElement, svgDrawingHelper) => { + const { viewport } = enabledElement; + this._renderingViewport = viewport; + return this._lengthToolRenderAnnotation(enabledElement, svgDrawingHelper); + }; + + _getTextLines(data, targetId) { + const [canvasPoint1, canvasPoint2] = data.handles.points.map(p => + this._renderingViewport.worldToCanvas(p) + ); + // for display, round to 2 decimal points + const lengthPx = Math.round(calculateLength2(canvasPoint1, canvasPoint2) * 100) / 100; + + const textLines = [`${lengthPx}px`]; + + return textLines; + } +} + +function calculateLength2(point1, point2) { + const dx = point1[0] - point2[0]; + const dy = point1[1] - point2[1]; + return Math.sqrt(dx * dx + dy * dy); +} + +function calculateLength3(pos1, pos2) { + const dx = pos1[0] - pos2[0]; + const dy = pos1[1] - pos2[1]; + const dz = pos1[2] - pos2[2]; + + return Math.sqrt(dx * dx + dy * dy + dz * dz); +} + +export default CalibrationLineTool; + +export function onCompletedCalibrationLine( + servicesManager: AppTypes.ServicesManager, + csToolsEvent +) { + const { uiDialogService, viewportGridService } = servicesManager.services; + + // calculate length (mm) with the current Pixel Spacing + const annotationAddedEventDetail = csToolsEvent.detail; + const { + annotation: { metadata, data: annotationData }, + } = annotationAddedEventDetail; + const { referencedImageId: imageId } = metadata; + const enabledElement = getActiveViewportEnabledElement(viewportGridService); + const { viewport } = enabledElement; + + const length = + Math.round( + calculateLength3(annotationData.handles.points[0], annotationData.handles.points[1]) * 100 + ) / 100; + + const adjustCalibration = newLength => { + const spacingScale = newLength / length; + + // trigger resize of the viewport to adjust the world/pixel mapping + calibrateImageSpacing(imageId, viewport.getRenderingEngine(), { + type: 'User', + scale: 1 / spacingScale, + }); + }; + + return new Promise((resolve, reject) => { + if (!uiDialogService) { + reject('UIDialogService is not initiated'); + return; + } + + callInputDialog( + uiDialogService, + { + text: '', + label: `${length}`, + }, + (value, id) => { + if (id === 'save') { + adjustCalibration(Number.parseFloat(value)); + resolve(true); + } else { + reject('cancel'); + } + }, + false, + { + dialogTitle: 'Calibration', + inputLabel: 'Actual Physical distance (mm)', + + // the input value must be a number + validateFunc: val => { + try { + const v = Number.parseFloat(val); + return !isNaN(v) && v !== 0.0; + } catch { + return false; + } + }, + } + ); + }); +} diff --git a/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx b/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx new file mode 100644 index 0000000..f5d036e --- /dev/null +++ b/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx @@ -0,0 +1,269 @@ +import { VolumeViewport, metaData, utilities } from '@cornerstonejs/core'; +import { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/types'; +import { AnnotationDisplayTool, drawing } from '@cornerstonejs/tools'; +import { guid, b64toBlob } from '@ohif/core/src/utils'; +import OverlayPlaneModuleProvider from './OverlayPlaneModuleProvider'; + +interface CachedStat { + color: number[]; // [r, g, b, a] + overlays: { + // ...overlayPlaneModule + _id: string; + type: 'G' | 'R'; // G for Graphics, R for ROI + color?: number[]; // Rendered color [r, g, b, a] + dataUrl?: string; // Rendered image in Data URL expression + }[]; +} + +/** + * Image Overlay Viewer tool is not a traditional tool that requires user interactin. + * But it is used to display Pixel Overlays. And it will provide toggling capability. + * + * The documentation for Overlay Plane Module of DICOM can be found in [C.9.2 of + * Part-3 of DICOM standard](https://dicom.nema.org/medical/dicom/2018b/output/chtml/part03/sect_C.9.2.html) + * + * Image Overlay rendered by this tool can be toggled on and off using + * toolGroup.setToolEnabled() and toolGroup.setToolDisabled() + */ +class ImageOverlayViewerTool extends AnnotationDisplayTool { + static toolName = 'ImageOverlayViewer'; + + /** + * The overlay plane module provider add method is exposed here to be used + * when updating the overlay for this tool to use for displaying data. + */ + public static addOverlayPlaneModule = OverlayPlaneModuleProvider.add; + + constructor( + toolProps = {}, + defaultToolProps = { + supportedInteractionTypes: [], + configuration: { + fillColor: [255, 127, 127, 255], + }, + } + ) { + super(toolProps, defaultToolProps); + } + + onSetToolDisabled = (): void => {}; + + protected getReferencedImageId(viewport: IStackViewport | IVolumeViewport): string { + if (viewport instanceof VolumeViewport) { + return; + } + + const targetId = this.getTargetId(viewport); + return targetId.split('imageId:')[1]; + } + + renderAnnotation = (enabledElement, svgDrawingHelper) => { + const { viewport } = enabledElement; + + const imageId = this.getReferencedImageId(viewport); + if (!imageId) { + return; + } + + const overlayMetadata = metaData.get('overlayPlaneModule', imageId); + const overlays = overlayMetadata?.overlays; + + // no overlays + if (!overlays?.length) { + return; + } + + // Fix the x, y positions + overlays.forEach(overlay => { + overlay.x ||= 0; + overlay.y ||= 0; + }); + + // Will clear cached stat data when the overlay data changes + ImageOverlayViewerTool.addOverlayPlaneModule(imageId, overlayMetadata); + + this._getCachedStat(imageId, overlayMetadata, this.configuration.fillColor).then(cachedStat => { + cachedStat.overlays.forEach(overlay => { + this._renderOverlay(enabledElement, svgDrawingHelper, overlay); + }); + }); + + return true; + }; + + /** + * Render to DOM + * + * @param enabledElement + * @param svgDrawingHelper + * @param overlayData + * @returns + */ + private _renderOverlay(enabledElement, svgDrawingHelper, overlayData) { + const { viewport } = enabledElement; + const imageId = this.getReferencedImageId(viewport); + if (!imageId) { + return; + } + + // Decide the rendering position of the overlay image on the current canvas + const { _id, columns: width, rows: height, x, y } = overlayData; + const overlayTopLeftWorldPos = utilities.imageToWorldCoords(imageId, [ + x - 1, // Remind that top-left corner's (x, y) is be (1, 1) + y - 1, + ]); + const overlayTopLeftOnCanvas = viewport.worldToCanvas(overlayTopLeftWorldPos); + const overlayBottomRightWorldPos = utilities.imageToWorldCoords(imageId, [width, height]); + const overlayBottomRightOnCanvas = viewport.worldToCanvas(overlayBottomRightWorldPos); + + // add image to the annotations svg layer + const svgns = 'http://www.w3.org/2000/svg'; + const svgNodeHash = `image-overlay-${_id}`; + const existingImageElement = svgDrawingHelper.getSvgNode(svgNodeHash); + + const attributes = { + 'data-id': svgNodeHash, + width: overlayBottomRightOnCanvas[0] - overlayTopLeftOnCanvas[0], + height: overlayBottomRightOnCanvas[1] - overlayTopLeftOnCanvas[1], + x: overlayTopLeftOnCanvas[0], + y: overlayTopLeftOnCanvas[1], + href: overlayData.dataUrl, + }; + + if ( + isNaN(attributes.x) || + isNaN(attributes.y) || + isNaN(attributes.width) || + isNaN(attributes.height) + ) { + console.warn('Invalid rendering attribute for image overlay', attributes['data-id']); + return false; + } + + if (existingImageElement) { + drawing.setAttributesIfNecessary(attributes, existingImageElement); + svgDrawingHelper.setNodeTouched(svgNodeHash); + } else { + const newImageElement = document.createElementNS(svgns, 'image'); + drawing.setNewAttributesIfValid(attributes, newImageElement); + svgDrawingHelper.appendNode(newImageElement, svgNodeHash); + } + return true; + } + + private async _getCachedStat( + imageId: string, + overlayMetadata, + color: number[] + ): Promise { + const missingOverlay = overlayMetadata.overlays.filter( + overlay => overlay.pixelData && !overlay.dataUrl + ); + if (missingOverlay.length === 0) { + return overlayMetadata; + } + + const overlays = await Promise.all( + overlayMetadata.overlays + .filter(overlay => overlay.pixelData) + .map(async (overlay, idx) => { + let pixelData = null; + if (overlay.pixelData.Value) { + pixelData = overlay.pixelData.Value; + } else if (overlay.pixelData instanceof Array) { + pixelData = overlay.pixelData[0]; + } else if (overlay.pixelData.retrieveBulkData) { + pixelData = await overlay.pixelData.retrieveBulkData(); + } else if (overlay.pixelData.InlineBinary) { + const blob = b64toBlob(overlay.pixelData.InlineBinary); + const arrayBuffer = await blob.arrayBuffer(); + pixelData = arrayBuffer; + } + + if (!pixelData) { + return; + } + + const dataUrl = this._renderOverlayToDataUrl( + { width: overlay.columns, height: overlay.rows }, + overlay.color || color, + pixelData + ); + + return { + ...overlay, + _id: guid(), + dataUrl, // this will be a data url expression of the rendered image + color, + }; + }) + ); + overlayMetadata.overlays = overlays; + + return overlayMetadata; + } + + /** + * compare two RGBA expression of colors. + * + * @param color1 + * @param color2 + * @returns + */ + private _isSameColor(color1: number[], color2: number[]) { + return ( + color1 && + color2 && + color1[0] === color2[0] && + color1[1] === color2[1] && + color1[2] === color2[2] && + color1[3] === color2[3] + ); + } + + /** + * pixelData of overlayPlane module is an array of bits corresponding + * to each of the underlying pixels of the image. + * Let's create pixel data from bit array of overlay data + * + * @param pixelDataRaw + * @param color + * @returns + */ + private _renderOverlayToDataUrl({ width, height }, color, pixelDataRaw) { + const pixelDataView = new DataView(pixelDataRaw); + const totalBits = width * height; + + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, width, height); // make it transparent + ctx.globalCompositeOperation = 'copy'; + + const imageData = ctx.getImageData(0, 0, width, height); + const data = imageData.data; + for (let i = 0, bitIdx = 0, byteIdx = 0; i < totalBits; i++) { + if (pixelDataView.getUint8(byteIdx) & (1 << bitIdx)) { + data[i * 4] = color[0]; + data[i * 4 + 1] = color[1]; + data[i * 4 + 2] = color[2]; + data[i * 4 + 3] = color[3]; + } + + // next bit, byte + if (bitIdx >= 7) { + bitIdx = 0; + byteIdx++; + } else { + bitIdx++; + } + } + ctx.putImageData(imageData, 0, 0); + + return canvas.toDataURL(); + } +} + +export default ImageOverlayViewerTool; diff --git a/extensions/cornerstone/src/tools/OverlayPlaneModuleProvider.ts b/extensions/cornerstone/src/tools/OverlayPlaneModuleProvider.ts new file mode 100644 index 0000000..d36b543 --- /dev/null +++ b/extensions/cornerstone/src/tools/OverlayPlaneModuleProvider.ts @@ -0,0 +1,40 @@ +import { metaData } from '@cornerstonejs/core'; + +const _cachedOverlayMetadata: Map = new Map(); + +/** + * Image Overlay Viewer tool is not a traditional tool that requires user interactin. + * But it is used to display Pixel Overlays. And it will provide toggling capability. + * + * The documentation for Overlay Plane Module of DICOM can be found in [C.9.2 of + * Part-3 of DICOM standard](https://dicom.nema.org/medical/dicom/2018b/output/chtml/part03/sect_C.9.2.html) + * + * Image Overlay rendered by this tool can be toggled on and off using + * toolGroup.setToolEnabled() and toolGroup.setToolDisabled() + */ +const OverlayPlaneModuleProvider = { + /** Adds the metadata for overlayPlaneModule */ + add: (imageId, metadata) => { + if (_cachedOverlayMetadata.get(imageId) === metadata) { + // This is a no-op here as the tool re-caches the data + return; + } + _cachedOverlayMetadata.set(imageId, metadata); + }, + + /** Standard getter for metadata */ + get: (type: string, query: string | string[]) => { + if (Array.isArray(query)) { + return; + } + if (type !== 'overlayPlaneModule') { + return; + } + return _cachedOverlayMetadata.get(query); + }, +}; + +// Needs to be higher priority than default provider +metaData.addProvider(OverlayPlaneModuleProvider.get, 10_000); + +export default OverlayPlaneModuleProvider; diff --git a/extensions/cornerstone/src/types/AppTypes.ts b/extensions/cornerstone/src/types/AppTypes.ts new file mode 100644 index 0000000..36cb848 --- /dev/null +++ b/extensions/cornerstone/src/types/AppTypes.ts @@ -0,0 +1,58 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +import CornerstoneCacheServiceType from '../services/CornerstoneCacheService'; +import CornerstoneViewportServiceType from '../services/ViewportService/CornerstoneViewportService'; +import SegmentationServiceType from '../services/SegmentationService'; +import SyncGroupServiceType from '../services/SyncGroupService'; +import ToolGroupServiceType from '../services/ToolGroupService'; +import ViewportActionCornersServiceType from '../services/ViewportActionCornersService/ViewportActionCornersService'; +import ColorbarServiceType from '../services/ColorbarService'; +import * as cornerstone from '@cornerstonejs/core'; +import * as cornerstoneTools from '@cornerstonejs/tools'; + +import type { + SegmentRepresentation as SegmentRep, + SegmentationData as SegData, + SegmentationRepresentation as SegRep, + SegmentationInfo as SegInfo, +} from '../services/SegmentationService/SegmentationService'; + +declare global { + namespace AppTypes { + export type CornerstoneCacheService = CornerstoneCacheServiceType; + export type CornerstoneViewportService = CornerstoneViewportServiceType; + export type SegmentationService = SegmentationServiceType; + export type SyncGroupService = SyncGroupServiceType; + export type ToolGroupService = ToolGroupServiceType; + export type ViewportActionCornersService = ViewportActionCornersServiceType; + export type ColorbarService = ColorbarServiceType; + + export interface Services { + cornerstoneViewportService?: CornerstoneViewportServiceType; + toolGroupService?: ToolGroupServiceType; + syncGroupService?: SyncGroupServiceType; + segmentationService?: SegmentationServiceType; + cornerstoneCacheService?: CornerstoneCacheServiceType; + viewportActionCornersService?: ViewportActionCornersServiceType; + colorbarService?: ColorbarServiceType; + } + + export namespace Segmentation { + export type SegmentRepresentation = SegmentRep; + export type SegmentationData = SegData; + export type SegmentationRepresentation = SegRep; + export type SegmentationInfo = SegInfo; + } + + export interface PresentationIds { + lutPresentationId: string; + positionPresentationId: string; + segmentationPresentationId: string; + } + + export interface Test { + services?: Services; + cornerstone?: typeof cornerstone; + cornerstoneTools?: typeof cornerstoneTools; + } + } +} diff --git a/extensions/cornerstone/src/types/Colorbar.ts b/extensions/cornerstone/src/types/Colorbar.ts new file mode 100644 index 0000000..9eea755 --- /dev/null +++ b/extensions/cornerstone/src/types/Colorbar.ts @@ -0,0 +1,29 @@ +import { ColorMapPreset } from './Colormap'; + +export type ColorbarOptions = { + position: string; + colormaps: Array; + activeColormapName: string; + ticks: object; + width: string; +}; + +export type ColorbarProps = { + viewportId: string; + displaySets: Array; + colorbarProperties: ColorbarProperties; +}; + +export type ColorbarProperties = { + width: string; + colorbarTickPosition: string; + colorbarContainerPosition: string; + colormaps: Array; + colorbarInitialColormap: string; +}; + +export enum ChangeTypes { + Removed = 'removed', + Added = 'added', + Modified = 'modified', +} diff --git a/extensions/cornerstone/src/types/Colormap.ts b/extensions/cornerstone/src/types/Colormap.ts new file mode 100644 index 0000000..6e30322 --- /dev/null +++ b/extensions/cornerstone/src/types/Colormap.ts @@ -0,0 +1,16 @@ +import { CommandsManager } from '@ohif/core'; + +export type ColorMapPreset = { + ColorSpace; + description: string; + RGBPoints; + Name; +}; + +export type ColormapProps = { + viewportId: string; + commandsManager: CommandsManager; + servicesManager: AppTypes.ServicesManager; + colormaps: Array; + displaySets: Array; +}; diff --git a/extensions/cornerstone/src/types/CornerstoneCacheService.ts b/extensions/cornerstone/src/types/CornerstoneCacheService.ts new file mode 100644 index 0000000..261d1b8 --- /dev/null +++ b/extensions/cornerstone/src/types/CornerstoneCacheService.ts @@ -0,0 +1,32 @@ +import { Enums, Types } from '@cornerstonejs/core'; + +type StackData = { + StudyInstanceUID: string; + displaySetInstanceUID: string; + // A composite stack is one created from other display sets - kind of like + // madeInClient, but specific to indicating that the imageIds can come from + // different series or even studies. + isCompositeStack?: boolean; + imageIds: string[]; + frameRate?: number; + initialImageIndex?: number | string | null; +}; + +type VolumeData = { + studyInstanceUID: string; + displaySetInstanceUID: string; + volume?: Types.IVolume; + imageIds?: string[]; +}; + +type StackViewportData = { + viewportType: Enums.ViewportType; + data: StackData[]; +}; + +type VolumeViewportData = { + viewportType: Enums.ViewportType; + data: VolumeData[]; +}; + +export type { StackViewportData, VolumeViewportData, StackData, VolumeData }; diff --git a/extensions/cornerstone/src/types/CornerstoneServices.ts b/extensions/cornerstone/src/types/CornerstoneServices.ts new file mode 100644 index 0000000..b11dc97 --- /dev/null +++ b/extensions/cornerstone/src/types/CornerstoneServices.ts @@ -0,0 +1,20 @@ +import { Types } from '@ohif/core'; +import ToolGroupService from '../services/ToolGroupService'; +import SyncGroupService from '../services/SyncGroupService'; +import SegmentationService from '../services/SegmentationService'; +import CornerstoneCacheService from '../services/CornerstoneCacheService'; +import CornerstoneViewportService from '../services/ViewportService/CornerstoneViewportService'; +import ViewportActionCornersService from '../services/ViewportActionCornersService/ViewportActionCornersService'; +import ColorbarService from '../services/ColorbarService'; + +interface CornerstoneServices extends Types.Services { + cornerstoneViewportService: CornerstoneViewportService; + toolGroupService: ToolGroupService; + syncGroupService: SyncGroupService; + segmentationService: SegmentationService; + cornerstoneCacheService: CornerstoneCacheService; + viewportActionCornersService: ViewportActionCornersService; + colorbarService: ColorbarService; +} + +export default CornerstoneServices; diff --git a/extensions/cornerstone/src/types/Presentation.ts b/extensions/cornerstone/src/types/Presentation.ts new file mode 100644 index 0000000..1d3d0a9 --- /dev/null +++ b/extensions/cornerstone/src/types/Presentation.ts @@ -0,0 +1,76 @@ +import type { Types } from '@cornerstonejs/core'; +import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; + +/** + * Represents a position presentation in a viewport. This is basically + * viewport specific camera position and zoom, and not the display set + */ +export type PositionPresentation = { + viewportType: string; + // The view reference has the basic information as to what image orientation/slice is shown + viewReference: Types.ViewReference; + // The position information has the zoom/pan and possibly other related information, but not LUT + viewPresentation: Types.ViewPresentation; + /** + * Optionals + */ + initialImageIndex?: number; + // viewportId helps when hydrating SR or SEG - we can use it to filter + // presentations to get the one prior to the current viewport and reuse it + // for viewReference and viewPresentation + viewportId?: string; +}; + +/** + * Represents a LUT presentation in a viewport, and is really related + * to displaySets and not the viewport itself. So that is why it can + * be an object with volumeId keys, or a single object with the properties + * itself + */ +export interface LutPresentation { + viewportType: string; + // either a single object with the properties itself or a map of properties with volumeId keys + properties: Record | Types.ViewportProperties; +} + +/** + * Represents a LUT presentation in a viewport, and is really related + * to displaySets and not the viewport itself. So that is why it can + * be an object with volumeId keys, or a single object with the properties + * itself + * + * each presentation has a segmentationId and a type and a value for + * hydrated and config. + * + * The hydrated property can be a boolean or null. It's null if the segmentation + * representation hasn't been created yet. It's true if the representation is + * currently in the viewport. It's false if the representation was in the viewport + * but has been removed. + * + * Config is the segmentation config, Todo: add stuff here + */ +export type SegmentationPresentationItem = { + segmentationId: string; + type: SegmentationRepresentations; + hydrated: boolean | null; + config?: unknown; +}; + +export type SegmentationPresentation = SegmentationPresentationItem[]; + +/** + * Presentation can be a PositionPresentation or a LutPresentation. + */ +type Presentation = PositionPresentation | LutPresentation | SegmentationPresentation; + +/** + * Viewport presentations object that can contain a positionPresentation + * and or a lutPresentation. + */ +export type Presentations = { + positionPresentation?: PositionPresentation; + lutPresentation?: LutPresentation; + segmentationPresentation?: SegmentationPresentation; +}; + +export default Presentation; diff --git a/extensions/cornerstone/src/types/ViewportPresets.ts b/extensions/cornerstone/src/types/ViewportPresets.ts new file mode 100644 index 0000000..fc27bd3 --- /dev/null +++ b/extensions/cornerstone/src/types/ViewportPresets.ts @@ -0,0 +1,68 @@ +import { CommandsManager } from '@ohif/core'; + +export type ViewportPreset = { + name: string; + gradientOpacity: string; + specularPower: string; + scalarOpacity: string; + specular: string; + shade: string; + ambient: string; + colorTransfer: string; + diffuse: string; + interpolation: string; +}; + +export type VolumeRenderingPresetsProps = { + viewportId: string; + servicesManager: AppTypes.ServicesManager; + commandsManager: CommandsManager; + volumeRenderingPresets: ViewportPreset[]; +}; + +export type VolumeRenderingPresetsContentProps = { + presets: ViewportPreset[]; + onClose: () => void; + viewportId: string; + commandsManager: CommandsManager; +}; + +export type VolumeRenderingOptionsProps = { + viewportId: string; + commandsManager: CommandsManager; + servicesManager: AppTypes.ServicesManager; + volumeRenderingQualityRange: VolumeRenderingQualityRange; +}; + +export type VolumeRenderingQualityRange = { + min: number; + max: number; + step: number; +}; + +export type VolumeRenderingQualityProps = { + viewportId: string; + commandsManager: CommandsManager; + servicesManager: AppTypes.ServicesManager; + volumeRenderingQualityRange: VolumeRenderingQualityRange; +}; + +export type VolumeShiftProps = { + viewportId: string; + commandsManager: CommandsManager; + servicesManager: AppTypes.ServicesManager; +}; + +export type VolumeShadeProps = { + viewportId: string; + commandsManager: CommandsManager; + servicesManager: AppTypes.ServicesManager; + onClickShade?: (bool: boolean) => void; +}; + +export type VolumeLightingProps = { + viewportId: string; + commandsManager: CommandsManager; + servicesManager: AppTypes.ServicesManager; + hasShade: boolean; +}; diff --git a/extensions/cornerstone/src/types/WindowLevel.ts b/extensions/cornerstone/src/types/WindowLevel.ts new file mode 100644 index 0000000..556d0ca --- /dev/null +++ b/extensions/cornerstone/src/types/WindowLevel.ts @@ -0,0 +1,5 @@ +export type WindowLevelPreset = { + description: string; + window: string; + level: string; +}; diff --git a/extensions/cornerstone/src/types/index.ts b/extensions/cornerstone/src/types/index.ts new file mode 100644 index 0000000..c8db85f --- /dev/null +++ b/extensions/cornerstone/src/types/index.ts @@ -0,0 +1,4 @@ +import * as CornerstoneCacheService from './CornerstoneCacheService'; +import CornerstoneServices from './CornerstoneServices'; + +export type { CornerstoneCacheService, CornerstoneServices }; diff --git a/extensions/cornerstone/src/utils/ActiveViewportBehavior.tsx b/extensions/cornerstone/src/utils/ActiveViewportBehavior.tsx new file mode 100644 index 0000000..89a2600 --- /dev/null +++ b/extensions/cornerstone/src/utils/ActiveViewportBehavior.tsx @@ -0,0 +1,93 @@ +import { useEffect, useState, memo, useCallback } from 'react'; + +const ActiveViewportBehavior = memo( + ({ servicesManager, viewportId }: withAppTypes<{ viewportId: string }>) => { + const { + displaySetService, + cineService, + viewportGridService, + customizationService, + cornerstoneViewportService, + } = servicesManager.services; + + const [activeViewportId, setActiveViewportId] = useState(viewportId); + + const handleCineEnable = useCallback(() => { + if (cineService.isViewportCineClosed(activeViewportId)) { + return; + } + + const displaySetInstanceUIDs = + viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId); + + if (!displaySetInstanceUIDs) { + return; + } + + const displaySets = displaySetInstanceUIDs.map(uid => + displaySetService.getDisplaySetByUID(uid) + ); + + if (!displaySets.length) { + return; + } + + const modalities = displaySets.map(displaySet => displaySet?.Modality); + const isDynamicVolume = displaySets.some(displaySet => displaySet?.isDynamicVolume); + + const sourceModalities = customizationService.getCustomization('autoCineModalities'); + + const requiresCine = modalities.some(modality => sourceModalities.includes(modality)); + + if ((requiresCine || isDynamicVolume) && !cineService.getState().isCineEnabled) { + cineService.setIsCineEnabled(true); + } + }, [ + activeViewportId, + cineService, + viewportGridService, + displaySetService, + customizationService, + ]); + + useEffect(() => { + const subscription = viewportGridService.subscribe( + viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED, + ({ viewportId }) => setActiveViewportId(viewportId) + ); + + return () => subscription.unsubscribe(); + }, [viewportId, viewportGridService]); + + useEffect(() => { + const subscription = cornerstoneViewportService.subscribe( + cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED, + () => { + const activeViewportId = viewportGridService.getActiveViewportId(); + setActiveViewportId(activeViewportId); + handleCineEnable(); + } + ); + + return () => subscription.unsubscribe(); + }, [viewportId, cornerstoneViewportService, viewportGridService, handleCineEnable]); + + useEffect(() => { + handleCineEnable(); + }, [handleCineEnable]); + + return null; + }, + arePropsEqual +); + +ActiveViewportBehavior.displayName = 'ActiveViewportBehavior'; + +function arePropsEqual(prevProps, nextProps) { + return ( + prevProps.viewportId === nextProps.viewportId && + prevProps.servicesManager === nextProps.servicesManager + ); +} + +export default ActiveViewportBehavior; diff --git a/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx b/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx new file mode 100644 index 0000000..5e72d37 --- /dev/null +++ b/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx @@ -0,0 +1,248 @@ +import React, { useEffect } from 'react'; +import html2canvas from 'html2canvas'; +import { + Enums, + getEnabledElement, + getOrCreateCanvas, + StackViewport, + BaseVolumeViewport, +} from '@cornerstonejs/core'; +import { ToolGroupManager } from '@cornerstonejs/tools'; +import { ViewportDownloadForm } from '@ohif/ui'; + +import { getEnabledElement as OHIFgetEnabledElement } from '../state'; + +const MINIMUM_SIZE = 100; +const DEFAULT_SIZE = 512; +const MAX_TEXTURE_SIZE = 10000; +const VIEWPORT_ID = 'cornerstone-viewport-download-form'; + +const CornerstoneViewportDownloadForm = ({ + onClose, + activeViewportId: activeViewportIdProp, + cornerstoneViewportService, +}: withAppTypes) => { + const enabledElement = OHIFgetEnabledElement(activeViewportIdProp); + const activeViewportElement = enabledElement?.element; + const activeViewportEnabledElement = getEnabledElement(activeViewportElement); + + const { + viewportId: activeViewportId, + renderingEngineId, + viewport: activeViewport, + } = activeViewportEnabledElement; + + const toolGroup = ToolGroupManager.getToolGroupForViewport(activeViewportId, renderingEngineId); + + const toolModeAndBindings = Object.keys(toolGroup.toolOptions).reduce((acc, toolName) => { + const tool = toolGroup.toolOptions[toolName]; + const { mode, bindings } = tool; + + return { + ...acc, + [toolName]: { + mode, + bindings, + }, + }; + }, {}); + + useEffect(() => { + return () => { + Object.keys(toolModeAndBindings).forEach(toolName => { + const { mode, bindings } = toolModeAndBindings[toolName]; + toolGroup.setToolMode(toolName, mode, { bindings }); + }); + }; + }, []); + + const enableViewport = viewportElement => { + if (viewportElement) { + const { renderingEngine, viewport } = getEnabledElement(activeViewportElement); + + const viewportInput = { + viewportId: VIEWPORT_ID, + element: viewportElement, + type: viewport.type, + defaultOptions: { + background: viewport.defaultOptions.background, + orientation: viewport.defaultOptions.orientation, + }, + }; + + renderingEngine.enableElement(viewportInput); + } + }; + + const disableViewport = viewportElement => { + if (viewportElement) { + const { renderingEngine } = getEnabledElement(viewportElement); + return new Promise(resolve => { + renderingEngine.disableElement(VIEWPORT_ID); + }); + } + }; + + const updateViewportPreview = (downloadViewportElement, internalCanvas, fileType) => + new Promise(resolve => { + const enabledElement = getEnabledElement(downloadViewportElement); + + const { viewport: downloadViewport, renderingEngine } = enabledElement; + + // Note: Since any trigger of dimensions will update the viewport, + // we need to resize the offScreenCanvas to accommodate for the new + // dimensions, this is due to the reason that we are using the GPU offScreenCanvas + // to render the viewport for the downloadViewport. + renderingEngine.resize(); + + // Trigger the render on the viewport to update the on screen + // downloadViewport.resetCamera(); + downloadViewport.render(); + + downloadViewportElement.addEventListener( + Enums.Events.IMAGE_RENDERED, + function updateViewport(event) { + const enabledElement = getEnabledElement(event.target); + const { viewport } = enabledElement; + const { element } = viewport; + + const downloadCanvas = getOrCreateCanvas(element); + + const type = 'image/' + fileType; + const dataUrl = downloadCanvas.toDataURL(type, 1); + + let newWidth = element.offsetHeight; + let newHeight = element.offsetWidth; + + if (newWidth > DEFAULT_SIZE || newHeight > DEFAULT_SIZE) { + const multiplier = DEFAULT_SIZE / Math.max(newWidth, newHeight); + newHeight *= multiplier; + newWidth *= multiplier; + } + + resolve({ dataUrl, width: newWidth, height: newHeight }); + + downloadViewportElement.removeEventListener(Enums.Events.IMAGE_RENDERED, updateViewport); + + // for some reason we need a reset camera here, and I don't know why + downloadViewport.resetCamera(); + const presentation = activeViewport.getViewPresentation(); + if (downloadViewport.setView) { + downloadViewport.setView(activeViewport.getViewReference(), presentation); + } + downloadViewport.render(); + } + ); + }); + + const loadImage = (activeViewportElement, viewportElement, width, height) => + new Promise(resolve => { + if (activeViewportElement && viewportElement) { + const activeViewportEnabledElement = getEnabledElement(activeViewportElement); + + if (!activeViewportEnabledElement) { + return; + } + + const { viewport } = activeViewportEnabledElement; + + const renderingEngine = cornerstoneViewportService.getRenderingEngine(); + const downloadViewport = renderingEngine.getViewport(VIEWPORT_ID); + + if (downloadViewport instanceof StackViewport) { + const imageId = viewport.getCurrentImageId(); + const properties = viewport.getProperties(); + + downloadViewport.setStack([imageId]).then(() => { + try { + downloadViewport.setProperties(properties); + const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE); + const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE); + + resolve({ width: newWidth, height: newHeight }); + } catch (e) { + // Happens on clicking the cancel button + console.warn('Unable to set properties', e); + } + }); + } else if (downloadViewport instanceof BaseVolumeViewport) { + const actors = viewport.getActors(); + // downloadViewport.setActors(actors); + actors.forEach(actor => { + downloadViewport.addActor(actor); + }); + + downloadViewport.render(); + + const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE); + const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE); + + resolve({ width: newWidth, height: newHeight }); + } + } + }); + + const toggleAnnotations = (toggle, viewportElement, activeViewportElement) => { + const activeViewportEnabledElement = getEnabledElement(activeViewportElement); + + const downloadViewportElement = getEnabledElement(viewportElement); + + const { viewportId: activeViewportId, renderingEngineId } = activeViewportEnabledElement; + const { viewportId: downloadViewportId } = downloadViewportElement; + + if (!activeViewportEnabledElement || !downloadViewportElement) { + return; + } + + const toolGroup = ToolGroupManager.getToolGroupForViewport(activeViewportId, renderingEngineId); + + // add the viewport to the toolGroup + toolGroup.addViewport(downloadViewportId, renderingEngineId); + + Object.keys(toolGroup.getToolInstances()).forEach(toolName => { + // make all tools Enabled so that they can not be interacted with + // in the download viewport + if (toggle && toolName !== 'Crosshairs') { + try { + toolGroup.setToolEnabled(toolName); + } catch (e) { + console.log(e); + } + } else { + toolGroup.setToolDisabled(toolName); + } + }); + }; + + const downloadBlob = (filename, fileType) => { + const file = `${filename}.${fileType}`; + const divForDownloadViewport = document.querySelector( + `div[data-viewport-uid="${VIEWPORT_ID}"]` + ); + + html2canvas(divForDownloadViewport).then(canvas => { + const link = document.createElement('a'); + link.download = file; + link.href = canvas.toDataURL(fileType, 1.0); + link.click(); + }); + }; + + return ( + + ); +}; + +export default CornerstoneViewportDownloadForm; diff --git a/extensions/cornerstone/src/utils/DicomFileUploader.ts b/extensions/cornerstone/src/utils/DicomFileUploader.ts new file mode 100644 index 0000000..cfbad07 --- /dev/null +++ b/extensions/cornerstone/src/utils/DicomFileUploader.ts @@ -0,0 +1,204 @@ +import dicomImageLoader from '@cornerstonejs/dicom-image-loader'; + +import { PubSubService } from '@ohif/core'; + +export const EVENTS = { + PROGRESS: 'event:DicomFileUploader:progress', +}; + +export interface DicomFileUploaderEvent { + fileId: number; +} + +export interface DicomFileUploaderProgressEvent extends DicomFileUploaderEvent { + percentComplete: number; +} + +export enum UploadStatus { + NotStarted, + InProgress, + Success, + Failed, + Cancelled, +} + +type CancelOrFailed = UploadStatus.Cancelled | UploadStatus.Failed; + +export class UploadRejection { + message: string; + status: CancelOrFailed; + + constructor(status: CancelOrFailed, message: string) { + this.message = message; + this.status = status; + } +} + +export default class DicomFileUploader extends PubSubService { + private _file; + private _fileId; + private _dataSource; + private _loadPromise; + private _abortController = new AbortController(); + private _status: UploadStatus = UploadStatus.NotStarted; + private _percentComplete = 0; + + constructor(file, dataSource) { + super(EVENTS); + this._file = file; + this._fileId = dicomImageLoader.wadouri.fileManager.add(file); + this._dataSource = dataSource; + } + + getFileId(): string { + return this._fileId; + } + + getFileName(): string { + return this._file.name; + } + + getFileSize(): number { + return this._file.size; + } + + cancel(): void { + this._abortController.abort(); + } + + getStatus(): UploadStatus { + return this._status; + } + + getPercentComplete(): number { + return this._percentComplete; + } + + async load(): Promise { + if (this._loadPromise) { + // Already started loading, return the load promise. + return this._loadPromise; + } + + this._loadPromise = new Promise((resolve, reject) => { + // The upload listeners: fire progress events and/or settle the promise. + const uploadCallbacks = { + progress: evt => { + if (!evt.lengthComputable) { + // Progress computation is not possible. + return; + } + + this._status = UploadStatus.InProgress; + + this._percentComplete = Math.round((100 * evt.loaded) / evt.total); + this._broadcastEvent(EVENTS.PROGRESS, { + fileId: this._fileId, + percentComplete: this._percentComplete, + }); + }, + timeout: () => { + this._reject(reject, new UploadRejection(UploadStatus.Failed, 'The request timed out.')); + }, + abort: () => { + this._reject(reject, new UploadRejection(UploadStatus.Cancelled, 'Cancelled')); + }, + error: () => { + this._reject(reject, new UploadRejection(UploadStatus.Failed, 'The request failed.')); + }, + }; + + // First try to load the file. + dicomImageLoader.wadouri + .loadFileRequest(this._fileId) + .then(dicomFile => { + if (this._abortController.signal.aborted) { + this._reject(reject, new UploadRejection(UploadStatus.Cancelled, 'Cancelled')); + return; + } + + if (!this._checkDicomFile(dicomFile)) { + // The file is not DICOM + this._reject( + reject, + new UploadRejection(UploadStatus.Failed, 'Not a valid DICOM file.') + ); + return; + } + + const request = new XMLHttpRequest(); + this._addRequestCallbacks(request, uploadCallbacks); + + // Do the actual upload by supplying the DICOM file and upload callbacks/listeners. + return this._dataSource.store + .dicom(dicomFile, request) + .then(() => { + this._status = UploadStatus.Success; + resolve(); + }) + .catch(reason => { + this._reject(reject, reason); + }); + }) + .catch(reason => { + this._reject(reject, reason); + }); + }); + + return this._loadPromise; + } + + private _isRejected(): boolean { + return this._status === UploadStatus.Failed || this._status === UploadStatus.Cancelled; + } + + private _reject(reject: (reason?: any) => void, reason: any) { + if (this._isRejected()) { + return; + } + + if (reason instanceof UploadRejection) { + this._status = reason.status; + reject(reason); + return; + } + + this._status = UploadStatus.Failed; + + if (reason.message) { + reject(new UploadRejection(UploadStatus.Failed, reason.message)); + return; + } + + reject(new UploadRejection(UploadStatus.Failed, reason)); + } + + private _addRequestCallbacks(request: XMLHttpRequest, uploadCallbacks) { + const abortCallback = () => request.abort(); + this._abortController.signal.addEventListener('abort', abortCallback); + + for (const [eventName, callback] of Object.entries(uploadCallbacks)) { + request.upload.addEventListener(eventName, callback); + } + + const cleanUpCallback = () => { + this._abortController.signal.removeEventListener('abort', abortCallback); + + for (const [eventName, callback] of Object.entries(uploadCallbacks)) { + request.upload.removeEventListener(eventName, callback); + } + + request.removeEventListener('loadend', cleanUpCallback); + }; + request.addEventListener('loadend', cleanUpCallback); + } + + private _checkDicomFile(arrayBuffer: ArrayBuffer) { + if (arrayBuffer.length <= 132) { + return false; + } + const arr = new Uint8Array(arrayBuffer.slice(128, 132)); + // bytes from 128 to 132 must be "DICM" + return Array.from('DICM').every((char, i) => char.charCodeAt(0) === arr[i]); + } +} diff --git a/extensions/cornerstone/src/utils/JumpPresets.ts b/extensions/cornerstone/src/utils/JumpPresets.ts new file mode 100644 index 0000000..e9417de --- /dev/null +++ b/extensions/cornerstone/src/utils/JumpPresets.ts @@ -0,0 +1,14 @@ +/** + * Jump Presets - This enum defines the 3 jump states which are available + * to be used with the jumpToSlice utility function. + */ +enum JumpPresets { + /** Jumps to first slice */ + First = 'first', + /** Jumps to last slice */ + Last = 'last', + /** Jumps to the middle slice */ + Middle = 'middle', +} + +export default JumpPresets; diff --git a/extensions/cornerstone/src/utils/colormaps.js b/extensions/cornerstone/src/utils/colormaps.js new file mode 100644 index 0000000..a21df96 --- /dev/null +++ b/extensions/cornerstone/src/utils/colormaps.js @@ -0,0 +1,1600 @@ +const colormaps = [ + { + ColorSpace: 'RGB', + Name: 'Grayscale', + name: 'Grayscale', + NanColor: [1, 0, 0], + RGBPoints: [0, 0, 0, 0, 1, 1, 1, 1], + description: 'Grayscale', + }, + { + ColorSpace: 'RGB', + Name: 'X Ray', + name: 'X Ray', + NanColor: [1, 0, 0], + RGBPoints: [0, 1, 1, 1, 1, 0, 0, 0], + description: 'X Ray', + }, + { + ColorSpace: 'RGB', + Name: 'hsv', + name: 'hsv', + RGBPoints: [ + -1, 1, 0, 0, -0.666666, 1, 0, 1, -0.333333, 0, 0, 1, 0, 0, 1, 1, 0.33333, 0, 1, 0, 0.66666, 1, + 1, 0, 1, 1, 0, 0, + ], + description: 'HSV', + }, + { + ColorSpace: 'RGB', + Name: 'hot_iron', + name: 'hot_iron', + RGBPoints: [ + 0.0, 0.0039215686, 0.0039215686, 0.0156862745, 0.00392156862745098, 0.0039215686, + 0.0039215686, 0.0156862745, 0.00784313725490196, 0.0039215686, 0.0039215686, 0.031372549, + 0.011764705882352941, 0.0039215686, 0.0039215686, 0.0470588235, 0.01568627450980392, + 0.0039215686, 0.0039215686, 0.062745098, 0.0196078431372549, 0.0039215686, 0.0039215686, + 0.0784313725, 0.023529411764705882, 0.0039215686, 0.0039215686, 0.0941176471, + 0.027450980392156862, 0.0039215686, 0.0039215686, 0.1098039216, 0.03137254901960784, + 0.0039215686, 0.0039215686, 0.1254901961, 0.03529411764705882, 0.0039215686, 0.0039215686, + 0.1411764706, 0.0392156862745098, 0.0039215686, 0.0039215686, 0.1568627451, + 0.043137254901960784, 0.0039215686, 0.0039215686, 0.1725490196, 0.047058823529411764, + 0.0039215686, 0.0039215686, 0.1882352941, 0.050980392156862744, 0.0039215686, 0.0039215686, + 0.2039215686, 0.054901960784313725, 0.0039215686, 0.0039215686, 0.2196078431, + 0.05882352941176471, 0.0039215686, 0.0039215686, 0.2352941176, 0.06274509803921569, + 0.0039215686, 0.0039215686, 0.2509803922, 0.06666666666666667, 0.0039215686, 0.0039215686, + 0.262745098, 0.07058823529411765, 0.0039215686, 0.0039215686, 0.2784313725, + 0.07450980392156863, 0.0039215686, 0.0039215686, 0.2941176471, 0.0784313725490196, + 0.0039215686, 0.0039215686, 0.3098039216, 0.08235294117647059, 0.0039215686, 0.0039215686, + 0.3254901961, 0.08627450980392157, 0.0039215686, 0.0039215686, 0.3411764706, + 0.09019607843137255, 0.0039215686, 0.0039215686, 0.3568627451, 0.09411764705882353, + 0.0039215686, 0.0039215686, 0.3725490196, 0.09803921568627451, 0.0039215686, 0.0039215686, + 0.3882352941, 0.10196078431372549, 0.0039215686, 0.0039215686, 0.4039215686, + 0.10588235294117647, 0.0039215686, 0.0039215686, 0.4196078431, 0.10980392156862745, + 0.0039215686, 0.0039215686, 0.4352941176, 0.11372549019607843, 0.0039215686, 0.0039215686, + 0.4509803922, 0.11764705882352942, 0.0039215686, 0.0039215686, 0.4666666667, + 0.12156862745098039, 0.0039215686, 0.0039215686, 0.4823529412, 0.12549019607843137, + 0.0039215686, 0.0039215686, 0.4980392157, 0.12941176470588237, 0.0039215686, 0.0039215686, + 0.5137254902, 0.13333333333333333, 0.0039215686, 0.0039215686, 0.5294117647, + 0.13725490196078433, 0.0039215686, 0.0039215686, 0.5450980392, 0.1411764705882353, + 0.0039215686, 0.0039215686, 0.5607843137, 0.1450980392156863, 0.0039215686, 0.0039215686, + 0.5764705882, 0.14901960784313725, 0.0039215686, 0.0039215686, 0.5921568627, + 0.15294117647058825, 0.0039215686, 0.0039215686, 0.6078431373, 0.1568627450980392, + 0.0039215686, 0.0039215686, 0.6235294118, 0.1607843137254902, 0.0039215686, 0.0039215686, + 0.6392156863, 0.16470588235294117, 0.0039215686, 0.0039215686, 0.6549019608, + 0.16862745098039217, 0.0039215686, 0.0039215686, 0.6705882353, 0.17254901960784313, + 0.0039215686, 0.0039215686, 0.6862745098, 0.17647058823529413, 0.0039215686, 0.0039215686, + 0.7019607843, 0.1803921568627451, 0.0039215686, 0.0039215686, 0.7176470588, + 0.1843137254901961, 0.0039215686, 0.0039215686, 0.7333333333, 0.18823529411764706, + 0.0039215686, 0.0039215686, 0.7490196078, 0.19215686274509805, 0.0039215686, 0.0039215686, + 0.7607843137, 0.19607843137254902, 0.0039215686, 0.0039215686, 0.7764705882, 0.2, + 0.0039215686, 0.0039215686, 0.7921568627, 0.20392156862745098, 0.0039215686, 0.0039215686, + 0.8078431373, 0.20784313725490197, 0.0039215686, 0.0039215686, 0.8235294118, + 0.21176470588235294, 0.0039215686, 0.0039215686, 0.8392156863, 0.21568627450980393, + 0.0039215686, 0.0039215686, 0.8549019608, 0.2196078431372549, 0.0039215686, 0.0039215686, + 0.8705882353, 0.2235294117647059, 0.0039215686, 0.0039215686, 0.8862745098, + 0.22745098039215686, 0.0039215686, 0.0039215686, 0.9019607843, 0.23137254901960785, + 0.0039215686, 0.0039215686, 0.9176470588, 0.23529411764705885, 0.0039215686, 0.0039215686, + 0.9333333333, 0.23921568627450984, 0.0039215686, 0.0039215686, 0.9490196078, + 0.24313725490196078, 0.0039215686, 0.0039215686, 0.9647058824, 0.24705882352941178, + 0.0039215686, 0.0039215686, 0.9803921569, 0.25098039215686274, 0.0039215686, 0.0039215686, + 0.9960784314, 0.2549019607843137, 0.0039215686, 0.0039215686, 0.9960784314, + 0.25882352941176473, 0.0156862745, 0.0039215686, 0.9803921569, 0.2627450980392157, + 0.031372549, 0.0039215686, 0.9647058824, 0.26666666666666666, 0.0470588235, 0.0039215686, + 0.9490196078, 0.27058823529411763, 0.062745098, 0.0039215686, 0.9333333333, + 0.27450980392156865, 0.0784313725, 0.0039215686, 0.9176470588, 0.2784313725490196, + 0.0941176471, 0.0039215686, 0.9019607843, 0.2823529411764706, 0.1098039216, 0.0039215686, + 0.8862745098, 0.28627450980392155, 0.1254901961, 0.0039215686, 0.8705882353, + 0.2901960784313726, 0.1411764706, 0.0039215686, 0.8549019608, 0.29411764705882354, + 0.1568627451, 0.0039215686, 0.8392156863, 0.2980392156862745, 0.1725490196, 0.0039215686, + 0.8235294118, 0.30196078431372547, 0.1882352941, 0.0039215686, 0.8078431373, + 0.3058823529411765, 0.2039215686, 0.0039215686, 0.7921568627, 0.30980392156862746, + 0.2196078431, 0.0039215686, 0.7764705882, 0.3137254901960784, 0.2352941176, 0.0039215686, + 0.7607843137, 0.3176470588235294, 0.2509803922, 0.0039215686, 0.7490196078, + 0.3215686274509804, 0.262745098, 0.0039215686, 0.7333333333, 0.3254901960784314, 0.2784313725, + 0.0039215686, 0.7176470588, 0.32941176470588235, 0.2941176471, 0.0039215686, 0.7019607843, + 0.3333333333333333, 0.3098039216, 0.0039215686, 0.6862745098, 0.33725490196078434, + 0.3254901961, 0.0039215686, 0.6705882353, 0.3411764705882353, 0.3411764706, 0.0039215686, + 0.6549019608, 0.34509803921568627, 0.3568627451, 0.0039215686, 0.6392156863, + 0.34901960784313724, 0.3725490196, 0.0039215686, 0.6235294118, 0.35294117647058826, + 0.3882352941, 0.0039215686, 0.6078431373, 0.3568627450980392, 0.4039215686, 0.0039215686, + 0.5921568627, 0.3607843137254902, 0.4196078431, 0.0039215686, 0.5764705882, + 0.36470588235294116, 0.4352941176, 0.0039215686, 0.5607843137, 0.3686274509803922, + 0.4509803922, 0.0039215686, 0.5450980392, 0.37254901960784315, 0.4666666667, 0.0039215686, + 0.5294117647, 0.3764705882352941, 0.4823529412, 0.0039215686, 0.5137254902, + 0.3803921568627451, 0.4980392157, 0.0039215686, 0.4980392157, 0.3843137254901961, + 0.5137254902, 0.0039215686, 0.4823529412, 0.38823529411764707, 0.5294117647, 0.0039215686, + 0.4666666667, 0.39215686274509803, 0.5450980392, 0.0039215686, 0.4509803922, + 0.396078431372549, 0.5607843137, 0.0039215686, 0.4352941176, 0.4, 0.5764705882, 0.0039215686, + 0.4196078431, 0.403921568627451, 0.5921568627, 0.0039215686, 0.4039215686, + 0.40784313725490196, 0.6078431373, 0.0039215686, 0.3882352941, 0.4117647058823529, + 0.6235294118, 0.0039215686, 0.3725490196, 0.41568627450980394, 0.6392156863, 0.0039215686, + 0.3568627451, 0.4196078431372549, 0.6549019608, 0.0039215686, 0.3411764706, + 0.4235294117647059, 0.6705882353, 0.0039215686, 0.3254901961, 0.42745098039215684, + 0.6862745098, 0.0039215686, 0.3098039216, 0.43137254901960786, 0.7019607843, 0.0039215686, + 0.2941176471, 0.43529411764705883, 0.7176470588, 0.0039215686, 0.2784313725, + 0.4392156862745098, 0.7333333333, 0.0039215686, 0.262745098, 0.44313725490196076, + 0.7490196078, 0.0039215686, 0.2509803922, 0.4470588235294118, 0.7607843137, 0.0039215686, + 0.2352941176, 0.45098039215686275, 0.7764705882, 0.0039215686, 0.2196078431, + 0.4549019607843137, 0.7921568627, 0.0039215686, 0.2039215686, 0.4588235294117647, + 0.8078431373, 0.0039215686, 0.1882352941, 0.4627450980392157, 0.8235294118, 0.0039215686, + 0.1725490196, 0.4666666666666667, 0.8392156863, 0.0039215686, 0.1568627451, + 0.4705882352941177, 0.8549019608, 0.0039215686, 0.1411764706, 0.4745098039215686, + 0.8705882353, 0.0039215686, 0.1254901961, 0.4784313725490197, 0.8862745098, 0.0039215686, + 0.1098039216, 0.48235294117647065, 0.9019607843, 0.0039215686, 0.0941176471, + 0.48627450980392156, 0.9176470588, 0.0039215686, 0.0784313725, 0.49019607843137253, + 0.9333333333, 0.0039215686, 0.062745098, 0.49411764705882355, 0.9490196078, 0.0039215686, + 0.0470588235, 0.4980392156862745, 0.9647058824, 0.0039215686, 0.031372549, 0.5019607843137255, + 0.9803921569, 0.0039215686, 0.0156862745, 0.5058823529411764, 0.9960784314, 0.0039215686, + 0.0039215686, 0.5098039215686274, 0.9960784314, 0.0156862745, 0.0039215686, + 0.5137254901960784, 0.9960784314, 0.031372549, 0.0039215686, 0.5176470588235295, 0.9960784314, + 0.0470588235, 0.0039215686, 0.5215686274509804, 0.9960784314, 0.062745098, 0.0039215686, + 0.5254901960784314, 0.9960784314, 0.0784313725, 0.0039215686, 0.5294117647058824, + 0.9960784314, 0.0941176471, 0.0039215686, 0.5333333333333333, 0.9960784314, 0.1098039216, + 0.0039215686, 0.5372549019607843, 0.9960784314, 0.1254901961, 0.0039215686, + 0.5411764705882353, 0.9960784314, 0.1411764706, 0.0039215686, 0.5450980392156862, + 0.9960784314, 0.1568627451, 0.0039215686, 0.5490196078431373, 0.9960784314, 0.1725490196, + 0.0039215686, 0.5529411764705883, 0.9960784314, 0.1882352941, 0.0039215686, + 0.5568627450980392, 0.9960784314, 0.2039215686, 0.0039215686, 0.5607843137254902, + 0.9960784314, 0.2196078431, 0.0039215686, 0.5647058823529412, 0.9960784314, 0.2352941176, + 0.0039215686, 0.5686274509803921, 0.9960784314, 0.2509803922, 0.0039215686, + 0.5725490196078431, 0.9960784314, 0.262745098, 0.0039215686, 0.5764705882352941, 0.9960784314, + 0.2784313725, 0.0039215686, 0.5803921568627451, 0.9960784314, 0.2941176471, 0.0039215686, + 0.5843137254901961, 0.9960784314, 0.3098039216, 0.0039215686, 0.5882352941176471, + 0.9960784314, 0.3254901961, 0.0039215686, 0.592156862745098, 0.9960784314, 0.3411764706, + 0.0039215686, 0.596078431372549, 0.9960784314, 0.3568627451, 0.0039215686, 0.6, 0.9960784314, + 0.3725490196, 0.0039215686, 0.6039215686274509, 0.9960784314, 0.3882352941, 0.0039215686, + 0.6078431372549019, 0.9960784314, 0.4039215686, 0.0039215686, 0.611764705882353, 0.9960784314, + 0.4196078431, 0.0039215686, 0.615686274509804, 0.9960784314, 0.4352941176, 0.0039215686, + 0.6196078431372549, 0.9960784314, 0.4509803922, 0.0039215686, 0.6235294117647059, + 0.9960784314, 0.4666666667, 0.0039215686, 0.6274509803921569, 0.9960784314, 0.4823529412, + 0.0039215686, 0.6313725490196078, 0.9960784314, 0.4980392157, 0.0039215686, + 0.6352941176470588, 0.9960784314, 0.5137254902, 0.0039215686, 0.6392156862745098, + 0.9960784314, 0.5294117647, 0.0039215686, 0.6431372549019608, 0.9960784314, 0.5450980392, + 0.0039215686, 0.6470588235294118, 0.9960784314, 0.5607843137, 0.0039215686, + 0.6509803921568628, 0.9960784314, 0.5764705882, 0.0039215686, 0.6549019607843137, + 0.9960784314, 0.5921568627, 0.0039215686, 0.6588235294117647, 0.9960784314, 0.6078431373, + 0.0039215686, 0.6627450980392157, 0.9960784314, 0.6235294118, 0.0039215686, + 0.6666666666666666, 0.9960784314, 0.6392156863, 0.0039215686, 0.6705882352941176, + 0.9960784314, 0.6549019608, 0.0039215686, 0.6745098039215687, 0.9960784314, 0.6705882353, + 0.0039215686, 0.6784313725490196, 0.9960784314, 0.6862745098, 0.0039215686, + 0.6823529411764706, 0.9960784314, 0.7019607843, 0.0039215686, 0.6862745098039216, + 0.9960784314, 0.7176470588, 0.0039215686, 0.6901960784313725, 0.9960784314, 0.7333333333, + 0.0039215686, 0.6941176470588235, 0.9960784314, 0.7490196078, 0.0039215686, + 0.6980392156862745, 0.9960784314, 0.7607843137, 0.0039215686, 0.7019607843137254, + 0.9960784314, 0.7764705882, 0.0039215686, 0.7058823529411765, 0.9960784314, 0.7921568627, + 0.0039215686, 0.7098039215686275, 0.9960784314, 0.8078431373, 0.0039215686, + 0.7137254901960784, 0.9960784314, 0.8235294118, 0.0039215686, 0.7176470588235294, + 0.9960784314, 0.8392156863, 0.0039215686, 0.7215686274509804, 0.9960784314, 0.8549019608, + 0.0039215686, 0.7254901960784313, 0.9960784314, 0.8705882353, 0.0039215686, + 0.7294117647058823, 0.9960784314, 0.8862745098, 0.0039215686, 0.7333333333333333, + 0.9960784314, 0.9019607843, 0.0039215686, 0.7372549019607844, 0.9960784314, 0.9176470588, + 0.0039215686, 0.7411764705882353, 0.9960784314, 0.9333333333, 0.0039215686, + 0.7450980392156863, 0.9960784314, 0.9490196078, 0.0039215686, 0.7490196078431373, + 0.9960784314, 0.9647058824, 0.0039215686, 0.7529411764705882, 0.9960784314, 0.9803921569, + 0.0039215686, 0.7568627450980392, 0.9960784314, 0.9960784314, 0.0039215686, + 0.7607843137254902, 0.9960784314, 0.9960784314, 0.0196078431, 0.7647058823529411, + 0.9960784314, 0.9960784314, 0.0352941176, 0.7686274509803922, 0.9960784314, 0.9960784314, + 0.0509803922, 0.7725490196078432, 0.9960784314, 0.9960784314, 0.0666666667, + 0.7764705882352941, 0.9960784314, 0.9960784314, 0.0823529412, 0.7803921568627451, + 0.9960784314, 0.9960784314, 0.0980392157, 0.7843137254901961, 0.9960784314, 0.9960784314, + 0.1137254902, 0.788235294117647, 0.9960784314, 0.9960784314, 0.1294117647, 0.792156862745098, + 0.9960784314, 0.9960784314, 0.1450980392, 0.796078431372549, 0.9960784314, 0.9960784314, + 0.1607843137, 0.8, 0.9960784314, 0.9960784314, 0.1764705882, 0.803921568627451, 0.9960784314, + 0.9960784314, 0.1921568627, 0.807843137254902, 0.9960784314, 0.9960784314, 0.2078431373, + 0.8117647058823529, 0.9960784314, 0.9960784314, 0.2235294118, 0.8156862745098039, + 0.9960784314, 0.9960784314, 0.2392156863, 0.8196078431372549, 0.9960784314, 0.9960784314, + 0.2509803922, 0.8235294117647058, 0.9960784314, 0.9960784314, 0.2666666667, + 0.8274509803921568, 0.9960784314, 0.9960784314, 0.2823529412, 0.8313725490196079, + 0.9960784314, 0.9960784314, 0.2980392157, 0.8352941176470589, 0.9960784314, 0.9960784314, + 0.3137254902, 0.8392156862745098, 0.9960784314, 0.9960784314, 0.3333333333, + 0.8431372549019608, 0.9960784314, 0.9960784314, 0.3490196078, 0.8470588235294118, + 0.9960784314, 0.9960784314, 0.3647058824, 0.8509803921568627, 0.9960784314, 0.9960784314, + 0.3803921569, 0.8549019607843137, 0.9960784314, 0.9960784314, 0.3960784314, + 0.8588235294117647, 0.9960784314, 0.9960784314, 0.4117647059, 0.8627450980392157, + 0.9960784314, 0.9960784314, 0.4274509804, 0.8666666666666667, 0.9960784314, 0.9960784314, + 0.4431372549, 0.8705882352941177, 0.9960784314, 0.9960784314, 0.4588235294, + 0.8745098039215686, 0.9960784314, 0.9960784314, 0.4745098039, 0.8784313725490196, + 0.9960784314, 0.9960784314, 0.4901960784, 0.8823529411764706, 0.9960784314, 0.9960784314, + 0.5058823529, 0.8862745098039215, 0.9960784314, 0.9960784314, 0.5215686275, + 0.8901960784313725, 0.9960784314, 0.9960784314, 0.537254902, 0.8941176470588236, 0.9960784314, + 0.9960784314, 0.5529411765, 0.8980392156862745, 0.9960784314, 0.9960784314, 0.568627451, + 0.9019607843137255, 0.9960784314, 0.9960784314, 0.5843137255, 0.9058823529411765, + 0.9960784314, 0.9960784314, 0.6, 0.9098039215686274, 0.9960784314, 0.9960784314, 0.6156862745, + 0.9137254901960784, 0.9960784314, 0.9960784314, 0.631372549, 0.9176470588235294, 0.9960784314, + 0.9960784314, 0.6470588235, 0.9215686274509803, 0.9960784314, 0.9960784314, 0.6666666667, + 0.9254901960784314, 0.9960784314, 0.9960784314, 0.6823529412, 0.9294117647058824, + 0.9960784314, 0.9960784314, 0.6980392157, 0.9333333333333333, 0.9960784314, 0.9960784314, + 0.7137254902, 0.9372549019607843, 0.9960784314, 0.9960784314, 0.7294117647, + 0.9411764705882354, 0.9960784314, 0.9960784314, 0.7450980392, 0.9450980392156864, + 0.9960784314, 0.9960784314, 0.7568627451, 0.9490196078431372, 0.9960784314, 0.9960784314, + 0.7725490196, 0.9529411764705882, 0.9960784314, 0.9960784314, 0.7882352941, + 0.9568627450980394, 0.9960784314, 0.9960784314, 0.8039215686, 0.9607843137254903, + 0.9960784314, 0.9960784314, 0.8196078431, 0.9647058823529413, 0.9960784314, 0.9960784314, + 0.8352941176, 0.9686274509803922, 0.9960784314, 0.9960784314, 0.8509803922, + 0.9725490196078431, 0.9960784314, 0.9960784314, 0.8666666667, 0.9764705882352941, + 0.9960784314, 0.9960784314, 0.8823529412, 0.9803921568627451, 0.9960784314, 0.9960784314, + 0.8980392157, 0.984313725490196, 0.9960784314, 0.9960784314, 0.9137254902, 0.9882352941176471, + 0.9960784314, 0.9960784314, 0.9294117647, 0.9921568627450981, 0.9960784314, 0.9960784314, + 0.9450980392, 0.996078431372549, 0.9960784314, 0.9960784314, 0.9607843137, 1.0, 0.9960784314, + 0.9960784314, 0.9607843137, + ], + description: 'Hot Iron', + }, + { + ColorSpace: 'RGB', + Name: 'red_hot', + name: 'red_hot', + RGBPoints: [ + 0.0, 0.0, 0.0, 0.0, 0.00392156862745098, 0.0, 0.0, 0.0, 0.00784313725490196, 0.0, 0.0, 0.0, + 0.011764705882352941, 0.0, 0.0, 0.0, 0.01568627450980392, 0.0039215686, 0.0039215686, + 0.0039215686, 0.0196078431372549, 0.0039215686, 0.0039215686, 0.0039215686, + 0.023529411764705882, 0.0039215686, 0.0039215686, 0.0039215686, 0.027450980392156862, + 0.0039215686, 0.0039215686, 0.0039215686, 0.03137254901960784, 0.0039215686, 0.0039215686, + 0.0039215686, 0.03529411764705882, 0.0156862745, 0.0, 0.0, 0.0392156862745098, 0.0274509804, + 0.0, 0.0, 0.043137254901960784, 0.0392156863, 0.0, 0.0, 0.047058823529411764, 0.0509803922, + 0.0, 0.0, 0.050980392156862744, 0.062745098, 0.0, 0.0, 0.054901960784313725, 0.0784313725, + 0.0, 0.0, 0.05882352941176471, 0.0901960784, 0.0, 0.0, 0.06274509803921569, 0.1058823529, 0.0, + 0.0, 0.06666666666666667, 0.1176470588, 0.0, 0.0, 0.07058823529411765, 0.1294117647, 0.0, 0.0, + 0.07450980392156863, 0.1411764706, 0.0, 0.0, 0.0784313725490196, 0.1529411765, 0.0, 0.0, + 0.08235294117647059, 0.1647058824, 0.0, 0.0, 0.08627450980392157, 0.1764705882, 0.0, 0.0, + 0.09019607843137255, 0.1882352941, 0.0, 0.0, 0.09411764705882353, 0.2039215686, 0.0, 0.0, + 0.09803921568627451, 0.2156862745, 0.0, 0.0, 0.10196078431372549, 0.2274509804, 0.0, 0.0, + 0.10588235294117647, 0.2392156863, 0.0, 0.0, 0.10980392156862745, 0.2549019608, 0.0, 0.0, + 0.11372549019607843, 0.2666666667, 0.0, 0.0, 0.11764705882352942, 0.2784313725, 0.0, 0.0, + 0.12156862745098039, 0.2901960784, 0.0, 0.0, 0.12549019607843137, 0.3058823529, 0.0, 0.0, + 0.12941176470588237, 0.3176470588, 0.0, 0.0, 0.13333333333333333, 0.3294117647, 0.0, 0.0, + 0.13725490196078433, 0.3411764706, 0.0, 0.0, 0.1411764705882353, 0.3529411765, 0.0, 0.0, + 0.1450980392156863, 0.3647058824, 0.0, 0.0, 0.14901960784313725, 0.3764705882, 0.0, 0.0, + 0.15294117647058825, 0.3882352941, 0.0, 0.0, 0.1568627450980392, 0.4039215686, 0.0, 0.0, + 0.1607843137254902, 0.4156862745, 0.0, 0.0, 0.16470588235294117, 0.431372549, 0.0, 0.0, + 0.16862745098039217, 0.4431372549, 0.0, 0.0, 0.17254901960784313, 0.4588235294, 0.0, 0.0, + 0.17647058823529413, 0.4705882353, 0.0, 0.0, 0.1803921568627451, 0.4823529412, 0.0, 0.0, + 0.1843137254901961, 0.4941176471, 0.0, 0.0, 0.18823529411764706, 0.5098039216, 0.0, 0.0, + 0.19215686274509805, 0.5215686275, 0.0, 0.0, 0.19607843137254902, 0.5333333333, 0.0, 0.0, 0.2, + 0.5450980392, 0.0, 0.0, 0.20392156862745098, 0.5568627451, 0.0, 0.0, 0.20784313725490197, + 0.568627451, 0.0, 0.0, 0.21176470588235294, 0.5803921569, 0.0, 0.0, 0.21568627450980393, + 0.5921568627, 0.0, 0.0, 0.2196078431372549, 0.6078431373, 0.0, 0.0, 0.2235294117647059, + 0.6196078431, 0.0, 0.0, 0.22745098039215686, 0.631372549, 0.0, 0.0, 0.23137254901960785, + 0.6431372549, 0.0, 0.0, 0.23529411764705885, 0.6588235294, 0.0, 0.0, 0.23921568627450984, + 0.6705882353, 0.0, 0.0, 0.24313725490196078, 0.6823529412, 0.0, 0.0, 0.24705882352941178, + 0.6941176471, 0.0, 0.0, 0.25098039215686274, 0.7098039216, 0.0, 0.0, 0.2549019607843137, + 0.7215686275, 0.0, 0.0, 0.25882352941176473, 0.7333333333, 0.0, 0.0, 0.2627450980392157, + 0.7450980392, 0.0, 0.0, 0.26666666666666666, 0.7568627451, 0.0, 0.0, 0.27058823529411763, + 0.768627451, 0.0, 0.0, 0.27450980392156865, 0.7843137255, 0.0, 0.0, 0.2784313725490196, + 0.7960784314, 0.0, 0.0, 0.2823529411764706, 0.8117647059, 0.0, 0.0, 0.28627450980392155, + 0.8235294118, 0.0, 0.0, 0.2901960784313726, 0.8352941176, 0.0, 0.0, 0.29411764705882354, + 0.8470588235, 0.0, 0.0, 0.2980392156862745, 0.862745098, 0.0, 0.0, 0.30196078431372547, + 0.8745098039, 0.0, 0.0, 0.3058823529411765, 0.8862745098, 0.0, 0.0, 0.30980392156862746, + 0.8980392157, 0.0, 0.0, 0.3137254901960784, 0.9137254902, 0.0, 0.0, 0.3176470588235294, + 0.9254901961, 0.0, 0.0, 0.3215686274509804, 0.937254902, 0.0, 0.0, 0.3254901960784314, + 0.9490196078, 0.0, 0.0, 0.32941176470588235, 0.9607843137, 0.0, 0.0, 0.3333333333333333, + 0.968627451, 0.0, 0.0, 0.33725490196078434, 0.9803921569, 0.0039215686, 0.0, + 0.3411764705882353, 0.9882352941, 0.0078431373, 0.0, 0.34509803921568627, 1.0, 0.0117647059, + 0.0, 0.34901960784313724, 1.0, 0.0235294118, 0.0, 0.35294117647058826, 1.0, 0.0352941176, 0.0, + 0.3568627450980392, 1.0, 0.0470588235, 0.0, 0.3607843137254902, 1.0, 0.062745098, 0.0, + 0.36470588235294116, 1.0, 0.0745098039, 0.0, 0.3686274509803922, 1.0, 0.0862745098, 0.0, + 0.37254901960784315, 1.0, 0.0980392157, 0.0, 0.3764705882352941, 1.0, 0.1137254902, 0.0, + 0.3803921568627451, 1.0, 0.1254901961, 0.0, 0.3843137254901961, 1.0, 0.137254902, 0.0, + 0.38823529411764707, 1.0, 0.1490196078, 0.0, 0.39215686274509803, 1.0, 0.1647058824, 0.0, + 0.396078431372549, 1.0, 0.1764705882, 0.0, 0.4, 1.0, 0.1882352941, 0.0, 0.403921568627451, + 1.0, 0.2, 0.0, 0.40784313725490196, 1.0, 0.2156862745, 0.0, 0.4117647058823529, 1.0, + 0.2274509804, 0.0, 0.41568627450980394, 1.0, 0.2392156863, 0.0, 0.4196078431372549, 1.0, + 0.2509803922, 0.0, 0.4235294117647059, 1.0, 0.2666666667, 0.0, 0.42745098039215684, 1.0, + 0.2784313725, 0.0, 0.43137254901960786, 1.0, 0.2901960784, 0.0, 0.43529411764705883, 1.0, + 0.3019607843, 0.0, 0.4392156862745098, 1.0, 0.3176470588, 0.0, 0.44313725490196076, 1.0, + 0.3294117647, 0.0, 0.4470588235294118, 1.0, 0.3411764706, 0.0, 0.45098039215686275, 1.0, + 0.3529411765, 0.0, 0.4549019607843137, 1.0, 0.368627451, 0.0, 0.4588235294117647, 1.0, + 0.3803921569, 0.0, 0.4627450980392157, 1.0, 0.3921568627, 0.0, 0.4666666666666667, 1.0, + 0.4039215686, 0.0, 0.4705882352941177, 1.0, 0.4156862745, 0.0, 0.4745098039215686, 1.0, + 0.4274509804, 0.0, 0.4784313725490197, 1.0, 0.4392156863, 0.0, 0.48235294117647065, 1.0, + 0.4509803922, 0.0, 0.48627450980392156, 1.0, 0.4666666667, 0.0, 0.49019607843137253, 1.0, + 0.4784313725, 0.0, 0.49411764705882355, 1.0, 0.4941176471, 0.0, 0.4980392156862745, 1.0, + 0.5058823529, 0.0, 0.5019607843137255, 1.0, 0.5215686275, 0.0, 0.5058823529411764, 1.0, + 0.5333333333, 0.0, 0.5098039215686274, 1.0, 0.5450980392, 0.0, 0.5137254901960784, 1.0, + 0.5568627451, 0.0, 0.5176470588235295, 1.0, 0.568627451, 0.0, 0.5215686274509804, 1.0, + 0.5803921569, 0.0, 0.5254901960784314, 1.0, 0.5921568627, 0.0, 0.5294117647058824, 1.0, + 0.6039215686, 0.0, 0.5333333333333333, 1.0, 0.6196078431, 0.0, 0.5372549019607843, 1.0, + 0.631372549, 0.0, 0.5411764705882353, 1.0, 0.6431372549, 0.0, 0.5450980392156862, 1.0, + 0.6549019608, 0.0, 0.5490196078431373, 1.0, 0.6705882353, 0.0, 0.5529411764705883, 1.0, + 0.6823529412, 0.0, 0.5568627450980392, 1.0, 0.6941176471, 0.0, 0.5607843137254902, 1.0, + 0.7058823529, 0.0, 0.5647058823529412, 1.0, 0.7215686275, 0.0, 0.5686274509803921, 1.0, + 0.7333333333, 0.0, 0.5725490196078431, 1.0, 0.7450980392, 0.0, 0.5764705882352941, 1.0, + 0.7568627451, 0.0, 0.5803921568627451, 1.0, 0.7725490196, 0.0, 0.5843137254901961, 1.0, + 0.7843137255, 0.0, 0.5882352941176471, 1.0, 0.7960784314, 0.0, 0.592156862745098, 1.0, + 0.8078431373, 0.0, 0.596078431372549, 1.0, 0.8196078431, 0.0, 0.6, 1.0, 0.831372549, 0.0, + 0.6039215686274509, 1.0, 0.8470588235, 0.0, 0.6078431372549019, 1.0, 0.8588235294, 0.0, + 0.611764705882353, 1.0, 0.8745098039, 0.0, 0.615686274509804, 1.0, 0.8862745098, 0.0, + 0.6196078431372549, 1.0, 0.8980392157, 0.0, 0.6235294117647059, 1.0, 0.9098039216, 0.0, + 0.6274509803921569, 1.0, 0.9254901961, 0.0, 0.6313725490196078, 1.0, 0.937254902, 0.0, + 0.6352941176470588, 1.0, 0.9490196078, 0.0, 0.6392156862745098, 1.0, 0.9607843137, 0.0, + 0.6431372549019608, 1.0, 0.9764705882, 0.0, 0.6470588235294118, 1.0, 0.9803921569, + 0.0039215686, 0.6509803921568628, 1.0, 0.9882352941, 0.0117647059, 0.6549019607843137, 1.0, + 0.9921568627, 0.0156862745, 0.6588235294117647, 1.0, 1.0, 0.0235294118, 0.6627450980392157, + 1.0, 1.0, 0.0352941176, 0.6666666666666666, 1.0, 1.0, 0.0470588235, 0.6705882352941176, 1.0, + 1.0, 0.0588235294, 0.6745098039215687, 1.0, 1.0, 0.0745098039, 0.6784313725490196, 1.0, 1.0, + 0.0862745098, 0.6823529411764706, 1.0, 1.0, 0.0980392157, 0.6862745098039216, 1.0, 1.0, + 0.1098039216, 0.6901960784313725, 1.0, 1.0, 0.1254901961, 0.6941176470588235, 1.0, 1.0, + 0.137254902, 0.6980392156862745, 1.0, 1.0, 0.1490196078, 0.7019607843137254, 1.0, 1.0, + 0.1607843137, 0.7058823529411765, 1.0, 1.0, 0.1764705882, 0.7098039215686275, 1.0, 1.0, + 0.1882352941, 0.7137254901960784, 1.0, 1.0, 0.2, 0.7176470588235294, 1.0, 1.0, 0.2117647059, + 0.7215686274509804, 1.0, 1.0, 0.2274509804, 0.7254901960784313, 1.0, 1.0, 0.2392156863, + 0.7294117647058823, 1.0, 1.0, 0.2509803922, 0.7333333333333333, 1.0, 1.0, 0.262745098, + 0.7372549019607844, 1.0, 1.0, 0.2784313725, 0.7411764705882353, 1.0, 1.0, 0.2901960784, + 0.7450980392156863, 1.0, 1.0, 0.3019607843, 0.7490196078431373, 1.0, 1.0, 0.3137254902, + 0.7529411764705882, 1.0, 1.0, 0.3294117647, 0.7568627450980392, 1.0, 1.0, 0.3411764706, + 0.7607843137254902, 1.0, 1.0, 0.3529411765, 0.7647058823529411, 1.0, 1.0, 0.3647058824, + 0.7686274509803922, 1.0, 1.0, 0.3803921569, 0.7725490196078432, 1.0, 1.0, 0.3921568627, + 0.7764705882352941, 1.0, 1.0, 0.4039215686, 0.7803921568627451, 1.0, 1.0, 0.4156862745, + 0.7843137254901961, 1.0, 1.0, 0.431372549, 0.788235294117647, 1.0, 1.0, 0.4431372549, + 0.792156862745098, 1.0, 1.0, 0.4549019608, 0.796078431372549, 1.0, 1.0, 0.4666666667, 0.8, + 1.0, 1.0, 0.4784313725, 0.803921568627451, 1.0, 1.0, 0.4901960784, 0.807843137254902, 1.0, + 1.0, 0.5019607843, 0.8117647058823529, 1.0, 1.0, 0.5137254902, 0.8156862745098039, 1.0, 1.0, + 0.5294117647, 0.8196078431372549, 1.0, 1.0, 0.5411764706, 0.8235294117647058, 1.0, 1.0, + 0.5568627451, 0.8274509803921568, 1.0, 1.0, 0.568627451, 0.8313725490196079, 1.0, 1.0, + 0.5843137255, 0.8352941176470589, 1.0, 1.0, 0.5960784314, 0.8392156862745098, 1.0, 1.0, + 0.6078431373, 0.8431372549019608, 1.0, 1.0, 0.6196078431, 0.8470588235294118, 1.0, 1.0, + 0.631372549, 0.8509803921568627, 1.0, 1.0, 0.6431372549, 0.8549019607843137, 1.0, 1.0, + 0.6549019608, 0.8588235294117647, 1.0, 1.0, 0.6666666667, 0.8627450980392157, 1.0, 1.0, + 0.6823529412, 0.8666666666666667, 1.0, 1.0, 0.6941176471, 0.8705882352941177, 1.0, 1.0, + 0.7058823529, 0.8745098039215686, 1.0, 1.0, 0.7176470588, 0.8784313725490196, 1.0, 1.0, + 0.7333333333, 0.8823529411764706, 1.0, 1.0, 0.7450980392, 0.8862745098039215, 1.0, 1.0, + 0.7568627451, 0.8901960784313725, 1.0, 1.0, 0.768627451, 0.8941176470588236, 1.0, 1.0, + 0.7843137255, 0.8980392156862745, 1.0, 1.0, 0.7960784314, 0.9019607843137255, 1.0, 1.0, + 0.8078431373, 0.9058823529411765, 1.0, 1.0, 0.8196078431, 0.9098039215686274, 1.0, 1.0, + 0.8352941176, 0.9137254901960784, 1.0, 1.0, 0.8470588235, 0.9176470588235294, 1.0, 1.0, + 0.8588235294, 0.9215686274509803, 1.0, 1.0, 0.8705882353, 0.9254901960784314, 1.0, 1.0, + 0.8823529412, 0.9294117647058824, 1.0, 1.0, 0.8941176471, 0.9333333333333333, 1.0, 1.0, + 0.9098039216, 0.9372549019607843, 1.0, 1.0, 0.9215686275, 0.9411764705882354, 1.0, 1.0, + 0.937254902, 0.9450980392156864, 1.0, 1.0, 0.9490196078, 0.9490196078431372, 1.0, 1.0, + 0.9607843137, 0.9529411764705882, 1.0, 1.0, 0.9725490196, 0.9568627450980394, 1.0, 1.0, + 0.9882352941, 0.9607843137254903, 1.0, 1.0, 0.9882352941, 0.9647058823529413, 1.0, 1.0, + 0.9921568627, 0.9686274509803922, 1.0, 1.0, 0.9960784314, 0.9725490196078431, 1.0, 1.0, 1.0, + 0.9764705882352941, 1.0, 1.0, 1.0, 0.9803921568627451, 1.0, 1.0, 1.0, 0.984313725490196, 1.0, + 1.0, 1.0, 0.9882352941176471, 1.0, 1.0, 1.0, 0.9921568627450981, 1.0, 1.0, 1.0, + 0.996078431372549, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + ], + description: 'Red Hot', + }, + { + ColorSpace: 'RGB', + Name: 's_pet', + name: 's_pet', + RGBPoints: [ + 0.0, 0.0156862745, 0.0039215686, 0.0156862745, 0.00392156862745098, 0.0156862745, + 0.0039215686, 0.0156862745, 0.00784313725490196, 0.0274509804, 0.0039215686, 0.031372549, + 0.011764705882352941, 0.0352941176, 0.0039215686, 0.0509803922, 0.01568627450980392, + 0.0392156863, 0.0039215686, 0.0666666667, 0.0196078431372549, 0.0509803922, 0.0039215686, + 0.0823529412, 0.023529411764705882, 0.062745098, 0.0039215686, 0.0980392157, + 0.027450980392156862, 0.0705882353, 0.0039215686, 0.1176470588, 0.03137254901960784, + 0.0745098039, 0.0039215686, 0.1333333333, 0.03529411764705882, 0.0862745098, 0.0039215686, + 0.1490196078, 0.0392156862745098, 0.0980392157, 0.0039215686, 0.1647058824, + 0.043137254901960784, 0.1058823529, 0.0039215686, 0.1843137255, 0.047058823529411764, + 0.1098039216, 0.0039215686, 0.2, 0.050980392156862744, 0.1215686275, 0.0039215686, + 0.2156862745, 0.054901960784313725, 0.1333333333, 0.0039215686, 0.231372549, + 0.05882352941176471, 0.137254902, 0.0039215686, 0.2509803922, 0.06274509803921569, + 0.1490196078, 0.0039215686, 0.262745098, 0.06666666666666667, 0.1607843137, 0.0039215686, + 0.2784313725, 0.07058823529411765, 0.168627451, 0.0039215686, 0.2941176471, + 0.07450980392156863, 0.1725490196, 0.0039215686, 0.3137254902, 0.0784313725490196, + 0.1843137255, 0.0039215686, 0.3294117647, 0.08235294117647059, 0.1960784314, 0.0039215686, + 0.3450980392, 0.08627450980392157, 0.2039215686, 0.0039215686, 0.3607843137, + 0.09019607843137255, 0.2078431373, 0.0039215686, 0.3803921569, 0.09411764705882353, + 0.2196078431, 0.0039215686, 0.3960784314, 0.09803921568627451, 0.231372549, 0.0039215686, + 0.4117647059, 0.10196078431372549, 0.2392156863, 0.0039215686, 0.4274509804, + 0.10588235294117647, 0.2431372549, 0.0039215686, 0.4470588235, 0.10980392156862745, + 0.2509803922, 0.0039215686, 0.462745098, 0.11372549019607843, 0.262745098, 0.0039215686, + 0.4784313725, 0.11764705882352942, 0.2666666667, 0.0039215686, 0.4980392157, + 0.12156862745098039, 0.2666666667, 0.0039215686, 0.4980392157, 0.12549019607843137, + 0.262745098, 0.0039215686, 0.5137254902, 0.12941176470588237, 0.2509803922, 0.0039215686, + 0.5294117647, 0.13333333333333333, 0.2431372549, 0.0039215686, 0.5450980392, + 0.13725490196078433, 0.2392156863, 0.0039215686, 0.5607843137, 0.1411764705882353, + 0.231372549, 0.0039215686, 0.5764705882, 0.1450980392156863, 0.2196078431, 0.0039215686, + 0.5921568627, 0.14901960784313725, 0.2078431373, 0.0039215686, 0.6078431373, + 0.15294117647058825, 0.2039215686, 0.0039215686, 0.6235294118, 0.1568627450980392, + 0.1960784314, 0.0039215686, 0.6392156863, 0.1607843137254902, 0.1843137255, 0.0039215686, + 0.6549019608, 0.16470588235294117, 0.1725490196, 0.0039215686, 0.6705882353, + 0.16862745098039217, 0.168627451, 0.0039215686, 0.6862745098, 0.17254901960784313, + 0.1607843137, 0.0039215686, 0.7019607843, 0.17647058823529413, 0.1490196078, 0.0039215686, + 0.7176470588, 0.1803921568627451, 0.137254902, 0.0039215686, 0.7333333333, 0.1843137254901961, + 0.1333333333, 0.0039215686, 0.7490196078, 0.18823529411764706, 0.1215686275, 0.0039215686, + 0.7607843137, 0.19215686274509805, 0.1098039216, 0.0039215686, 0.7764705882, + 0.19607843137254902, 0.1058823529, 0.0039215686, 0.7921568627, 0.2, 0.0980392157, + 0.0039215686, 0.8078431373, 0.20392156862745098, 0.0862745098, 0.0039215686, 0.8235294118, + 0.20784313725490197, 0.0745098039, 0.0039215686, 0.8392156863, 0.21176470588235294, + 0.0705882353, 0.0039215686, 0.8549019608, 0.21568627450980393, 0.062745098, 0.0039215686, + 0.8705882353, 0.2196078431372549, 0.0509803922, 0.0039215686, 0.8862745098, + 0.2235294117647059, 0.0392156863, 0.0039215686, 0.9019607843, 0.22745098039215686, + 0.0352941176, 0.0039215686, 0.9176470588, 0.23137254901960785, 0.0274509804, 0.0039215686, + 0.9333333333, 0.23529411764705885, 0.0156862745, 0.0039215686, 0.9490196078, + 0.23921568627450984, 0.0078431373, 0.0039215686, 0.9647058824, 0.24313725490196078, + 0.0039215686, 0.0039215686, 0.9960784314, 0.24705882352941178, 0.0039215686, 0.0039215686, + 0.9960784314, 0.25098039215686274, 0.0039215686, 0.0196078431, 0.9647058824, + 0.2549019607843137, 0.0039215686, 0.0392156863, 0.9490196078, 0.25882352941176473, + 0.0039215686, 0.0549019608, 0.9333333333, 0.2627450980392157, 0.0039215686, 0.0745098039, + 0.9176470588, 0.26666666666666666, 0.0039215686, 0.0901960784, 0.9019607843, + 0.27058823529411763, 0.0039215686, 0.1098039216, 0.8862745098, 0.27450980392156865, + 0.0039215686, 0.1254901961, 0.8705882353, 0.2784313725490196, 0.0039215686, 0.1450980392, + 0.8549019608, 0.2823529411764706, 0.0039215686, 0.1607843137, 0.8392156863, + 0.28627450980392155, 0.0039215686, 0.1803921569, 0.8235294118, 0.2901960784313726, + 0.0039215686, 0.1960784314, 0.8078431373, 0.29411764705882354, 0.0039215686, 0.2156862745, + 0.7921568627, 0.2980392156862745, 0.0039215686, 0.231372549, 0.7764705882, + 0.30196078431372547, 0.0039215686, 0.2509803922, 0.7607843137, 0.3058823529411765, + 0.0039215686, 0.262745098, 0.7490196078, 0.30980392156862746, 0.0039215686, 0.2823529412, + 0.7333333333, 0.3137254901960784, 0.0039215686, 0.2980392157, 0.7176470588, + 0.3176470588235294, 0.0039215686, 0.3176470588, 0.7019607843, 0.3215686274509804, + 0.0039215686, 0.3333333333, 0.6862745098, 0.3254901960784314, 0.0039215686, 0.3529411765, + 0.6705882353, 0.32941176470588235, 0.0039215686, 0.368627451, 0.6549019608, + 0.3333333333333333, 0.0039215686, 0.3882352941, 0.6392156863, 0.33725490196078434, + 0.0039215686, 0.4039215686, 0.6235294118, 0.3411764705882353, 0.0039215686, 0.4235294118, + 0.6078431373, 0.34509803921568627, 0.0039215686, 0.4392156863, 0.5921568627, + 0.34901960784313724, 0.0039215686, 0.4588235294, 0.5764705882, 0.35294117647058826, + 0.0039215686, 0.4745098039, 0.5607843137, 0.3568627450980392, 0.0039215686, 0.4941176471, + 0.5450980392, 0.3607843137254902, 0.0039215686, 0.5098039216, 0.5294117647, + 0.36470588235294116, 0.0039215686, 0.5294117647, 0.5137254902, 0.3686274509803922, + 0.0039215686, 0.5450980392, 0.4980392157, 0.37254901960784315, 0.0039215686, 0.5647058824, + 0.4784313725, 0.3764705882352941, 0.0039215686, 0.5803921569, 0.462745098, 0.3803921568627451, + 0.0039215686, 0.6, 0.4470588235, 0.3843137254901961, 0.0039215686, 0.6156862745, 0.4274509804, + 0.38823529411764707, 0.0039215686, 0.6352941176, 0.4117647059, 0.39215686274509803, + 0.0039215686, 0.6509803922, 0.3960784314, 0.396078431372549, 0.0039215686, 0.6705882353, + 0.3803921569, 0.4, 0.0039215686, 0.6862745098, 0.3607843137, 0.403921568627451, 0.0039215686, + 0.7058823529, 0.3450980392, 0.40784313725490196, 0.0039215686, 0.7215686275, 0.3294117647, + 0.4117647058823529, 0.0039215686, 0.7411764706, 0.3137254902, 0.41568627450980394, + 0.0039215686, 0.7529411765, 0.2941176471, 0.4196078431372549, 0.0039215686, 0.7960784314, + 0.2784313725, 0.4235294117647059, 0.0039215686, 0.7960784314, 0.262745098, + 0.42745098039215684, 0.0392156863, 0.8039215686, 0.2509803922, 0.43137254901960786, + 0.0745098039, 0.8117647059, 0.231372549, 0.43529411764705883, 0.1098039216, 0.8196078431, + 0.2156862745, 0.4392156862745098, 0.1450980392, 0.8274509804, 0.2, 0.44313725490196076, + 0.1803921569, 0.8352941176, 0.1843137255, 0.4470588235294118, 0.2156862745, 0.8431372549, + 0.1647058824, 0.45098039215686275, 0.2509803922, 0.8509803922, 0.1490196078, + 0.4549019607843137, 0.2823529412, 0.8588235294, 0.1333333333, 0.4588235294117647, + 0.3176470588, 0.8666666667, 0.1176470588, 0.4627450980392157, 0.3529411765, 0.8745098039, + 0.0980392157, 0.4666666666666667, 0.3882352941, 0.8823529412, 0.0823529412, + 0.4705882352941177, 0.4235294118, 0.8901960784, 0.0666666667, 0.4745098039215686, + 0.4588235294, 0.8980392157, 0.0509803922, 0.4784313725490197, 0.4941176471, 0.9058823529, + 0.0431372549, 0.48235294117647065, 0.5294117647, 0.9137254902, 0.031372549, + 0.48627450980392156, 0.5647058824, 0.9215686275, 0.0196078431, 0.49019607843137253, 0.6, + 0.9294117647, 0.0078431373, 0.49411764705882355, 0.6352941176, 0.937254902, 0.0039215686, + 0.4980392156862745, 0.6705882353, 0.9450980392, 0.0039215686, 0.5019607843137255, + 0.7058823529, 0.9490196078, 0.0039215686, 0.5058823529411764, 0.7411764706, 0.9568627451, + 0.0039215686, 0.5098039215686274, 0.7725490196, 0.9607843137, 0.0039215686, + 0.5137254901960784, 0.8078431373, 0.968627451, 0.0039215686, 0.5176470588235295, 0.8431372549, + 0.9725490196, 0.0039215686, 0.5215686274509804, 0.8784313725, 0.9803921569, 0.0039215686, + 0.5254901960784314, 0.9137254902, 0.9843137255, 0.0039215686, 0.5294117647058824, + 0.9490196078, 0.9921568627, 0.0039215686, 0.5333333333333333, 0.9960784314, 0.9960784314, + 0.0039215686, 0.5372549019607843, 0.9960784314, 0.9960784314, 0.0039215686, + 0.5411764705882353, 0.9960784314, 0.9921568627, 0.0039215686, 0.5450980392156862, + 0.9960784314, 0.9843137255, 0.0039215686, 0.5490196078431373, 0.9960784314, 0.9764705882, + 0.0039215686, 0.5529411764705883, 0.9960784314, 0.968627451, 0.0039215686, 0.5568627450980392, + 0.9960784314, 0.9607843137, 0.0039215686, 0.5607843137254902, 0.9960784314, 0.9529411765, + 0.0039215686, 0.5647058823529412, 0.9960784314, 0.9450980392, 0.0039215686, + 0.5686274509803921, 0.9960784314, 0.937254902, 0.0039215686, 0.5725490196078431, 0.9960784314, + 0.9294117647, 0.0039215686, 0.5764705882352941, 0.9960784314, 0.9215686275, 0.0039215686, + 0.5803921568627451, 0.9960784314, 0.9137254902, 0.0039215686, 0.5843137254901961, + 0.9960784314, 0.9058823529, 0.0039215686, 0.5882352941176471, 0.9960784314, 0.8980392157, + 0.0039215686, 0.592156862745098, 0.9960784314, 0.8901960784, 0.0039215686, 0.596078431372549, + 0.9960784314, 0.8823529412, 0.0039215686, 0.6, 0.9960784314, 0.8745098039, 0.0039215686, + 0.6039215686274509, 0.9960784314, 0.8666666667, 0.0039215686, 0.6078431372549019, + 0.9960784314, 0.8588235294, 0.0039215686, 0.611764705882353, 0.9960784314, 0.8509803922, + 0.0039215686, 0.615686274509804, 0.9960784314, 0.8431372549, 0.0039215686, 0.6196078431372549, + 0.9960784314, 0.8352941176, 0.0039215686, 0.6235294117647059, 0.9960784314, 0.8274509804, + 0.0039215686, 0.6274509803921569, 0.9960784314, 0.8196078431, 0.0039215686, + 0.6313725490196078, 0.9960784314, 0.8117647059, 0.0039215686, 0.6352941176470588, + 0.9960784314, 0.8039215686, 0.0039215686, 0.6392156862745098, 0.9960784314, 0.7960784314, + 0.0039215686, 0.6431372549019608, 0.9960784314, 0.7882352941, 0.0039215686, + 0.6470588235294118, 0.9960784314, 0.7803921569, 0.0039215686, 0.6509803921568628, + 0.9960784314, 0.7725490196, 0.0039215686, 0.6549019607843137, 0.9960784314, 0.7647058824, + 0.0039215686, 0.6588235294117647, 0.9960784314, 0.7568627451, 0.0039215686, + 0.6627450980392157, 0.9960784314, 0.7490196078, 0.0039215686, 0.6666666666666666, + 0.9960784314, 0.7450980392, 0.0039215686, 0.6705882352941176, 0.9960784314, 0.737254902, + 0.0039215686, 0.6745098039215687, 0.9960784314, 0.7294117647, 0.0039215686, + 0.6784313725490196, 0.9960784314, 0.7215686275, 0.0039215686, 0.6823529411764706, + 0.9960784314, 0.7137254902, 0.0039215686, 0.6862745098039216, 0.9960784314, 0.7058823529, + 0.0039215686, 0.6901960784313725, 0.9960784314, 0.6980392157, 0.0039215686, + 0.6941176470588235, 0.9960784314, 0.6901960784, 0.0039215686, 0.6980392156862745, + 0.9960784314, 0.6823529412, 0.0039215686, 0.7019607843137254, 0.9960784314, 0.6745098039, + 0.0039215686, 0.7058823529411765, 0.9960784314, 0.6666666667, 0.0039215686, + 0.7098039215686275, 0.9960784314, 0.6588235294, 0.0039215686, 0.7137254901960784, + 0.9960784314, 0.6509803922, 0.0039215686, 0.7176470588235294, 0.9960784314, 0.6431372549, + 0.0039215686, 0.7215686274509804, 0.9960784314, 0.6352941176, 0.0039215686, + 0.7254901960784313, 0.9960784314, 0.6274509804, 0.0039215686, 0.7294117647058823, + 0.9960784314, 0.6196078431, 0.0039215686, 0.7333333333333333, 0.9960784314, 0.6117647059, + 0.0039215686, 0.7372549019607844, 0.9960784314, 0.6039215686, 0.0039215686, + 0.7411764705882353, 0.9960784314, 0.5960784314, 0.0039215686, 0.7450980392156863, + 0.9960784314, 0.5882352941, 0.0039215686, 0.7490196078431373, 0.9960784314, 0.5803921569, + 0.0039215686, 0.7529411764705882, 0.9960784314, 0.5725490196, 0.0039215686, + 0.7568627450980392, 0.9960784314, 0.5647058824, 0.0039215686, 0.7607843137254902, + 0.9960784314, 0.5568627451, 0.0039215686, 0.7647058823529411, 0.9960784314, 0.5490196078, + 0.0039215686, 0.7686274509803922, 0.9960784314, 0.5411764706, 0.0039215686, + 0.7725490196078432, 0.9960784314, 0.5333333333, 0.0039215686, 0.7764705882352941, + 0.9960784314, 0.5254901961, 0.0039215686, 0.7803921568627451, 0.9960784314, 0.5176470588, + 0.0039215686, 0.7843137254901961, 0.9960784314, 0.5098039216, 0.0039215686, 0.788235294117647, + 0.9960784314, 0.5019607843, 0.0039215686, 0.792156862745098, 0.9960784314, 0.4941176471, + 0.0039215686, 0.796078431372549, 0.9960784314, 0.4862745098, 0.0039215686, 0.8, 0.9960784314, + 0.4784313725, 0.0039215686, 0.803921568627451, 0.9960784314, 0.4705882353, 0.0039215686, + 0.807843137254902, 0.9960784314, 0.462745098, 0.0039215686, 0.8117647058823529, 0.9960784314, + 0.4549019608, 0.0039215686, 0.8156862745098039, 0.9960784314, 0.4470588235, 0.0039215686, + 0.8196078431372549, 0.9960784314, 0.4392156863, 0.0039215686, 0.8235294117647058, + 0.9960784314, 0.431372549, 0.0039215686, 0.8274509803921568, 0.9960784314, 0.4235294118, + 0.0039215686, 0.8313725490196079, 0.9960784314, 0.4156862745, 0.0039215686, + 0.8352941176470589, 0.9960784314, 0.4078431373, 0.0039215686, 0.8392156862745098, + 0.9960784314, 0.4, 0.0039215686, 0.8431372549019608, 0.9960784314, 0.3921568627, 0.0039215686, + 0.8470588235294118, 0.9960784314, 0.3843137255, 0.0039215686, 0.8509803921568627, + 0.9960784314, 0.3764705882, 0.0039215686, 0.8549019607843137, 0.9960784314, 0.368627451, + 0.0039215686, 0.8588235294117647, 0.9960784314, 0.3607843137, 0.0039215686, + 0.8627450980392157, 0.9960784314, 0.3529411765, 0.0039215686, 0.8666666666666667, + 0.9960784314, 0.3450980392, 0.0039215686, 0.8705882352941177, 0.9960784314, 0.337254902, + 0.0039215686, 0.8745098039215686, 0.9960784314, 0.3294117647, 0.0039215686, + 0.8784313725490196, 0.9960784314, 0.3215686275, 0.0039215686, 0.8823529411764706, + 0.9960784314, 0.3137254902, 0.0039215686, 0.8862745098039215, 0.9960784314, 0.3058823529, + 0.0039215686, 0.8901960784313725, 0.9960784314, 0.2980392157, 0.0039215686, + 0.8941176470588236, 0.9960784314, 0.2901960784, 0.0039215686, 0.8980392156862745, + 0.9960784314, 0.2823529412, 0.0039215686, 0.9019607843137255, 0.9960784314, 0.2705882353, + 0.0039215686, 0.9058823529411765, 0.9960784314, 0.2588235294, 0.0039215686, + 0.9098039215686274, 0.9960784314, 0.2509803922, 0.0039215686, 0.9137254901960784, + 0.9960784314, 0.2431372549, 0.0039215686, 0.9176470588235294, 0.9960784314, 0.231372549, + 0.0039215686, 0.9215686274509803, 0.9960784314, 0.2196078431, 0.0039215686, + 0.9254901960784314, 0.9960784314, 0.2117647059, 0.0039215686, 0.9294117647058824, + 0.9960784314, 0.2, 0.0039215686, 0.9333333333333333, 0.9960784314, 0.1882352941, 0.0039215686, + 0.9372549019607843, 0.9960784314, 0.1764705882, 0.0039215686, 0.9411764705882354, + 0.9960784314, 0.168627451, 0.0039215686, 0.9450980392156864, 0.9960784314, 0.1568627451, + 0.0039215686, 0.9490196078431372, 0.9960784314, 0.1450980392, 0.0039215686, + 0.9529411764705882, 0.9960784314, 0.1333333333, 0.0039215686, 0.9568627450980394, + 0.9960784314, 0.1254901961, 0.0039215686, 0.9607843137254903, 0.9960784314, 0.1137254902, + 0.0039215686, 0.9647058823529413, 0.9960784314, 0.1019607843, 0.0039215686, + 0.9686274509803922, 0.9960784314, 0.0901960784, 0.0039215686, 0.9725490196078431, + 0.9960784314, 0.0823529412, 0.0039215686, 0.9764705882352941, 0.9960784314, 0.0705882353, + 0.0039215686, 0.9803921568627451, 0.9960784314, 0.0588235294, 0.0039215686, 0.984313725490196, + 0.9960784314, 0.0470588235, 0.0039215686, 0.9882352941176471, 0.9960784314, 0.0392156863, + 0.0039215686, 0.9921568627450981, 0.9960784314, 0.0274509804, 0.0039215686, 0.996078431372549, + 0.9960784314, 0.0156862745, 0.0039215686, 1.0, 0.9960784314, 0.0156862745, 0.0039215686, + ], + description: 'S PET', + }, + { + ColorSpace: 'RGB', + Name: 'perfusion', + name: 'perfusion', + RGBPoints: [ + 0.0, 0.0, 0.0, 0.0, 0.00392156862745098, 0.0078431373, 0.0235294118, 0.0235294118, + 0.00784313725490196, 0.0078431373, 0.031372549, 0.0470588235, 0.011764705882352941, + 0.0078431373, 0.0392156863, 0.062745098, 0.01568627450980392, 0.0078431373, 0.0470588235, + 0.0862745098, 0.0196078431372549, 0.0078431373, 0.0549019608, 0.1019607843, + 0.023529411764705882, 0.0078431373, 0.0549019608, 0.1254901961, 0.027450980392156862, + 0.0078431373, 0.062745098, 0.1411764706, 0.03137254901960784, 0.0078431373, 0.0705882353, + 0.1647058824, 0.03529411764705882, 0.0078431373, 0.0784313725, 0.1803921569, + 0.0392156862745098, 0.0078431373, 0.0862745098, 0.2039215686, 0.043137254901960784, + 0.0078431373, 0.0862745098, 0.2196078431, 0.047058823529411764, 0.0078431373, 0.0941176471, + 0.2431372549, 0.050980392156862744, 0.0078431373, 0.1019607843, 0.2666666667, + 0.054901960784313725, 0.0078431373, 0.1098039216, 0.2823529412, 0.05882352941176471, + 0.0078431373, 0.1176470588, 0.3058823529, 0.06274509803921569, 0.0078431373, 0.1176470588, + 0.3215686275, 0.06666666666666667, 0.0078431373, 0.1254901961, 0.3450980392, + 0.07058823529411765, 0.0078431373, 0.1333333333, 0.3607843137, 0.07450980392156863, + 0.0078431373, 0.1411764706, 0.3843137255, 0.0784313725490196, 0.0078431373, 0.1490196078, 0.4, + 0.08235294117647059, 0.0078431373, 0.1490196078, 0.4235294118, 0.08627450980392157, + 0.0078431373, 0.1568627451, 0.4392156863, 0.09019607843137255, 0.0078431373, 0.1647058824, + 0.462745098, 0.09411764705882353, 0.0078431373, 0.1725490196, 0.4784313725, + 0.09803921568627451, 0.0078431373, 0.1803921569, 0.5019607843, 0.10196078431372549, + 0.0078431373, 0.1803921569, 0.5254901961, 0.10588235294117647, 0.0078431373, 0.1882352941, + 0.5411764706, 0.10980392156862745, 0.0078431373, 0.1960784314, 0.5647058824, + 0.11372549019607843, 0.0078431373, 0.2039215686, 0.5803921569, 0.11764705882352942, + 0.0078431373, 0.2117647059, 0.6039215686, 0.12156862745098039, 0.0078431373, 0.2117647059, + 0.6196078431, 0.12549019607843137, 0.0078431373, 0.2196078431, 0.6431372549, + 0.12941176470588237, 0.0078431373, 0.2274509804, 0.6588235294, 0.13333333333333333, + 0.0078431373, 0.2352941176, 0.6823529412, 0.13725490196078433, 0.0078431373, 0.2431372549, + 0.6980392157, 0.1411764705882353, 0.0078431373, 0.2431372549, 0.7215686275, + 0.1450980392156863, 0.0078431373, 0.2509803922, 0.737254902, 0.14901960784313725, + 0.0078431373, 0.2588235294, 0.7607843137, 0.15294117647058825, 0.0078431373, 0.2666666667, + 0.7843137255, 0.1568627450980392, 0.0078431373, 0.2745098039, 0.8, 0.1607843137254902, + 0.0078431373, 0.2745098039, 0.8235294118, 0.16470588235294117, 0.0078431373, 0.2823529412, + 0.8392156863, 0.16862745098039217, 0.0078431373, 0.2901960784, 0.862745098, + 0.17254901960784313, 0.0078431373, 0.2980392157, 0.8784313725, 0.17647058823529413, + 0.0078431373, 0.3058823529, 0.9019607843, 0.1803921568627451, 0.0078431373, 0.3058823529, + 0.9176470588, 0.1843137254901961, 0.0078431373, 0.2980392157, 0.9411764706, + 0.18823529411764706, 0.0078431373, 0.3058823529, 0.9568627451, 0.19215686274509805, + 0.0078431373, 0.2980392157, 0.9803921569, 0.19607843137254902, 0.0078431373, 0.2980392157, + 0.9882352941, 0.2, 0.0078431373, 0.2901960784, 0.9803921569, 0.20392156862745098, + 0.0078431373, 0.2901960784, 0.9647058824, 0.20784313725490197, 0.0078431373, 0.2823529412, + 0.9568627451, 0.21176470588235294, 0.0078431373, 0.2823529412, 0.9411764706, + 0.21568627450980393, 0.0078431373, 0.2745098039, 0.9333333333, 0.2196078431372549, + 0.0078431373, 0.2666666667, 0.9176470588, 0.2235294117647059, 0.0078431373, 0.2666666667, + 0.9098039216, 0.22745098039215686, 0.0078431373, 0.2588235294, 0.9019607843, + 0.23137254901960785, 0.0078431373, 0.2588235294, 0.8862745098, 0.23529411764705885, + 0.0078431373, 0.2509803922, 0.8784313725, 0.23921568627450984, 0.0078431373, 0.2509803922, + 0.862745098, 0.24313725490196078, 0.0078431373, 0.2431372549, 0.8549019608, + 0.24705882352941178, 0.0078431373, 0.2352941176, 0.8392156863, 0.25098039215686274, + 0.0078431373, 0.2352941176, 0.831372549, 0.2549019607843137, 0.0078431373, 0.2274509804, + 0.8235294118, 0.25882352941176473, 0.0078431373, 0.2274509804, 0.8078431373, + 0.2627450980392157, 0.0078431373, 0.2196078431, 0.8, 0.26666666666666666, 0.0078431373, + 0.2196078431, 0.7843137255, 0.27058823529411763, 0.0078431373, 0.2117647059, 0.7764705882, + 0.27450980392156865, 0.0078431373, 0.2039215686, 0.7607843137, 0.2784313725490196, + 0.0078431373, 0.2039215686, 0.7529411765, 0.2823529411764706, 0.0078431373, 0.1960784314, + 0.7450980392, 0.28627450980392155, 0.0078431373, 0.1960784314, 0.7294117647, + 0.2901960784313726, 0.0078431373, 0.1882352941, 0.7215686275, 0.29411764705882354, + 0.0078431373, 0.1882352941, 0.7058823529, 0.2980392156862745, 0.0078431373, 0.1803921569, + 0.6980392157, 0.30196078431372547, 0.0078431373, 0.1803921569, 0.6823529412, + 0.3058823529411765, 0.0078431373, 0.1725490196, 0.6745098039, 0.30980392156862746, + 0.0078431373, 0.1647058824, 0.6666666667, 0.3137254901960784, 0.0078431373, 0.1647058824, + 0.6509803922, 0.3176470588235294, 0.0078431373, 0.1568627451, 0.6431372549, + 0.3215686274509804, 0.0078431373, 0.1568627451, 0.6274509804, 0.3254901960784314, + 0.0078431373, 0.1490196078, 0.6196078431, 0.32941176470588235, 0.0078431373, 0.1490196078, + 0.6039215686, 0.3333333333333333, 0.0078431373, 0.1411764706, 0.5960784314, + 0.33725490196078434, 0.0078431373, 0.1333333333, 0.5882352941, 0.3411764705882353, + 0.0078431373, 0.1333333333, 0.5725490196, 0.34509803921568627, 0.0078431373, 0.1254901961, + 0.5647058824, 0.34901960784313724, 0.0078431373, 0.1254901961, 0.5490196078, + 0.35294117647058826, 0.0078431373, 0.1176470588, 0.5411764706, 0.3568627450980392, + 0.0078431373, 0.1176470588, 0.5254901961, 0.3607843137254902, 0.0078431373, 0.1098039216, + 0.5176470588, 0.36470588235294116, 0.0078431373, 0.1019607843, 0.5098039216, + 0.3686274509803922, 0.0078431373, 0.1019607843, 0.4941176471, 0.37254901960784315, + 0.0078431373, 0.0941176471, 0.4862745098, 0.3764705882352941, 0.0078431373, 0.0941176471, + 0.4705882353, 0.3803921568627451, 0.0078431373, 0.0862745098, 0.462745098, 0.3843137254901961, + 0.0078431373, 0.0862745098, 0.4470588235, 0.38823529411764707, 0.0078431373, 0.0784313725, + 0.4392156863, 0.39215686274509803, 0.0078431373, 0.0705882353, 0.431372549, 0.396078431372549, + 0.0078431373, 0.0705882353, 0.4156862745, 0.4, 0.0078431373, 0.062745098, 0.4078431373, + 0.403921568627451, 0.0078431373, 0.062745098, 0.3921568627, 0.40784313725490196, 0.0078431373, + 0.0549019608, 0.3843137255, 0.4117647058823529, 0.0078431373, 0.0549019608, 0.368627451, + 0.41568627450980394, 0.0078431373, 0.0470588235, 0.3607843137, 0.4196078431372549, + 0.0078431373, 0.0470588235, 0.3529411765, 0.4235294117647059, 0.0078431373, 0.0392156863, + 0.337254902, 0.42745098039215684, 0.0078431373, 0.031372549, 0.3294117647, + 0.43137254901960786, 0.0078431373, 0.031372549, 0.3137254902, 0.43529411764705883, + 0.0078431373, 0.0235294118, 0.3058823529, 0.4392156862745098, 0.0078431373, 0.0235294118, + 0.2901960784, 0.44313725490196076, 0.0078431373, 0.0156862745, 0.2823529412, + 0.4470588235294118, 0.0078431373, 0.0156862745, 0.2745098039, 0.45098039215686275, + 0.0078431373, 0.0078431373, 0.2588235294, 0.4549019607843137, 0.0235294118, 0.0078431373, + 0.2509803922, 0.4588235294117647, 0.0078431373, 0.0078431373, 0.2352941176, + 0.4627450980392157, 0.0078431373, 0.0078431373, 0.2274509804, 0.4666666666666667, + 0.0078431373, 0.0078431373, 0.2117647059, 0.4705882352941177, 0.0078431373, 0.0078431373, + 0.2039215686, 0.4745098039215686, 0.0078431373, 0.0078431373, 0.1960784314, + 0.4784313725490197, 0.0078431373, 0.0078431373, 0.1803921569, 0.48235294117647065, + 0.0078431373, 0.0078431373, 0.1725490196, 0.48627450980392156, 0.0078431373, 0.0078431373, + 0.1568627451, 0.49019607843137253, 0.0078431373, 0.0078431373, 0.1490196078, + 0.49411764705882355, 0.0078431373, 0.0078431373, 0.1333333333, 0.4980392156862745, + 0.0078431373, 0.0078431373, 0.1254901961, 0.5019607843137255, 0.0078431373, 0.0078431373, + 0.1176470588, 0.5058823529411764, 0.0078431373, 0.0078431373, 0.1019607843, + 0.5098039215686274, 0.0078431373, 0.0078431373, 0.0941176471, 0.5137254901960784, + 0.0078431373, 0.0078431373, 0.0784313725, 0.5176470588235295, 0.0078431373, 0.0078431373, + 0.0705882353, 0.5215686274509804, 0.0078431373, 0.0078431373, 0.0549019608, + 0.5254901960784314, 0.0078431373, 0.0078431373, 0.0470588235, 0.5294117647058824, + 0.0235294118, 0.0078431373, 0.0392156863, 0.5333333333333333, 0.031372549, 0.0078431373, + 0.0235294118, 0.5372549019607843, 0.0392156863, 0.0078431373, 0.0156862745, + 0.5411764705882353, 0.0549019608, 0.0078431373, 0.0, 0.5450980392156862, 0.062745098, + 0.0078431373, 0.0, 0.5490196078431373, 0.0705882353, 0.0078431373, 0.0, 0.5529411764705883, + 0.0862745098, 0.0078431373, 0.0, 0.5568627450980392, 0.0941176471, 0.0078431373, 0.0, + 0.5607843137254902, 0.1019607843, 0.0078431373, 0.0, 0.5647058823529412, 0.1098039216, + 0.0078431373, 0.0, 0.5686274509803921, 0.1254901961, 0.0078431373, 0.0, 0.5725490196078431, + 0.1333333333, 0.0078431373, 0.0, 0.5764705882352941, 0.1411764706, 0.0078431373, 0.0, + 0.5803921568627451, 0.1568627451, 0.0078431373, 0.0, 0.5843137254901961, 0.1647058824, + 0.0078431373, 0.0, 0.5882352941176471, 0.1725490196, 0.0078431373, 0.0, 0.592156862745098, + 0.1882352941, 0.0078431373, 0.0, 0.596078431372549, 0.1960784314, 0.0078431373, 0.0, 0.6, + 0.2039215686, 0.0078431373, 0.0, 0.6039215686274509, 0.2117647059, 0.0078431373, 0.0, + 0.6078431372549019, 0.2274509804, 0.0078431373, 0.0, 0.611764705882353, 0.2352941176, + 0.0078431373, 0.0, 0.615686274509804, 0.2431372549, 0.0078431373, 0.0, 0.6196078431372549, + 0.2588235294, 0.0078431373, 0.0, 0.6235294117647059, 0.2666666667, 0.0078431373, 0.0, + 0.6274509803921569, 0.2745098039, 0.0, 0.0, 0.6313725490196078, 0.2901960784, 0.0156862745, + 0.0, 0.6352941176470588, 0.2980392157, 0.0235294118, 0.0, 0.6392156862745098, 0.3058823529, + 0.0392156863, 0.0, 0.6431372549019608, 0.3137254902, 0.0470588235, 0.0, 0.6470588235294118, + 0.3294117647, 0.0549019608, 0.0, 0.6509803921568628, 0.337254902, 0.0705882353, 0.0, + 0.6549019607843137, 0.3450980392, 0.0784313725, 0.0, 0.6588235294117647, 0.3607843137, + 0.0862745098, 0.0, 0.6627450980392157, 0.368627451, 0.1019607843, 0.0, 0.6666666666666666, + 0.3764705882, 0.1098039216, 0.0, 0.6705882352941176, 0.3843137255, 0.1176470588, 0.0, + 0.6745098039215687, 0.4, 0.1333333333, 0.0, 0.6784313725490196, 0.4078431373, 0.1411764706, + 0.0, 0.6823529411764706, 0.4156862745, 0.1490196078, 0.0, 0.6862745098039216, 0.431372549, + 0.1647058824, 0.0, 0.6901960784313725, 0.4392156863, 0.1725490196, 0.0, 0.6941176470588235, + 0.4470588235, 0.1803921569, 0.0, 0.6980392156862745, 0.462745098, 0.1960784314, 0.0, + 0.7019607843137254, 0.4705882353, 0.2039215686, 0.0, 0.7058823529411765, 0.4784313725, + 0.2117647059, 0.0, 0.7098039215686275, 0.4862745098, 0.2274509804, 0.0, 0.7137254901960784, + 0.5019607843, 0.2352941176, 0.0, 0.7176470588235294, 0.5098039216, 0.2431372549, 0.0, + 0.7215686274509804, 0.5176470588, 0.2588235294, 0.0, 0.7254901960784313, 0.5333333333, + 0.2666666667, 0.0, 0.7294117647058823, 0.5411764706, 0.2745098039, 0.0, 0.7333333333333333, + 0.5490196078, 0.2901960784, 0.0, 0.7372549019607844, 0.5647058824, 0.2980392157, 0.0, + 0.7411764705882353, 0.5725490196, 0.3058823529, 0.0, 0.7450980392156863, 0.5803921569, + 0.3215686275, 0.0, 0.7490196078431373, 0.5882352941, 0.3294117647, 0.0, 0.7529411764705882, + 0.6039215686, 0.337254902, 0.0, 0.7568627450980392, 0.6117647059, 0.3529411765, 0.0, + 0.7607843137254902, 0.6196078431, 0.3607843137, 0.0, 0.7647058823529411, 0.6352941176, + 0.368627451, 0.0, 0.7686274509803922, 0.6431372549, 0.3843137255, 0.0, 0.7725490196078432, + 0.6509803922, 0.3921568627, 0.0, 0.7764705882352941, 0.6588235294, 0.4, 0.0, + 0.7803921568627451, 0.6745098039, 0.4156862745, 0.0, 0.7843137254901961, 0.6823529412, + 0.4235294118, 0.0, 0.788235294117647, 0.6901960784, 0.431372549, 0.0, 0.792156862745098, + 0.7058823529, 0.4470588235, 0.0, 0.796078431372549, 0.7137254902, 0.4549019608, 0.0, 0.8, + 0.7215686275, 0.462745098, 0.0, 0.803921568627451, 0.737254902, 0.4784313725, 0.0, + 0.807843137254902, 0.7450980392, 0.4862745098, 0.0, 0.8117647058823529, 0.7529411765, + 0.4941176471, 0.0, 0.8156862745098039, 0.7607843137, 0.5098039216, 0.0, 0.8196078431372549, + 0.7764705882, 0.5176470588, 0.0, 0.8235294117647058, 0.7843137255, 0.5254901961, 0.0, + 0.8274509803921568, 0.7921568627, 0.5411764706, 0.0, 0.8313725490196079, 0.8078431373, + 0.5490196078, 0.0, 0.8352941176470589, 0.8156862745, 0.5568627451, 0.0, 0.8392156862745098, + 0.8235294118, 0.5725490196, 0.0, 0.8431372549019608, 0.8392156863, 0.5803921569, 0.0, + 0.8470588235294118, 0.8470588235, 0.5882352941, 0.0, 0.8509803921568627, 0.8549019608, + 0.6039215686, 0.0, 0.8549019607843137, 0.862745098, 0.6117647059, 0.0, 0.8588235294117647, + 0.8784313725, 0.6196078431, 0.0, 0.8627450980392157, 0.8862745098, 0.6352941176, 0.0, + 0.8666666666666667, 0.8941176471, 0.6431372549, 0.0, 0.8705882352941177, 0.9098039216, + 0.6509803922, 0.0, 0.8745098039215686, 0.9176470588, 0.6666666667, 0.0, 0.8784313725490196, + 0.9254901961, 0.6745098039, 0.0, 0.8823529411764706, 0.9411764706, 0.6823529412, 0.0, + 0.8862745098039215, 0.9490196078, 0.6980392157, 0.0, 0.8901960784313725, 0.9568627451, + 0.7058823529, 0.0, 0.8941176470588236, 0.9647058824, 0.7137254902, 0.0, 0.8980392156862745, + 0.9803921569, 0.7294117647, 0.0, 0.9019607843137255, 0.9882352941, 0.737254902, 0.0, + 0.9058823529411765, 0.9960784314, 0.7450980392, 0.0, 0.9098039215686274, 0.9960784314, + 0.7607843137, 0.0, 0.9137254901960784, 0.9960784314, 0.768627451, 0.0, 0.9176470588235294, + 0.9960784314, 0.7764705882, 0.0, 0.9215686274509803, 0.9960784314, 0.7921568627, 0.0, + 0.9254901960784314, 0.9960784314, 0.8, 0.0, 0.9294117647058824, 0.9960784314, 0.8078431373, + 0.0, 0.9333333333333333, 0.9960784314, 0.8235294118, 0.0, 0.9372549019607843, 0.9960784314, + 0.831372549, 0.0, 0.9411764705882354, 0.9960784314, 0.8392156863, 0.0, 0.9450980392156864, + 0.9960784314, 0.8549019608, 0.0, 0.9490196078431372, 0.9960784314, 0.862745098, 0.0549019608, + 0.9529411764705882, 0.9960784314, 0.8705882353, 0.1098039216, 0.9568627450980394, + 0.9960784314, 0.8862745098, 0.1647058824, 0.9607843137254903, 0.9960784314, 0.8941176471, + 0.2196078431, 0.9647058823529413, 0.9960784314, 0.9019607843, 0.2666666667, + 0.9686274509803922, 0.9960784314, 0.9176470588, 0.3215686275, 0.9725490196078431, + 0.9960784314, 0.9254901961, 0.3764705882, 0.9764705882352941, 0.9960784314, 0.9333333333, + 0.431372549, 0.9803921568627451, 0.9960784314, 0.9490196078, 0.4862745098, 0.984313725490196, + 0.9960784314, 0.9568627451, 0.5333333333, 0.9882352941176471, 0.9960784314, 0.9647058824, + 0.5882352941, 0.9921568627450981, 0.9960784314, 0.9803921569, 0.6431372549, 0.996078431372549, + 0.9960784314, 0.9882352941, 0.6980392157, 1.0, 0.9960784314, 0.9960784314, 0.7450980392, + ], + description: 'Perfusion', + }, + { + ColorSpace: 'RGB', + Name: 'rainbow_2', + name: 'rainbow_2', + RGBPoints: [ + 0.0, 0.0, 0.0, 0.0, 0.00392156862745098, 0.0156862745, 0.0, 0.0117647059, 0.00784313725490196, + 0.0352941176, 0.0, 0.0274509804, 0.011764705882352941, 0.0509803922, 0.0, 0.0392156863, + 0.01568627450980392, 0.0705882353, 0.0, 0.0549019608, 0.0196078431372549, 0.0862745098, 0.0, + 0.0745098039, 0.023529411764705882, 0.1058823529, 0.0, 0.0901960784, 0.027450980392156862, + 0.1215686275, 0.0, 0.1098039216, 0.03137254901960784, 0.1411764706, 0.0, 0.1254901961, + 0.03529411764705882, 0.1568627451, 0.0, 0.1490196078, 0.0392156862745098, 0.1764705882, 0.0, + 0.168627451, 0.043137254901960784, 0.1960784314, 0.0, 0.1882352941, 0.047058823529411764, + 0.2117647059, 0.0, 0.2078431373, 0.050980392156862744, 0.2274509804, 0.0, 0.231372549, + 0.054901960784313725, 0.2392156863, 0.0, 0.2470588235, 0.05882352941176471, 0.2509803922, 0.0, + 0.2666666667, 0.06274509803921569, 0.2666666667, 0.0, 0.2823529412, 0.06666666666666667, + 0.2705882353, 0.0, 0.3019607843, 0.07058823529411765, 0.2823529412, 0.0, 0.3176470588, + 0.07450980392156863, 0.2901960784, 0.0, 0.337254902, 0.0784313725490196, 0.3019607843, 0.0, + 0.3568627451, 0.08235294117647059, 0.3098039216, 0.0, 0.3725490196, 0.08627450980392157, + 0.3137254902, 0.0, 0.3921568627, 0.09019607843137255, 0.3215686275, 0.0, 0.4078431373, + 0.09411764705882353, 0.3254901961, 0.0, 0.4274509804, 0.09803921568627451, 0.3333333333, 0.0, + 0.4431372549, 0.10196078431372549, 0.3294117647, 0.0, 0.462745098, 0.10588235294117647, + 0.337254902, 0.0, 0.4784313725, 0.10980392156862745, 0.3411764706, 0.0, 0.4980392157, + 0.11372549019607843, 0.3450980392, 0.0, 0.5176470588, 0.11764705882352942, 0.337254902, 0.0, + 0.5333333333, 0.12156862745098039, 0.3411764706, 0.0, 0.5529411765, 0.12549019607843137, + 0.3411764706, 0.0, 0.568627451, 0.12941176470588237, 0.3411764706, 0.0, 0.5882352941, + 0.13333333333333333, 0.3333333333, 0.0, 0.6039215686, 0.13725490196078433, 0.3294117647, 0.0, + 0.6235294118, 0.1411764705882353, 0.3294117647, 0.0, 0.6392156863, 0.1450980392156863, + 0.3294117647, 0.0, 0.6588235294, 0.14901960784313725, 0.3254901961, 0.0, 0.6784313725, + 0.15294117647058825, 0.3098039216, 0.0, 0.6941176471, 0.1568627450980392, 0.3058823529, 0.0, + 0.7137254902, 0.1607843137254902, 0.3019607843, 0.0, 0.7294117647, 0.16470588235294117, + 0.2980392157, 0.0, 0.7490196078, 0.16862745098039217, 0.2784313725, 0.0, 0.7647058824, + 0.17254901960784313, 0.2745098039, 0.0, 0.7843137255, 0.17647058823529413, 0.2666666667, 0.0, + 0.8, 0.1803921568627451, 0.2588235294, 0.0, 0.8196078431, 0.1843137254901961, 0.2352941176, + 0.0, 0.8392156863, 0.18823529411764706, 0.2274509804, 0.0, 0.8549019608, 0.19215686274509805, + 0.2156862745, 0.0, 0.8745098039, 0.19607843137254902, 0.2078431373, 0.0, 0.8901960784, 0.2, + 0.1803921569, 0.0, 0.9098039216, 0.20392156862745098, 0.168627451, 0.0, 0.9254901961, + 0.20784313725490197, 0.1568627451, 0.0, 0.9450980392, 0.21176470588235294, 0.1411764706, 0.0, + 0.9607843137, 0.21568627450980393, 0.1294117647, 0.0, 0.9803921569, 0.2196078431372549, + 0.0980392157, 0.0, 1.0, 0.2235294117647059, 0.0823529412, 0.0, 1.0, 0.22745098039215686, + 0.062745098, 0.0, 1.0, 0.23137254901960785, 0.0470588235, 0.0, 1.0, 0.23529411764705885, + 0.0156862745, 0.0, 1.0, 0.23921568627450984, 0.0, 0.0, 1.0, 0.24313725490196078, 0.0, + 0.0156862745, 1.0, 0.24705882352941178, 0.0, 0.031372549, 1.0, 0.25098039215686274, 0.0, + 0.062745098, 1.0, 0.2549019607843137, 0.0, 0.0823529412, 1.0, 0.25882352941176473, 0.0, + 0.0980392157, 1.0, 0.2627450980392157, 0.0, 0.1137254902, 1.0, 0.26666666666666666, 0.0, + 0.1490196078, 1.0, 0.27058823529411763, 0.0, 0.1647058824, 1.0, 0.27450980392156865, 0.0, + 0.1803921569, 1.0, 0.2784313725490196, 0.0, 0.2, 1.0, 0.2823529411764706, 0.0, 0.2156862745, + 1.0, 0.28627450980392155, 0.0, 0.2470588235, 1.0, 0.2901960784313726, 0.0, 0.262745098, 1.0, + 0.29411764705882354, 0.0, 0.2823529412, 1.0, 0.2980392156862745, 0.0, 0.2980392157, 1.0, + 0.30196078431372547, 0.0, 0.3294117647, 1.0, 0.3058823529411765, 0.0, 0.3490196078, 1.0, + 0.30980392156862746, 0.0, 0.3647058824, 1.0, 0.3137254901960784, 0.0, 0.3803921569, 1.0, + 0.3176470588235294, 0.0, 0.4156862745, 1.0, 0.3215686274509804, 0.0, 0.431372549, 1.0, + 0.3254901960784314, 0.0, 0.4470588235, 1.0, 0.32941176470588235, 0.0, 0.4666666667, 1.0, + 0.3333333333333333, 0.0, 0.4980392157, 1.0, 0.33725490196078434, 0.0, 0.5137254902, 1.0, + 0.3411764705882353, 0.0, 0.5294117647, 1.0, 0.34509803921568627, 0.0, 0.5490196078, 1.0, + 0.34901960784313724, 0.0, 0.5647058824, 1.0, 0.35294117647058826, 0.0, 0.5960784314, 1.0, + 0.3568627450980392, 0.0, 0.6156862745, 1.0, 0.3607843137254902, 0.0, 0.631372549, 1.0, + 0.36470588235294116, 0.0, 0.6470588235, 1.0, 0.3686274509803922, 0.0, 0.6823529412, 1.0, + 0.37254901960784315, 0.0, 0.6980392157, 1.0, 0.3764705882352941, 0.0, 0.7137254902, 1.0, + 0.3803921568627451, 0.0, 0.7333333333, 1.0, 0.3843137254901961, 0.0, 0.7647058824, 1.0, + 0.38823529411764707, 0.0, 0.7803921569, 1.0, 0.39215686274509803, 0.0, 0.7960784314, 1.0, + 0.396078431372549, 0.0, 0.8156862745, 1.0, 0.4, 0.0, 0.8470588235, 1.0, 0.403921568627451, + 0.0, 0.862745098, 1.0, 0.40784313725490196, 0.0, 0.8823529412, 1.0, 0.4117647058823529, 0.0, + 0.8980392157, 1.0, 0.41568627450980394, 0.0, 0.9137254902, 1.0, 0.4196078431372549, 0.0, + 0.9490196078, 1.0, 0.4235294117647059, 0.0, 0.9647058824, 1.0, 0.42745098039215684, 0.0, + 0.9803921569, 1.0, 0.43137254901960786, 0.0, 1.0, 1.0, 0.43529411764705883, 0.0, 1.0, + 0.9647058824, 0.4392156862745098, 0.0, 1.0, 0.9490196078, 0.44313725490196076, 0.0, 1.0, + 0.9333333333, 0.4470588235294118, 0.0, 1.0, 0.9137254902, 0.45098039215686275, 0.0, 1.0, + 0.8823529412, 0.4549019607843137, 0.0, 1.0, 0.862745098, 0.4588235294117647, 0.0, 1.0, + 0.8470588235, 0.4627450980392157, 0.0, 1.0, 0.831372549, 0.4666666666666667, 0.0, 1.0, + 0.7960784314, 0.4705882352941177, 0.0, 1.0, 0.7803921569, 0.4745098039215686, 0.0, 1.0, + 0.7647058824, 0.4784313725490197, 0.0, 1.0, 0.7490196078, 0.48235294117647065, 0.0, 1.0, + 0.7333333333, 0.48627450980392156, 0.0, 1.0, 0.6980392157, 0.49019607843137253, 0.0, 1.0, + 0.6823529412, 0.49411764705882355, 0.0, 1.0, 0.6666666667, 0.4980392156862745, 0.0, 1.0, + 0.6470588235, 0.5019607843137255, 0.0, 1.0, 0.6156862745, 0.5058823529411764, 0.0, 1.0, + 0.5960784314, 0.5098039215686274, 0.0, 1.0, 0.5803921569, 0.5137254901960784, 0.0, 1.0, + 0.5647058824, 0.5176470588235295, 0.0, 1.0, 0.5294117647, 0.5215686274509804, 0.0, 1.0, + 0.5137254902, 0.5254901960784314, 0.0, 1.0, 0.4980392157, 0.5294117647058824, 0.0, 1.0, + 0.4823529412, 0.5333333333333333, 0.0, 1.0, 0.4470588235, 0.5372549019607843, 0.0, 1.0, + 0.431372549, 0.5411764705882353, 0.0, 1.0, 0.4156862745, 0.5450980392156862, 0.0, 1.0, 0.4, + 0.5490196078431373, 0.0, 1.0, 0.3803921569, 0.5529411764705883, 0.0, 1.0, 0.3490196078, + 0.5568627450980392, 0.0, 1.0, 0.3294117647, 0.5607843137254902, 0.0, 1.0, 0.3137254902, + 0.5647058823529412, 0.0, 1.0, 0.2980392157, 0.5686274509803921, 0.0, 1.0, 0.262745098, + 0.5725490196078431, 0.0, 1.0, 0.2470588235, 0.5764705882352941, 0.0, 1.0, 0.231372549, + 0.5803921568627451, 0.0, 1.0, 0.2156862745, 0.5843137254901961, 0.0, 1.0, 0.1803921569, + 0.5882352941176471, 0.0, 1.0, 0.1647058824, 0.592156862745098, 0.0, 1.0, 0.1490196078, + 0.596078431372549, 0.0, 1.0, 0.1333333333, 0.6, 0.0, 1.0, 0.0980392157, 0.6039215686274509, + 0.0, 1.0, 0.0823529412, 0.6078431372549019, 0.0, 1.0, 0.062745098, 0.611764705882353, 0.0, + 1.0, 0.0470588235, 0.615686274509804, 0.0, 1.0, 0.031372549, 0.6196078431372549, 0.0, 1.0, + 0.0, 0.6235294117647059, 0.0156862745, 1.0, 0.0, 0.6274509803921569, 0.031372549, 1.0, 0.0, + 0.6313725490196078, 0.0470588235, 1.0, 0.0, 0.6352941176470588, 0.0823529412, 1.0, 0.0, + 0.6392156862745098, 0.0980392157, 1.0, 0.0, 0.6431372549019608, 0.1137254902, 1.0, 0.0, + 0.6470588235294118, 0.1294117647, 1.0, 0.0, 0.6509803921568628, 0.1647058824, 1.0, 0.0, + 0.6549019607843137, 0.1803921569, 1.0, 0.0, 0.6588235294117647, 0.2, 1.0, 0.0, + 0.6627450980392157, 0.2156862745, 1.0, 0.0, 0.6666666666666666, 0.2470588235, 1.0, 0.0, + 0.6705882352941176, 0.262745098, 1.0, 0.0, 0.6745098039215687, 0.2823529412, 1.0, 0.0, + 0.6784313725490196, 0.2980392157, 1.0, 0.0, 0.6823529411764706, 0.3137254902, 1.0, 0.0, + 0.6862745098039216, 0.3490196078, 1.0, 0.0, 0.6901960784313725, 0.3647058824, 1.0, 0.0, + 0.6941176470588235, 0.3803921569, 1.0, 0.0, 0.6980392156862745, 0.3960784314, 1.0, 0.0, + 0.7019607843137254, 0.431372549, 1.0, 0.0, 0.7058823529411765, 0.4470588235, 1.0, 0.0, + 0.7098039215686275, 0.4666666667, 1.0, 0.0, 0.7137254901960784, 0.4823529412, 1.0, 0.0, + 0.7176470588235294, 0.5137254902, 1.0, 0.0, 0.7215686274509804, 0.5294117647, 1.0, 0.0, + 0.7254901960784313, 0.5490196078, 1.0, 0.0, 0.7294117647058823, 0.5647058824, 1.0, 0.0, + 0.7333333333333333, 0.6, 1.0, 0.0, 0.7372549019607844, 0.6156862745, 1.0, 0.0, + 0.7411764705882353, 0.631372549, 1.0, 0.0, 0.7450980392156863, 0.6470588235, 1.0, 0.0, + 0.7490196078431373, 0.662745098, 1.0, 0.0, 0.7529411764705882, 0.6980392157, 1.0, 0.0, + 0.7568627450980392, 0.7137254902, 1.0, 0.0, 0.7607843137254902, 0.7333333333, 1.0, 0.0, + 0.7647058823529411, 0.7490196078, 1.0, 0.0, 0.7686274509803922, 0.7803921569, 1.0, 0.0, + 0.7725490196078432, 0.7960784314, 1.0, 0.0, 0.7764705882352941, 0.8156862745, 1.0, 0.0, + 0.7803921568627451, 0.831372549, 1.0, 0.0, 0.7843137254901961, 0.8666666667, 1.0, 0.0, + 0.788235294117647, 0.8823529412, 1.0, 0.0, 0.792156862745098, 0.8980392157, 1.0, 0.0, + 0.796078431372549, 0.9137254902, 1.0, 0.0, 0.8, 0.9490196078, 1.0, 0.0, 0.803921568627451, + 0.9647058824, 1.0, 0.0, 0.807843137254902, 0.9803921569, 1.0, 0.0, 0.8117647058823529, 1.0, + 1.0, 0.0, 0.8156862745098039, 1.0, 0.9803921569, 0.0, 0.8196078431372549, 1.0, 0.9490196078, + 0.0, 0.8235294117647058, 1.0, 0.9333333333, 0.0, 0.8274509803921568, 1.0, 0.9137254902, 0.0, + 0.8313725490196079, 1.0, 0.8980392157, 0.0, 0.8352941176470589, 1.0, 0.8666666667, 0.0, + 0.8392156862745098, 1.0, 0.8470588235, 0.0, 0.8431372549019608, 1.0, 0.831372549, 0.0, + 0.8470588235294118, 1.0, 0.8156862745, 0.0, 0.8509803921568627, 1.0, 0.7803921569, 0.0, + 0.8549019607843137, 1.0, 0.7647058824, 0.0, 0.8588235294117647, 1.0, 0.7490196078, 0.0, + 0.8627450980392157, 1.0, 0.7333333333, 0.0, 0.8666666666666667, 1.0, 0.6980392157, 0.0, + 0.8705882352941177, 1.0, 0.6823529412, 0.0, 0.8745098039215686, 1.0, 0.6666666667, 0.0, + 0.8784313725490196, 1.0, 0.6470588235, 0.0, 0.8823529411764706, 1.0, 0.631372549, 0.0, + 0.8862745098039215, 1.0, 0.6, 0.0, 0.8901960784313725, 1.0, 0.5803921569, 0.0, + 0.8941176470588236, 1.0, 0.5647058824, 0.0, 0.8980392156862745, 1.0, 0.5490196078, 0.0, + 0.9019607843137255, 1.0, 0.5137254902, 0.0, 0.9058823529411765, 1.0, 0.4980392157, 0.0, + 0.9098039215686274, 1.0, 0.4823529412, 0.0, 0.9137254901960784, 1.0, 0.4666666667, 0.0, + 0.9176470588235294, 1.0, 0.431372549, 0.0, 0.9215686274509803, 1.0, 0.4156862745, 0.0, + 0.9254901960784314, 1.0, 0.4, 0.0, 0.9294117647058824, 1.0, 0.3803921569, 0.0, + 0.9333333333333333, 1.0, 0.3490196078, 0.0, 0.9372549019607843, 1.0, 0.3333333333, 0.0, + 0.9411764705882354, 1.0, 0.3137254902, 0.0, 0.9450980392156864, 1.0, 0.2980392157, 0.0, + 0.9490196078431372, 1.0, 0.2823529412, 0.0, 0.9529411764705882, 1.0, 0.2470588235, 0.0, + 0.9568627450980394, 1.0, 0.231372549, 0.0, 0.9607843137254903, 1.0, 0.2156862745, 0.0, + 0.9647058823529413, 1.0, 0.2, 0.0, 0.9686274509803922, 1.0, 0.1647058824, 0.0, + 0.9725490196078431, 1.0, 0.1490196078, 0.0, 0.9764705882352941, 1.0, 0.1333333333, 0.0, + 0.9803921568627451, 1.0, 0.1137254902, 0.0, 0.984313725490196, 1.0, 0.0823529412, 0.0, + 0.9882352941176471, 1.0, 0.0666666667, 0.0, 0.9921568627450981, 1.0, 0.0470588235, 0.0, + 0.996078431372549, 1.0, 0.031372549, 0.0, 1.0, 1.0, 0.0, 0.0, + ], + description: 'Rainbow', + }, + { + ColorSpace: 'RGB', + Name: 'suv', + name: 'suv', + RGBPoints: [ + 0.0, 1.0, 1.0, 1.0, 0.00392156862745098, 1.0, 1.0, 1.0, 0.00784313725490196, 1.0, 1.0, 1.0, + 0.011764705882352941, 1.0, 1.0, 1.0, 0.01568627450980392, 1.0, 1.0, 1.0, 0.0196078431372549, + 1.0, 1.0, 1.0, 0.023529411764705882, 1.0, 1.0, 1.0, 0.027450980392156862, 1.0, 1.0, 1.0, + 0.03137254901960784, 1.0, 1.0, 1.0, 0.03529411764705882, 1.0, 1.0, 1.0, 0.0392156862745098, + 1.0, 1.0, 1.0, 0.043137254901960784, 1.0, 1.0, 1.0, 0.047058823529411764, 1.0, 1.0, 1.0, + 0.050980392156862744, 1.0, 1.0, 1.0, 0.054901960784313725, 1.0, 1.0, 1.0, 0.05882352941176471, + 1.0, 1.0, 1.0, 0.06274509803921569, 1.0, 1.0, 1.0, 0.06666666666666667, 1.0, 1.0, 1.0, + 0.07058823529411765, 1.0, 1.0, 1.0, 0.07450980392156863, 1.0, 1.0, 1.0, 0.0784313725490196, + 1.0, 1.0, 1.0, 0.08235294117647059, 1.0, 1.0, 1.0, 0.08627450980392157, 1.0, 1.0, 1.0, + 0.09019607843137255, 1.0, 1.0, 1.0, 0.09411764705882353, 1.0, 1.0, 1.0, 0.09803921568627451, + 1.0, 1.0, 1.0, 0.10196078431372549, 0.737254902, 0.737254902, 0.737254902, + 0.10588235294117647, 0.737254902, 0.737254902, 0.737254902, 0.10980392156862745, 0.737254902, + 0.737254902, 0.737254902, 0.11372549019607843, 0.737254902, 0.737254902, 0.737254902, + 0.11764705882352942, 0.737254902, 0.737254902, 0.737254902, 0.12156862745098039, 0.737254902, + 0.737254902, 0.737254902, 0.12549019607843137, 0.737254902, 0.737254902, 0.737254902, + 0.12941176470588237, 0.737254902, 0.737254902, 0.737254902, 0.13333333333333333, 0.737254902, + 0.737254902, 0.737254902, 0.13725490196078433, 0.737254902, 0.737254902, 0.737254902, + 0.1411764705882353, 0.737254902, 0.737254902, 0.737254902, 0.1450980392156863, 0.737254902, + 0.737254902, 0.737254902, 0.14901960784313725, 0.737254902, 0.737254902, 0.737254902, + 0.15294117647058825, 0.737254902, 0.737254902, 0.737254902, 0.1568627450980392, 0.737254902, + 0.737254902, 0.737254902, 0.1607843137254902, 0.737254902, 0.737254902, 0.737254902, + 0.16470588235294117, 0.737254902, 0.737254902, 0.737254902, 0.16862745098039217, 0.737254902, + 0.737254902, 0.737254902, 0.17254901960784313, 0.737254902, 0.737254902, 0.737254902, + 0.17647058823529413, 0.737254902, 0.737254902, 0.737254902, 0.1803921568627451, 0.737254902, + 0.737254902, 0.737254902, 0.1843137254901961, 0.737254902, 0.737254902, 0.737254902, + 0.18823529411764706, 0.737254902, 0.737254902, 0.737254902, 0.19215686274509805, 0.737254902, + 0.737254902, 0.737254902, 0.19607843137254902, 0.737254902, 0.737254902, 0.737254902, 0.2, + 0.737254902, 0.737254902, 0.737254902, 0.20392156862745098, 0.431372549, 0.0, 0.568627451, + 0.20784313725490197, 0.431372549, 0.0, 0.568627451, 0.21176470588235294, 0.431372549, 0.0, + 0.568627451, 0.21568627450980393, 0.431372549, 0.0, 0.568627451, 0.2196078431372549, + 0.431372549, 0.0, 0.568627451, 0.2235294117647059, 0.431372549, 0.0, 0.568627451, + 0.22745098039215686, 0.431372549, 0.0, 0.568627451, 0.23137254901960785, 0.431372549, 0.0, + 0.568627451, 0.23529411764705885, 0.431372549, 0.0, 0.568627451, 0.23921568627450984, + 0.431372549, 0.0, 0.568627451, 0.24313725490196078, 0.431372549, 0.0, 0.568627451, + 0.24705882352941178, 0.431372549, 0.0, 0.568627451, 0.25098039215686274, 0.431372549, 0.0, + 0.568627451, 0.2549019607843137, 0.431372549, 0.0, 0.568627451, 0.25882352941176473, + 0.431372549, 0.0, 0.568627451, 0.2627450980392157, 0.431372549, 0.0, 0.568627451, + 0.26666666666666666, 0.431372549, 0.0, 0.568627451, 0.27058823529411763, 0.431372549, 0.0, + 0.568627451, 0.27450980392156865, 0.431372549, 0.0, 0.568627451, 0.2784313725490196, + 0.431372549, 0.0, 0.568627451, 0.2823529411764706, 0.431372549, 0.0, 0.568627451, + 0.28627450980392155, 0.431372549, 0.0, 0.568627451, 0.2901960784313726, 0.431372549, 0.0, + 0.568627451, 0.29411764705882354, 0.431372549, 0.0, 0.568627451, 0.2980392156862745, + 0.431372549, 0.0, 0.568627451, 0.30196078431372547, 0.431372549, 0.0, 0.568627451, + 0.3058823529411765, 0.2509803922, 0.3333333333, 0.6509803922, 0.30980392156862746, + 0.2509803922, 0.3333333333, 0.6509803922, 0.3137254901960784, 0.2509803922, 0.3333333333, + 0.6509803922, 0.3176470588235294, 0.2509803922, 0.3333333333, 0.6509803922, + 0.3215686274509804, 0.2509803922, 0.3333333333, 0.6509803922, 0.3254901960784314, + 0.2509803922, 0.3333333333, 0.6509803922, 0.32941176470588235, 0.2509803922, 0.3333333333, + 0.6509803922, 0.3333333333333333, 0.2509803922, 0.3333333333, 0.6509803922, + 0.33725490196078434, 0.2509803922, 0.3333333333, 0.6509803922, 0.3411764705882353, + 0.2509803922, 0.3333333333, 0.6509803922, 0.34509803921568627, 0.2509803922, 0.3333333333, + 0.6509803922, 0.34901960784313724, 0.2509803922, 0.3333333333, 0.6509803922, + 0.35294117647058826, 0.2509803922, 0.3333333333, 0.6509803922, 0.3568627450980392, + 0.2509803922, 0.3333333333, 0.6509803922, 0.3607843137254902, 0.2509803922, 0.3333333333, + 0.6509803922, 0.36470588235294116, 0.2509803922, 0.3333333333, 0.6509803922, + 0.3686274509803922, 0.2509803922, 0.3333333333, 0.6509803922, 0.37254901960784315, + 0.2509803922, 0.3333333333, 0.6509803922, 0.3764705882352941, 0.2509803922, 0.3333333333, + 0.6509803922, 0.3803921568627451, 0.2509803922, 0.3333333333, 0.6509803922, + 0.3843137254901961, 0.2509803922, 0.3333333333, 0.6509803922, 0.38823529411764707, + 0.2509803922, 0.3333333333, 0.6509803922, 0.39215686274509803, 0.2509803922, 0.3333333333, + 0.6509803922, 0.396078431372549, 0.2509803922, 0.3333333333, 0.6509803922, 0.4, 0.2509803922, + 0.3333333333, 0.6509803922, 0.403921568627451, 0.2509803922, 0.3333333333, 0.6509803922, + 0.40784313725490196, 0.0, 0.8, 1.0, 0.4117647058823529, 0.0, 0.8, 1.0, 0.41568627450980394, + 0.0, 0.8, 1.0, 0.4196078431372549, 0.0, 0.8, 1.0, 0.4235294117647059, 0.0, 0.8, 1.0, + 0.42745098039215684, 0.0, 0.8, 1.0, 0.43137254901960786, 0.0, 0.8, 1.0, 0.43529411764705883, + 0.0, 0.8, 1.0, 0.4392156862745098, 0.0, 0.8, 1.0, 0.44313725490196076, 0.0, 0.8, 1.0, + 0.4470588235294118, 0.0, 0.8, 1.0, 0.45098039215686275, 0.0, 0.8, 1.0, 0.4549019607843137, + 0.0, 0.8, 1.0, 0.4588235294117647, 0.0, 0.8, 1.0, 0.4627450980392157, 0.0, 0.8, 1.0, + 0.4666666666666667, 0.0, 0.8, 1.0, 0.4705882352941177, 0.0, 0.8, 1.0, 0.4745098039215686, 0.0, + 0.8, 1.0, 0.4784313725490197, 0.0, 0.8, 1.0, 0.48235294117647065, 0.0, 0.8, 1.0, + 0.48627450980392156, 0.0, 0.8, 1.0, 0.49019607843137253, 0.0, 0.8, 1.0, 0.49411764705882355, + 0.0, 0.8, 1.0, 0.4980392156862745, 0.0, 0.8, 1.0, 0.5019607843137255, 0.0, 0.8, 1.0, + 0.5058823529411764, 0.0, 0.6666666667, 0.5333333333, 0.5098039215686274, 0.0, 0.6666666667, + 0.5333333333, 0.5137254901960784, 0.0, 0.6666666667, 0.5333333333, 0.5176470588235295, 0.0, + 0.6666666667, 0.5333333333, 0.5215686274509804, 0.0, 0.6666666667, 0.5333333333, + 0.5254901960784314, 0.0, 0.6666666667, 0.5333333333, 0.5294117647058824, 0.0, 0.6666666667, + 0.5333333333, 0.5333333333333333, 0.0, 0.6666666667, 0.5333333333, 0.5372549019607843, 0.0, + 0.6666666667, 0.5333333333, 0.5411764705882353, 0.0, 0.6666666667, 0.5333333333, + 0.5450980392156862, 0.0, 0.6666666667, 0.5333333333, 0.5490196078431373, 0.0, 0.6666666667, + 0.5333333333, 0.5529411764705883, 0.0, 0.6666666667, 0.5333333333, 0.5568627450980392, 0.0, + 0.6666666667, 0.5333333333, 0.5607843137254902, 0.0, 0.6666666667, 0.5333333333, + 0.5647058823529412, 0.0, 0.6666666667, 0.5333333333, 0.5686274509803921, 0.0, 0.6666666667, + 0.5333333333, 0.5725490196078431, 0.0, 0.6666666667, 0.5333333333, 0.5764705882352941, 0.0, + 0.6666666667, 0.5333333333, 0.5803921568627451, 0.0, 0.6666666667, 0.5333333333, + 0.5843137254901961, 0.0, 0.6666666667, 0.5333333333, 0.5882352941176471, 0.0, 0.6666666667, + 0.5333333333, 0.592156862745098, 0.0, 0.6666666667, 0.5333333333, 0.596078431372549, 0.0, + 0.6666666667, 0.5333333333, 0.6, 0.0, 0.6666666667, 0.5333333333, 0.6039215686274509, 0.0, + 0.6666666667, 0.5333333333, 0.6078431372549019, 0.4, 1.0, 0.4, 0.611764705882353, 0.4, 1.0, + 0.4, 0.615686274509804, 0.4, 1.0, 0.4, 0.6196078431372549, 0.4, 1.0, 0.4, 0.6235294117647059, + 0.4, 1.0, 0.4, 0.6274509803921569, 0.4, 1.0, 0.4, 0.6313725490196078, 0.4, 1.0, 0.4, + 0.6352941176470588, 0.4, 1.0, 0.4, 0.6392156862745098, 0.4, 1.0, 0.4, 0.6431372549019608, 0.4, + 1.0, 0.4, 0.6470588235294118, 0.4, 1.0, 0.4, 0.6509803921568628, 0.4, 1.0, 0.4, + 0.6549019607843137, 0.4, 1.0, 0.4, 0.6588235294117647, 0.4, 1.0, 0.4, 0.6627450980392157, 0.4, + 1.0, 0.4, 0.6666666666666666, 0.4, 1.0, 0.4, 0.6705882352941176, 0.4, 1.0, 0.4, + 0.6745098039215687, 0.4, 1.0, 0.4, 0.6784313725490196, 0.4, 1.0, 0.4, 0.6823529411764706, 0.4, + 1.0, 0.4, 0.6862745098039216, 0.4, 1.0, 0.4, 0.6901960784313725, 0.4, 1.0, 0.4, + 0.6941176470588235, 0.4, 1.0, 0.4, 0.6980392156862745, 0.4, 1.0, 0.4, 0.7019607843137254, 0.4, + 1.0, 0.4, 0.7058823529411765, 1.0, 0.9490196078, 0.0, 0.7098039215686275, 1.0, 0.9490196078, + 0.0, 0.7137254901960784, 1.0, 0.9490196078, 0.0, 0.7176470588235294, 1.0, 0.9490196078, 0.0, + 0.7215686274509804, 1.0, 0.9490196078, 0.0, 0.7254901960784313, 1.0, 0.9490196078, 0.0, + 0.7294117647058823, 1.0, 0.9490196078, 0.0, 0.7333333333333333, 1.0, 0.9490196078, 0.0, + 0.7372549019607844, 1.0, 0.9490196078, 0.0, 0.7411764705882353, 1.0, 0.9490196078, 0.0, + 0.7450980392156863, 1.0, 0.9490196078, 0.0, 0.7490196078431373, 1.0, 0.9490196078, 0.0, + 0.7529411764705882, 1.0, 0.9490196078, 0.0, 0.7568627450980392, 1.0, 0.9490196078, 0.0, + 0.7607843137254902, 1.0, 0.9490196078, 0.0, 0.7647058823529411, 1.0, 0.9490196078, 0.0, + 0.7686274509803922, 1.0, 0.9490196078, 0.0, 0.7725490196078432, 1.0, 0.9490196078, 0.0, + 0.7764705882352941, 1.0, 0.9490196078, 0.0, 0.7803921568627451, 1.0, 0.9490196078, 0.0, + 0.7843137254901961, 1.0, 0.9490196078, 0.0, 0.788235294117647, 1.0, 0.9490196078, 0.0, + 0.792156862745098, 1.0, 0.9490196078, 0.0, 0.796078431372549, 1.0, 0.9490196078, 0.0, 0.8, + 1.0, 0.9490196078, 0.0, 0.803921568627451, 1.0, 0.9490196078, 0.0, 0.807843137254902, + 0.9490196078, 0.6509803922, 0.2509803922, 0.8117647058823529, 0.9490196078, 0.6509803922, + 0.2509803922, 0.8156862745098039, 0.9490196078, 0.6509803922, 0.2509803922, + 0.8196078431372549, 0.9490196078, 0.6509803922, 0.2509803922, 0.8235294117647058, + 0.9490196078, 0.6509803922, 0.2509803922, 0.8274509803921568, 0.9490196078, 0.6509803922, + 0.2509803922, 0.8313725490196079, 0.9490196078, 0.6509803922, 0.2509803922, + 0.8352941176470589, 0.9490196078, 0.6509803922, 0.2509803922, 0.8392156862745098, + 0.9490196078, 0.6509803922, 0.2509803922, 0.8431372549019608, 0.9490196078, 0.6509803922, + 0.2509803922, 0.8470588235294118, 0.9490196078, 0.6509803922, 0.2509803922, + 0.8509803921568627, 0.9490196078, 0.6509803922, 0.2509803922, 0.8549019607843137, + 0.9490196078, 0.6509803922, 0.2509803922, 0.8588235294117647, 0.9490196078, 0.6509803922, + 0.2509803922, 0.8627450980392157, 0.9490196078, 0.6509803922, 0.2509803922, + 0.8666666666666667, 0.9490196078, 0.6509803922, 0.2509803922, 0.8705882352941177, + 0.9490196078, 0.6509803922, 0.2509803922, 0.8745098039215686, 0.9490196078, 0.6509803922, + 0.2509803922, 0.8784313725490196, 0.9490196078, 0.6509803922, 0.2509803922, + 0.8823529411764706, 0.9490196078, 0.6509803922, 0.2509803922, 0.8862745098039215, + 0.9490196078, 0.6509803922, 0.2509803922, 0.8901960784313725, 0.9490196078, 0.6509803922, + 0.2509803922, 0.8941176470588236, 0.9490196078, 0.6509803922, 0.2509803922, + 0.8980392156862745, 0.9490196078, 0.6509803922, 0.2509803922, 0.9019607843137255, + 0.9490196078, 0.6509803922, 0.2509803922, 0.9058823529411765, 0.9490196078, 0.6509803922, + 0.2509803922, 0.9098039215686274, 1.0, 0.0, 0.0, 0.9137254901960784, 1.0, 0.0, 0.0, + 0.9176470588235294, 1.0, 0.0, 0.0, 0.9215686274509803, 1.0, 0.0, 0.0, 0.9254901960784314, 1.0, + 0.0, 0.0, 0.9294117647058824, 1.0, 0.0, 0.0, 0.9333333333333333, 1.0, 0.0, 0.0, + 0.9372549019607843, 1.0, 0.0, 0.0, 0.9411764705882354, 1.0, 0.0, 0.0, 0.9450980392156864, 1.0, + 0.0, 0.0, 0.9490196078431372, 1.0, 0.0, 0.0, 0.9529411764705882, 1.0, 0.0, 0.0, + 0.9568627450980394, 1.0, 0.0, 0.0, 0.9607843137254903, 1.0, 0.0, 0.0, 0.9647058823529413, 1.0, + 0.0, 0.0, 0.9686274509803922, 1.0, 0.0, 0.0, 0.9725490196078431, 1.0, 0.0, 0.0, + 0.9764705882352941, 1.0, 0.0, 0.0, 0.9803921568627451, 1.0, 0.0, 0.0, 0.984313725490196, 1.0, + 0.0, 0.0, 0.9882352941176471, 1.0, 0.0, 0.0, 0.9921568627450981, 1.0, 0.0, 0.0, + 0.996078431372549, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, + ], + description: 'SUV', + }, + { + ColorSpace: 'RGB', + Name: 'ge_256', + name: 'ge_256', + RGBPoints: [ + 0.0, 0.0039215686, 0.0078431373, 0.0078431373, 0.00392156862745098, 0.0039215686, + 0.0078431373, 0.0078431373, 0.00784313725490196, 0.0039215686, 0.0078431373, 0.0117647059, + 0.011764705882352941, 0.0039215686, 0.0117647059, 0.0156862745, 0.01568627450980392, + 0.0039215686, 0.0117647059, 0.0196078431, 0.0196078431372549, 0.0039215686, 0.0156862745, + 0.0235294118, 0.023529411764705882, 0.0039215686, 0.0156862745, 0.0274509804, + 0.027450980392156862, 0.0039215686, 0.0196078431, 0.031372549, 0.03137254901960784, + 0.0039215686, 0.0196078431, 0.0352941176, 0.03529411764705882, 0.0039215686, 0.0235294118, + 0.0392156863, 0.0392156862745098, 0.0039215686, 0.0235294118, 0.0431372549, + 0.043137254901960784, 0.0039215686, 0.0274509804, 0.0470588235, 0.047058823529411764, + 0.0039215686, 0.0274509804, 0.0509803922, 0.050980392156862744, 0.0039215686, 0.031372549, + 0.0549019608, 0.054901960784313725, 0.0039215686, 0.031372549, 0.0588235294, + 0.05882352941176471, 0.0039215686, 0.0352941176, 0.062745098, 0.06274509803921569, + 0.0039215686, 0.0352941176, 0.0666666667, 0.06666666666666667, 0.0039215686, 0.0392156863, + 0.0705882353, 0.07058823529411765, 0.0039215686, 0.0392156863, 0.0745098039, + 0.07450980392156863, 0.0039215686, 0.0431372549, 0.0784313725, 0.0784313725490196, + 0.0039215686, 0.0431372549, 0.0823529412, 0.08235294117647059, 0.0039215686, 0.0470588235, + 0.0862745098, 0.08627450980392157, 0.0039215686, 0.0470588235, 0.0901960784, + 0.09019607843137255, 0.0039215686, 0.0509803922, 0.0941176471, 0.09411764705882353, + 0.0039215686, 0.0509803922, 0.0980392157, 0.09803921568627451, 0.0039215686, 0.0549019608, + 0.1019607843, 0.10196078431372549, 0.0039215686, 0.0549019608, 0.1058823529, + 0.10588235294117647, 0.0039215686, 0.0588235294, 0.1098039216, 0.10980392156862745, + 0.0039215686, 0.0588235294, 0.1137254902, 0.11372549019607843, 0.0039215686, 0.062745098, + 0.1176470588, 0.11764705882352942, 0.0039215686, 0.062745098, 0.1215686275, + 0.12156862745098039, 0.0039215686, 0.0666666667, 0.1254901961, 0.12549019607843137, + 0.0039215686, 0.0666666667, 0.1294117647, 0.12941176470588237, 0.0039215686, 0.0705882353, + 0.1333333333, 0.13333333333333333, 0.0039215686, 0.0705882353, 0.137254902, + 0.13725490196078433, 0.0039215686, 0.0745098039, 0.1411764706, 0.1411764705882353, + 0.0039215686, 0.0745098039, 0.1450980392, 0.1450980392156863, 0.0039215686, 0.0784313725, + 0.1490196078, 0.14901960784313725, 0.0039215686, 0.0784313725, 0.1529411765, + 0.15294117647058825, 0.0039215686, 0.0823529412, 0.1568627451, 0.1568627450980392, + 0.0039215686, 0.0823529412, 0.1607843137, 0.1607843137254902, 0.0039215686, 0.0862745098, + 0.1647058824, 0.16470588235294117, 0.0039215686, 0.0862745098, 0.168627451, + 0.16862745098039217, 0.0039215686, 0.0901960784, 0.1725490196, 0.17254901960784313, + 0.0039215686, 0.0901960784, 0.1764705882, 0.17647058823529413, 0.0039215686, 0.0941176471, + 0.1803921569, 0.1803921568627451, 0.0039215686, 0.0941176471, 0.1843137255, + 0.1843137254901961, 0.0039215686, 0.0980392157, 0.1882352941, 0.18823529411764706, + 0.0039215686, 0.0980392157, 0.1921568627, 0.19215686274509805, 0.0039215686, 0.1019607843, + 0.1960784314, 0.19607843137254902, 0.0039215686, 0.1019607843, 0.2, 0.2, 0.0039215686, + 0.1058823529, 0.2039215686, 0.20392156862745098, 0.0039215686, 0.1058823529, 0.2078431373, + 0.20784313725490197, 0.0039215686, 0.1098039216, 0.2117647059, 0.21176470588235294, + 0.0039215686, 0.1098039216, 0.2156862745, 0.21568627450980393, 0.0039215686, 0.1137254902, + 0.2196078431, 0.2196078431372549, 0.0039215686, 0.1137254902, 0.2235294118, + 0.2235294117647059, 0.0039215686, 0.1176470588, 0.2274509804, 0.22745098039215686, + 0.0039215686, 0.1176470588, 0.231372549, 0.23137254901960785, 0.0039215686, 0.1215686275, + 0.2352941176, 0.23529411764705885, 0.0039215686, 0.1215686275, 0.2392156863, + 0.23921568627450984, 0.0039215686, 0.1254901961, 0.2431372549, 0.24313725490196078, + 0.0039215686, 0.1254901961, 0.2470588235, 0.24705882352941178, 0.0039215686, 0.1294117647, + 0.2509803922, 0.25098039215686274, 0.0039215686, 0.1294117647, 0.2509803922, + 0.2549019607843137, 0.0078431373, 0.1254901961, 0.2549019608, 0.25882352941176473, + 0.0156862745, 0.1254901961, 0.2588235294, 0.2627450980392157, 0.0235294118, 0.1215686275, + 0.262745098, 0.26666666666666666, 0.031372549, 0.1215686275, 0.2666666667, + 0.27058823529411763, 0.0392156863, 0.1176470588, 0.2705882353, 0.27450980392156865, + 0.0470588235, 0.1176470588, 0.2745098039, 0.2784313725490196, 0.0549019608, 0.1137254902, + 0.2784313725, 0.2823529411764706, 0.062745098, 0.1137254902, 0.2823529412, + 0.28627450980392155, 0.0705882353, 0.1098039216, 0.2862745098, 0.2901960784313726, + 0.0784313725, 0.1098039216, 0.2901960784, 0.29411764705882354, 0.0862745098, 0.1058823529, + 0.2941176471, 0.2980392156862745, 0.0941176471, 0.1058823529, 0.2980392157, + 0.30196078431372547, 0.1019607843, 0.1019607843, 0.3019607843, 0.3058823529411765, + 0.1098039216, 0.1019607843, 0.3058823529, 0.30980392156862746, 0.1176470588, 0.0980392157, + 0.3098039216, 0.3137254901960784, 0.1254901961, 0.0980392157, 0.3137254902, + 0.3176470588235294, 0.1333333333, 0.0941176471, 0.3176470588, 0.3215686274509804, + 0.1411764706, 0.0941176471, 0.3215686275, 0.3254901960784314, 0.1490196078, 0.0901960784, + 0.3254901961, 0.32941176470588235, 0.1568627451, 0.0901960784, 0.3294117647, + 0.3333333333333333, 0.1647058824, 0.0862745098, 0.3333333333, 0.33725490196078434, + 0.1725490196, 0.0862745098, 0.337254902, 0.3411764705882353, 0.1803921569, 0.0823529412, + 0.3411764706, 0.34509803921568627, 0.1882352941, 0.0823529412, 0.3450980392, + 0.34901960784313724, 0.1960784314, 0.0784313725, 0.3490196078, 0.35294117647058826, + 0.2039215686, 0.0784313725, 0.3529411765, 0.3568627450980392, 0.2117647059, 0.0745098039, + 0.3568627451, 0.3607843137254902, 0.2196078431, 0.0745098039, 0.3607843137, + 0.36470588235294116, 0.2274509804, 0.0705882353, 0.3647058824, 0.3686274509803922, + 0.2352941176, 0.0705882353, 0.368627451, 0.37254901960784315, 0.2431372549, 0.0666666667, + 0.3725490196, 0.3764705882352941, 0.2509803922, 0.0666666667, 0.3764705882, + 0.3803921568627451, 0.2549019608, 0.062745098, 0.3803921569, 0.3843137254901961, 0.262745098, + 0.062745098, 0.3843137255, 0.38823529411764707, 0.2705882353, 0.0588235294, 0.3882352941, + 0.39215686274509803, 0.2784313725, 0.0588235294, 0.3921568627, 0.396078431372549, + 0.2862745098, 0.0549019608, 0.3960784314, 0.4, 0.2941176471, 0.0549019608, 0.4, + 0.403921568627451, 0.3019607843, 0.0509803922, 0.4039215686, 0.40784313725490196, + 0.3098039216, 0.0509803922, 0.4078431373, 0.4117647058823529, 0.3176470588, 0.0470588235, + 0.4117647059, 0.41568627450980394, 0.3254901961, 0.0470588235, 0.4156862745, + 0.4196078431372549, 0.3333333333, 0.0431372549, 0.4196078431, 0.4235294117647059, + 0.3411764706, 0.0431372549, 0.4235294118, 0.42745098039215684, 0.3490196078, 0.0392156863, + 0.4274509804, 0.43137254901960786, 0.3568627451, 0.0392156863, 0.431372549, + 0.43529411764705883, 0.3647058824, 0.0352941176, 0.4352941176, 0.4392156862745098, + 0.3725490196, 0.0352941176, 0.4392156863, 0.44313725490196076, 0.3803921569, 0.031372549, + 0.4431372549, 0.4470588235294118, 0.3882352941, 0.031372549, 0.4470588235, + 0.45098039215686275, 0.3960784314, 0.0274509804, 0.4509803922, 0.4549019607843137, + 0.4039215686, 0.0274509804, 0.4549019608, 0.4588235294117647, 0.4117647059, 0.0235294118, + 0.4588235294, 0.4627450980392157, 0.4196078431, 0.0235294118, 0.462745098, 0.4666666666666667, + 0.4274509804, 0.0196078431, 0.4666666667, 0.4705882352941177, 0.4352941176, 0.0196078431, + 0.4705882353, 0.4745098039215686, 0.4431372549, 0.0156862745, 0.4745098039, + 0.4784313725490197, 0.4509803922, 0.0156862745, 0.4784313725, 0.48235294117647065, + 0.4588235294, 0.0117647059, 0.4823529412, 0.48627450980392156, 0.4666666667, 0.0117647059, + 0.4862745098, 0.49019607843137253, 0.4745098039, 0.0078431373, 0.4901960784, + 0.49411764705882355, 0.4823529412, 0.0078431373, 0.4941176471, 0.4980392156862745, + 0.4901960784, 0.0039215686, 0.4980392157, 0.5019607843137255, 0.4980392157, 0.0117647059, + 0.4980392157, 0.5058823529411764, 0.5058823529, 0.0156862745, 0.4901960784, + 0.5098039215686274, 0.5137254902, 0.0235294118, 0.4823529412, 0.5137254901960784, + 0.5215686275, 0.0274509804, 0.4745098039, 0.5176470588235295, 0.5294117647, 0.0352941176, + 0.4666666667, 0.5215686274509804, 0.537254902, 0.0392156863, 0.4588235294, 0.5254901960784314, + 0.5450980392, 0.0470588235, 0.4509803922, 0.5294117647058824, 0.5529411765, 0.0509803922, + 0.4431372549, 0.5333333333333333, 0.5607843137, 0.0588235294, 0.4352941176, + 0.5372549019607843, 0.568627451, 0.062745098, 0.4274509804, 0.5411764705882353, 0.5764705882, + 0.0705882353, 0.4196078431, 0.5450980392156862, 0.5843137255, 0.0745098039, 0.4117647059, + 0.5490196078431373, 0.5921568627, 0.0823529412, 0.4039215686, 0.5529411764705883, 0.6, + 0.0862745098, 0.3960784314, 0.5568627450980392, 0.6078431373, 0.0941176471, 0.3882352941, + 0.5607843137254902, 0.6156862745, 0.0980392157, 0.3803921569, 0.5647058823529412, + 0.6235294118, 0.1058823529, 0.3725490196, 0.5686274509803921, 0.631372549, 0.1098039216, + 0.3647058824, 0.5725490196078431, 0.6392156863, 0.1176470588, 0.3568627451, + 0.5764705882352941, 0.6470588235, 0.1215686275, 0.3490196078, 0.5803921568627451, + 0.6549019608, 0.1294117647, 0.3411764706, 0.5843137254901961, 0.662745098, 0.1333333333, + 0.3333333333, 0.5882352941176471, 0.6705882353, 0.1411764706, 0.3254901961, 0.592156862745098, + 0.6784313725, 0.1450980392, 0.3176470588, 0.596078431372549, 0.6862745098, 0.1529411765, + 0.3098039216, 0.6, 0.6941176471, 0.1568627451, 0.3019607843, 0.6039215686274509, 0.7019607843, + 0.1647058824, 0.2941176471, 0.6078431372549019, 0.7098039216, 0.168627451, 0.2862745098, + 0.611764705882353, 0.7176470588, 0.1764705882, 0.2784313725, 0.615686274509804, 0.7254901961, + 0.1803921569, 0.2705882353, 0.6196078431372549, 0.7333333333, 0.1882352941, 0.262745098, + 0.6235294117647059, 0.7411764706, 0.1921568627, 0.2549019608, 0.6274509803921569, + 0.7490196078, 0.2, 0.2509803922, 0.6313725490196078, 0.7529411765, 0.2039215686, 0.2431372549, + 0.6352941176470588, 0.7607843137, 0.2117647059, 0.2352941176, 0.6392156862745098, 0.768627451, + 0.2156862745, 0.2274509804, 0.6431372549019608, 0.7764705882, 0.2235294118, 0.2196078431, + 0.6470588235294118, 0.7843137255, 0.2274509804, 0.2117647059, 0.6509803921568628, + 0.7921568627, 0.2352941176, 0.2039215686, 0.6549019607843137, 0.8, 0.2392156863, 0.1960784314, + 0.6588235294117647, 0.8078431373, 0.2470588235, 0.1882352941, 0.6627450980392157, + 0.8156862745, 0.2509803922, 0.1803921569, 0.6666666666666666, 0.8235294118, 0.2549019608, + 0.1725490196, 0.6705882352941176, 0.831372549, 0.2588235294, 0.1647058824, 0.6745098039215687, + 0.8392156863, 0.2666666667, 0.1568627451, 0.6784313725490196, 0.8470588235, 0.2705882353, + 0.1490196078, 0.6823529411764706, 0.8549019608, 0.2784313725, 0.1411764706, + 0.6862745098039216, 0.862745098, 0.2823529412, 0.1333333333, 0.6901960784313725, 0.8705882353, + 0.2901960784, 0.1254901961, 0.6941176470588235, 0.8784313725, 0.2941176471, 0.1176470588, + 0.6980392156862745, 0.8862745098, 0.3019607843, 0.1098039216, 0.7019607843137254, + 0.8941176471, 0.3058823529, 0.1019607843, 0.7058823529411765, 0.9019607843, 0.3137254902, + 0.0941176471, 0.7098039215686275, 0.9098039216, 0.3176470588, 0.0862745098, + 0.7137254901960784, 0.9176470588, 0.3254901961, 0.0784313725, 0.7176470588235294, + 0.9254901961, 0.3294117647, 0.0705882353, 0.7215686274509804, 0.9333333333, 0.337254902, + 0.062745098, 0.7254901960784313, 0.9411764706, 0.3411764706, 0.0549019608, 0.7294117647058823, + 0.9490196078, 0.3490196078, 0.0470588235, 0.7333333333333333, 0.9568627451, 0.3529411765, + 0.0392156863, 0.7372549019607844, 0.9647058824, 0.3607843137, 0.031372549, 0.7411764705882353, + 0.9725490196, 0.3647058824, 0.0235294118, 0.7450980392156863, 0.9803921569, 0.3725490196, + 0.0156862745, 0.7490196078431373, 0.9882352941, 0.3725490196, 0.0039215686, + 0.7529411764705882, 0.9960784314, 0.3843137255, 0.0156862745, 0.7568627450980392, + 0.9960784314, 0.3921568627, 0.031372549, 0.7607843137254902, 0.9960784314, 0.4039215686, + 0.0470588235, 0.7647058823529411, 0.9960784314, 0.4117647059, 0.062745098, 0.7686274509803922, + 0.9960784314, 0.4235294118, 0.0784313725, 0.7725490196078432, 0.9960784314, 0.431372549, + 0.0941176471, 0.7764705882352941, 0.9960784314, 0.4431372549, 0.1098039216, + 0.7803921568627451, 0.9960784314, 0.4509803922, 0.1254901961, 0.7843137254901961, + 0.9960784314, 0.462745098, 0.1411764706, 0.788235294117647, 0.9960784314, 0.4705882353, + 0.1568627451, 0.792156862745098, 0.9960784314, 0.4823529412, 0.1725490196, 0.796078431372549, + 0.9960784314, 0.4901960784, 0.1882352941, 0.8, 0.9960784314, 0.5019607843, 0.2039215686, + 0.803921568627451, 0.9960784314, 0.5098039216, 0.2196078431, 0.807843137254902, 0.9960784314, + 0.5215686275, 0.2352941176, 0.8117647058823529, 0.9960784314, 0.5294117647, 0.2509803922, + 0.8156862745098039, 0.9960784314, 0.5411764706, 0.262745098, 0.8196078431372549, 0.9960784314, + 0.5490196078, 0.2784313725, 0.8235294117647058, 0.9960784314, 0.5607843137, 0.2941176471, + 0.8274509803921568, 0.9960784314, 0.568627451, 0.3098039216, 0.8313725490196079, 0.9960784314, + 0.5803921569, 0.3254901961, 0.8352941176470589, 0.9960784314, 0.5882352941, 0.3411764706, + 0.8392156862745098, 0.9960784314, 0.6, 0.3568627451, 0.8431372549019608, 0.9960784314, + 0.6078431373, 0.3725490196, 0.8470588235294118, 0.9960784314, 0.6196078431, 0.3882352941, + 0.8509803921568627, 0.9960784314, 0.6274509804, 0.4039215686, 0.8549019607843137, + 0.9960784314, 0.6392156863, 0.4196078431, 0.8588235294117647, 0.9960784314, 0.6470588235, + 0.4352941176, 0.8627450980392157, 0.9960784314, 0.6588235294, 0.4509803922, + 0.8666666666666667, 0.9960784314, 0.6666666667, 0.4666666667, 0.8705882352941177, + 0.9960784314, 0.6784313725, 0.4823529412, 0.8745098039215686, 0.9960784314, 0.6862745098, + 0.4980392157, 0.8784313725490196, 0.9960784314, 0.6980392157, 0.5137254902, + 0.8823529411764706, 0.9960784314, 0.7058823529, 0.5294117647, 0.8862745098039215, + 0.9960784314, 0.7176470588, 0.5450980392, 0.8901960784313725, 0.9960784314, 0.7254901961, + 0.5607843137, 0.8941176470588236, 0.9960784314, 0.737254902, 0.5764705882, 0.8980392156862745, + 0.9960784314, 0.7450980392, 0.5921568627, 0.9019607843137255, 0.9960784314, 0.7529411765, + 0.6078431373, 0.9058823529411765, 0.9960784314, 0.7607843137, 0.6235294118, + 0.9098039215686274, 0.9960784314, 0.7725490196, 0.6392156863, 0.9137254901960784, + 0.9960784314, 0.7803921569, 0.6549019608, 0.9176470588235294, 0.9960784314, 0.7921568627, + 0.6705882353, 0.9215686274509803, 0.9960784314, 0.8, 0.6862745098, 0.9254901960784314, + 0.9960784314, 0.8117647059, 0.7019607843, 0.9294117647058824, 0.9960784314, 0.8196078431, + 0.7176470588, 0.9333333333333333, 0.9960784314, 0.831372549, 0.7333333333, 0.9372549019607843, + 0.9960784314, 0.8392156863, 0.7490196078, 0.9411764705882354, 0.9960784314, 0.8509803922, + 0.7607843137, 0.9450980392156864, 0.9960784314, 0.8588235294, 0.7764705882, + 0.9490196078431372, 0.9960784314, 0.8705882353, 0.7921568627, 0.9529411764705882, + 0.9960784314, 0.8784313725, 0.8078431373, 0.9568627450980394, 0.9960784314, 0.8901960784, + 0.8235294118, 0.9607843137254903, 0.9960784314, 0.8980392157, 0.8392156863, + 0.9647058823529413, 0.9960784314, 0.9098039216, 0.8549019608, 0.9686274509803922, + 0.9960784314, 0.9176470588, 0.8705882353, 0.9725490196078431, 0.9960784314, 0.9294117647, + 0.8862745098, 0.9764705882352941, 0.9960784314, 0.937254902, 0.9019607843, 0.9803921568627451, + 0.9960784314, 0.9490196078, 0.9176470588, 0.984313725490196, 0.9960784314, 0.9568627451, + 0.9333333333, 0.9882352941176471, 0.9960784314, 0.968627451, 0.9490196078, 0.9921568627450981, + 0.9960784314, 0.9764705882, 0.9647058824, 0.996078431372549, 0.9960784314, 0.9882352941, + 0.9803921569, 1.0, 0.9960784314, 0.9882352941, 0.9803921569, + ], + description: 'GE 256', + }, + { + ColorSpace: 'RGB', + Name: 'ge', + name: 'ge', + RGBPoints: [ + 0.0, 0.0078431373, 0.0078431373, 0.0078431373, 0.00392156862745098, 0.0078431373, + 0.0078431373, 0.0078431373, 0.00784313725490196, 0.0078431373, 0.0078431373, 0.0078431373, + 0.011764705882352941, 0.0078431373, 0.0078431373, 0.0078431373, 0.01568627450980392, + 0.0078431373, 0.0078431373, 0.0078431373, 0.0196078431372549, 0.0078431373, 0.0078431373, + 0.0078431373, 0.023529411764705882, 0.0078431373, 0.0078431373, 0.0078431373, + 0.027450980392156862, 0.0078431373, 0.0078431373, 0.0078431373, 0.03137254901960784, + 0.0078431373, 0.0078431373, 0.0078431373, 0.03529411764705882, 0.0078431373, 0.0078431373, + 0.0078431373, 0.0392156862745098, 0.0078431373, 0.0078431373, 0.0078431373, + 0.043137254901960784, 0.0078431373, 0.0078431373, 0.0078431373, 0.047058823529411764, + 0.0078431373, 0.0078431373, 0.0078431373, 0.050980392156862744, 0.0078431373, 0.0078431373, + 0.0078431373, 0.054901960784313725, 0.0078431373, 0.0078431373, 0.0078431373, + 0.05882352941176471, 0.0117647059, 0.0078431373, 0.0078431373, 0.06274509803921569, + 0.0078431373, 0.0156862745, 0.0156862745, 0.06666666666666667, 0.0078431373, 0.0235294118, + 0.0235294118, 0.07058823529411765, 0.0078431373, 0.031372549, 0.031372549, + 0.07450980392156863, 0.0078431373, 0.0392156863, 0.0392156863, 0.0784313725490196, + 0.0078431373, 0.0470588235, 0.0470588235, 0.08235294117647059, 0.0078431373, 0.0549019608, + 0.0549019608, 0.08627450980392157, 0.0078431373, 0.062745098, 0.062745098, + 0.09019607843137255, 0.0078431373, 0.0705882353, 0.0705882353, 0.09411764705882353, + 0.0078431373, 0.0784313725, 0.0784313725, 0.09803921568627451, 0.0078431373, 0.0901960784, + 0.0862745098, 0.10196078431372549, 0.0078431373, 0.0980392157, 0.0941176471, + 0.10588235294117647, 0.0078431373, 0.1058823529, 0.1019607843, 0.10980392156862745, + 0.0078431373, 0.1137254902, 0.1098039216, 0.11372549019607843, 0.0078431373, 0.1215686275, + 0.1176470588, 0.11764705882352942, 0.0078431373, 0.1294117647, 0.1254901961, + 0.12156862745098039, 0.0078431373, 0.137254902, 0.1333333333, 0.12549019607843137, + 0.0078431373, 0.1450980392, 0.1411764706, 0.12941176470588237, 0.0078431373, 0.1529411765, + 0.1490196078, 0.13333333333333333, 0.0078431373, 0.1647058824, 0.1568627451, + 0.13725490196078433, 0.0078431373, 0.1725490196, 0.1647058824, 0.1411764705882353, + 0.0078431373, 0.1803921569, 0.1725490196, 0.1450980392156863, 0.0078431373, 0.1882352941, + 0.1803921569, 0.14901960784313725, 0.0078431373, 0.1960784314, 0.1882352941, + 0.15294117647058825, 0.0078431373, 0.2039215686, 0.1960784314, 0.1568627450980392, + 0.0078431373, 0.2117647059, 0.2039215686, 0.1607843137254902, 0.0078431373, 0.2196078431, + 0.2117647059, 0.16470588235294117, 0.0078431373, 0.2274509804, 0.2196078431, + 0.16862745098039217, 0.0078431373, 0.2352941176, 0.2274509804, 0.17254901960784313, + 0.0078431373, 0.2470588235, 0.2352941176, 0.17647058823529413, 0.0078431373, 0.2509803922, + 0.2431372549, 0.1803921568627451, 0.0078431373, 0.2549019608, 0.2509803922, + 0.1843137254901961, 0.0078431373, 0.262745098, 0.2509803922, 0.18823529411764706, + 0.0078431373, 0.2705882353, 0.2588235294, 0.19215686274509805, 0.0078431373, 0.2784313725, + 0.2666666667, 0.19607843137254902, 0.0078431373, 0.2862745098, 0.2745098039, 0.2, + 0.0078431373, 0.2941176471, 0.2823529412, 0.20392156862745098, 0.0078431373, 0.3019607843, + 0.2901960784, 0.20784313725490197, 0.0078431373, 0.3137254902, 0.2980392157, + 0.21176470588235294, 0.0078431373, 0.3215686275, 0.3058823529, 0.21568627450980393, + 0.0078431373, 0.3294117647, 0.3137254902, 0.2196078431372549, 0.0078431373, 0.337254902, + 0.3215686275, 0.2235294117647059, 0.0078431373, 0.3450980392, 0.3294117647, + 0.22745098039215686, 0.0078431373, 0.3529411765, 0.337254902, 0.23137254901960785, + 0.0078431373, 0.3607843137, 0.3450980392, 0.23529411764705885, 0.0078431373, 0.368627451, + 0.3529411765, 0.23921568627450984, 0.0078431373, 0.3764705882, 0.3607843137, + 0.24313725490196078, 0.0078431373, 0.3843137255, 0.368627451, 0.24705882352941178, + 0.0078431373, 0.3960784314, 0.3764705882, 0.25098039215686274, 0.0078431373, 0.4039215686, + 0.3843137255, 0.2549019607843137, 0.0078431373, 0.4117647059, 0.3921568627, + 0.25882352941176473, 0.0078431373, 0.4196078431, 0.4, 0.2627450980392157, 0.0078431373, + 0.4274509804, 0.4078431373, 0.26666666666666666, 0.0078431373, 0.4352941176, 0.4156862745, + 0.27058823529411763, 0.0078431373, 0.4431372549, 0.4235294118, 0.27450980392156865, + 0.0078431373, 0.4509803922, 0.431372549, 0.2784313725490196, 0.0078431373, 0.4588235294, + 0.4392156863, 0.2823529411764706, 0.0078431373, 0.4705882353, 0.4470588235, + 0.28627450980392155, 0.0078431373, 0.4784313725, 0.4549019608, 0.2901960784313726, + 0.0078431373, 0.4862745098, 0.462745098, 0.29411764705882354, 0.0078431373, 0.4941176471, + 0.4705882353, 0.2980392156862745, 0.0078431373, 0.5019607843, 0.4784313725, + 0.30196078431372547, 0.0117647059, 0.5098039216, 0.4862745098, 0.3058823529411765, + 0.0196078431, 0.5019607843, 0.4941176471, 0.30980392156862746, 0.0274509804, 0.4941176471, + 0.5058823529, 0.3137254901960784, 0.0352941176, 0.4862745098, 0.5137254902, + 0.3176470588235294, 0.0431372549, 0.4784313725, 0.5215686275, 0.3215686274509804, + 0.0509803922, 0.4705882353, 0.5294117647, 0.3254901960784314, 0.0588235294, 0.462745098, + 0.537254902, 0.32941176470588235, 0.0666666667, 0.4549019608, 0.5450980392, + 0.3333333333333333, 0.0745098039, 0.4470588235, 0.5529411765, 0.33725490196078434, + 0.0823529412, 0.4392156863, 0.5607843137, 0.3411764705882353, 0.0901960784, 0.431372549, + 0.568627451, 0.34509803921568627, 0.0980392157, 0.4235294118, 0.5764705882, + 0.34901960784313724, 0.1058823529, 0.4156862745, 0.5843137255, 0.35294117647058826, + 0.1137254902, 0.4078431373, 0.5921568627, 0.3568627450980392, 0.1215686275, 0.4, 0.6, + 0.3607843137254902, 0.1294117647, 0.3921568627, 0.6078431373, 0.36470588235294116, + 0.137254902, 0.3843137255, 0.6156862745, 0.3686274509803922, 0.1450980392, 0.3764705882, + 0.6235294118, 0.37254901960784315, 0.1529411765, 0.368627451, 0.631372549, 0.3764705882352941, + 0.1607843137, 0.3607843137, 0.6392156863, 0.3803921568627451, 0.168627451, 0.3529411765, + 0.6470588235, 0.3843137254901961, 0.1764705882, 0.3450980392, 0.6549019608, + 0.38823529411764707, 0.1843137255, 0.337254902, 0.662745098, 0.39215686274509803, + 0.1921568627, 0.3294117647, 0.6705882353, 0.396078431372549, 0.2, 0.3215686275, 0.6784313725, + 0.4, 0.2078431373, 0.3137254902, 0.6862745098, 0.403921568627451, 0.2156862745, 0.3058823529, + 0.6941176471, 0.40784313725490196, 0.2235294118, 0.2980392157, 0.7019607843, + 0.4117647058823529, 0.231372549, 0.2901960784, 0.7098039216, 0.41568627450980394, + 0.2392156863, 0.2823529412, 0.7176470588, 0.4196078431372549, 0.2470588235, 0.2745098039, + 0.7254901961, 0.4235294117647059, 0.2509803922, 0.2666666667, 0.7333333333, + 0.42745098039215684, 0.2509803922, 0.2588235294, 0.7411764706, 0.43137254901960786, + 0.2588235294, 0.2509803922, 0.7490196078, 0.43529411764705883, 0.2666666667, 0.2509803922, + 0.7490196078, 0.4392156862745098, 0.2745098039, 0.2431372549, 0.7568627451, + 0.44313725490196076, 0.2823529412, 0.2352941176, 0.7647058824, 0.4470588235294118, + 0.2901960784, 0.2274509804, 0.7725490196, 0.45098039215686275, 0.2980392157, 0.2196078431, + 0.7803921569, 0.4549019607843137, 0.3058823529, 0.2117647059, 0.7882352941, + 0.4588235294117647, 0.3137254902, 0.2039215686, 0.7960784314, 0.4627450980392157, + 0.3215686275, 0.1960784314, 0.8039215686, 0.4666666666666667, 0.3294117647, 0.1882352941, + 0.8117647059, 0.4705882352941177, 0.337254902, 0.1803921569, 0.8196078431, 0.4745098039215686, + 0.3450980392, 0.1725490196, 0.8274509804, 0.4784313725490197, 0.3529411765, 0.1647058824, + 0.8352941176, 0.48235294117647065, 0.3607843137, 0.1568627451, 0.8431372549, + 0.48627450980392156, 0.368627451, 0.1490196078, 0.8509803922, 0.49019607843137253, + 0.3764705882, 0.1411764706, 0.8588235294, 0.49411764705882355, 0.3843137255, 0.1333333333, + 0.8666666667, 0.4980392156862745, 0.3921568627, 0.1254901961, 0.8745098039, + 0.5019607843137255, 0.4, 0.1176470588, 0.8823529412, 0.5058823529411764, 0.4078431373, + 0.1098039216, 0.8901960784, 0.5098039215686274, 0.4156862745, 0.1019607843, 0.8980392157, + 0.5137254901960784, 0.4235294118, 0.0941176471, 0.9058823529, 0.5176470588235295, 0.431372549, + 0.0862745098, 0.9137254902, 0.5215686274509804, 0.4392156863, 0.0784313725, 0.9215686275, + 0.5254901960784314, 0.4470588235, 0.0705882353, 0.9294117647, 0.5294117647058824, + 0.4549019608, 0.062745098, 0.937254902, 0.5333333333333333, 0.462745098, 0.0549019608, + 0.9450980392, 0.5372549019607843, 0.4705882353, 0.0470588235, 0.9529411765, + 0.5411764705882353, 0.4784313725, 0.0392156863, 0.9607843137, 0.5450980392156862, + 0.4862745098, 0.031372549, 0.968627451, 0.5490196078431373, 0.4941176471, 0.0235294118, + 0.9764705882, 0.5529411764705883, 0.4980392157, 0.0156862745, 0.9843137255, + 0.5568627450980392, 0.5058823529, 0.0078431373, 0.9921568627, 0.5607843137254902, + 0.5137254902, 0.0156862745, 0.9803921569, 0.5647058823529412, 0.5215686275, 0.0235294118, + 0.9647058824, 0.5686274509803921, 0.5294117647, 0.0352941176, 0.9490196078, + 0.5725490196078431, 0.537254902, 0.0431372549, 0.9333333333, 0.5764705882352941, 0.5450980392, + 0.0509803922, 0.9176470588, 0.5803921568627451, 0.5529411765, 0.062745098, 0.9019607843, + 0.5843137254901961, 0.5607843137, 0.0705882353, 0.8862745098, 0.5882352941176471, 0.568627451, + 0.0784313725, 0.8705882353, 0.592156862745098, 0.5764705882, 0.0901960784, 0.8549019608, + 0.596078431372549, 0.5843137255, 0.0980392157, 0.8392156863, 0.6, 0.5921568627, 0.1098039216, + 0.8235294118, 0.6039215686274509, 0.6, 0.1176470588, 0.8078431373, 0.6078431372549019, + 0.6078431373, 0.1254901961, 0.7921568627, 0.611764705882353, 0.6156862745, 0.137254902, + 0.7764705882, 0.615686274509804, 0.6235294118, 0.1450980392, 0.7607843137, 0.6196078431372549, + 0.631372549, 0.1529411765, 0.7490196078, 0.6235294117647059, 0.6392156863, 0.1647058824, + 0.737254902, 0.6274509803921569, 0.6470588235, 0.1725490196, 0.7215686275, 0.6313725490196078, + 0.6549019608, 0.1843137255, 0.7058823529, 0.6352941176470588, 0.662745098, 0.1921568627, + 0.6901960784, 0.6392156862745098, 0.6705882353, 0.2, 0.6745098039, 0.6431372549019608, + 0.6784313725, 0.2117647059, 0.6588235294, 0.6470588235294118, 0.6862745098, 0.2196078431, + 0.6431372549, 0.6509803921568628, 0.6941176471, 0.2274509804, 0.6274509804, + 0.6549019607843137, 0.7019607843, 0.2392156863, 0.6117647059, 0.6588235294117647, + 0.7098039216, 0.2470588235, 0.5960784314, 0.6627450980392157, 0.7176470588, 0.2509803922, + 0.5803921569, 0.6666666666666666, 0.7254901961, 0.2588235294, 0.5647058824, + 0.6705882352941176, 0.7333333333, 0.2666666667, 0.5490196078, 0.6745098039215687, + 0.7411764706, 0.2784313725, 0.5333333333, 0.6784313725490196, 0.7490196078, 0.2862745098, + 0.5176470588, 0.6823529411764706, 0.7490196078, 0.2941176471, 0.5019607843, + 0.6862745098039216, 0.7529411765, 0.3058823529, 0.4862745098, 0.6901960784313725, + 0.7607843137, 0.3137254902, 0.4705882353, 0.6941176470588235, 0.768627451, 0.3215686275, + 0.4549019608, 0.6980392156862745, 0.7764705882, 0.3333333333, 0.4392156863, + 0.7019607843137254, 0.7843137255, 0.3411764706, 0.4235294118, 0.7058823529411765, + 0.7921568627, 0.3529411765, 0.4078431373, 0.7098039215686275, 0.8, 0.3607843137, 0.3921568627, + 0.7137254901960784, 0.8078431373, 0.368627451, 0.3764705882, 0.7176470588235294, 0.8156862745, + 0.3803921569, 0.3607843137, 0.7215686274509804, 0.8235294118, 0.3882352941, 0.3450980392, + 0.7254901960784313, 0.831372549, 0.3960784314, 0.3294117647, 0.7294117647058823, 0.8392156863, + 0.4078431373, 0.3137254902, 0.7333333333333333, 0.8470588235, 0.4156862745, 0.2980392157, + 0.7372549019607844, 0.8549019608, 0.4274509804, 0.2823529412, 0.7411764705882353, 0.862745098, + 0.4352941176, 0.2666666667, 0.7450980392156863, 0.8705882353, 0.4431372549, 0.2509803922, + 0.7490196078431373, 0.8784313725, 0.4549019608, 0.2431372549, 0.7529411764705882, + 0.8862745098, 0.462745098, 0.2274509804, 0.7568627450980392, 0.8941176471, 0.4705882353, + 0.2117647059, 0.7607843137254902, 0.9019607843, 0.4823529412, 0.1960784314, + 0.7647058823529411, 0.9098039216, 0.4901960784, 0.1803921569, 0.7686274509803922, + 0.9176470588, 0.4980392157, 0.1647058824, 0.7725490196078432, 0.9254901961, 0.5098039216, + 0.1490196078, 0.7764705882352941, 0.9333333333, 0.5176470588, 0.1333333333, + 0.7803921568627451, 0.9411764706, 0.5294117647, 0.1176470588, 0.7843137254901961, + 0.9490196078, 0.537254902, 0.1019607843, 0.788235294117647, 0.9568627451, 0.5450980392, + 0.0862745098, 0.792156862745098, 0.9647058824, 0.5568627451, 0.0705882353, 0.796078431372549, + 0.9725490196, 0.5647058824, 0.0549019608, 0.8, 0.9803921569, 0.5725490196, 0.0392156863, + 0.803921568627451, 0.9882352941, 0.5843137255, 0.0235294118, 0.807843137254902, 0.9921568627, + 0.5921568627, 0.0078431373, 0.8117647058823529, 0.9921568627, 0.6039215686, 0.0274509804, + 0.8156862745098039, 0.9921568627, 0.6117647059, 0.0509803922, 0.8196078431372549, + 0.9921568627, 0.6196078431, 0.0745098039, 0.8235294117647058, 0.9921568627, 0.631372549, + 0.0980392157, 0.8274509803921568, 0.9921568627, 0.6392156863, 0.1215686275, + 0.8313725490196079, 0.9921568627, 0.6470588235, 0.1411764706, 0.8352941176470589, + 0.9921568627, 0.6588235294, 0.1647058824, 0.8392156862745098, 0.9921568627, 0.6666666667, + 0.1882352941, 0.8431372549019608, 0.9921568627, 0.6784313725, 0.2117647059, + 0.8470588235294118, 0.9921568627, 0.6862745098, 0.2352941176, 0.8509803921568627, + 0.9921568627, 0.6941176471, 0.2509803922, 0.8549019607843137, 0.9921568627, 0.7058823529, + 0.2705882353, 0.8588235294117647, 0.9921568627, 0.7137254902, 0.2941176471, + 0.8627450980392157, 0.9921568627, 0.7215686275, 0.3176470588, 0.8666666666666667, + 0.9921568627, 0.7333333333, 0.3411764706, 0.8705882352941177, 0.9921568627, 0.7411764706, + 0.3647058824, 0.8745098039215686, 0.9921568627, 0.7490196078, 0.3843137255, + 0.8784313725490196, 0.9921568627, 0.7529411765, 0.4078431373, 0.8823529411764706, + 0.9921568627, 0.7607843137, 0.431372549, 0.8862745098039215, 0.9921568627, 0.7725490196, + 0.4549019608, 0.8901960784313725, 0.9921568627, 0.7803921569, 0.4784313725, + 0.8941176470588236, 0.9921568627, 0.7882352941, 0.4980392157, 0.8980392156862745, + 0.9921568627, 0.8, 0.5215686275, 0.9019607843137255, 0.9921568627, 0.8078431373, 0.5450980392, + 0.9058823529411765, 0.9921568627, 0.8156862745, 0.568627451, 0.9098039215686274, 0.9921568627, + 0.8274509804, 0.5921568627, 0.9137254901960784, 0.9921568627, 0.8352941176, 0.6156862745, + 0.9176470588235294, 0.9921568627, 0.8470588235, 0.6352941176, 0.9215686274509803, + 0.9921568627, 0.8549019608, 0.6588235294, 0.9254901960784314, 0.9921568627, 0.862745098, + 0.6823529412, 0.9294117647058824, 0.9921568627, 0.8745098039, 0.7058823529, + 0.9333333333333333, 0.9921568627, 0.8823529412, 0.7294117647, 0.9372549019607843, + 0.9921568627, 0.8901960784, 0.7490196078, 0.9411764705882354, 0.9921568627, 0.9019607843, + 0.7647058824, 0.9450980392156864, 0.9921568627, 0.9098039216, 0.7882352941, + 0.9490196078431372, 0.9921568627, 0.9215686275, 0.8117647059, 0.9529411764705882, + 0.9921568627, 0.9294117647, 0.8352941176, 0.9568627450980394, 0.9921568627, 0.937254902, + 0.8588235294, 0.9607843137254903, 0.9921568627, 0.9490196078, 0.8784313725, + 0.9647058823529413, 0.9921568627, 0.9568627451, 0.9019607843, 0.9686274509803922, + 0.9921568627, 0.9647058824, 0.9254901961, 0.9725490196078431, 0.9921568627, 0.9764705882, + 0.9490196078, 0.9764705882352941, 0.9921568627, 0.9843137255, 0.9725490196, + 0.9803921568627451, 0.9921568627, 0.9921568627, 0.9921568627, 0.984313725490196, 0.9921568627, + 0.9921568627, 0.9921568627, 0.9882352941176471, 0.9921568627, 0.9921568627, 0.9921568627, + 0.9921568627450981, 0.9921568627, 0.9921568627, 0.9921568627, 0.996078431372549, 0.9921568627, + 0.9921568627, 0.9921568627, 1.0, 0.9921568627, 0.9921568627, 0.9921568627, + ], + description: 'GE', + }, + { + ColorSpace: 'RGB', + Name: 'siemens', + name: 'siemens', + RGBPoints: [ + 0.0, 0.0078431373, 0.0039215686, 0.1254901961, 0.00392156862745098, 0.0078431373, + 0.0039215686, 0.1254901961, 0.00784313725490196, 0.0078431373, 0.0039215686, 0.1882352941, + 0.011764705882352941, 0.0117647059, 0.0039215686, 0.2509803922, 0.01568627450980392, + 0.0117647059, 0.0039215686, 0.3098039216, 0.0196078431372549, 0.0156862745, 0.0039215686, + 0.3725490196, 0.023529411764705882, 0.0156862745, 0.0039215686, 0.3725490196, + 0.027450980392156862, 0.0156862745, 0.0039215686, 0.3725490196, 0.03137254901960784, + 0.0156862745, 0.0039215686, 0.3725490196, 0.03529411764705882, 0.0156862745, 0.0039215686, + 0.3725490196, 0.0392156862745098, 0.0156862745, 0.0039215686, 0.3725490196, + 0.043137254901960784, 0.0156862745, 0.0039215686, 0.3725490196, 0.047058823529411764, + 0.0156862745, 0.0039215686, 0.3725490196, 0.050980392156862744, 0.0156862745, 0.0039215686, + 0.3725490196, 0.054901960784313725, 0.0156862745, 0.0039215686, 0.3725490196, + 0.05882352941176471, 0.0156862745, 0.0039215686, 0.3725490196, 0.06274509803921569, + 0.0156862745, 0.0039215686, 0.3882352941, 0.06666666666666667, 0.0156862745, 0.0039215686, + 0.4078431373, 0.07058823529411765, 0.0156862745, 0.0039215686, 0.4235294118, + 0.07450980392156863, 0.0156862745, 0.0039215686, 0.4431372549, 0.0784313725490196, + 0.0156862745, 0.0039215686, 0.462745098, 0.08235294117647059, 0.0156862745, 0.0039215686, + 0.4784313725, 0.08627450980392157, 0.0156862745, 0.0039215686, 0.4980392157, + 0.09019607843137255, 0.0196078431, 0.0039215686, 0.5137254902, 0.09411764705882353, + 0.0196078431, 0.0039215686, 0.5333333333, 0.09803921568627451, 0.0196078431, 0.0039215686, + 0.5529411765, 0.10196078431372549, 0.0196078431, 0.0039215686, 0.568627451, + 0.10588235294117647, 0.0196078431, 0.0039215686, 0.5882352941, 0.10980392156862745, + 0.0196078431, 0.0039215686, 0.6039215686, 0.11372549019607843, 0.0196078431, 0.0039215686, + 0.6235294118, 0.11764705882352942, 0.0196078431, 0.0039215686, 0.6431372549, + 0.12156862745098039, 0.0235294118, 0.0039215686, 0.6588235294, 0.12549019607843137, + 0.0235294118, 0.0039215686, 0.6784313725, 0.12941176470588237, 0.0235294118, 0.0039215686, + 0.6980392157, 0.13333333333333333, 0.0235294118, 0.0039215686, 0.7137254902, + 0.13725490196078433, 0.0235294118, 0.0039215686, 0.7333333333, 0.1411764705882353, + 0.0235294118, 0.0039215686, 0.7490196078, 0.1450980392156863, 0.0235294118, 0.0039215686, + 0.7647058824, 0.14901960784313725, 0.0235294118, 0.0039215686, 0.7843137255, + 0.15294117647058825, 0.0274509804, 0.0039215686, 0.8, 0.1568627450980392, 0.0274509804, + 0.0039215686, 0.8196078431, 0.1607843137254902, 0.0274509804, 0.0039215686, 0.8352941176, + 0.16470588235294117, 0.0274509804, 0.0039215686, 0.8549019608, 0.16862745098039217, + 0.0274509804, 0.0039215686, 0.8745098039, 0.17254901960784313, 0.0274509804, 0.0039215686, + 0.8901960784, 0.17647058823529413, 0.0274509804, 0.0039215686, 0.9098039216, + 0.1803921568627451, 0.031372549, 0.0039215686, 0.9294117647, 0.1843137254901961, 0.031372549, + 0.0039215686, 0.9254901961, 0.18823529411764706, 0.0509803922, 0.0039215686, 0.9098039216, + 0.19215686274509805, 0.0705882353, 0.0039215686, 0.8901960784, 0.19607843137254902, + 0.0901960784, 0.0039215686, 0.8705882353, 0.2, 0.1137254902, 0.0039215686, 0.8509803922, + 0.20392156862745098, 0.1333333333, 0.0039215686, 0.831372549, 0.20784313725490197, + 0.1529411765, 0.0039215686, 0.8117647059, 0.21176470588235294, 0.1725490196, 0.0039215686, + 0.7921568627, 0.21568627450980393, 0.1960784314, 0.0039215686, 0.7725490196, + 0.2196078431372549, 0.2156862745, 0.0039215686, 0.7529411765, 0.2235294117647059, + 0.2352941176, 0.0039215686, 0.737254902, 0.22745098039215686, 0.2509803922, 0.0039215686, + 0.7176470588, 0.23137254901960785, 0.2745098039, 0.0039215686, 0.6980392157, + 0.23529411764705885, 0.2941176471, 0.0039215686, 0.6784313725, 0.23921568627450984, + 0.3137254902, 0.0039215686, 0.6588235294, 0.24313725490196078, 0.3333333333, 0.0039215686, + 0.6392156863, 0.24705882352941178, 0.3568627451, 0.0039215686, 0.6196078431, + 0.25098039215686274, 0.3764705882, 0.0039215686, 0.6, 0.2549019607843137, 0.3960784314, + 0.0039215686, 0.5803921569, 0.25882352941176473, 0.4156862745, 0.0039215686, 0.5607843137, + 0.2627450980392157, 0.4392156863, 0.0039215686, 0.5411764706, 0.26666666666666666, + 0.4588235294, 0.0039215686, 0.5215686275, 0.27058823529411763, 0.4784313725, 0.0039215686, + 0.5019607843, 0.27450980392156865, 0.4980392157, 0.0039215686, 0.4823529412, + 0.2784313725490196, 0.5215686275, 0.0039215686, 0.4666666667, 0.2823529411764706, + 0.5411764706, 0.0039215686, 0.4470588235, 0.28627450980392155, 0.5607843137, 0.0039215686, + 0.4274509804, 0.2901960784313726, 0.5803921569, 0.0039215686, 0.4078431373, + 0.29411764705882354, 0.6039215686, 0.0039215686, 0.3882352941, 0.2980392156862745, + 0.6235294118, 0.0039215686, 0.368627451, 0.30196078431372547, 0.6431372549, 0.0039215686, + 0.3490196078, 0.3058823529411765, 0.662745098, 0.0039215686, 0.3294117647, + 0.30980392156862746, 0.6862745098, 0.0039215686, 0.3098039216, 0.3137254901960784, + 0.7058823529, 0.0039215686, 0.2901960784, 0.3176470588235294, 0.7254901961, 0.0039215686, + 0.2705882353, 0.3215686274509804, 0.7450980392, 0.0039215686, 0.2509803922, + 0.3254901960784314, 0.7647058824, 0.0039215686, 0.2352941176, 0.32941176470588235, + 0.7843137255, 0.0039215686, 0.2156862745, 0.3333333333333333, 0.8039215686, 0.0039215686, + 0.1960784314, 0.33725490196078434, 0.8235294118, 0.0039215686, 0.1764705882, + 0.3411764705882353, 0.8470588235, 0.0039215686, 0.1568627451, 0.34509803921568627, + 0.8666666667, 0.0039215686, 0.137254902, 0.34901960784313724, 0.8862745098, 0.0039215686, + 0.1176470588, 0.35294117647058826, 0.9058823529, 0.0039215686, 0.0980392157, + 0.3568627450980392, 0.9294117647, 0.0039215686, 0.0784313725, 0.3607843137254902, + 0.9490196078, 0.0039215686, 0.0588235294, 0.36470588235294116, 0.968627451, 0.0039215686, + 0.0392156863, 0.3686274509803922, 0.9921568627, 0.0039215686, 0.0235294118, + 0.37254901960784315, 0.9529411765, 0.0039215686, 0.0588235294, 0.3764705882352941, + 0.9529411765, 0.0078431373, 0.0549019608, 0.3803921568627451, 0.9529411765, 0.0156862745, + 0.0549019608, 0.3843137254901961, 0.9529411765, 0.0235294118, 0.0549019608, + 0.38823529411764707, 0.9529411765, 0.031372549, 0.0549019608, 0.39215686274509803, + 0.9529411765, 0.0352941176, 0.0549019608, 0.396078431372549, 0.9529411765, 0.0431372549, + 0.0549019608, 0.4, 0.9529411765, 0.0509803922, 0.0549019608, 0.403921568627451, 0.9529411765, + 0.0588235294, 0.0549019608, 0.40784313725490196, 0.9529411765, 0.062745098, 0.0549019608, + 0.4117647058823529, 0.9529411765, 0.0705882353, 0.0549019608, 0.41568627450980394, + 0.9529411765, 0.0784313725, 0.0509803922, 0.4196078431372549, 0.9529411765, 0.0862745098, + 0.0509803922, 0.4235294117647059, 0.9568627451, 0.0941176471, 0.0509803922, + 0.42745098039215684, 0.9568627451, 0.0980392157, 0.0509803922, 0.43137254901960786, + 0.9568627451, 0.1058823529, 0.0509803922, 0.43529411764705883, 0.9568627451, 0.1137254902, + 0.0509803922, 0.4392156862745098, 0.9568627451, 0.1215686275, 0.0509803922, + 0.44313725490196076, 0.9568627451, 0.1254901961, 0.0509803922, 0.4470588235294118, + 0.9568627451, 0.1333333333, 0.0509803922, 0.45098039215686275, 0.9568627451, 0.1411764706, + 0.0509803922, 0.4549019607843137, 0.9568627451, 0.1490196078, 0.0470588235, + 0.4588235294117647, 0.9568627451, 0.1568627451, 0.0470588235, 0.4627450980392157, + 0.9568627451, 0.1607843137, 0.0470588235, 0.4666666666666667, 0.9568627451, 0.168627451, + 0.0470588235, 0.4705882352941177, 0.9607843137, 0.1764705882, 0.0470588235, + 0.4745098039215686, 0.9607843137, 0.1843137255, 0.0470588235, 0.4784313725490197, + 0.9607843137, 0.1882352941, 0.0470588235, 0.48235294117647065, 0.9607843137, 0.1960784314, + 0.0470588235, 0.48627450980392156, 0.9607843137, 0.2039215686, 0.0470588235, + 0.49019607843137253, 0.9607843137, 0.2117647059, 0.0470588235, 0.49411764705882355, + 0.9607843137, 0.2196078431, 0.0431372549, 0.4980392156862745, 0.9607843137, 0.2235294118, + 0.0431372549, 0.5019607843137255, 0.9607843137, 0.231372549, 0.0431372549, 0.5058823529411764, + 0.9607843137, 0.2392156863, 0.0431372549, 0.5098039215686274, 0.9607843137, 0.2470588235, + 0.0431372549, 0.5137254901960784, 0.9607843137, 0.2509803922, 0.0431372549, + 0.5176470588235295, 0.9647058824, 0.2549019608, 0.0431372549, 0.5215686274509804, + 0.9647058824, 0.262745098, 0.0431372549, 0.5254901960784314, 0.9647058824, 0.2705882353, + 0.0431372549, 0.5294117647058824, 0.9647058824, 0.2745098039, 0.0431372549, + 0.5333333333333333, 0.9647058824, 0.2823529412, 0.0392156863, 0.5372549019607843, + 0.9647058824, 0.2901960784, 0.0392156863, 0.5411764705882353, 0.9647058824, 0.2980392157, + 0.0392156863, 0.5450980392156862, 0.9647058824, 0.3058823529, 0.0392156863, + 0.5490196078431373, 0.9647058824, 0.3098039216, 0.0392156863, 0.5529411764705883, + 0.9647058824, 0.3176470588, 0.0392156863, 0.5568627450980392, 0.9647058824, 0.3254901961, + 0.0392156863, 0.5607843137254902, 0.9647058824, 0.3333333333, 0.0392156863, + 0.5647058823529412, 0.9647058824, 0.337254902, 0.0392156863, 0.5686274509803921, 0.968627451, + 0.3450980392, 0.0392156863, 0.5725490196078431, 0.968627451, 0.3529411765, 0.0352941176, + 0.5764705882352941, 0.968627451, 0.3607843137, 0.0352941176, 0.5803921568627451, 0.968627451, + 0.368627451, 0.0352941176, 0.5843137254901961, 0.968627451, 0.3725490196, 0.0352941176, + 0.5882352941176471, 0.968627451, 0.3803921569, 0.0352941176, 0.592156862745098, 0.968627451, + 0.3882352941, 0.0352941176, 0.596078431372549, 0.968627451, 0.3960784314, 0.0352941176, 0.6, + 0.968627451, 0.4, 0.0352941176, 0.6039215686274509, 0.968627451, 0.4078431373, 0.0352941176, + 0.6078431372549019, 0.968627451, 0.4156862745, 0.0352941176, 0.611764705882353, 0.968627451, + 0.4235294118, 0.031372549, 0.615686274509804, 0.9725490196, 0.431372549, 0.031372549, + 0.6196078431372549, 0.9725490196, 0.4352941176, 0.031372549, 0.6235294117647059, 0.9725490196, + 0.4431372549, 0.031372549, 0.6274509803921569, 0.9725490196, 0.4509803922, 0.031372549, + 0.6313725490196078, 0.9725490196, 0.4588235294, 0.031372549, 0.6352941176470588, 0.9725490196, + 0.462745098, 0.031372549, 0.6392156862745098, 0.9725490196, 0.4705882353, 0.031372549, + 0.6431372549019608, 0.9725490196, 0.4784313725, 0.031372549, 0.6470588235294118, 0.9725490196, + 0.4862745098, 0.031372549, 0.6509803921568628, 0.9725490196, 0.4941176471, 0.0274509804, + 0.6549019607843137, 0.9725490196, 0.4980392157, 0.0274509804, 0.6588235294117647, + 0.9725490196, 0.5058823529, 0.0274509804, 0.6627450980392157, 0.9764705882, 0.5137254902, + 0.0274509804, 0.6666666666666666, 0.9764705882, 0.5215686275, 0.0274509804, + 0.6705882352941176, 0.9764705882, 0.5254901961, 0.0274509804, 0.6745098039215687, + 0.9764705882, 0.5333333333, 0.0274509804, 0.6784313725490196, 0.9764705882, 0.5411764706, + 0.0274509804, 0.6823529411764706, 0.9764705882, 0.5490196078, 0.0274509804, + 0.6862745098039216, 0.9764705882, 0.5529411765, 0.0274509804, 0.6901960784313725, + 0.9764705882, 0.5607843137, 0.0235294118, 0.6941176470588235, 0.9764705882, 0.568627451, + 0.0235294118, 0.6980392156862745, 0.9764705882, 0.5764705882, 0.0235294118, + 0.7019607843137254, 0.9764705882, 0.5843137255, 0.0235294118, 0.7058823529411765, + 0.9764705882, 0.5882352941, 0.0235294118, 0.7098039215686275, 0.9764705882, 0.5960784314, + 0.0235294118, 0.7137254901960784, 0.9803921569, 0.6039215686, 0.0235294118, + 0.7176470588235294, 0.9803921569, 0.6117647059, 0.0235294118, 0.7215686274509804, + 0.9803921569, 0.6156862745, 0.0235294118, 0.7254901960784313, 0.9803921569, 0.6235294118, + 0.0235294118, 0.7294117647058823, 0.9803921569, 0.631372549, 0.0196078431, 0.7333333333333333, + 0.9803921569, 0.6392156863, 0.0196078431, 0.7372549019607844, 0.9803921569, 0.6470588235, + 0.0196078431, 0.7411764705882353, 0.9803921569, 0.6509803922, 0.0196078431, + 0.7450980392156863, 0.9803921569, 0.6588235294, 0.0196078431, 0.7490196078431373, + 0.9803921569, 0.6666666667, 0.0196078431, 0.7529411764705882, 0.9803921569, 0.6745098039, + 0.0196078431, 0.7568627450980392, 0.9803921569, 0.6784313725, 0.0196078431, + 0.7607843137254902, 0.9843137255, 0.6862745098, 0.0196078431, 0.7647058823529411, + 0.9843137255, 0.6941176471, 0.0196078431, 0.7686274509803922, 0.9843137255, 0.7019607843, + 0.0156862745, 0.7725490196078432, 0.9843137255, 0.7098039216, 0.0156862745, + 0.7764705882352941, 0.9843137255, 0.7137254902, 0.0156862745, 0.7803921568627451, + 0.9843137255, 0.7215686275, 0.0156862745, 0.7843137254901961, 0.9843137255, 0.7294117647, + 0.0156862745, 0.788235294117647, 0.9843137255, 0.737254902, 0.0156862745, 0.792156862745098, + 0.9843137255, 0.7411764706, 0.0156862745, 0.796078431372549, 0.9843137255, 0.7490196078, + 0.0156862745, 0.8, 0.9843137255, 0.7529411765, 0.0156862745, 0.803921568627451, 0.9843137255, + 0.7607843137, 0.0156862745, 0.807843137254902, 0.9882352941, 0.768627451, 0.0156862745, + 0.8117647058823529, 0.9882352941, 0.768627451, 0.0156862745, 0.8156862745098039, 0.9843137255, + 0.7843137255, 0.0117647059, 0.8196078431372549, 0.9843137255, 0.8, 0.0117647059, + 0.8235294117647058, 0.9843137255, 0.8156862745, 0.0117647059, 0.8274509803921568, + 0.9803921569, 0.831372549, 0.0117647059, 0.8313725490196079, 0.9803921569, 0.8431372549, + 0.0117647059, 0.8352941176470589, 0.9803921569, 0.8588235294, 0.0078431373, + 0.8392156862745098, 0.9803921569, 0.8745098039, 0.0078431373, 0.8431372549019608, + 0.9764705882, 0.8901960784, 0.0078431373, 0.8470588235294118, 0.9764705882, 0.9058823529, + 0.0078431373, 0.8509803921568627, 0.9764705882, 0.9176470588, 0.0078431373, + 0.8549019607843137, 0.9764705882, 0.9333333333, 0.0039215686, 0.8588235294117647, + 0.9725490196, 0.9490196078, 0.0039215686, 0.8627450980392157, 0.9725490196, 0.9647058824, + 0.0039215686, 0.8666666666666667, 0.9725490196, 0.9803921569, 0.0039215686, + 0.8705882352941177, 0.9725490196, 0.9960784314, 0.0039215686, 0.8745098039215686, + 0.9725490196, 0.9960784314, 0.0039215686, 0.8784313725490196, 0.9725490196, 0.9960784314, + 0.0352941176, 0.8823529411764706, 0.9725490196, 0.9960784314, 0.0666666667, + 0.8862745098039215, 0.9725490196, 0.9960784314, 0.0980392157, 0.8901960784313725, + 0.9725490196, 0.9960784314, 0.1294117647, 0.8941176470588236, 0.9725490196, 0.9960784314, + 0.1647058824, 0.8980392156862745, 0.9764705882, 0.9960784314, 0.1960784314, + 0.9019607843137255, 0.9764705882, 0.9960784314, 0.2274509804, 0.9058823529411765, + 0.9764705882, 0.9960784314, 0.2549019608, 0.9098039215686274, 0.9764705882, 0.9960784314, + 0.2901960784, 0.9137254901960784, 0.9764705882, 0.9960784314, 0.3215686275, + 0.9176470588235294, 0.9803921569, 0.9960784314, 0.3529411765, 0.9215686274509803, + 0.9803921569, 0.9960784314, 0.3843137255, 0.9254901960784314, 0.9803921569, 0.9960784314, + 0.4156862745, 0.9294117647058824, 0.9803921569, 0.9960784314, 0.4509803922, + 0.9333333333333333, 0.9803921569, 0.9960784314, 0.4823529412, 0.9372549019607843, + 0.9843137255, 0.9960784314, 0.5137254902, 0.9411764705882354, 0.9843137255, 0.9960784314, + 0.5450980392, 0.9450980392156864, 0.9843137255, 0.9960784314, 0.5803921569, + 0.9490196078431372, 0.9843137255, 0.9960784314, 0.6117647059, 0.9529411764705882, + 0.9843137255, 0.9960784314, 0.6431372549, 0.9568627450980394, 0.9882352941, 0.9960784314, + 0.6745098039, 0.9607843137254903, 0.9882352941, 0.9960784314, 0.7058823529, + 0.9647058823529413, 0.9882352941, 0.9960784314, 0.7411764706, 0.9686274509803922, + 0.9882352941, 0.9960784314, 0.768627451, 0.9725490196078431, 0.9882352941, 0.9960784314, 0.8, + 0.9764705882352941, 0.9921568627, 0.9960784314, 0.831372549, 0.9803921568627451, 0.9921568627, + 0.9960784314, 0.8666666667, 0.984313725490196, 0.9921568627, 0.9960784314, 0.8980392157, + 0.9882352941176471, 0.9921568627, 0.9960784314, 0.9294117647, 0.9921568627450981, + 0.9921568627, 0.9960784314, 0.9607843137, 0.996078431372549, 0.9960784314, 0.9960784314, + 0.9607843137, 1.0, 0.9960784314, 0.9960784314, 0.9607843137, + ], + description: 'Siemens', + }, +]; + +export { colormaps }; diff --git a/extensions/cornerstone/src/utils/dicomLoaderService.js b/extensions/cornerstone/src/utils/dicomLoaderService.js new file mode 100644 index 0000000..ba43213 --- /dev/null +++ b/extensions/cornerstone/src/utils/dicomLoaderService.js @@ -0,0 +1,225 @@ +import { imageLoader } from '@cornerstonejs/core'; +import dicomImageLoader from '@cornerstonejs/dicom-image-loader'; +import { api } from 'dicomweb-client'; +import { DICOMWeb, errorHandler } from '@ohif/core'; + +const getImageId = imageObj => { + if (!imageObj) { + return; + } + + return typeof imageObj.getImageId === 'function' ? imageObj.getImageId() : imageObj.url; +}; + +const findImageIdOnStudies = (studies, displaySetInstanceUID) => { + const study = studies.find(study => { + const displaySet = study.displaySets.some( + displaySet => displaySet.displaySetInstanceUID === displaySetInstanceUID + ); + return displaySet; + }); + const { series = [] } = study; + const { instances = [] } = series[0] || {}; + const instance = instances[0]; + + return getImageId(instance); +}; + +const someInvalidStrings = strings => { + const stringsArray = Array.isArray(strings) ? strings : [strings]; + const emptyString = string => !string; + let invalid = stringsArray.some(emptyString); + return invalid; +}; + +const getImageInstance = dataset => { + return dataset && dataset.images && dataset.images[0]; +}; + +const getNonImageInstance = dataset => { + return dataset && dataset.instance; +}; + +const getImageInstanceId = imageInstance => { + return getImageId(imageInstance); +}; + +const fetchIt = (url, headers = DICOMWeb.getAuthorizationHeader()) => { + return fetch(url, headers).then(response => response.arrayBuffer()); +}; + +const cornerstoneRetriever = imageId => { + return imageLoader.loadAndCacheImage(imageId).then(image => { + return image && image.data && image.data.byteArray.buffer; + }); +}; + +const wadorsRetriever = ( + url, + studyInstanceUID, + seriesInstanceUID, + sopInstanceUID, + headers = DICOMWeb.getAuthorizationHeader(), + errorInterceptor = errorHandler.getHTTPErrorHandler() +) => { + const config = { + url, + headers, + errorInterceptor, + }; + const dicomWeb = new api.DICOMwebClient(config); + + return dicomWeb.retrieveInstance({ + studyInstanceUID, + seriesInstanceUID, + sopInstanceUID, + }); +}; + +const getImageLoaderType = imageId => { + const loaderRegExp = /^\w+\:/; + const loaderType = loaderRegExp.exec(imageId); + + return ( + (loaderRegExp.lastIndex === 0 && + loaderType && + loaderType[0] && + loaderType[0].replace(':', '')) || + '' + ); +}; + +class DicomLoaderService { + getLocalData(dataset, studies) { + // Use referenced imageInstance + const imageInstance = getImageInstance(dataset); + const nonImageInstance = getNonImageInstance(dataset); + + if ( + (!imageInstance && !nonImageInstance) || + !nonImageInstance.imageId?.startsWith('dicomfile') + ) { + return; + } + + const instance = imageInstance || nonImageInstance; + + let imageId = getImageInstanceId(instance); + + // or Try to get it from studies + if (someInvalidStrings(imageId)) { + imageId = findImageIdOnStudies(studies, dataset.displaySetInstanceUID); + } + + if (!someInvalidStrings(imageId)) { + return dicomImageLoader.wadouri.loadFileRequest(imageId); + } + } + + getDataByImageType(dataset) { + const imageInstance = getImageInstance(dataset); + + if (imageInstance) { + const imageId = getImageInstanceId(imageInstance); + let getDicomDataMethod = fetchIt; + const loaderType = getImageLoaderType(imageId); + + switch (loaderType) { + case 'dicomfile': + getDicomDataMethod = cornerstoneRetriever.bind(this, imageId); + break; + case 'wadors': + const url = imageInstance.getData().wadoRoot; + const studyInstanceUID = imageInstance.getStudyInstanceUID(); + const seriesInstanceUID = imageInstance.getSeriesInstanceUID(); + const sopInstanceUID = imageInstance.getSOPInstanceUID(); + const invalidParams = someInvalidStrings([ + url, + studyInstanceUID, + seriesInstanceUID, + sopInstanceUID, + ]); + if (invalidParams) { + return; + } + + getDicomDataMethod = wadorsRetriever.bind( + this, + url, + studyInstanceUID, + seriesInstanceUID, + sopInstanceUID + ); + break; + case 'wadouri': + // Strip out the image loader specifier + imageId = imageId.substring(imageId.indexOf(':') + 1); + + if (someInvalidStrings(imageId)) { + return; + } + getDicomDataMethod = fetchIt.bind(this, imageId); + break; + default: + return; + } + + return getDicomDataMethod(); + } + } + + getDataByDatasetType(dataset) { + const { + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + authorizationHeaders, + wadoRoot, + wadoUri, + instance, + } = dataset; + // Retrieve wadors or just try to fetch wadouri + if (!someInvalidStrings(wadoRoot)) { + return wadorsRetriever( + wadoRoot, + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + authorizationHeaders + ); + } else if (!someInvalidStrings(wadoUri)) { + return fetchIt(wadoUri, { headers: authorizationHeaders }); + } else if (!someInvalidStrings(instance?.url)) { + // make sure the url is absolute, remove the scope + // from it if it is not absolute. For instance it might be dicomweb:http://.... + // and we need to remove the dicomweb: part + const url = instance.url; + const absoluteUrl = url.startsWith('http') ? url : url.substring(url.indexOf(':') + 1); + return fetchIt(absoluteUrl, { headers: authorizationHeaders }); + } + } + + *getLoaderIterator(dataset, studies, headers) { + yield this.getLocalData(dataset, studies); + yield this.getDataByImageType(dataset); + yield this.getDataByDatasetType(dataset); + } + + findDicomDataPromise(dataset, studies, headers) { + dataset.authorizationHeaders = headers; + const loaderIterator = this.getLoaderIterator(dataset, studies); + // it returns first valid retriever method. + for (const loader of loaderIterator) { + if (loader) { + return loader; + } + } + + // in case of no valid loader + throw new Error('Invalid dicom data loader'); + } +} + +const dicomLoaderService = new DicomLoaderService(); + +export default dicomLoaderService; diff --git a/extensions/cornerstone/src/utils/findNearbyToolData.ts b/extensions/cornerstone/src/utils/findNearbyToolData.ts new file mode 100644 index 0000000..95e9347 --- /dev/null +++ b/extensions/cornerstone/src/utils/findNearbyToolData.ts @@ -0,0 +1,21 @@ +/** + * Finds tool nearby event position triggered. + * + * @param {Object} commandsManager mannager of commands + * @param {Object} event that has being triggered + * @returns cs toolData or undefined if not found. + */ +export const findNearbyToolData = (commandsManager, evt) => { + if (!evt?.detail) { + return; + } + const { element, currentPoints } = evt.detail; + return commandsManager.runCommand( + 'getNearbyAnnotation', + { + element, + canvasCoordinates: currentPoints?.canvas, + }, + 'CORNERSTONE' + ); +}; diff --git a/extensions/cornerstone/src/utils/getActiveViewportEnabledElement.ts b/extensions/cornerstone/src/utils/getActiveViewportEnabledElement.ts new file mode 100644 index 0000000..c8f4cf3 --- /dev/null +++ b/extensions/cornerstone/src/utils/getActiveViewportEnabledElement.ts @@ -0,0 +1,11 @@ +import { getEnabledElement } from '@cornerstonejs/core'; +import { IEnabledElement } from '@cornerstonejs/core/types'; + +import { getEnabledElement as OHIFgetEnabledElement } from '../state'; + +export default function getActiveViewportEnabledElement(viewportGridService): IEnabledElement { + const { activeViewportId } = viewportGridService.getState(); + const { element } = OHIFgetEnabledElement(activeViewportId) || {}; + const enabledElement = getEnabledElement(element); + return enabledElement; +} diff --git a/extensions/cornerstone/src/utils/getCornerstoneBlendMode.ts b/extensions/cornerstone/src/utils/getCornerstoneBlendMode.ts new file mode 100644 index 0000000..11ff710 --- /dev/null +++ b/extensions/cornerstone/src/utils/getCornerstoneBlendMode.ts @@ -0,0 +1,25 @@ +import { Enums } from '@cornerstonejs/core'; + +const MIP = 'mip'; +const MINIP = 'minip'; +const AVG = 'avg'; + +export default function getCornerstoneBlendMode(blendMode: string): Enums.BlendModes { + if (!blendMode) { + return Enums.BlendModes.COMPOSITE; + } + + if (blendMode.toLowerCase() === MIP) { + return Enums.BlendModes.MAXIMUM_INTENSITY_BLEND; + } + + if (blendMode.toLowerCase() === MINIP) { + return Enums.BlendModes.MINIMUM_INTENSITY_BLEND; + } + + if (blendMode.toLowerCase() === AVG) { + return Enums.BlendModes.AVERAGE_INTENSITY_BLEND; + } + + throw new Error(`Unsupported blend mode: ${blendMode}`); +} diff --git a/extensions/cornerstone/src/utils/getCornerstoneOrientation.ts b/extensions/cornerstone/src/utils/getCornerstoneOrientation.ts new file mode 100644 index 0000000..6e47fe8 --- /dev/null +++ b/extensions/cornerstone/src/utils/getCornerstoneOrientation.ts @@ -0,0 +1,22 @@ +import { Enums } from '@cornerstonejs/core'; + +const AXIAL = 'axial'; +const SAGITTAL = 'sagittal'; +const CORONAL = 'coronal'; + +export default function getCornerstoneOrientation(orientation: string): Enums.OrientationAxis { + if (orientation) { + switch (orientation.toLowerCase()) { + case AXIAL: + return Enums.OrientationAxis.AXIAL; + case SAGITTAL: + return Enums.OrientationAxis.SAGITTAL; + case CORONAL: + return Enums.OrientationAxis.CORONAL; + default: + return Enums.OrientationAxis.ACQUISITION; + } + } + + return Enums.OrientationAxis.ACQUISITION; +} diff --git a/extensions/cornerstone/src/utils/getCornerstoneViewportType.ts b/extensions/cornerstone/src/utils/getCornerstoneViewportType.ts new file mode 100644 index 0000000..d704101 --- /dev/null +++ b/extensions/cornerstone/src/utils/getCornerstoneViewportType.ts @@ -0,0 +1,39 @@ +import type { Types } from '@ohif/core'; +import { Enums } from '@cornerstonejs/core'; + +const STACK = 'stack'; +const VOLUME = 'volume'; +const ORTHOGRAPHIC = 'orthographic'; +const VOLUME_3D = 'volume3d'; +const VIDEO = 'video'; +const WHOLESLIDE = 'wholeslide'; + +export default function getCornerstoneViewportType( + viewportType: string, + displaySets?: Types.DisplaySet[] +): Enums.ViewportType { + const lowerViewportType = + displaySets?.[0]?.viewportType?.toLowerCase() || viewportType.toLowerCase(); + if (lowerViewportType === STACK) { + return Enums.ViewportType.STACK; + } + + if (lowerViewportType === VIDEO) { + return Enums.ViewportType.VIDEO; + } + if (lowerViewportType === WHOLESLIDE) { + return Enums.ViewportType.WHOLE_SLIDE; + } + + if (lowerViewportType === VOLUME || lowerViewportType === ORTHOGRAPHIC) { + return Enums.ViewportType.ORTHOGRAPHIC; + } + + if (lowerViewportType === VOLUME_3D) { + return Enums.ViewportType.VOLUME_3D; + } + + throw new Error( + `Invalid viewport type: ${viewportType}. Valid types are: stack, volume, video, wholeslide` + ); +} diff --git a/extensions/cornerstone/src/utils/getInterleavedFrames.js b/extensions/cornerstone/src/utils/getInterleavedFrames.js new file mode 100644 index 0000000..b380a93 --- /dev/null +++ b/extensions/cornerstone/src/utils/getInterleavedFrames.js @@ -0,0 +1,60 @@ +export default function getInterleavedFrames(imageIds) { + const minImageIdIndex = 0; + const maxImageIdIndex = imageIds.length - 1; + + const middleImageIdIndex = Math.floor(imageIds.length / 2); + + let lowerImageIdIndex = middleImageIdIndex; + let upperImageIdIndex = middleImageIdIndex; + + // Build up an array of images to prefetch, starting with the current image. + const imageIdsToPrefetch = [ + { imageId: imageIds[middleImageIdIndex], imageIdIndex: middleImageIdIndex }, + ]; + + const prefetchQueuedFilled = { + currentPositionDownToMinimum: false, + currentPositionUpToMaximum: false, + }; + + // Check if on edges and some criteria is already fulfilled + + if (middleImageIdIndex === minImageIdIndex) { + prefetchQueuedFilled.currentPositionDownToMinimum = true; + } else if (middleImageIdIndex === maxImageIdIndex) { + prefetchQueuedFilled.currentPositionUpToMaximum = true; + } + + while ( + !prefetchQueuedFilled.currentPositionDownToMinimum || + !prefetchQueuedFilled.currentPositionUpToMaximum + ) { + if (!prefetchQueuedFilled.currentPositionDownToMinimum) { + // Add imageId below + lowerImageIdIndex--; + imageIdsToPrefetch.push({ + imageId: imageIds[lowerImageIdIndex], + imageIdIndex: lowerImageIdIndex, + }); + + if (lowerImageIdIndex === minImageIdIndex) { + prefetchQueuedFilled.currentPositionDownToMinimum = true; + } + } + + if (!prefetchQueuedFilled.currentPositionUpToMaximum) { + // Add imageId above + upperImageIdIndex++; + imageIdsToPrefetch.push({ + imageId: imageIds[upperImageIdIndex], + imageIdIndex: upperImageIdIndex, + }); + + if (upperImageIdIndex === maxImageIdIndex) { + prefetchQueuedFilled.currentPositionUpToMaximum = true; + } + } + } + + return imageIdsToPrefetch; +} diff --git a/extensions/cornerstone/src/utils/getNthFrames.js b/extensions/cornerstone/src/utils/getNthFrames.js new file mode 100644 index 0000000..38df66d --- /dev/null +++ b/extensions/cornerstone/src/utils/getNthFrames.js @@ -0,0 +1,34 @@ +/** + * Returns a re-ordered array consisting of, in order: + * 1. First few objects + * 2. Center objects + * 3. Last few objects + * 4. nth Objects (n=7), set 2 + * 5. nth Objects set 5, + * 6. Remaining objects + * What this does is return the first/center/start objects, as those + * are often used first, then a selection of objects scattered over the + * instances in order to allow making requests over a set of image instances. + * + * @param {[]} imageIds + * @returns [] reordered to be an nth selection + */ +export default function getNthFrames(imageIds) { + const frames = [[], [], [], [], []]; + const centerStart = imageIds.length / 2 - 3; + const centerEnd = centerStart + 6; + + for (let i = 0; i < imageIds.length; i++) { + if (i < 2 || i > imageIds.length - 4 || (i > centerStart && i < centerEnd)) { + frames[0].push(imageIds[i]); + } else if (i % 7 === 2) { + frames[1].push(imageIds[i]); + } else if (i % 7 === 5) { + frames[2].push(imageIds[i]); + } else { + frames[(i % 2) + 3].push(imageIds[i]); + } + } + const ret = [...frames[0], ...frames[1], ...frames[2], ...frames[3], ...frames[4]]; + return ret; +} diff --git a/extensions/cornerstone/src/utils/getViewportOrientationFromImageOrientationPatient.ts b/extensions/cornerstone/src/utils/getViewportOrientationFromImageOrientationPatient.ts new file mode 100644 index 0000000..583b972 --- /dev/null +++ b/extensions/cornerstone/src/utils/getViewportOrientationFromImageOrientationPatient.ts @@ -0,0 +1,56 @@ +import { CONSTANTS, utilities } from '@cornerstonejs/core'; + +const { MPR_CAMERA_VALUES } = CONSTANTS; + +/** + * Determines the viewport orientation (axial, sagittal, or coronal) based on the image orientation patient values. + * This is done by comparing the view vectors with predefined MPR camera values. + * + * @param imageOrientationPatient - Array of 6 numbers representing the image orientation patient values. + * The first 3 numbers represent the direction cosines of the first row and the second 3 numbers + * represent the direction cosines of the first column. + * + * @returns The viewport orientation as a string ('axial', 'sagittal', 'coronal') or undefined if + * the orientation cannot be determined or if the input is invalid. + * + * @example + * ```typescript + * const orientation = getViewportOrientationFromImageOrientationPatient([1,0,0,0,1,0]); + * console.debug(orientation); // 'axial' + * ``` + */ +export const getViewportOrientationFromImageOrientationPatient = ( + imageOrientationPatient: number[] +): string | undefined => { + if (!imageOrientationPatient || imageOrientationPatient.length !== 6) { + return undefined; + } + + const viewRight = imageOrientationPatient.slice(0, 3); + const viewDown = imageOrientationPatient.slice(3, 6); + const viewUp = [-viewDown[0], -viewDown[1], -viewDown[2]]; + + // Compare vectors with MPR camera values using utilities.isEqual + if ( + utilities.isEqual(viewRight, MPR_CAMERA_VALUES.axial.viewRight) && + utilities.isEqual(viewUp, MPR_CAMERA_VALUES.axial.viewUp) + ) { + return 'axial'; + } + + if ( + utilities.isEqual(viewRight, MPR_CAMERA_VALUES.sagittal.viewRight) && + utilities.isEqual(viewUp, MPR_CAMERA_VALUES.sagittal.viewUp) + ) { + return 'sagittal'; + } + + if ( + utilities.isEqual(viewRight, MPR_CAMERA_VALUES.coronal.viewRight) && + utilities.isEqual(viewUp, MPR_CAMERA_VALUES.coronal.viewUp) + ) { + return 'coronal'; + } + + return undefined; +}; diff --git a/extensions/cornerstone/src/utils/imageSliceSync/calculateViewportRegistrations.ts b/extensions/cornerstone/src/utils/imageSliceSync/calculateViewportRegistrations.ts new file mode 100644 index 0000000..aae2882 --- /dev/null +++ b/extensions/cornerstone/src/utils/imageSliceSync/calculateViewportRegistrations.ts @@ -0,0 +1,28 @@ +import { Types, getRenderingEngine, utilities } from '@cornerstonejs/core'; + +export default function calculateViewportRegistrations(viewports: Types.IViewportId[]) { + const viewportPairs = _getViewportPairs(viewports); + + for (const [viewport, nextViewport] of viewportPairs) { + // check if they are in the same Frame of Reference + const renderingEngine1 = getRenderingEngine(viewport.renderingEngineId); + const renderingEngine2 = getRenderingEngine(nextViewport.renderingEngineId); + + const csViewport1 = renderingEngine1.getViewport(viewport.viewportId); + const csViewport2 = renderingEngine2.getViewport(nextViewport.viewportId); + + utilities.calculateViewportsSpatialRegistration(csViewport1, csViewport2); + } +} + +const _getViewportPairs = (viewports: Types.IViewportId[]) => { + const viewportPairs = []; + + for (let i = 0; i < viewports.length; i++) { + for (let j = i + 1; j < viewports.length; j++) { + viewportPairs.push([viewports[i], viewports[j]]); + } + } + + return viewportPairs; +}; diff --git a/extensions/cornerstone/src/utils/imageSliceSync/toggleImageSliceSync.ts b/extensions/cornerstone/src/utils/imageSliceSync/toggleImageSliceSync.ts new file mode 100644 index 0000000..1ba3931 --- /dev/null +++ b/extensions/cornerstone/src/utils/imageSliceSync/toggleImageSliceSync.ts @@ -0,0 +1,101 @@ +import { DisplaySetService, ViewportGridService } from '@ohif/core'; + +const IMAGE_SLICE_SYNC_NAME = 'IMAGE_SLICE_SYNC'; + +export default function toggleImageSliceSync({ + servicesManager, + viewports: providedViewports, + syncId, +}: withAppTypes) { + const { syncGroupService, viewportGridService, displaySetService, cornerstoneViewportService } = + servicesManager.services; + + syncId ||= IMAGE_SLICE_SYNC_NAME; + + const viewports = + providedViewports || getReconstructableStackViewports(viewportGridService, displaySetService); + + // Todo: right now we don't have a proper way to define specific + // viewports to add to synchronizers, and right now it is global or not + // after we do that, we should do fine grained control of the synchronizers + const someViewportHasSync = viewports.some(viewport => { + const syncStates = syncGroupService.getSynchronizersForViewport( + viewport.viewportOptions.viewportId + ); + + const imageSync = syncStates.find(syncState => syncState.id === syncId); + + return !!imageSync; + }); + + if (someViewportHasSync) { + return disableSync(syncId, servicesManager); + } + + // create synchronization group and add the viewports to it. + viewports.forEach(gridViewport => { + const { viewportId } = gridViewport.viewportOptions; + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + if (!viewport) { + return; + } + syncGroupService.addViewportToSyncGroup(viewportId, viewport.getRenderingEngine().id, { + type: 'imageSlice', + id: syncId, + source: true, + target: true, + }); + }); +} + +function disableSync(syncName, servicesManager: AppTypes.ServicesManager) { + const { syncGroupService, viewportGridService, displaySetService, cornerstoneViewportService } = + servicesManager.services; + const viewports = getReconstructableStackViewports(viewportGridService, displaySetService); + viewports.forEach(gridViewport => { + const { viewportId } = gridViewport.viewportOptions; + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + if (!viewport) { + return; + } + syncGroupService.removeViewportFromSyncGroup( + viewport.id, + viewport.getRenderingEngine().id, + syncName + ); + }); +} + +/** + * Gets the consistent spacing stack viewport types, which are the ones which + * can be navigated using the stack image sync right now. + */ +function getReconstructableStackViewports( + viewportGridService: ViewportGridService, + displaySetService: DisplaySetService +) { + let { viewports } = viewportGridService.getState(); + + viewports = [...viewports.values()]; + // filter empty viewports + viewports = viewports.filter( + viewport => viewport.displaySetInstanceUIDs && viewport.displaySetInstanceUIDs.length + ); + + // filter reconstructable viewports + viewports = viewports.filter(viewport => { + const { displaySetInstanceUIDs } = viewport; + + for (const displaySetInstanceUID of displaySetInstanceUIDs) { + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + + // TODO - add a better test than isReconstructable + if (displaySet && displaySet.isReconstructable) { + return true; + } + + return false; + } + }); + return viewports; +} diff --git a/extensions/cornerstone/src/utils/index.ts b/extensions/cornerstone/src/utils/index.ts new file mode 100644 index 0000000..4df1f24 --- /dev/null +++ b/extensions/cornerstone/src/utils/index.ts @@ -0,0 +1,7 @@ +import { handleSegmentChange } from './segmentUtils'; + +const utils = { + handleSegmentChange, +}; + +export default utils; diff --git a/extensions/cornerstone/src/utils/initViewTiming.ts b/extensions/cornerstone/src/utils/initViewTiming.ts new file mode 100644 index 0000000..a9d1544 --- /dev/null +++ b/extensions/cornerstone/src/utils/initViewTiming.ts @@ -0,0 +1,52 @@ +import { log, Enums } from '@ohif/core'; +import { EVENTS } from '@cornerstonejs/core'; + +const IMAGE_TIMING_KEYS = []; + +const imageTiming = { + viewportsWaiting: 0, +}; + +/** + * Defines the initial view timing reporting. + * This allows knowing how many viewports are waiting for initial views and + * when the IMAGE_RENDERED gets sent out. + * The first image rendered will fire the FIRST_IMAGE timeEnd logs, while + * the last of the enabled viewport will fire the ALL_IMAGES timeEnd logs. + * + */ + +export default function initViewTiming({ element }) { + if (!IMAGE_TIMING_KEYS.length) { + // Work around a bug in WebPack that doesn't getting the enums initialized + // quite fast enough to be declared statically. + const { TimingEnum } = Enums; + + IMAGE_TIMING_KEYS.push( + TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES, + TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE, + TimingEnum.STUDY_TO_FIRST_IMAGE, + ); + } + + if (!IMAGE_TIMING_KEYS.find(key => log.timingKeys[key])) { + return; + } + imageTiming.viewportsWaiting += 1; + element.addEventListener(EVENTS.IMAGE_RENDERED, imageRenderedListener); +} + +function imageRenderedListener(evt) { + if (evt.detail.viewportStatus === 'preRender') { + return; + } + const { TimingEnum } = Enums; + log.timeEnd(TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE); + log.timeEnd(TimingEnum.STUDY_TO_FIRST_IMAGE); + log.timeEnd(TimingEnum.SCRIPT_TO_VIEW); + imageTiming.viewportsWaiting -= 1; + evt.detail.element.removeEventListener(EVENTS.IMAGE_RENDERED, imageRenderedListener); + if (!imageTiming.viewportsWaiting) { + log.timeEnd(TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES); + } +} diff --git a/extensions/cornerstone/src/utils/interleave.js b/extensions/cornerstone/src/utils/interleave.js new file mode 100644 index 0000000..a21d3fb --- /dev/null +++ b/extensions/cornerstone/src/utils/interleave.js @@ -0,0 +1,30 @@ +/** + * Interleave the items from all the lists so that the first items are first + * in the returned list, the second items are next etc. + * Does this in a O(n) fashion, and return lists[0] if there is only one list. + * + * @param {[]} lists + * @returns [] reordered to be breadth first traversal of lists + */ +export default function interleave(lists) { + if (!lists || !lists.length) { + return []; + } + if (lists.length === 1) { + return lists[0]; + } + console.time('interleave'); + const useLists = [...lists]; + const ret = []; + for (let i = 0; useLists.length > 0; i++) { + for (const list of useLists) { + if (i >= list.length) { + useLists.splice(useLists.indexOf(list), 1); + continue; + } + ret.push(list[i]); + } + } + console.timeEnd('interleave'); + return ret; +} diff --git a/extensions/cornerstone/src/utils/interleaveCenterLoader.ts b/extensions/cornerstone/src/utils/interleaveCenterLoader.ts new file mode 100644 index 0000000..413a6a8 --- /dev/null +++ b/extensions/cornerstone/src/utils/interleaveCenterLoader.ts @@ -0,0 +1,143 @@ +import { cache, imageLoadPoolManager, Enums } from '@cornerstonejs/core'; +import getInterleavedFrames from './getInterleavedFrames'; +import zip from 'lodash.zip'; +import compact from 'lodash.compact'; +import flatten from 'lodash.flatten'; + +// Map of volumeId and SeriesInstanceId +const volumeIdMapsToLoad = new Map(); +const viewportIdVolumeInputArrayMap = new Map(); + +/** + * This function caches the volumeUIDs until all the volumes inside the + * hanging protocol are initialized. Then it goes through the imageIds + * of the volumes, and interleave them, in order for the volumes to be loaded + * together from middle to the start and the end. + * @param {Object} props image loading properties from Cornerstone ViewportService + * @returns + */ +export default function interleaveCenterLoader({ + data: { viewportId, volumeInputArray }, + displaySetsMatchDetails, + viewportMatchDetails: matchDetails, +}) { + viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray); + + // Based on the volumeInputs store the volumeIds and SeriesInstanceIds + // to keep track of the volumes being loaded + for (const volumeInput of volumeInputArray) { + const { volumeId } = volumeInput; + const volume = cache.getVolume(volumeId); + + if (!volume) { + return; + } + + // if the volumeUID is not in the volumeUIDs array, add it + if (!volumeIdMapsToLoad.has(volumeId)) { + const { metadata } = volume; + volumeIdMapsToLoad.set(volumeId, metadata.SeriesInstanceUID); + } + } + + /** + * The following is checking if all the viewports that were matched in the HP has been + * successfully created their cornerstone viewport or not. Todo: This can be + * improved by not checking it, and as soon as the matched DisplaySets have their + * volume loaded, we start the loading, but that comes at the cost of viewports + * not being created yet (e.g., in a 10 viewport ptCT fusion, when one ct viewport and one + * pt viewport are created we have a guarantee that the volumes are created in the cache + * but the rest of the viewports (fusion, mip etc.) are not created yet. So + * we can't initiate setting the volumes for those viewports. One solution can be + * to add an event when a viewport is created (not enabled element event) and then + * listen to it and as the other viewports are created we can set the volumes for them + * since volumes are already started loading. + */ + const uniqueViewportVolumeDisplaySetUIDs = new Set(); + viewportIdVolumeInputArrayMap.forEach((volumeInputArray, viewportId) => { + volumeInputArray.forEach(volumeInput => { + const { volumeId } = volumeInput; + uniqueViewportVolumeDisplaySetUIDs.add(volumeId); + }); + }); + + const uniqueMatchedDisplaySetUIDs = new Set(); + + matchDetails.forEach(matchDetail => { + const { displaySetsInfo } = matchDetail; + displaySetsInfo.forEach(({ displaySetInstanceUID }) => { + uniqueMatchedDisplaySetUIDs.add(displaySetInstanceUID); + }); + }); + + if (uniqueViewportVolumeDisplaySetUIDs.size !== uniqueMatchedDisplaySetUIDs.size) { + return; + } + + const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice(); + // get volumes from cache + const volumes = volumeIds.map(volumeId => { + return cache.getVolume(volumeId); + }); + + // iterate over all volumes, and get their imageIds, and interleave + // the imageIds and save them in AllRequests for later use + const AllRequests = []; + volumes.forEach(volume => { + const requests = volume.getImageLoadRequests(); + + if (!requests.length || !requests[0] || !requests[0].imageId) { + return; + } + + const requestImageIds = requests.map(request => { + return request.imageId; + }); + + const imageIds = getInterleavedFrames(requestImageIds); + + const reOrderedRequests = imageIds.map(({ imageId }) => { + const request = requests.find(req => req.imageId === imageId); + return request; + }); + + AllRequests.push(reOrderedRequests); + }); + + // flatten the AllRequests array, which will result in a list of all the + // imageIds for all the volumes but interleaved + const interleavedRequests = compact(flatten(zip(...AllRequests))); + + // set the finalRequests to the imageLoadPoolManager + const finalRequests = []; + interleavedRequests.forEach(request => { + const { imageId } = request; + + AllRequests.forEach(volumeRequests => { + const volumeImageIdRequest = volumeRequests.find(req => req.imageId === imageId); + if (volumeImageIdRequest) { + finalRequests.push(volumeImageIdRequest); + } + }); + }); + + const requestType = Enums.RequestType.Prefetch; + const priority = 0; + + finalRequests.forEach(({ callLoadImage, additionalDetails, imageId, imageIdIndex, options }) => { + const callLoadImageBound = callLoadImage.bind(null, imageId, imageIdIndex, options); + + imageLoadPoolManager.addRequest(callLoadImageBound, requestType, additionalDetails, priority); + }); + + // clear the volumeIdMapsToLoad + volumeIdMapsToLoad.clear(); + + // copy the viewportIdVolumeInputArrayMap + const viewportIdVolumeInputArrayMapCopy = new Map(viewportIdVolumeInputArrayMap); + + // reset the viewportIdVolumeInputArrayMap + viewportIdVolumeInputArrayMap.clear(); + + return viewportIdVolumeInputArrayMapCopy; +} diff --git a/extensions/cornerstone/src/utils/interleaveTopToBottom.ts b/extensions/cornerstone/src/utils/interleaveTopToBottom.ts new file mode 100644 index 0000000..8ef729b --- /dev/null +++ b/extensions/cornerstone/src/utils/interleaveTopToBottom.ts @@ -0,0 +1,157 @@ +import { cache, imageLoadPoolManager, Enums } from '@cornerstonejs/core'; +import zip from 'lodash.zip'; +import compact from 'lodash.compact'; +import flatten from 'lodash.flatten'; + +// Map of volumeId and SeriesInstanceId +const volumeIdMapsToLoad = new Map(); +const viewportIdVolumeInputArrayMap = new Map(); + +/** + * This function caches the volumeIds until all the volumes inside the + * hanging protocol are initialized. Then it goes through the imageIds + * of the volumes, and interleave them, in order for the volumes to be loaded + * together from middle to the start and the end. + * @param {Object} {viewportData, displaySetMatchDetails} + * @returns + */ +export default function interleaveTopToBottom({ + data: { viewportId, volumeInputArray }, + displaySetsMatchDetails, + viewportMatchDetails: matchDetails, +}) { + viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray); + + // Based on the volumeInputs store the volumeIds and SeriesInstanceIds + // to keep track of the volumes being loaded + for (const volumeInput of volumeInputArray) { + const { volumeId } = volumeInput; + const volume = cache.getVolume(volumeId); + + if (!volume) { + return; + } + + // if the volumeUID is not in the volumeUIDs array, add it + if (!volumeIdMapsToLoad.has(volumeId)) { + const { metadata } = volume; + volumeIdMapsToLoad.set(volumeId, metadata.SeriesInstanceUID); + } + } + + const filteredMatchDetails = []; + const displaySetsToLoad = new Set(); + + // Check all viewports that have a displaySet to be loaded. In some cases + // (eg: line chart viewports which is not a Cornerstone viewport) the + // displaySet is created on the client and there are no instances to be + // downloaded. For those viewports the displaySet may have the `skipLoading` + // option set to true otherwise it may block the download of all other + // instances resulting in blank viewports. + Array.from(matchDetails.values()).forEach(curMatchDetails => { + const { displaySetsInfo } = curMatchDetails; + let numDisplaySetsToLoad = 0; + + displaySetsInfo.forEach(({ displaySetInstanceUID, displaySetOptions }) => { + if (!displaySetOptions?.options?.skipLoading) { + numDisplaySetsToLoad++; + displaySetsToLoad.add(displaySetInstanceUID); + } + }); + + if (numDisplaySetsToLoad) { + filteredMatchDetails.push(curMatchDetails); + } + }); + + /** + * The following is checking if all the viewports that were matched in the HP has been + * successfully created their cornerstone viewport or not. Todo: This can be + * improved by not checking it, and as soon as the matched DisplaySets have their + * volume loaded, we start the loading, but that comes at the cost of viewports + * not being created yet (e.g., in a 10 viewport ptCT fusion, when one ct viewport and one + * pt viewport are created we have a guarantee that the volumes are created in the cache + * but the rest of the viewports (fusion, mip etc.) are not created yet. So + * we can't initiate setting the volumes for those viewports. One solution can be + * to add an event when a viewport is created (not enabled element event) and then + * listen to it and as the other viewports are created we can set the volumes for them + * since volumes are already started loading. + */ + const uniqueViewportVolumeDisplaySetUIDs = new Set(); + viewportIdVolumeInputArrayMap.forEach((volumeInputArray, viewportId) => { + volumeInputArray.forEach(volumeInput => { + const { volumeId } = volumeInput; + uniqueViewportVolumeDisplaySetUIDs.add(volumeId); + }); + }); + + const uniqueMatchedDisplaySetUIDs = new Set(); + + matchDetails.forEach(matchDetail => { + const { displaySetsInfo } = matchDetail; + displaySetsInfo.forEach(({ displaySetInstanceUID }) => { + uniqueMatchedDisplaySetUIDs.add(displaySetInstanceUID); + }); + }); + + if (uniqueViewportVolumeDisplaySetUIDs.size !== uniqueMatchedDisplaySetUIDs.size) { + return; + } + + const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice(); + // get volumes from cache + const volumes = volumeIds.map(volumeId => { + return cache.getVolume(volumeId); + }); + + // iterate over all volumes, and get their imageIds, and interleave + // the imageIds and save them in AllRequests for later use + const AllRequests = []; + volumes.forEach(volume => { + const requests = volume.getImageLoadRequests(); + + if (!requests?.[0]?.imageId) { + return; + } + + // reverse the requests + AllRequests.push(requests.reverse()); + }); + + // flatten the AllRequests array, which will result in a list of all the + // imageIds for all the volumes but interleaved + const interleavedRequests = compact(flatten(zip(...AllRequests))); + + // set the finalRequests to the imageLoadPoolManager + const finalRequests = []; + interleavedRequests.forEach(request => { + const { imageId } = request; + + AllRequests.forEach(volumeRequests => { + const volumeImageIdRequest = volumeRequests.find(req => req.imageId === imageId); + if (volumeImageIdRequest) { + finalRequests.push(volumeImageIdRequest); + } + }); + }); + + const requestType = Enums.RequestType.Prefetch; + const priority = 0; + + finalRequests.forEach(({ callLoadImage, additionalDetails, imageId, imageIdIndex, options }) => { + const callLoadImageBound = callLoadImage.bind(null, imageId, imageIdIndex, options); + + imageLoadPoolManager.addRequest(callLoadImageBound, requestType, additionalDetails, priority); + }); + + // clear the volumeIdMapsToLoad + volumeIdMapsToLoad.clear(); + + // copy the viewportIdVolumeInputArrayMap + const viewportIdVolumeInputArrayMapCopy = new Map(viewportIdVolumeInputArrayMap); + + // reset the viewportIdVolumeInputArrayMap + viewportIdVolumeInputArrayMap.clear(); + + return viewportIdVolumeInputArrayMapCopy; +} diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Angle.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Angle.ts new file mode 100644 index 0000000..a21756c --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Angle.ts @@ -0,0 +1,200 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import { getDisplayUnit } from './utils'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { utils } from '@ohif/core'; + +const Angle = { + toAnnotation: measurement => {}, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} cornerstone Cornerstone event data + * @return {Measurement} Measurement instance + */ + toMeasurement: ( + csToolsEventDetail, + displaySetService, + CornerstoneViewportService, + getValueTypeFromToolType, + customizationService + ) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + if (!metadata || !data) { + console.warn('Length tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType) { + throw new Error('Tool not supported'); + } + + const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + } + + const { points, textBox } = data.handles; + + const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); + + const displayText = getDisplayText(mappedAnnotations, displaySet); + const getReport = () => + _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points, + textBox, + isLocked, + isVisible, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + frameNumber: mappedAnnotations?.[0]?.frameNumber || 1, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType(toolName), + getReport, + referencedImageId, + }; + }, +}; + +function getMappedAnnotations(annotation, displaySetService) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId } = metadata; + const targets = Object.keys(cachedStats); + + if (!targets.length) { + return; + } + + const annotations = []; + Object.keys(cachedStats).forEach(targetId => { + const targetStats = cachedStats[targetId]; + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + + const { SeriesNumber } = displaySet; + const { angle } = targetStats; + const unit = '\u00B0'; + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + unit, + angle, + }); + }); + + return annotations; +} + +/* +This function is used to convert the measurement data to a format that is +suitable for the report generation (e.g. for the csv report). The report +returns a list of columns and corresponding values. +*/ +function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService) { + const columns = []; + const values = []; + + // Add Type + columns.push('AnnotationType'); + values.push('Cornerstone:Angle'); + + mappedAnnotations.forEach(annotation => { + const { angle, unit } = annotation; + columns.push(`Angle (${unit})`); + values.push(angle); + }); + + if (FrameOfReferenceUID) { + columns.push('FrameOfReferenceUID'); + values.push(FrameOfReferenceUID); + } + + if (points) { + columns.push('points'); + // points has the form of [[x1, y1, z1], [x2, y2, z2], ...] + // convert it to string of [[x1 y1 z1];[x2 y2 z2];...] + // so that it can be used in the csv report + values.push(points.map(p => p.join(' ')).join(';')); + } + + return { + columns, + values, + }; +} + +function getDisplayText(mappedAnnotations, displaySet) { + const displayText = { + primary: [], + secondary: [], + }; + + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } + + // Area is the same for all series + const { angle, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; + + const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + if (angle === undefined) { + return displayText; + } + const roundedAngle = utils.roundNumber(angle, 2); + + displayText.primary.push(`${roundedAngle} ${getDisplayUnit(unit)}`); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); + + return displayText; +} + +export default Angle; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.ts new file mode 100644 index 0000000..edd5754 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.ts @@ -0,0 +1,175 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import { getIsLocked } from './utils/getIsLocked'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { getIsVisible } from './utils/getIsVisible'; +const Length = { + toAnnotation: measurement => {}, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} cornerstone Cornerstone event data + * @return {Measurement} Measurement instance + */ + toMeasurement: ( + csToolsEventDetail, + displaySetService, + cornerstoneViewportService, + getValueTypeFromToolType, + customizationService + ) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + if (!metadata || !data) { + console.warn('Length tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType) { + throw new Error('Tool not supported'); + } + + const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + } + + const { points, textBox } = data.handles; + + const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); + + const displayText = getDisplayText(mappedAnnotations, displaySet); + const getReport = () => _getReport(mappedAnnotations, points, FrameOfReferenceUID); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points, + textBox, + isLocked, + isVisible, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + referencedImageId, + frameNumber: mappedAnnotations[0]?.frameNumber || 1, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.text, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType(toolName), + getReport, + }; + }, +}; + +function getMappedAnnotations(annotation, displaySetService) { + const { metadata, data } = annotation; + const { text } = data; + const { referencedImageId } = metadata; + + const annotations = []; + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + + const { SeriesNumber } = displaySet; + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + text, + }); + + return annotations; +} + +function getDisplayText(mappedAnnotations, displaySet) { + const displayText = { + primary: [], + secondary: [], + }; + + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } + + const { SeriesNumber, SOPInstanceUID, frameNumber, text } = mappedAnnotations[0]; + + const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + + // Add the annotation text to the primary array + if (text) { + displayText.primary.push(text); + } + + // Add the series information to the secondary array + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); + + return displayText; +} + +function _getReport(mappedAnnotations, points, FrameOfReferenceUID) { + const columns = []; + const values = []; + + columns.push('AnnotationType'); + values.push('Cornerstone:ArrowAnnote'); + + mappedAnnotations.forEach(annotation => { + const { text } = annotation; + columns.push(`Text`); + values.push(text); + }); + + if (FrameOfReferenceUID) { + columns.push('FrameOfReferenceUID'); + values.push(FrameOfReferenceUID); + } + + if (points) { + columns.push('points'); + values.push(points.map(p => p.join(' ')).join(';')); + } + + return { + columns, + values, + }; +} +export default Length; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts new file mode 100644 index 0000000..241848b --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts @@ -0,0 +1,193 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { utils } from '@ohif/core'; +import { getDisplayUnit } from './utils'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; + +const Bidirectional = { + toAnnotation: measurement => {}, + toMeasurement: ( + csToolsEventDetail, + displaySetService, + cornerstoneViewportService, + getValueTypeFromToolType, + customizationService + ) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + + if (!metadata || !data) { + console.warn('Length tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType) { + throw new Error('Tool not supported'); + } + + const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + } + + const { points, textBox } = data.handles; + + const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); + + const displayText = getDisplayText(mappedAnnotations, displaySet); + const getReport = () => + _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points, + textBox, + isLocked, + isVisible, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + referencedImageId, + frameNumber: mappedAnnotations[0]?.frameNumber || 1, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType(toolName), + getReport, + }; + }, +}; + +function getMappedAnnotations(annotation, displaySetService) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId, referencedSeriesInstanceUID } = metadata; + const targets = Object.keys(cachedStats); + + if (!targets.length) { + return []; + } + + const annotations = []; + Object.keys(cachedStats).forEach(targetId => { + const targetStats = cachedStats[targetId]; + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + + const { SeriesNumber } = displaySet; + const { length, width, unit } = targetStats; + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + unit, + length, + width, + }); + }); + + return annotations; +} + +/* +This function is used to convert the measurement data to a format that is +suitable for the report generation (e.g. for the csv report). The report +returns a list of columns and corresponding values. +*/ +function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService) { + const columns = []; + const values = []; + + // Add Type + columns.push('AnnotationType'); + values.push('Cornerstone:Bidirectional'); + + mappedAnnotations.forEach(annotation => { + const { length, width, unit } = annotation; + columns.push(`Length`, `Width`, 'Unit'); + values.push(length, width, unit); + }); + + if (FrameOfReferenceUID) { + columns.push('FrameOfReferenceUID'); + values.push(FrameOfReferenceUID); + } + + if (points) { + columns.push('points'); + // points has the form of [[x1, y1, z1], [x2, y2, z2], ...] + // convert it to string of [[x1 y1 z1];[x2 y2 z2];...] + // so that it can be used in the csv report + values.push(points.map(p => p.join(' ')).join(';')); + } + + return { + columns, + values, + }; +} + +function getDisplayText(mappedAnnotations, displaySet) { + const displayText = { + primary: [], + secondary: [], + }; + + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } + + // Area is the same for all series + const { length, width, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; + const roundedLength = utils.roundNumber(length, 2); + const roundedWidth = utils.roundNumber(width, 2); + + const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + + displayText.primary.push(`L: ${roundedLength} ${getDisplayUnit(unit)}`); + displayText.primary.push(`W: ${roundedWidth} ${getDisplayUnit(unit)}`); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); + + return displayText; +} + +export default Bidirectional; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts new file mode 100644 index 0000000..ff52960 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts @@ -0,0 +1,211 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import { getDisplayUnit } from './utils'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { utils } from '@ohif/core'; +import { getStatisticDisplayString } from './utils/getValueDisplayString'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; + +const CircleROI = { + toAnnotation: measurement => {}, + toMeasurement: ( + csToolsEventDetail, + displaySetService, + CornerstoneViewportService, + getValueTypeFromToolType, + customizationService + ) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + + if (!metadata || !data) { + console.warn('Length tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType) { + throw new Error('Tool not supported'); + } + + const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + } + + const { points, textBox } = data.handles; + + const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); + + const displayText = getDisplayText(mappedAnnotations, displaySet); + const getReport = () => + _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points, + textBox, + isLocked, + isVisible, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + referencedImageId, + frameNumber: mappedAnnotations[0]?.frameNumber || 1, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType(toolName), + getReport, + }; + }, +}; + +function getMappedAnnotations(annotation, displaySetService) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId } = metadata; + const targets = Object.keys(cachedStats); + + if (!targets.length) { + return []; + } + + const annotations = []; + Object.keys(cachedStats).forEach(targetId => { + const targetStats = cachedStats[targetId]; + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + + const { SeriesNumber } = displaySet; + const { mean, stdDev, max, area, Modality, areaUnit, modalityUnit } = targetStats; + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + Modality, + unit: modalityUnit, + mean, + stdDev, + max, + area, + areaUnit, + }); + }); + + return annotations; +} + +/* +This function is used to convert the measurement data to a format that is +suitable for the report generation (e.g. for the csv report). The report +returns a list of columns and corresponding values. +*/ +function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService) { + const columns = []; + const values = []; + + // Add Type + columns.push('AnnotationType'); + values.push('Cornerstone:CircleROI'); + + mappedAnnotations.forEach(annotation => { + const { mean, stdDev, max, area, unit, areaUnit } = annotation; + + if (!mean || !unit || !max || !area) { + return; + } + + columns.push(`max (${unit})`, `mean (${unit})`, `std (${unit})`, 'Area', 'Unit'); + values.push(max, mean, stdDev, area, areaUnit); + }); + + if (FrameOfReferenceUID) { + columns.push('FrameOfReferenceUID'); + values.push(FrameOfReferenceUID); + } + + if (points) { + columns.push('points'); + // points has the form of [[x1, y1, z1], [x2, y2, z2], ...] + // convert it to string of [[x1 y1 z1];[x2 y2 z2];...] + // so that it can be used in the csv report + values.push(points.map(p => p.join(' ')).join(';')); + } + + return { + columns, + values, + }; +} + +function getDisplayText(mappedAnnotations, displaySet) { + const displayText = { + primary: [], + secondary: [], + }; + + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } + + // Area is the same for all series + const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0]; + + const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + + // Area sometimes becomes undefined if `preventHandleOutsideImage` is off. + const roundedArea = utils.roundNumber(area || 0, 2); + displayText.primary.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`); + + // Todo: we need a better UI for displaying all these information + mappedAnnotations.forEach(mappedAnnotation => { + const { unit, max, SeriesNumber } = mappedAnnotation; + + const maxStr = getStatisticDisplayString(max, unit, 'max'); + + displayText.primary.push(maxStr); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); + }); + + return displayText; +} + +export default CircleROI; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/CobbAngle.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/CobbAngle.ts new file mode 100644 index 0000000..e4f3449 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/CobbAngle.ts @@ -0,0 +1,201 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import { getDisplayUnit } from './utils'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { utils } from '@ohif/core'; + +const CobbAngle = { + toAnnotation: measurement => {}, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} cornerstone Cornerstone event data + * @return {Measurement} Measurement instance + */ + toMeasurement: ( + csToolsEventDetail, + displaySetService, + CornerstoneViewportService, + getValueTypeFromToolType, + customizationService + ) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + + if (!metadata || !data) { + console.warn('Cobb Angle tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType) { + throw new Error('Tool not supported'); + } + + const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + } + + const { points, textBox } = data.handles; + + const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); + + const displayText = getDisplayText(mappedAnnotations, displaySet); + const getReport = () => + _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points, + textBox, + isLocked, + isVisible, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + referencedImageId, + frameNumber: mappedAnnotations?.[0]?.frameNumber || 1, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType(toolName), + getReport, + }; + }, +}; + +function getMappedAnnotations(annotation, displaySetService) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId } = metadata; + const targets = Object.keys(cachedStats); + + if (!targets.length) { + return; + } + + const annotations = []; + Object.keys(cachedStats).forEach(targetId => { + const targetStats = cachedStats[targetId]; + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + + const { SeriesNumber } = displaySet; + const { angle } = targetStats; + const unit = '\u00B0'; + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + unit, + angle, + }); + }); + + return annotations; +} + +/* +This function is used to convert the measurement data to a format that is +suitable for the report generation (e.g. for the csv report). The report +returns a list of columns and corresponding values. +*/ +function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService) { + const columns = []; + const values = []; + + // Add Type + columns.push('AnnotationType'); + values.push('Cornerstone:CobbAngle'); + + mappedAnnotations.forEach(annotation => { + const { angle, unit } = annotation; + columns.push(`Angle (${unit})`); + values.push(angle); + }); + + if (FrameOfReferenceUID) { + columns.push('FrameOfReferenceUID'); + values.push(FrameOfReferenceUID); + } + + if (points) { + columns.push('points'); + // points has the form of [[x1, y1, z1], [x2, y2, z2], ...] + // convert it to string of [[x1 y1 z1];[x2 y2 z2];...] + // so that it can be used in the csv report + values.push(points.map(p => p.join(' ')).join(';')); + } + + return { + columns, + values, + }; +} + +function getDisplayText(mappedAnnotations, displaySet) { + const displayText = { + primary: [], + secondary: [], + }; + + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } + + // Angle is the same for all series + const { angle, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; + + const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + if (angle === undefined) { + return displayText; + } + const roundedAngle = utils.roundNumber(angle, 2); + + displayText.primary.push(`${roundedAngle} ${getDisplayUnit(unit)}`); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); + + return displayText; +} + +export default CobbAngle; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts new file mode 100644 index 0000000..7cdbfce --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts @@ -0,0 +1,209 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import { getDisplayUnit } from './utils'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { utils } from '@ohif/core'; +import { getStatisticDisplayString } from './utils/getValueDisplayString'; + +const EllipticalROI = { + toAnnotation: measurement => {}, + toMeasurement: ( + csToolsEventDetail, + displaySetService, + cornerstoneViewportService, + getValueTypeFromToolType, + customizationService + ) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + + if (!metadata || !data) { + console.warn('Length tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType) { + throw new Error('Tool not supported'); + } + + const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + } + + const { points, textBox } = data.handles; + + const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); + + const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService); + const getReport = () => + _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points, + textBox, + metadata, + isLocked, + isVisible, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + referencedImageId, + frameNumber: mappedAnnotations[0]?.frameNumber || 1, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType(toolName), + getReport, + }; + }, +}; + +function getMappedAnnotations(annotation, displaySetService) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId } = metadata; + const targets = Object.keys(cachedStats); + + if (!targets.length) { + return []; + } + + const annotations = []; + Object.keys(cachedStats).forEach(targetId => { + const targetStats = cachedStats[targetId]; + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + + const { SeriesNumber } = displaySet; + const { mean, stdDev, max, area, Modality, areaUnit, modalityUnit } = targetStats; + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + Modality, + unit: modalityUnit, + areaUnit, + mean, + stdDev, + max, + area, + }); + }); + + return annotations; +} + +/* +This function is used to convert the measurement data to a format that is +suitable for the report generation (e.g. for the csv report). The report +returns a list of columns and corresponding values. +*/ +function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService) { + const columns = []; + const values = []; + + // Add Type + columns.push('AnnotationType'); + values.push('Cornerstone:EllipticalROI'); + + mappedAnnotations.forEach(annotation => { + const { mean, stdDev, max, area, unit, areaUnit } = annotation; + + if (!mean || !unit || !max || !area) { + return; + } + + columns.push(`max (${unit})`, `mean (${unit})`, `std (${unit})`, 'Area', 'Unit'); + values.push(max, mean, stdDev, area, areaUnit); + }); + + if (FrameOfReferenceUID) { + columns.push('FrameOfReferenceUID'); + values.push(FrameOfReferenceUID); + } + + if (points) { + columns.push('points'); + // points has the form of [[x1, y1, z1], [x2, y2, z2], ...] + // convert it to string of [[x1 y1 z1];[x2 y2 z2];...] + // so that it can be used in the csv report + values.push(points.map(p => p.join(' ')).join(';')); + } + + return { + columns, + values, + }; +} + +function getDisplayText(mappedAnnotations, displaySet, customizationService) { + const displayText = { + primary: [], + secondary: [], + }; + + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } + + // Area is the same for all series + const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0]; + + const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + + const roundedArea = utils.roundNumber(area, 2); + displayText.primary.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`); + + // Todo: we need a better UI for displaying all these information + mappedAnnotations.forEach(mappedAnnotation => { + const { unit, max, SeriesNumber } = mappedAnnotation; + + const maxStr = getStatisticDisplayString(max, unit, 'max'); + displayText.primary.push(maxStr); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); + }); + + return displayText; +} + +export default EllipticalROI; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts new file mode 100644 index 0000000..0b0c1c6 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts @@ -0,0 +1,208 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { utils } from '@ohif/core'; +import { config } from '@cornerstonejs/tools/annotation'; + +const Length = { + toAnnotation: measurement => {}, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} cornerstone Cornerstone event data + * @return {Measurement} Measurement instance + */ + toMeasurement: ( + csToolsEventDetail, + displaySetService, + cornerstoneViewportService, + getValueTypeFromToolType, + customizationService + ) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + const colorString = config.style.getStyleProperty('color', { annotationUID }); + + // color string is like 'rgb(255, 255, 255)' we need them to be in RGBA array [255, 255, 255, 255] + // Todo: this should be in a utility + // const color = colorString.replace('rgb(', '').replace(')', '').split(',').map(Number); + + if (!metadata || !data) { + console.warn('Length tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType) { + throw new Error('Tool not supported'); + } + + const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + } + + const { points, textBox } = data.handles; + + const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); + + const displayText = getDisplayText(mappedAnnotations, displaySet); + const getReport = () => + _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points, + textBox, + isLocked, + isVisible, + metadata, + // color, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + referencedImageId, + frameNumber: mappedAnnotations[0]?.frameNumber || 1, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType(toolName), + getReport, + }; + }, +}; + +function getMappedAnnotations(annotation, displaySetService) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId } = metadata; + const targets = Object.keys(cachedStats); + + if (!targets.length) { + return []; + } + + const annotations = []; + Object.keys(cachedStats).forEach(targetId => { + const targetStats = cachedStats[targetId]; + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + + const { SeriesNumber } = displaySet; + const { length, unit = 'mm' } = targetStats; + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + unit, + length, + }); + }); + + return annotations; +} + +/* +This function is used to convert the measurement data to a format that is +suitable for the report generation (e.g. for the csv report). The report +returns a list of columns and corresponding values. +*/ +function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService) { + const columns = []; + const values = []; + + // Add Type + columns.push('AnnotationType'); + values.push('Cornerstone:Length'); + + mappedAnnotations.forEach(annotation => { + const { length, unit } = annotation; + columns.push(`Length`); + values.push(length); + columns.push('Unit'); + values.push(unit); + }); + + if (FrameOfReferenceUID) { + columns.push('FrameOfReferenceUID'); + values.push(FrameOfReferenceUID); + } + + if (points) { + columns.push('points'); + // points has the form of [[x1, y1, z1], [x2, y2, z2], ...] + // convert it to string of [[x1 y1 z1];[x2 y2 z2];...] + // so that it can be used in the csv report + values.push(points.map(p => p.join(' ')).join(';')); + } + + return { + columns, + values, + }; +} + +function getDisplayText(mappedAnnotations, displaySet) { + const displayText = { + primary: [], + secondary: [], + }; + + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } + + // Length is the same for all series + const { length, SeriesNumber, SOPInstanceUID, frameNumber, unit } = mappedAnnotations[0]; + + const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + + if (length === null || length === undefined) { + return displayText; + } + const roundedLength = utils.roundNumber(length, 2); + displayText.primary.push(`${roundedLength} ${unit}`); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); + + return displayText; +} + +export default Length; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/LivewireContour.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/LivewireContour.ts new file mode 100644 index 0000000..a9a3d03 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/LivewireContour.ts @@ -0,0 +1,173 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { getDisplayUnit } from './utils'; +import { utils } from '@ohif/core'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; +/** + * Represents a mapping utility for Livewire measurements. + */ +const LivewireContour = { + toAnnotation: measurement => {}, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} csToolsEventDetail Cornerstone event data + * @param {DisplaySetService} DisplaySetService Service for managing display sets + * @param {CornerstoneViewportService} CornerstoneViewportService Service for managing viewports + * @param {Function} getValueTypeFromToolType Function to get value type from tool type + * @returns {Measurement} Measurement instance + */ + toMeasurement: ( + csToolsEventDetail, + DisplaySetService, + CornerstoneViewportService, + getValueTypeFromToolType, + customizationService + ) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + if (!metadata || !data) { + console.warn('Livewire tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + if (!validToolType) { + throw new Error(`Tool ${toolName} not supported`); + } + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber, StudyInstanceUID } = + getSOPInstanceAttributes(referencedImageId); + + let displaySet; + if (SOPInstanceUID) { + displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID); + } + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points: data.contour.polyline, + textBox: data.handles.textBox, + metadata, + frameNumber, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + isLocked, + isVisible, + displayText: getDisplayText(annotation, displaySet), + data: data.cachedStats, + type: getValueTypeFromToolType(toolName), + getReport: () => getColumnValueReport(annotation, customizationService), + }; + }, +}; + +/** + * This function is used to convert the measurement data to a + * format that is suitable for report generation (e.g. for the csv report). + * The report returns a list of columns and corresponding values. + * + * @param {object} annotation + * @returns {object} Report's content from this tool + */ +function getColumnValueReport(annotation, customizationService) { + const columns = []; + const values = []; + + /** Add type */ + columns.push('AnnotationType'); + values.push('Cornerstone:Livewire'); + + /** Add cachedStats */ + const { metadata, data } = annotation; + + /** Add FOR */ + if (metadata.FrameOfReferenceUID) { + columns.push('FrameOfReferenceUID'); + values.push(metadata.FrameOfReferenceUID); + } + + /** Add points */ + if (data.contour.polyline) { + /** + * Points has the form of [[x1, y1, z1], [x2, y2, z2], ...] + * convert it to string of [[x1 y1 z1];[x2 y2 z2];...] + * so that it can be used in the CSV report + */ + columns.push('points'); + values.push(data.contour.polyline.map(p => p.join(' ')).join(';')); + } + + return { columns, values }; +} + +/** + * Retrieves the display text for an annotation in a display set. + * + * @param {Object} annotation - The annotation object. + * @param {Object} displaySet - The display set object. + * @returns {string[]} - An array of display text. + */ +function getDisplayText(annotation, displaySet) { + const { metadata, data } = annotation; + + if (!data.cachedStats || !data.cachedStats[`imageId:${metadata.referencedImageId}`]) { + return []; + } + + const { area, areaUnit } = data.cachedStats[`imageId:${metadata.referencedImageId}`]; + + const { SOPInstanceUID, frameNumber } = getSOPInstanceAttributes(metadata.referencedImageId); + + const displayText = []; + + const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + + const { SeriesNumber } = displaySet; + let seriesText = null; + if (SeriesNumber !== undefined) { + seriesText = `S: ${SeriesNumber}${instanceText}${frameText}`; + } + + const texts = []; + if (area) { + const roundedArea = utils.roundNumber(area || 0, 2); + texts.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`); + } + + if (seriesText) { + texts.push(seriesText); + } + + displayText.push({ + text: texts, + series: seriesText, + }); + + return displayText; +} + +export default LivewireContour; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts new file mode 100644 index 0000000..eccf537 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts @@ -0,0 +1,223 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { utils } from '@ohif/core'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; +import { getDisplayUnit } from './utils'; +import { getStatisticDisplayString } from './utils/getValueDisplayString'; +/** + * Represents a mapping utility for Planar Freehand ROI measurements. + */ +const PlanarFreehandROI = { + toAnnotation: measurement => {}, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} csToolsEventDetail Cornerstone event data + * @param {DisplaySetService} displaySetService Service for managing display sets + * @param {CornerstoneViewportService} CornerstoneViewportService Service for managing viewports + * @param {Function} getValueTypeFromToolType Function to get value type from tool type + * @param {CustomizationService} customizationService Service for customization + * @returns {Measurement | null} Measurement instance or null if invalid + */ + toMeasurement: ( + csToolsEventDetail, + displaySetService, + CornerstoneViewportService, + getValueTypeFromToolType, + customizationService + ) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + if (!metadata || !data) { + console.debug('PlanarFreehandROI tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + if (!validToolType) { + throw new Error(`Tool ${toolName} not supported`); + } + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber, StudyInstanceUID } = + getSOPInstanceAttributes(referencedImageId, displaySetService, annotation); + + let displaySet; + if (SOPInstanceUID) { + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + } + + const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); + const displayText = getDisplayText(mappedAnnotations, displaySet); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points: data.contour.polyline, + textBox: data.handles.textBox, + metadata, + frameNumber, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + referencedImageId, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType(toolName), + getReport: () => getColumnValueReport(annotation, customizationService), + isLocked, + isVisible, + }; + }, +}; + +/** + * Maps annotations to a structured format with relevant attributes. + * + * @param {Object} annotation The annotation object. + * @param {DisplaySetService} displaySetService Service for managing display sets. + * @returns {Array} Mapped annotations. + */ +function getMappedAnnotations(annotation, displaySetService) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId } = metadata; + const targets = Object.keys(cachedStats); + + if (!targets.length) { + return []; + } + + const annotations = []; + Object.keys(cachedStats).forEach(targetId => { + const targetStats = cachedStats[targetId]; + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + + const { SeriesNumber } = displaySet; + const { mean, stdDev, max, area, Modality, areaUnit, modalityUnit } = targetStats; + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + Modality, + unit: modalityUnit, + mean, + stdDev, + max, + area, + areaUnit, + }); + }); + + return annotations; +} + +/** + * Converts the measurement data to a format suitable for report generation. + * + * @param {object} annotation The annotation object. + * @param {CustomizationService} customizationService Service for customization. + * @returns {object} Report's content. + */ +function getColumnValueReport(annotation, customizationService) { + const { PlanarFreehandROI } = customizationService.getCustomization('cornerstone.measurements'); + const { report } = PlanarFreehandROI; + const columns = []; + const values = []; + + /** Add type */ + columns.push('AnnotationType'); + values.push('Cornerstone:PlanarFreehandROI'); + + /** Add cachedStats */ + const { metadata, data } = annotation; + const stats = data.cachedStats[`imageId:${metadata.referencedImageId}`]; + + report.forEach(({ name, value }) => { + columns.push(name); + stats[value] ? values.push(stats[value]) : values.push('not available'); + }); + + /** Add FOR */ + if (metadata.FrameOfReferenceUID) { + columns.push('FrameOfReferenceUID'); + values.push(metadata.FrameOfReferenceUID); + } + + /** Add points */ + if (data.contour.polyline) { + columns.push('points'); + values.push(data.contour.polyline.map(p => p.join(' ')).join(';')); + } + + return { columns, values }; +} + +/** + * Retrieves the display text for an annotation in a display set. + * + * @param {Array} mappedAnnotations The mapped annotations. + * @param {Object} displaySet The display set object. + * @returns {Object} Display text with primary and secondary information. + */ +function getDisplayText(mappedAnnotations, displaySet) { + const displayText = { + primary: [], + secondary: [], + }; + + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } + + // Area is the same for all series + const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0]; + + const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + + const roundedArea = utils.roundNumber(area || 0, 2); + displayText.primary.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`); + + mappedAnnotations.forEach(mappedAnnotation => { + const { unit, max, SeriesNumber } = mappedAnnotation; + + const maxStr = getStatisticDisplayString(max, unit, 'max'); + + displayText.primary.push(maxStr); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); + }); + + return displayText; +} + +export default PlanarFreehandROI; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Probe.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Probe.ts new file mode 100644 index 0000000..8ad3e70 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Probe.ts @@ -0,0 +1,192 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import { getDisplayUnit } from './utils'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { utils } from '@ohif/core'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; +const Probe = { + toAnnotation: measurement => {}, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} cornerstone Cornerstone event data + * @return {Measurement} Measurement instance + */ + toMeasurement: ( + csToolsEventDetail, + displaySetService, + CornerstoneViewportService, + getValueTypeFromToolType, + customizationService + ) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + if (!metadata || !data) { + console.warn('Probe tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType) { + throw new Error('Tool not supported'); + } + + const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + } + + const { points } = data.handles; + + const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); + + const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService); + const getReport = () => + _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points, + metadata, + isLocked, + isVisible, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + referencedImageId, + frameNumber: mappedAnnotations?.[0]?.frameNumber || 1, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType(toolName), + getReport, + }; + }, +}; + +function getMappedAnnotations(annotation, displaySetService) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId } = metadata; + const targets = Object.keys(cachedStats); + + if (!targets.length) { + return; + } + + const annotations = []; + Object.keys(cachedStats).forEach(targetId => { + const targetStats = cachedStats[targetId]; + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + + const { SeriesNumber } = displaySet; + const { value } = targetStats; + const unit = 'HU'; + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + unit, + value, + }); + }); + + return annotations; +} + +/* +This function is used to convert the measurement data to a format that is +suitable for the report generation (e.g. for the csv report). The report +returns a list of columns and corresponding values. +*/ +function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService) { + const columns = []; + const values = []; + + // Add Type + columns.push('AnnotationType'); + values.push('Cornerstone:Probe'); + + mappedAnnotations.forEach(annotation => { + const { value, unit } = annotation; + columns.push(`Probe (${unit})`); + values.push(value); + }); + + if (FrameOfReferenceUID) { + columns.push('FrameOfReferenceUID'); + values.push(FrameOfReferenceUID); + } + + if (points) { + columns.push('points'); + values.push(points.map(p => p.join(' ')).join(';')); + } + + return { + columns, + values, + }; +} + +function getDisplayText(mappedAnnotations, displaySet, customizationService) { + const displayText = { + primary: [], + secondary: [], + }; + + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } + + const { value, unit, SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0]; + + const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + + if (value !== undefined) { + const roundedValue = utils.roundNumber(value, 2); + displayText.primary.push(`${roundedValue} ${getDisplayUnit(unit)}`); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); + } + + return displayText; +} + +export default Probe; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts new file mode 100644 index 0000000..c35636b --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts @@ -0,0 +1,211 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import { getDisplayUnit } from './utils'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { utils } from '@ohif/core'; +import { getStatisticDisplayString } from './utils/getValueDisplayString'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; + +const RectangleROI = { + toAnnotation: measurement => {}, + toMeasurement: ( + csToolsEventDetail, + displaySetService, + CornerstoneViewportService, + getValueTypeFromToolType, + customizationService + ) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + + if (!metadata || !data) { + console.warn('Rectangle ROI tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType) { + throw new Error('Tool not supported'); + } + + const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + } + + const { points, textBox } = data.handles; + + const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); + + const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService); + const getReport = () => + _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points, + textBox, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + referencedImageId, + frameNumber: mappedAnnotations[0]?.frameNumber || 1, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType(toolName), + getReport, + isLocked, + isVisible, + }; + }, +}; + +function getMappedAnnotations(annotation, displaySetService) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId } = metadata; + const targets = Object.keys(cachedStats); + + if (!targets.length) { + return []; + } + + const annotations = []; + Object.keys(cachedStats).forEach(targetId => { + const targetStats = cachedStats[targetId]; + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + + const { SeriesNumber } = displaySet; + const { mean, stdDev, max, area, Modality, modalityUnit, areaUnit } = targetStats; + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + Modality, + unit: modalityUnit, + mean, + stdDev, + metadata, + max, + area, + areaUnit, + }); + }); + + return annotations; +} + +/* +This function is used to convert the measurement data to a format that is +suitable for the report generation (e.g. for the csv report). The report +returns a list of columns and corresponding values. +*/ +function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService) { + const columns = []; + const values = []; + + // Add Type + columns.push('AnnotationType'); + values.push('Cornerstone:RectangleROI'); + + mappedAnnotations.forEach(annotation => { + const { mean, stdDev, max, area, unit, areaUnit } = annotation; + + if (!mean || !unit || !max || !area) { + return; + } + + columns.push(`Maximum`, `Mean`, `Std Dev`, 'Pixel Unit', `Area`, 'Unit'); + values.push(max, mean, stdDev, unit, area, areaUnit); + }); + + if (FrameOfReferenceUID) { + columns.push('FrameOfReferenceUID'); + values.push(FrameOfReferenceUID); + } + + if (points) { + columns.push('points'); + // points has the form of [[x1, y1, z1], [x2, y2, z2], ...] + // convert it to string of [[x1 y1 z1];[x2 y2 z2];...] + // so that it can be used in the csv report + values.push(points.map(p => p.join(' ')).join(';')); + } + + return { + columns, + values, + }; +} + +function getDisplayText(mappedAnnotations, displaySet, customizationService) { + const displayText = { + primary: [], + secondary: [], + }; + + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } + + // Area is the same for all series + const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0]; + + const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + + // Area sometimes becomes undefined if `preventHandleOutsideImage` is off. + const roundedArea = utils.roundNumber(area || 0, 2); + displayText.primary.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`); + + // Todo: we need a better UI for displaying all these information + mappedAnnotations.forEach(mappedAnnotation => { + const { unit, max, SeriesNumber } = mappedAnnotation; + + const maxStr = getStatisticDisplayString(max, unit, 'max'); + + displayText.primary.push(maxStr); + displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); + }); + + return displayText; +} + +export default RectangleROI; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/SplineROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/SplineROI.ts new file mode 100644 index 0000000..80225b4 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/SplineROI.ts @@ -0,0 +1,227 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { utils } from '@ohif/core'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; +import { getDisplayUnit } from './utils'; +import { getStatisticDisplayString } from './utils/getValueDisplayString'; + +/** + * Represents a mapping utility for Spline ROI measurements. + */ +const SplineROI = { + toAnnotation: measurement => { + // Implementation for converting measurement to annotation + }, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} csToolsEventDetail - Cornerstone event data + * @param {DisplaySetService} displaySetService - Service for managing display sets + * @param {CornerstoneViewportService} CornerstoneViewportService - Service for managing viewports + * @param {Function} getValueTypeFromToolType - Function to get value type from tool type + * @param {CustomizationService} customizationService - Service for customization + * @returns {Measurement | null} Measurement instance or null if invalid + */ + toMeasurement: ( + csToolsEventDetail, + displaySetService, + CornerstoneViewportService, + getValueTypeFromToolType, + customizationService + ) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + if (!metadata || !data) { + console.warn('SplineROI tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + if (!validToolType) { + throw new Error(`Tool ${toolName} not supported`); + } + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber, StudyInstanceUID } = + getSOPInstanceAttributes(referencedImageId, displaySetService, annotation); + + let displaySet; + if (SOPInstanceUID) { + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + } + + const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); + const displayText = getDisplayText(mappedAnnotations, displaySet); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points: data.contour.polyline, + textBox: data.handles.textBox, + metadata, + frameNumber, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + referencedImageId, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType(toolName), + getReport: () => getColumnValueReport(annotation, customizationService), + isLocked, + isVisible, + }; + }, +}; + +/** + * Maps annotations to a structured format with relevant attributes. + * + * @param {Object} annotation - The annotation object. + * @param {DisplaySetService} displaySetService - Service for managing display sets. + * @returns {Array} Mapped annotations. + */ +function getMappedAnnotations(annotation, displaySetService) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId } = metadata; + const targets = Object.keys(cachedStats); + + if (!targets.length) { + return []; + } + + const annotations = []; + Object.keys(cachedStats).forEach(targetId => { + const targetStats = cachedStats[targetId]; + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = getSOPInstanceAttributes( + referencedImageId, + displaySetService, + annotation + ); + + const displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0]; + + const { SeriesNumber } = displaySet; + const { mean, stdDev, max, area, Modality, areaUnit, modalityUnit } = targetStats; + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + Modality, + unit: modalityUnit, + mean, + stdDev, + max, + area, + areaUnit, + }); + }); + + return annotations; +} + +/** + * Converts the measurement data to a format suitable for report generation. + * + * @param {object} annotation - The annotation object. + * @param {CustomizationService} customizationService - Service for customization. + * @returns {object} Report's content. + */ +function getColumnValueReport(annotation, customizationService) { + const { SplineROI } = customizationService.getCustomization('cornerstone.measurements'); + const { report } = SplineROI; + const columns = []; + const values = []; + + /** Add type */ + columns.push('AnnotationType'); + values.push('Cornerstone:SplineROI'); + + /** Add cachedStats */ + const { metadata, data } = annotation; + const stats = data.cachedStats[`imageId:${metadata.referencedImageId}`]; + + report.forEach(({ name, value }) => { + columns.push(name); + stats[value] ? values.push(stats[value]) : values.push('not available'); + }); + + /** Add FOR */ + if (metadata.FrameOfReferenceUID) { + columns.push('FrameOfReferenceUID'); + values.push(metadata.FrameOfReferenceUID); + } + + /** Add points */ + if (data.contour.polyline) { + columns.push('points'); + values.push(data.contour.polyline.map(p => p.join(' ')).join(';')); + } + + return { columns, values }; +} + +/** + * Retrieves the display text for an annotation in a display set. + * + * @param {Array} mappedAnnotations - The mapped annotations. + * @param {Object} displaySet - The display set object. + * @returns {Object} Display text with primary and secondary information. + */ +function getDisplayText(mappedAnnotations, displaySet) { + const displayText = { + primary: [], + secondary: [], + }; + + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } + + // Area is the same for all series + const { area, SOPInstanceUID, frameNumber, areaUnit } = mappedAnnotations[0]; + + const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + + const roundedArea = utils.roundNumber(area || 0, 2); + displayText.primary.push(`${roundedArea} ${getDisplayUnit(areaUnit)}`); + + // we don't have max yet for splines rois + // mappedAnnotations.forEach(mappedAnnotation => { + // const { unit, max, SeriesNumber } = mappedAnnotation; + + // const maxStr = getStatisticDisplayString(max, unit, 'max'); + + // displayText.primary.push(maxStr); + // displayText.secondary.push(`S: ${SeriesNumber}${instanceText}${frameText}`); + // }); + + return displayText; +} + +export default SplineROI; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/UltrasoundDirectional.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/UltrasoundDirectional.ts new file mode 100644 index 0000000..e11f698 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/UltrasoundDirectional.ts @@ -0,0 +1,214 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; +import { utils } from '@ohif/core'; +import { getIsLocked } from './utils/getIsLocked'; +import { getIsVisible } from './utils/getIsVisible'; +const UltrasoundDirectional = { + toAnnotation: measurement => {}, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} cornerstone Cornerstone event data + * @return {Measurement} Measurement instance + */ + toMeasurement: ( + csToolsEventDetail, + displaySetService, + CornerstoneViewportService, + getValueTypeFromToolType, + customizationService + ) => { + const { annotation } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + const isLocked = getIsLocked(annotationUID); + const isVisible = getIsVisible(annotationUID); + if (!metadata || !data) { + console.warn('Length tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType) { + throw new Error('Tool not supported'); + } + + const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = + getSOPInstanceAttributes(referencedImageId); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID); + } + + const { points } = data.handles; + + const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); + + const displayText = getDisplayText(mappedAnnotations, displaySet, customizationService); + const getReport = () => + _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService); + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + points, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + frameNumber: mappedAnnotations?.[0]?.frameNumber || 1, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: data.label, + displayText: displayText, + data: data.cachedStats, + type: getValueTypeFromToolType(toolName), + getReport, + isLocked, + isVisible, + }; + }, +}; + +function getMappedAnnotations(annotation, DisplaySetService) { + const { metadata, data } = annotation; + const { cachedStats } = data; + const { referencedImageId } = metadata; + const targets = Object.keys(cachedStats); + + if (!targets.length) { + return; + } + + const annotations = []; + Object.keys(cachedStats).forEach(targetId => { + const targetStats = cachedStats[targetId]; + + if (!referencedImageId) { + throw new Error('Non-acquisition plane measurement mapping not supported'); + } + + const { SOPInstanceUID, SeriesInstanceUID, frameNumber } = + getSOPInstanceAttributes(referencedImageId); + + const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID, + frameNumber + ); + + const { SeriesNumber } = displaySet; + const { xValues, yValues, units, isUnitless, isHorizontal } = targetStats; + + annotations.push({ + SeriesInstanceUID, + SOPInstanceUID, + SeriesNumber, + frameNumber, + xValues, + yValues, + units, + isUnitless, + isHorizontal, + }); + }); + + return annotations; +} + +/* +This function is used to convert the measurement data to a format that is +suitable for the report generation (e.g. for the csv report). The report +returns a list of columns and corresponding values. +*/ +function _getReport(mappedAnnotations, points, FrameOfReferenceUID, customizationService) { + const columns = []; + const values = []; + + // Add Type + columns.push('AnnotationType'); + values.push('Cornerstone:UltrasoundDirectional'); + + mappedAnnotations.forEach(annotation => { + const { xValues, yValues, units, isUnitless } = annotation; + if (isUnitless) { + columns.push('Length' + units[0]); + values.push(utils.roundNumber(xValues[0], 2)); + } else { + const dist1 = Math.abs(xValues[1] - xValues[0]); + const dist2 = Math.abs(yValues[1] - yValues[0]); + columns.push('Time' + units[0]); + values.push(utils.roundNumber(dist1, 2)); + columns.push('Length' + units[1]); + values.push(utils.roundNumber(dist2, 2)); + } + }); + + if (FrameOfReferenceUID) { + columns.push('FrameOfReferenceUID'); + values.push(FrameOfReferenceUID); + } + + if (points) { + columns.push('points'); + values.push(points.map(p => p.join(' ')).join(';')); + } + + return { + columns, + values, + }; +} + +function getDisplayText(mappedAnnotations, displaySet, customizationService) { + const displayText = { + primary: [], + secondary: [], + }; + + if (!mappedAnnotations || !mappedAnnotations.length) { + return displayText; + } + + const { xValues, yValues, units, isUnitless, SeriesNumber, SOPInstanceUID, frameNumber } = + mappedAnnotations[0]; + + const instance = displaySet.instances.find(image => image.SOPInstanceUID === SOPInstanceUID); + + let InstanceNumber; + if (instance) { + InstanceNumber = instance.InstanceNumber; + } + + const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : ''; + const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : ''; + const seriesText = `S: ${SeriesNumber}${instanceText}${frameText}`; + + if (xValues === undefined || yValues === undefined) { + return displayText; + } + + if (isUnitless) { + displayText.primary.push(`${utils.roundNumber(xValues[0], 2)} ${units[0]}`); + } else { + const dist1 = Math.abs(xValues[1] - xValues[0]); + const dist2 = Math.abs(yValues[1] - yValues[0]); + displayText.primary.push(`${utils.roundNumber(dist1)} ${units[0]}`); + displayText.primary.push(`${utils.roundNumber(dist2)} ${units[1]}`); + } + + displayText.secondary.push(seriesText); + + return displayText; +} + +export default UltrasoundDirectional; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js b/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js new file mode 100644 index 0000000..a3f9c83 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/constants/supportedTools.js @@ -0,0 +1,18 @@ +const supportedTools = [ + 'Length', + 'EllipticalROI', + 'CircleROI', + 'Bidirectional', + 'ArrowAnnotate', + 'Angle', + 'CobbAngle', + 'Probe', + 'RectangleROI', + 'PlanarFreehandROI', + 'SplineROI', + 'LivewireContour', + 'UltrasoundDirectionalTool', + 'SCOORD3DPoint', +]; + +export default supportedTools; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/index.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/index.ts new file mode 100644 index 0000000..795c744 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/index.ts @@ -0,0 +1,3 @@ +import * as measurementMappingUtils from './utils'; + +export { measurementMappingUtils }; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts new file mode 100644 index 0000000..041c767 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts @@ -0,0 +1,281 @@ +import { MeasurementService } from '@ohif/core'; +import Length from './Length'; +import Bidirectional from './Bidirectional'; +import EllipticalROI from './EllipticalROI'; +import CircleROI from './CircleROI'; +import ArrowAnnotate from './ArrowAnnotate'; +import CobbAngle from './CobbAngle'; +import Angle from './Angle'; +import PlanarFreehandROI from './PlanarFreehandROI'; +import RectangleROI from './RectangleROI'; +import SplineROI from './SplineROI'; +import LivewireContour from './LivewireContour'; +import Probe from './Probe'; +import UltrasoundDirectional from './UltrasoundDirectional'; + +const measurementServiceMappingsFactory = ( + measurementService: MeasurementService, + displaySetService, + cornerstoneViewportService, + customizationService +) => { + /** + * Maps measurement service format object to cornerstone annotation object. + * + * @param measurement The measurement instance + * @param definition The source definition + * @return Cornerstone annotation data + */ + + const _getValueTypeFromToolType = toolType => { + const { POLYLINE, ELLIPSE, CIRCLE, RECTANGLE, BIDIRECTIONAL, POINT, ANGLE } = + MeasurementService.VALUE_TYPES; + + // TODO -> I get why this was attempted, but its not nearly flexible enough. + // A single measurement may have an ellipse + a bidirectional measurement, for instances. + // You can't define a bidirectional tool as a single type.. + const TOOL_TYPE_TO_VALUE_TYPE = { + Length: POLYLINE, + EllipticalROI: ELLIPSE, + CircleROI: CIRCLE, + RectangleROI: RECTANGLE, + PlanarFreehandROI: POLYLINE, + Bidirectional: BIDIRECTIONAL, + ArrowAnnotate: POINT, + CobbAngle: ANGLE, + Angle: ANGLE, + SplineROI: POLYLINE, + LivewireContour: POLYLINE, + Probe: POINT, + UltrasoundDirectional: POLYLINE, + }; + + return TOOL_TYPE_TO_VALUE_TYPE[toolType]; + }; + + const factories = { + Length: { + toAnnotation: Length.toAnnotation, + toMeasurement: csToolsAnnotation => + Length.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType, + customizationService + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + ], + }, + Bidirectional: { + toAnnotation: Bidirectional.toAnnotation, + toMeasurement: csToolsAnnotation => + Bidirectional.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType, + customizationService + ), + matchingCriteria: [ + // TODO -> We should eventually do something like shortAxis + longAxis, + // But its still a little unclear how these automatic interpretations will work. + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + ], + }, + EllipticalROI: { + toAnnotation: EllipticalROI.toAnnotation, + toMeasurement: csToolsAnnotation => + EllipticalROI.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType, + customizationService + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.ELLIPSE, + }, + ], + }, + CircleROI: { + toAnnotation: CircleROI.toAnnotation, + toMeasurement: csToolsAnnotation => + CircleROI.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType, + customizationService + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.CIRCLE, + }, + ], + }, + RectangleROI: { + toAnnotation: RectangleROI.toAnnotation, + toMeasurement: csToolsAnnotation => + RectangleROI.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType, + customizationService + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + }, + ], + }, + PlanarFreehandROI: { + toAnnotation: PlanarFreehandROI.toAnnotation, + toMeasurement: csToolsAnnotation => + PlanarFreehandROI.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType, + customizationService + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + }, + ], + }, + SplineROI: { + toAnnotation: SplineROI.toAnnotation, + toMeasurement: csToolsAnnotation => + SplineROI.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType, + customizationService + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + }, + ], + }, + LivewireContour: { + toAnnotation: LivewireContour.toAnnotation, + toMeasurement: csToolsAnnotation => + LivewireContour.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType, + customizationService + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + }, + ], + }, + ArrowAnnotate: { + toAnnotation: ArrowAnnotate.toAnnotation, + toMeasurement: csToolsAnnotation => + ArrowAnnotate.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType, + customizationService + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.POINT, + points: 1, + }, + ], + }, + Probe: { + toAnnotation: Probe.toAnnotation, + toMeasurement: csToolsAnnotation => + Probe.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType, + customizationService + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.POINT, + points: 1, + }, + ], + }, + CobbAngle: { + toAnnotation: CobbAngle.toAnnotation, + toMeasurement: csToolsAnnotation => + CobbAngle.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType, + customizationService + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.ANGLE, + }, + ], + }, + Angle: { + toAnnotation: Angle.toAnnotation, + toMeasurement: csToolsAnnotation => + Angle.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType, + customizationService + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.ANGLE, + }, + ], + }, + UltrasoundDirectional: { + toAnnotation: UltrasoundDirectional.toAnnotation, + toMeasurement: csToolsAnnotation => + UltrasoundDirectional.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService, + _getValueTypeFromToolType, + customizationService + ), + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + ], + }, + }; + + return factories; +}; + +export default measurementServiceMappingsFactory; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getDisplayUnit.js b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getDisplayUnit.js new file mode 100644 index 0000000..4c9d864 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getDisplayUnit.js @@ -0,0 +1,3 @@ +const getDisplayUnit = unit => (unit == null ? '' : unit); + +export default getDisplayUnit; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getHandlesFromPoints.js b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getHandlesFromPoints.js new file mode 100644 index 0000000..6d38194 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getHandlesFromPoints.js @@ -0,0 +1,14 @@ +export default function getHandlesFromPoints(points) { + if (points.longAxis && points.shortAxis) { + const handles = {}; + handles.start = points.longAxis[0]; + handles.end = points.longAxis[1]; + handles.perpendicularStart = points.longAxis[0]; + handles.perpendicularEnd = points.longAxis[1]; + return handles; + } + + return points + .map((p, i) => (i % 10 === 0 ? { start: p } : { end: p })) + .reduce((obj, item) => Object.assign(obj, { ...item }), {}); +} diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getIsLocked.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getIsLocked.ts new file mode 100644 index 0000000..8a6ef27 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getIsLocked.ts @@ -0,0 +1,5 @@ +import { locking } from '@cornerstonejs/tools/annotation'; + +export const getIsLocked = annotationUID => { + return locking.isAnnotationLocked(annotationUID); +}; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getIsVisible.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getIsVisible.ts new file mode 100644 index 0000000..fb310a2 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getIsVisible.ts @@ -0,0 +1,6 @@ +import { visibility } from '@cornerstonejs/tools/annotation'; + +export const getIsVisible = annotationUID => { + const isVisible = visibility.isAnnotationVisible(annotationUID); + return isVisible; +}; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js new file mode 100644 index 0000000..101ef21 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js @@ -0,0 +1,39 @@ +import * as cornerstone from '@cornerstonejs/core'; + +/** + * It checks if the imageId is provided then it uses it to query + * the metadata and get the SOPInstanceUID, SeriesInstanceUID and StudyInstanceUID. + * If the imageId is not provided then undefined is returned. + * @param {string} imageId The image id of the referenced image + * @returns + */ +export default function getSOPInstanceAttributes(imageId, displaySetService, annotation) { + if (imageId) { + return _getUIDFromImageID(imageId); + } + + const { metadata } = annotation; + const { volumeId } = metadata; + + const displaySet = displaySetService.getDisplaySetsBy(displaySet => + volumeId.includes(displaySet.uid) + )[0]; + const { StudyInstanceUID, SeriesInstanceUID } = displaySet; + + return { + SOPInstanceUID: undefined, + SeriesInstanceUID, + StudyInstanceUID, + }; +} + +function _getUIDFromImageID(imageId) { + const instance = cornerstone.metaData.get('instance', imageId); + + return { + SOPInstanceUID: instance.SOPInstanceUID, + SeriesInstanceUID: instance.SeriesInstanceUID, + StudyInstanceUID: instance.StudyInstanceUID, + frameNumber: instance.frameNumber || 1, + }; +} diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getValueDisplayString.js b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getValueDisplayString.js new file mode 100644 index 0000000..8afadc0 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/getValueDisplayString.js @@ -0,0 +1,12 @@ +import { utils } from '@ohif/core'; +import getDisplayUnit from './getDisplayUnit'; + +export const getStatisticDisplayString = (numbers, unit, key) => { + if (Array.isArray(numbers) && numbers.length > 0) { + const results = numbers.map(number => utils.roundNumber(number, 2)); + return `${key.charAt(0).toUpperCase() + key.slice(1)}: ${results.join(', ')} ${getDisplayUnit(unit)}`; + } + + const result = utils.roundNumber(numbers, 2); + return `${key.charAt(0).toUpperCase() + key.slice(1)}: ${result} ${getDisplayUnit(unit)}`; +}; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/index.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/index.ts new file mode 100644 index 0000000..82ec07f --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/index.ts @@ -0,0 +1,17 @@ +import getHandlesFromPoints from './getHandlesFromPoints'; +import { + isAnnotationSelected, + setAnnotationSelected, + getFirstAnnotationSelected, +} from './selection'; +import getSOPInstanceAttributes from './getSOPInstanceAttributes'; +import getDisplayUnit from './getDisplayUnit'; + +export { + getHandlesFromPoints, + getSOPInstanceAttributes, + isAnnotationSelected, + setAnnotationSelected, + getFirstAnnotationSelected, + getDisplayUnit, +}; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/utils/selection.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/selection.ts new file mode 100644 index 0000000..1de09d9 --- /dev/null +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/utils/selection.ts @@ -0,0 +1,33 @@ +import { annotation as cs3dToolAnnotationUtils } from '@cornerstonejs/tools'; + +/** + * Check whether an annotation from imaging library is selected or not. + * @param {string} annotationUID uid of imaging library annotation + * @returns boolean + */ +function isAnnotationSelected(annotationUID: string): boolean { + return cs3dToolAnnotationUtils.selection.isAnnotationSelected(annotationUID); +} + +/** + * Change an annotation from imaging library's selected property. + * @param annotationUID - uid of imaging library annotation + * @param selected - new value for selected + */ +function setAnnotationSelected(annotationUID: string, selected: boolean): void { + const isCurrentSelected = isAnnotationSelected(annotationUID); + // branch cut, avoid invoking imaging library unnecessarily. + if (isCurrentSelected !== selected) { + cs3dToolAnnotationUtils.selection.setAnnotationSelected(annotationUID, selected); + } +} + +function getFirstAnnotationSelected(element) { + const [selectedAnnotationUID] = cs3dToolAnnotationUtils.selection.getAnnotationsSelected() || []; + + if (selectedAnnotationUID) { + return cs3dToolAnnotationUtils.state.getAnnotation(selectedAnnotationUID); + } +} + +export { isAnnotationSelected, setAnnotationSelected, getFirstAnnotationSelected }; diff --git a/extensions/cornerstone/src/utils/mpr/getProtocolViewportStructureFromGridViewports.ts b/extensions/cornerstone/src/utils/mpr/getProtocolViewportStructureFromGridViewports.ts new file mode 100644 index 0000000..331c901 --- /dev/null +++ b/extensions/cornerstone/src/utils/mpr/getProtocolViewportStructureFromGridViewports.ts @@ -0,0 +1,74 @@ +/** + * Given the ViewportGridService state it will re create the protocol viewport structure + * that was used at the hanging protocol creation time. This is used to re create the + * viewport structure when the user decides to go back to a previous cached + * layout in the viewport grid. + * + * + * viewportGrid's viewports look like + * + * viewports = [ + * { + * displaySetInstanceUIDs: string[], + * displaySetOptions: [], + * viewportOptions: {} + * height: number, + * width: number, + * x: number, + * y: number + * }, + * ] + * + * and hanging protocols viewport structure looks like + * + * viewportStructure: { + * layoutType: 'grid', + * properties: { + * rows: 3, + * columns: 4, + * layoutOptions: [ + * { + * x: 0, + * y: 0, + * width: 1 / 4, + * height: 1 / 3, + * }, + * { + * x: 1 / 4, + * y: 0, + * width: 1 / 4, + * height: 1 / 3, + * }, + * ], + * }, + * }, + */ +export default function getProtocolViewportStructureFromGridViewports({ + numRows, + numCols, + viewports, +}: { + numRows: number; + numCols: number; + viewports: any[]; +}) { + const viewportStructure = { + layoutType: 'grid', + properties: { + rows: numRows, + columns: numCols, + layoutOptions: [], + }, + }; + + viewports.forEach(viewport => { + viewportStructure.properties.layoutOptions.push({ + x: viewport.x, + y: viewport.y, + width: viewport.width, + height: viewport.height, + }); + }); + + return viewportStructure; +} diff --git a/extensions/cornerstone/src/utils/nthLoader.ts b/extensions/cornerstone/src/utils/nthLoader.ts new file mode 100644 index 0000000..90aab65 --- /dev/null +++ b/extensions/cornerstone/src/utils/nthLoader.ts @@ -0,0 +1,78 @@ +import { cache, imageLoadPoolManager, Enums } from '@cornerstonejs/core'; +import getNthFrames from './getNthFrames'; +import interleave from './interleave'; + +// Map of volumeId and SeriesInstanceId +const volumeIdMapsToLoad = new Map(); +const viewportIdVolumeInputArrayMap = new Map(); + +/** + * This function caches the volumeUIDs until all the volumes inside the + * hanging protocol are initialized. Then it goes through the requests and + * chooses a sub-selection starting the the first few objects, center objects + * and last objects, and then the remaining nth images until all instances are + * retrieved. This causes the image to have a progressive load order and looks + * visually much better. + * @param {Object} props image loading properties from Cornerstone ViewportService + */ +export default function interleaveNthLoader({ + data: { viewportId, volumeInputArray }, + displaySetsMatchDetails, +}) { + viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray); + + // Based on the volumeInputs store the volumeIds and SeriesInstanceIds + // to keep track of the volumes being loaded + for (const volumeInput of volumeInputArray) { + const { volumeId } = volumeInput; + const volume = cache.getVolume(volumeId); + + if (!volume) { + console.log("interleaveNthLoader::No volume, can't load it"); + return; + } + + // if the volumeUID is not in the volumeUIDs array, add it + if (!volumeIdMapsToLoad.has(volumeId)) { + const { metadata } = volume; + volumeIdMapsToLoad.set(volumeId, metadata.SeriesInstanceUID); + } + } + + const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice(); + // get volumes from cache + const volumes = volumeIds.map(volumeId => { + return cache.getVolume(volumeId); + }); + + // iterate over all volumes, and get their imageIds, and interleave + // the imageIds and save them in AllRequests for later use + const originalRequests = volumes + .map(volume => volume.getImageLoadRequests()) + .filter(requests => requests?.[0]?.imageId); + + const orderedRequests = originalRequests.map(request => getNthFrames(request)); + + // set the finalRequests to the imageLoadPoolManager + const finalRequests = interleave(orderedRequests); + + const requestType = Enums.RequestType.Prefetch; + const priority = 0; + + finalRequests.forEach(({ callLoadImage, additionalDetails, imageId, imageIdIndex, options }) => { + const callLoadImageBound = callLoadImage.bind(null, imageId, imageIdIndex, options); + + imageLoadPoolManager.addRequest(callLoadImageBound, requestType, additionalDetails, priority); + }); + + // clear the volumeIdMapsToLoad + volumeIdMapsToLoad.clear(); + + // copy the viewportIdVolumeInputArrayMap + const viewportIdVolumeInputArrayMapCopy = new Map(viewportIdVolumeInputArrayMap); + + // reset the viewportIdVolumeInputArrayMap + viewportIdVolumeInputArrayMap.clear(); + + return viewportIdVolumeInputArrayMapCopy; +} diff --git a/extensions/cornerstone/src/utils/presentations/getViewportPresentations.ts b/extensions/cornerstone/src/utils/presentations/getViewportPresentations.ts new file mode 100644 index 0000000..9fcf0f4 --- /dev/null +++ b/extensions/cornerstone/src/utils/presentations/getViewportPresentations.ts @@ -0,0 +1,32 @@ +import { usePositionPresentationStore } from '../../stores/usePositionPresentationStore'; +import { useLutPresentationStore } from '../../stores/useLutPresentationStore'; +import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore'; + +export function getViewportPresentations( + viewportId: string, + viewportOptions: AppTypes.ViewportGrid.GridViewportOptions +) { + const { lutPresentationStore } = useLutPresentationStore.getState(); + const { positionPresentationStore } = usePositionPresentationStore.getState(); + const { segmentationPresentationStore } = useSegmentationPresentationStore.getState(); + + // NOTE: this is the new viewport state, we should not get the presentationIds from the cornerstoneViewportService + // since that has the old viewport state + const { presentationIds } = viewportOptions; + + if (!presentationIds) { + return { + positionPresentation: null, + lutPresentation: null, + segmentationPresentation: null, + }; + } + + const { lutPresentationId, positionPresentationId, segmentationPresentationId } = presentationIds; + + return { + positionPresentation: positionPresentationStore[positionPresentationId], + lutPresentation: lutPresentationStore[lutPresentationId], + segmentationPresentation: segmentationPresentationStore[segmentationPresentationId], + }; +} diff --git a/extensions/cornerstone/src/utils/removeViewportSegmentationRepresentations.ts b/extensions/cornerstone/src/utils/removeViewportSegmentationRepresentations.ts new file mode 100644 index 0000000..edc7fc6 --- /dev/null +++ b/extensions/cornerstone/src/utils/removeViewportSegmentationRepresentations.ts @@ -0,0 +1,17 @@ +import { segmentation } from '@cornerstonejs/tools'; + +function removeViewportSegmentationRepresentations(viewportId) { + const representations = segmentation.state.getSegmentationRepresentations(viewportId); + + if (!representations || !representations.length) { + return; + } + + representations.forEach(representation => { + segmentation.state.removeSegmentationRepresentation( + representation.segmentationRepresentationUID + ); + }); +} + +export default removeViewportSegmentationRepresentations; diff --git a/extensions/cornerstone/src/utils/segmentUtils.ts b/extensions/cornerstone/src/utils/segmentUtils.ts new file mode 100644 index 0000000..dd43734 --- /dev/null +++ b/extensions/cornerstone/src/utils/segmentUtils.ts @@ -0,0 +1,47 @@ +import SegmentationServiceType from '../services/SegmentationService'; + +export const handleSegmentChange = ({ + direction, + segDisplaySet, + viewportId, + selectedSegmentObjectIndex, + segmentationService, +}: { + direction: number; + segDisplaySet: AppTypes.DisplaySet; + viewportId: string; + selectedSegmentObjectIndex: number; + segmentationService: SegmentationServiceType; +}) => { + const segmentationId = segDisplaySet.displaySetInstanceUID; + const segmentation = segmentationService.getSegmentation(segmentationId); + + const { segments } = segmentation; + + const numberOfSegments = Object.keys(segments).length; + + // Get activeSegment each time because the user can select any segment from the list and thus the index should be updated + const activeSegment = segmentationService.getActiveSegment(viewportId); + if (activeSegment) { + // from the activeSegment get the actual object array index to be used + selectedSegmentObjectIndex = Object.values(segments).findIndex( + segment => segment.segmentIndex === activeSegment.segmentIndex + ); + } + let newSelectedSegmentIndex = selectedSegmentObjectIndex + direction; + + // Handle looping through list of segments + if (newSelectedSegmentIndex > numberOfSegments - 1) { + newSelectedSegmentIndex = 0; + } else if (newSelectedSegmentIndex < 0) { + newSelectedSegmentIndex = numberOfSegments - 1; + } + + // Convert segmentationId from object array index to property value of type Segment + // Functions below use the segmentIndex object attribute so we have to do the conversion + const segmentIndex = Object.values(segments)[newSelectedSegmentIndex]?.segmentIndex; + + segmentationService.setActiveSegment(segmentationId, segmentIndex); + segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex, viewportId); + selectedSegmentObjectIndex = newSelectedSegmentIndex; +}; diff --git a/extensions/cornerstone/src/utils/toggleVOISliceSync.ts b/extensions/cornerstone/src/utils/toggleVOISliceSync.ts new file mode 100644 index 0000000..57e23d1 --- /dev/null +++ b/extensions/cornerstone/src/utils/toggleVOISliceSync.ts @@ -0,0 +1,99 @@ +import { DisplaySetService, ViewportGridService } from '@ohif/core'; + +const VOI_SYNC_NAME = 'VOI_SYNC'; + +const getSyncId = modality => `${VOI_SYNC_NAME}_${modality}`; + +export default function toggleVOISliceSync({ + servicesManager, + viewports: providedViewports, + syncId, +}: withAppTypes) { + const { syncGroupService, viewportGridService, displaySetService, cornerstoneViewportService } = + servicesManager.services; + + const viewports = + providedViewports || groupViewportsByModality(viewportGridService, displaySetService); + + // Todo: right now we don't have a proper way to define specific + // viewports to add to synchronizers, and right now it is global or not + // after we do that, we should do fine grained control of the synchronizers + + // we can apply voi sync within each modality group + for (const [modality, modalityViewports] of Object.entries(viewports)) { + const syncIdToUse = syncId || getSyncId(modality); + + const someViewportHasSync = modalityViewports.some(viewport => { + const syncStates = syncGroupService.getSynchronizersForViewport( + viewport.viewportOptions.viewportId + ); + + const imageSync = syncStates.find(syncState => syncState.id === syncIdToUse); + + return !!imageSync; + }); + + if (someViewportHasSync) { + return disableSync(modalityViewports, syncIdToUse, servicesManager); + } + + // create synchronization group and add the modalityViewports to it. + modalityViewports.forEach(gridViewport => { + const { viewportId } = gridViewport.viewportOptions; + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + if (!viewport) { + return; + } + syncGroupService.addViewportToSyncGroup(viewportId, viewport.getRenderingEngine().id, { + type: 'voi', + id: syncIdToUse, + source: true, + target: true, + }); + }); + } +} + +function disableSync(modalityViewports, syncId, servicesManager: AppTypes.ServicesManager) { + const { syncGroupService, cornerstoneViewportService } = servicesManager.services; + + const viewports = modalityViewports; + viewports.forEach(gridViewport => { + const { viewportId } = gridViewport.viewportOptions; + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + if (!viewport) { + return; + } + syncGroupService.removeViewportFromSyncGroup( + viewport.id, + viewport.getRenderingEngine().id, + syncId + ); + }); +} + +function groupViewportsByModality( + viewportGridService: ViewportGridService, + displaySetService: DisplaySetService +) { + let { viewports } = viewportGridService.getState(); + + viewports = [...viewports.values()]; + + // group the viewports by modality + return viewports.reduce((acc, viewport) => { + const { displaySetInstanceUIDs } = viewport; + // Todo: add proper fusion support + const displaySetInstanceUID = displaySetInstanceUIDs[0]; + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + + const modality = displaySet.Modality; + if (!acc[modality]) { + acc[modality] = []; + } + + acc[modality].push(viewport); + + return acc; + }, {}); +} diff --git a/extensions/cornerstone/src/utils/transitions.ts b/extensions/cornerstone/src/utils/transitions.ts new file mode 100644 index 0000000..7d63f26 --- /dev/null +++ b/extensions/cornerstone/src/utils/transitions.ts @@ -0,0 +1,63 @@ +/** + * It is a bell curved function that uses ease in out quadratic for css + * transition timing function for each side of the curve. + * + * @param {number} x - The current time, in the range [0, 1]. + * @param {number} baseline - The baseline value to start from and return to. + * @returns the value of the transition at time x. + */ +export function easeInOutBell(x: number, baseline: number): number { + const alpha = 1 - baseline; + + // prettier-ignore + if (x < 1 / 4) { + return 4 * Math.pow(2 * x, 3) * alpha + baseline; + } else if (x < 1 / 2) { + return (1 - Math.pow(-4 * x + 2, 3) / 2) * alpha + baseline; + } else if (x < 3 / 4) { + return (1 - Math.pow(4 * x - 2, 3) / 2) * alpha + baseline; + } else { + return (- 4 * Math.pow(2 * x - 2, 3)) * alpha + baseline; + } +} + +/** + * A reversed bell curved function that starts from 1 and goes to baseline and + * come back to 1 again. It uses ease in out quadratic for css transition + * timing function for each side of the curve. + * + * @param {number} x - The current time, in the range [0, 1]. + * @param {number} baseline - The baseline value to start from and return to. + * @returns the value of the transition at time x. + */ +export function reverseEaseInOutBell(x: number, baseline: number): number { + const y = easeInOutBell(x, baseline); + return -y + 1 + baseline; +} + +export function easeInOutBellRelative( + x: number, + baseline: number, + prevOutlineWidth: number +): number { + const range = baseline - prevOutlineWidth; + + if (x < 1 / 4) { + return prevOutlineWidth + 4 * Math.pow(2 * x, 3) * range; + } else if (x < 1 / 2) { + return prevOutlineWidth + (1 - Math.pow(-4 * x + 2, 3) / 2) * range; + } else if (x < 3 / 4) { + return prevOutlineWidth + (1 - Math.pow(4 * x - 2, 3) / 2) * range; + } else { + return prevOutlineWidth + -4 * Math.pow(2 * x - 2, 3) * range; + } +} + +export function reverseEaseInOutBellRelative( + x: number, + baseline: number, + prevOutlineWidth: number +): number { + const y = easeInOutBellRelative(x, baseline, prevOutlineWidth); + return y; +} diff --git a/extensions/default/.webpack/webpack.dev.js b/extensions/default/.webpack/webpack.dev.js new file mode 100644 index 0000000..6aea859 --- /dev/null +++ b/extensions/default/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/extensions/default/.webpack/webpack.prod.js b/extensions/default/.webpack/webpack.prod.js new file mode 100644 index 0000000..1151efc --- /dev/null +++ b/extensions/default/.webpack/webpack.prod.js @@ -0,0 +1,54 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +const pkg = require('./../package.json'); + +const ROOT_DIR = path.join(__dirname, '../'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); +const ENTRY = { + app: `${SRC_DIR}/index.ts`, +}; + +const outputName = `ohif-${pkg.name.split('/').pop()}`; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: true, + }, + output: { + path: ROOT_DIR, + library: 'ohif-extension-default', + libraryTarget: 'umd', + filename: pkg.main, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + new MiniCssExtractPlugin({ + filename: `./dist/${outputName}.css`, + chunkFilename: `./dist/${outputName}.css`, + }), + ], + }); +}; diff --git a/extensions/default/CHANGELOG.md b/extensions/default/CHANGELOG.md new file mode 100644 index 0000000..2f01785 --- /dev/null +++ b/extensions/default/CHANGELOG.md @@ -0,0 +1,3355 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + + +### Bug Fixes + +* **sr:** sr hydration and load was not working, Screenshot Comparison, and Testing ([#4814](https://github.com/OHIF/Viewers/issues/4814)) ([9233143](https://github.com/OHIF/Viewers/commit/9233143b9da5850080365e1526e24b44e9910075)) + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + + +### Features + +* **hotkeys:** Migrate hotkeys to customization service and fix issues with overrides ([#4777](https://github.com/OHIF/Viewers/issues/4777)) ([3e6913b](https://github.com/OHIF/Viewers/commit/3e6913b097569280a5cc2fa5bbe4add52f149305)) + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + + +### Bug Fixes + +* cache thumbnail in display set ([#4782](https://github.com/OHIF/Viewers/issues/4782)) ([2410c6a](https://github.com/OHIF/Viewers/commit/2410c6a50904c1235993900e837876cc26af019b)) + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + + +### Bug Fixes + +* lodash dependencies ([#4791](https://github.com/OHIF/Viewers/issues/4791)) ([4e16099](https://github.com/OHIF/Viewers/commit/4e16099ad3ab777b09f6ac8f181025cfd656ab6b)) + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + + +### Features + +* improve dicom tag browser with nested rows ([#4451](https://github.com/OHIF/Viewers/issues/4451)) ([0b5836c](https://github.com/OHIF/Viewers/commit/0b5836ca1a908e152336752672b196f0d533f4f9)) + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + + +### Bug Fixes + +* add commandsManager to MoreDropdownMenu onClick props ([#4765](https://github.com/OHIF/Viewers/issues/4765)) ([bbf1a19](https://github.com/OHIF/Viewers/commit/bbf1a19676b2b345a0f911dde319e5ffefe29fa6)) + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + + +### Features + +* **ui:** customization option for viewport notification ([#4638](https://github.com/OHIF/Viewers/issues/4638)) ([8acbd76](https://github.com/OHIF/Viewers/commit/8acbd760d801dcaf624c5d9fb636a029201b91e1)) + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + + +### Bug Fixes + +* **measurement label auto-completion:** Customization of measurement label auto-completion fails for measurements following arrow annotations. ([#4739](https://github.com/OHIF/Viewers/issues/4739)) ([e035ef1](https://github.com/OHIF/Viewers/commit/e035ef1dcc72ecbe2a757e3b814551d768d7e610)) + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + + +### Bug Fixes + +* **store-segmentation:** storing segmentations was hitting the wrong command resulting in an undefined datasource ([#4755](https://github.com/OHIF/Viewers/issues/4755)) ([9b8e5cf](https://github.com/OHIF/Viewers/commit/9b8e5cfd1a6121a58991c0f75660a2fd9913a4e7)) + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + + +### Features + +* Add customization support for more UI components ([#4634](https://github.com/OHIF/Viewers/issues/4634)) ([f15eb44](https://github.com/OHIF/Viewers/commit/f15eb44b4cf49de1b73a22512571cec02effaef3)) + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + + +### Features + +* **customization:** enable custom onDropHandler for viewportGrid ([#4641](https://github.com/OHIF/Viewers/issues/4641)) ([054b262](https://github.com/OHIF/Viewers/commit/054b262e9cbeb0f44de65d05641efe1e8944a4f5)) + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + + +### Bug Fixes + +* **dependencies:** Update dcmjs library and improve documentation links ([#4741](https://github.com/OHIF/Viewers/issues/4741)) ([d554f02](https://github.com/OHIF/Viewers/commit/d554f02f7cdb876e4132fb94e3b3df8d11b7bb5c)) + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + + +### Features + +* **side-panels:** Added resize handle interactive feedback for side panels ([#4734](https://github.com/OHIF/Viewers/issues/4734)) ([6abb095](https://github.com/OHIF/Viewers/commit/6abb095b8a39c5ae4f8df8852b3ddb3249ec463f)) + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + + +### Features + +* **static-wado:** add support for case-insensitive searching ([#4603](https://github.com/OHIF/Viewers/issues/4603)) ([ac6e674](https://github.com/OHIF/Viewers/commit/ac6e674b4d094f942556d045178011bbf3f81796)) + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + + +### Features + +* **customization:** new customization service api ([#4688](https://github.com/OHIF/Viewers/issues/4688)) ([55ad8ef](https://github.com/OHIF/Viewers/commit/55ad8efbabc3fabd8031fc08927b2f92ae5aec69)) + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + + +### Features + +* **panels:** responsive thumbnails based on panel size ([#4723](https://github.com/OHIF/Viewers/issues/4723)) ([d9abc3d](https://github.com/OHIF/Viewers/commit/d9abc3da8d94d6c5ab0cc5af25a5f61849905a35)) + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + + +### Bug Fixes + +* **seg:** sphere scissor on stack and cpu rendering reset properties was broken ([#4721](https://github.com/OHIF/Viewers/issues/4721)) ([f00d182](https://github.com/OHIF/Viewers/commit/f00d18292f02e8910215d913edfc994850a68d88)) + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + + +### Features + +* **resizable-side-panels:** Make the left and right side panels (optionally) resizable. ([#4672](https://github.com/OHIF/Viewers/issues/4672)) ([d90a4cf](https://github.com/OHIF/Viewers/commit/d90a4cfb16cc0daed9b905de9780f44cca1323f9)) + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + + +### Bug Fixes + +* **ui:** Update dependencies and add missing icons ([#4699](https://github.com/OHIF/Viewers/issues/4699)) ([cf97fa9](https://github.com/OHIF/Viewers/commit/cf97fa9b7b9687a9b73c1cf6926bc9fbc39b6512)) + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + + +### Bug Fixes + +* Inconsistent Handling of Patient Name Tag ([#4703](https://github.com/OHIF/Viewers/issues/4703)) ([8aedb2e](https://github.com/OHIF/Viewers/commit/8aedb2ec54a0ccf2550f745fed6f0b8aa184a860)) + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + + +### Bug Fixes + +* **hp:** Display set should allow remembered updates ([#4707](https://github.com/OHIF/Viewers/issues/4707)) ([464148e](https://github.com/OHIF/Viewers/commit/464148ece66b48b583dc6e998ca4d11c66746f3a)) + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + + +### Bug Fixes + +* context menu icon ([#4696](https://github.com/OHIF/Viewers/issues/4696)) ([1993161](https://github.com/OHIF/Viewers/commit/19931614dc53da440718e512d39a87ca9118b96e)) + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + + +### Bug Fixes + +* **multiframe:** handling proxies properly ([#4693](https://github.com/OHIF/Viewers/issues/4693)) ([ec4b5a6](https://github.com/OHIF/Viewers/commit/ec4b5a6876cea77278e5cffaf4108eeeefdc57dc)) + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + + +### Bug Fixes + +* bugs after multimonitor ([#4680](https://github.com/OHIF/Viewers/issues/4680)) ([c901a84](https://github.com/OHIF/Viewers/commit/c901a847af75d356509366c695ea46ff4f4bcdaf)) + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + + +### Features + +* **multimonitor:** Add simple multi-monitor support to open another study([#4178](https://github.com/OHIF/Viewers/issues/4178)) ([07c628e](https://github.com/OHIF/Viewers/commit/07c628e689b28f831317a7c28d712509b69c6b13)) + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + + +### Features + +* **dev:** move to rsbuild for dev - faster ([#4674](https://github.com/OHIF/Viewers/issues/4674)) ([d4a4267](https://github.com/OHIF/Viewers/commit/d4a4267429c02916dd51f6aefb290d96dd1c3b04)) + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + + +### Features + +* Start using group filtering to define measurements table layout ([#4501](https://github.com/OHIF/Viewers/issues/4501)) ([82440e8](https://github.com/OHIF/Viewers/commit/82440e88d5debe808f0b14281b77e430c2489779)) + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + + +### Bug Fixes + +* Convert Rows and Columns to numbers before comparison ([#4654](https://github.com/OHIF/Viewers/issues/4654)) ([#4656](https://github.com/OHIF/Viewers/issues/4656)) ([2f5076e](https://github.com/OHIF/Viewers/commit/2f5076ece8b3125c3426014efdf7fc6b498606d0)) + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + + +### Bug Fixes + +* **context menu:** restrict the context menu accessibility for locked and hidden annotations ([#4625](https://github.com/OHIF/Viewers/issues/4625)) ([e11ceb9](https://github.com/OHIF/Viewers/commit/e11ceb9d20fa5e680a0247f6ca7c27825daea6c5)) + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + + +### Bug Fixes + +* Docker build time was very slow on a tiny change ([#4559](https://github.com/OHIF/Viewers/issues/4559)) ([7e43b2f](https://github.com/OHIF/Viewers/commit/7e43b2f768cfc3e08ecde9dfdae275194daece2b)) + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + + +### Bug Fixes + +* **datasource:** attach auth headers for delete requests in the dicomweb datasource ([#4619](https://github.com/OHIF/Viewers/issues/4619)) ([8d0ed80](https://github.com/OHIF/Viewers/commit/8d0ed80e0c4570ab799281c29e487dbb39f47b95)) + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + + +### Bug Fixes + +* **segmentation:** black preview when loading a seg, and crash on opening panel ([#4602](https://github.com/OHIF/Viewers/issues/4602)) ([faf5515](https://github.com/OHIF/Viewers/commit/faf5515e4b93da58b673f1ae59ec345e30870446)) + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + + +### Features + +* **measurements:** Provide for the Load (SR) measurements button to optionally clear existing measurements prior to loading the SR. ([#4586](https://github.com/OHIF/Viewers/issues/4586)) ([4d3d5e7](https://github.com/OHIF/Viewers/commit/4d3d5e794cb99212eba06bf91dbb30a258725efe)) + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + + +### Bug Fixes + +* Documentation and default enabled for bulkdata load ([#4607](https://github.com/OHIF/Viewers/issues/4607)) ([d0ccdbd](https://github.com/OHIF/Viewers/commit/d0ccdbd68db1dcb190b5a288dd455f573eddc280)) + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + + +### Features + +* migrate icons to ui-next ([#4606](https://github.com/OHIF/Viewers/issues/4606)) ([4e2ae32](https://github.com/OHIF/Viewers/commit/4e2ae328744ed95589c2cdf7a531454a25bf88b5)) + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + + +### Bug Fixes + +* **seg:** jump to the first slice in SEG and RT that has data ([#4605](https://github.com/OHIF/Viewers/issues/4605)) ([9bf24d6](https://github.com/OHIF/Viewers/commit/9bf24d6dc58ed8f65c90899a17c11044b792cf40)) + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + + +### Bug Fixes + +* **tag-browser:** fix dicom tag browser not loading in segmentation mode in study panel ([#4601](https://github.com/OHIF/Viewers/issues/4601)) ([60fc7d6](https://github.com/OHIF/Viewers/commit/60fc7d6a112da99b47e26c5e3460b920bbc3c0b0)) + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + + +### Bug Fixes + +* **CinePlayer:** always show cine player for dynamic data ([#4575](https://github.com/OHIF/Viewers/issues/4575)) ([b8e8bbe](https://github.com/OHIF/Viewers/commit/b8e8bbe482b66e8cbe9167d03e9d8dedd2d3b6c5)) + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + + +### Bug Fixes + +* **multiframe:** metadata handling of NM studies and loading order ([#4554](https://github.com/OHIF/Viewers/issues/4554)) ([7624ccb](https://github.com/OHIF/Viewers/commit/7624ccb5e495c0a151227a458d8d5bfb8babb22c)) + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + + +### Features + +* **segmentation:** Enhance dropdown menu functionality in SegmentationTable ([#4553](https://github.com/OHIF/Viewers/issues/4553)) ([397fd85](https://github.com/OHIF/Viewers/commit/397fd856539cd3b949a9614a9ea32d0d04a90000)) + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + + +### Bug Fixes + +* Have an addIcon that adds to both ui and ui-next ([#4490](https://github.com/OHIF/Viewers/issues/4490)) ([4a12523](https://github.com/OHIF/Viewers/commit/4a125236ddbf8a4a95fb9c5820f511d0224e663f)) +* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed)) + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + + +### Bug Fixes + +* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30)) + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + + +### Bug Fixes + +* **tmtv:** fix toggle one up weird behaviours ([#4473](https://github.com/OHIF/Viewers/issues/4473)) ([aa2b649](https://github.com/OHIF/Viewers/commit/aa2b649444eb4fe5422e72ea7830a709c4d24a90)) + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + + +### Features + +* **new-study-panel:** default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser ([#4417](https://github.com/OHIF/Viewers/issues/4417)) ([a7fd9fa](https://github.com/OHIF/Viewers/commit/a7fd9fa5bfff7a1b533d99cb96f7147a35fd528f)) + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + + +### Bug Fixes + +* **createReport:** early return on cancel in prompt ([#4243](https://github.com/OHIF/Viewers/issues/4243)) ([2ec4692](https://github.com/OHIF/Viewers/commit/2ec4692eaf2349e21b141a2c0b5b104ee10f7a28)) +* **dicomjson:** Update getUIDsFromImageID to work with json data source + update getDisplaySetImageUIDs to work with mixed sop class json ([#4322](https://github.com/OHIF/Viewers/issues/4322)) ([3dd0666](https://github.com/OHIF/Viewers/commit/3dd0666c0c090cbd66161f24bc9795f96abb3697)) + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + + +### Features + +* **studies-panel:** New OHIF study panel - under experimental flag ([#4254](https://github.com/OHIF/Viewers/issues/4254)) ([7a96406](https://github.com/OHIF/Viewers/commit/7a96406a116e46e62c396855fa64f434e2984b58)) + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + + +### Bug Fixes + +* Add kheops integration into OHIF v3 again ([#4345](https://github.com/OHIF/Viewers/issues/4345)) ([e1feffa](https://github.com/OHIF/Viewers/commit/e1feffa42553d6c8650a4aceb09f72c637126660)) + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + + +### Bug Fixes + +* ๐Ÿ› SeriesInstanceUID fallback + update retrieve metadata filtered to check for lazy ([#4346](https://github.com/OHIF/Viewers/issues/4346)) ([14498d4](https://github.com/OHIF/Viewers/commit/14498d4e9a6a57324b8be9f0b314f2901459dc4a)) + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + + +### Features + +* Add CS3D WSI and Video Viewports and add annotation navigation for MPR ([#4182](https://github.com/OHIF/Viewers/issues/4182)) ([7599ec9](https://github.com/OHIF/Viewers/commit/7599ec9421129dcade94e6fa6ec7908424ab3134)) + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + + +### Bug Fixes + +* **url:** series query param filtering ([#4328](https://github.com/OHIF/Viewers/issues/4328)) ([9b10303](https://github.com/OHIF/Viewers/commit/9b10303a2efa809096156d4a2322b2b46f160a91)) + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + + +### Bug Fixes + +* **dicom:** Update multiframe DICOM JSON parsing for correct image ID generation ([#4307](https://github.com/OHIF/Viewers/issues/4307)) ([16b7aa4](https://github.com/OHIF/Viewers/commit/16b7aa4f6538b81e5915e47b9209d74575895dfe)) + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + + +### Features + +* **pmap:** added support for parametric map ([#4284](https://github.com/OHIF/Viewers/issues/4284)) ([fc0064f](https://github.com/OHIF/Viewers/commit/fc0064fd9d8cdc8fde81b81f0e71fd5d077ca22b)) + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + + +### Features + +* Add interleaved HTJ2K and volume progressive loading ([#4276](https://github.com/OHIF/Viewers/issues/4276)) ([a2084f3](https://github.com/OHIF/Viewers/commit/a2084f319b731d98b59485799fb80357094f8c38)) + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + + +### Features + +* **studyPrefetcher:** Study Prefetcher ([#4206](https://github.com/OHIF/Viewers/issues/4206)) ([2048b19](https://github.com/OHIF/Viewers/commit/2048b19484c0b1fae73f993cfaa814f861bbd230)) + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + + +### Bug Fixes + +* **orthanc:** Correct bulkdata URL handling and add configuration example PDF ([#4262](https://github.com/OHIF/Viewers/issues/4262)) ([fdf883a](https://github.com/OHIF/Viewers/commit/fdf883ada880c0979acba8fdff9b542dc05b7706)) + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) +* **CustomViewportOverlay:** pass accurate data to Custom Viewport Functions ([#4224](https://github.com/OHIF/Viewers/issues/4224)) ([aef00e9](https://github.com/OHIF/Viewers/commit/aef00e91d63e9bc2de289cc6f35975e36547fb20)) +* **studybrowser:** Differentiate recent and all in study panel based on a provided time period ([#4242](https://github.com/OHIF/Viewers/issues/4242)) ([6f93449](https://github.com/OHIF/Viewers/commit/6f9344914951c204feaff48aaeb43cd7d727623d)) + + +### Features + +* **sort:** custom series sort in study panel ([#4214](https://github.com/OHIF/Viewers/issues/4214)) ([a433d40](https://github.com/OHIF/Viewers/commit/a433d406e2cac13f644203996c682260b54e8865)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + + +### Bug Fixes + +* Use correct external URL for rendered responses with relative URI ([#4236](https://github.com/OHIF/Viewers/issues/4236)) ([d8f6991](https://github.com/OHIF/Viewers/commit/d8f6991dbe72465080cfc5de39c7ea225702f2e0)) + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + + +### Bug Fixes + +* Re-enable hpScale module ([#4237](https://github.com/OHIF/Viewers/issues/4237)) ([2eab049](https://github.com/OHIF/Viewers/commit/2eab049d7993bb834f7736093941c175f16d61fc)) + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + + +### Features + +* Add customization merge, append or replace functionality ([#3871](https://github.com/OHIF/Viewers/issues/3871)) ([55dcfa1](https://github.com/OHIF/Viewers/commit/55dcfa1f6994a7036e7e594efb23673382a41915)) + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + + +### Bug Fixes + +* get direct url pixel data should be optional for video ([#4152](https://github.com/OHIF/Viewers/issues/4152)) ([649ffab](https://github.com/OHIF/Viewers/commit/649ffab4d97be875d42e1a3473a4354aac14e87d)) + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + + +### Bug Fixes + +* **hydration:** Maintain the same slice that the user was on pre hydration in post hydration for SR and SEG. ([#4200](https://github.com/OHIF/Viewers/issues/4200)) ([430330f](https://github.com/OHIF/Viewers/commit/430330f7e384d503cb6fc695a7a9642ddfaac313)) + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + + +### Bug Fixes + +* **docker:** docker build was broken because of imports ([#4192](https://github.com/OHIF/Viewers/issues/4192)) ([d7aa386](https://github.com/OHIF/Viewers/commit/d7aa386800153e0bb9eea6bbf36c696c57750ad8)) +* segmentation creation and segmentation mode viewport rendering ([#4193](https://github.com/OHIF/Viewers/issues/4193)) ([2174026](https://github.com/OHIF/Viewers/commit/217402678981f74293dff615f6b6812e54216d37)) + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + + +### Features + +* **hp:** Add displayArea option for Hanging protocols and example with Mamo([#3808](https://github.com/OHIF/Viewers/issues/3808)) ([18ac08e](https://github.com/OHIF/Viewers/commit/18ac08ed860d119721c52e4ffc270332259100b6)) + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + + +### Bug Fixes + +* **crosshairs:** reset angle, position, and slabthickness for crosshairs when reset viewport tool is used ([#4113](https://github.com/OHIF/Viewers/issues/4113)) ([73d9e99](https://github.com/OHIF/Viewers/commit/73d9e99d5d6f38ab6c36f4471d54f18798feacb4)) + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + + +### Bug Fixes + +* **HangingProtocol:** fix hp when unsupported series load first ([#4145](https://github.com/OHIF/Viewers/issues/4145)) ([b124c91](https://github.com/OHIF/Viewers/commit/b124c91d8fa0def262d1fee8f105295b02864129)) + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + + +### Bug Fixes + +* **stack-invalidation:** Resolve stack invalidation if metadata invalidated ([#4147](https://github.com/OHIF/Viewers/issues/4147)) ([70bb6c4](https://github.com/OHIF/Viewers/commit/70bb6c46267b3733a665f12534b849c890ce54ad)) + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + + +### Bug Fixes + +* **DicomJSONDataSource:** Fix series filtering ([#4092](https://github.com/OHIF/Viewers/issues/4092)) ([2de102c](https://github.com/OHIF/Viewers/commit/2de102c73c795cfb48b49005b10aa788444a45b7)) + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + + +### Bug Fixes + +* **bugs:** enhancements and bugs in several areas ([#4086](https://github.com/OHIF/Viewers/issues/4086)) ([730f434](https://github.com/OHIF/Viewers/commit/730f4349100f21b4489a21707dbb2dca9dbfbba2)) + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + + +### Bug Fixes + +* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af)) + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes - more ([#4043](https://github.com/OHIF/Viewers/issues/4043)) ([3754c22](https://github.com/OHIF/Viewers/commit/3754c224b4dab28182adb0a41e37d890942144d8)) + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + + +### Bug Fixes + +* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025)) + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637)) + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + + +### Bug Fixes + +* **dicom-video:** Update get direct func for dicom json to use url if present and fix config argument ([#4017](https://github.com/OHIF/Viewers/issues/4017)) ([4f99244](https://github.com/OHIF/Viewers/commit/4f99244d864427d69be6f863cb7a6a78411adb12)) + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + + +### Bug Fixes + +* Microscopy bulkdata and image retrieve ([#3894](https://github.com/OHIF/Viewers/issues/3894)) ([7fac49b](https://github.com/OHIF/Viewers/commit/7fac49b4492b4bd5e9ece8e2e2b0fa2faa840d7f)) + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + + +### Features + +* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2)) + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + + +### Features + +* **advanced-roi-tools:** new tools and icon updates and overlay bug fixes ([#4014](https://github.com/OHIF/Viewers/issues/4014)) ([cea27d4](https://github.com/OHIF/Viewers/commit/cea27d438d1de2c1ec90cbaefdc2b31a1d9980a1)) + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + + +### Features + +* **segmentation:** Enhanced segmentation panel design for TMTV ([#3988](https://github.com/OHIF/Viewers/issues/3988)) ([9f3235f](https://github.com/OHIF/Viewers/commit/9f3235ff096636aafa88d8a42859e8dc85d9036d)) + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + + +### Features + +* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1)) + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + + +### Bug Fixes + +* **new layout:** address black screen bugs ([#4008](https://github.com/OHIF/Viewers/issues/4008)) ([158a181](https://github.com/OHIF/Viewers/commit/158a1816703e0ad66cae08cb9bd1ffb93bbd8d43)) + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + + +### Features + +* **layout:** new layout selector with 3D volume rendering ([#3923](https://github.com/OHIF/Viewers/issues/3923)) ([617043f](https://github.com/OHIF/Viewers/commit/617043fe0da5de91fbea4ac33a27f1df16ae1ca6)) + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + + +### Features + +* **worklist:** new investigational use text ([#3999](https://github.com/OHIF/Viewers/issues/3999)) ([45b68e8](https://github.com/OHIF/Viewers/commit/45b68e841dcb9e28a2ea991c37ee7ac4a8c5b71e)) + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + + +### Bug Fixes + +* **demo:** Deploy issue ([#3951](https://github.com/OHIF/Viewers/issues/3951)) ([21e8a2b](https://github.com/OHIF/Viewers/commit/21e8a2bd0b7cc72f90a31e472d285d761be15d30)) + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + + +### Bug Fixes + +* ๐Ÿ› Sort merge results based on default data source (input) ([#3903](https://github.com/OHIF/Viewers/issues/3903)) ([5bba98e](https://github.com/OHIF/Viewers/commit/5bba98ed848bdf46b5ba4fc4708527cced3308b5)) + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + + +### Bug Fixes + +* catch errors in getPTImageIdInstanceMetadata ([#3897](https://github.com/OHIF/Viewers/issues/3897)) ([a47aeb8](https://github.com/OHIF/Viewers/commit/a47aeb8bd729dcb8d2cfc13b27a31b0dd88f11ad)) + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + + +### Bug Fixes + +* ๐Ÿ› Check merge key for merge data source ([#3901](https://github.com/OHIF/Viewers/issues/3901)) ([911d672](https://github.com/OHIF/Viewers/commit/911d67283536b2fe7930948f2819ea0ad66e2a32)) + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + + +### Features + +* **hp:** enable OHIF to run with partial metadata for large studies at the cost of less effective hanging protocol ([#3804](https://github.com/OHIF/Viewers/issues/3804)) ([0049f4c](https://github.com/OHIF/Viewers/commit/0049f4c0303f0b6ea995972326fc8784259f5a47)) + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + + +### Features + +* **ui:** sidePanel expandedWidth ([#3728](https://github.com/OHIF/Viewers/issues/3728)) ([61bf22c](https://github.com/OHIF/Viewers/commit/61bf22c6f80e764bdf5c3b56bb0124a95aa0f793)) + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + + +### Features + +* improve disableEditing flag ([#3875](https://github.com/OHIF/Viewers/issues/3875)) ([2049c09](https://github.com/OHIF/Viewers/commit/2049c0936c86f819604c243d3dc7b3fe971b5b2c)) + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + + +### Bug Fixes + +* PDF display request in v3 ([#3878](https://github.com/OHIF/Viewers/issues/3878)) ([9865030](https://github.com/OHIF/Viewers/commit/98650302c7575f0aea386e32cfc4112c378035e6)) + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + + +### Features + +* **customizationService:** Enable saving and loading of private tags in SRs ([#3842](https://github.com/OHIF/Viewers/issues/3842)) ([e1f55e6](https://github.com/OHIF/Viewers/commit/e1f55e65f2d2a34136ad5d0b1ada77d337a0ea23)) + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + + +### Features + +* **i18n:** enhanced i18n support ([#3761](https://github.com/OHIF/Viewers/issues/3761)) ([d14a8f0](https://github.com/OHIF/Viewers/commit/d14a8f0199db95cd9e85866a011b64d6bf830d57)) + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + + +### Bug Fixes + +* **SM:** drag and drop is now fixed for SM ([#3813](https://github.com/OHIF/Viewers/issues/3813)) ([f1a6764](https://github.com/OHIF/Viewers/commit/f1a67647aed635437b188cea7cf5d5a8fb974bbe)) + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + + +### Features + +* Merge Data Source ([#3788](https://github.com/OHIF/Viewers/issues/3788)) ([c4ff2c2](https://github.com/OHIF/Viewers/commit/c4ff2c2f09546ce8b72eab9c5e7beed611e3cab0)) + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + + +### Features + +* **url:** Add SeriesInstanceUIDs wado query param ([#3746](https://github.com/OHIF/Viewers/issues/3746)) ([b694228](https://github.com/OHIF/Viewers/commit/b694228dd535e4b97cb86a1dc085b6e8716bdaf3)) + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + + +### Features + +* **dicomJSON:** Add Loading Other Display Sets and JSON Metadata Generation script ([#3777](https://github.com/OHIF/Viewers/issues/3777)) ([43b1c17](https://github.com/OHIF/Viewers/commit/43b1c17209502e4876ad59bae09ed9442eda8024)) + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + + +### Features + +* **hp callback:** Add viewport ready callback ([#3772](https://github.com/OHIF/Viewers/issues/3772)) ([bf252bc](https://github.com/OHIF/Viewers/commit/bf252bcec2aae3a00479fdcb732110b344bcf2c0)) + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + + +### Bug Fixes + +* **thumbnail:** Avoid multiple promise creations for thumbnails ([#3756](https://github.com/OHIF/Viewers/issues/3756)) ([b23eeff](https://github.com/OHIF/Viewers/commit/b23eeff93745769e67e60c33d75293d6242c5ec9)) + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + + +### Features + +* **i18n:** enhanced i18n support ([#3730](https://github.com/OHIF/Viewers/issues/3730)) ([330e11c](https://github.com/OHIF/Viewers/commit/330e11c7ff0151e1096e19b8ffdae7d64cae280e)) + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + + +### Bug Fixes + +* **toolbar:** allow customizable toolbar for active viewport and allow active tool to be deactivated via a click ([#3608](https://github.com/OHIF/Viewers/issues/3608)) ([dd6d976](https://github.com/OHIF/Viewers/commit/dd6d9768bbca1d3cc472e8c1e6d85822500b96ef)) + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + + +### Bug Fixes + +* **sr:** dcm4chee requires the patient name for an SR to match what is in the original study ([#3739](https://github.com/OHIF/Viewers/issues/3739)) ([d98439f](https://github.com/OHIF/Viewers/commit/d98439fe7f3825076dbc87b664a1d1480ff414d3)) + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + + +### Bug Fixes + +* **display messages:** broken after timings ([#3719](https://github.com/OHIF/Viewers/issues/3719)) ([157b88c](https://github.com/OHIF/Viewers/commit/157b88c909d3289cb89ace731c1f9a19d40797ac)) + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + + +### Bug Fixes + +* **export:** wrong export for the tmtv RT function ([#3715](https://github.com/OHIF/Viewers/issues/3715)) ([a3f2a1a](https://github.com/OHIF/Viewers/commit/a3f2a1a7b0d16bfcc0ecddc2ab731e54c5e377c8)) + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + + +### Bug Fixes + +* **bugs:** fixing lots of bugs regarding release candidate ([#3700](https://github.com/OHIF/Viewers/issues/3700)) ([8bc12a3](https://github.com/OHIF/Viewers/commit/8bc12a37d0353160ae5ea4624dc0b244b7d59c07)) + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + + +### Features + +* **locale:** add German translations - community PR ([#3697](https://github.com/OHIF/Viewers/issues/3697)) ([ebe8f71](https://github.com/OHIF/Viewers/commit/ebe8f71da22c1d24b58f889c5d803951e19817b6)) + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + + +### Features + +* **debug:** Add timing information about time to first image/all images, and query time ([#3681](https://github.com/OHIF/Viewers/issues/3681)) ([108383b](https://github.com/OHIF/Viewers/commit/108383b9ef51e4bef82d9c932b9bc7aa5354e799)) + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + + +### Bug Fixes + +* **StackSync:** Miscellaneous fixes for stack image sync ([#3663](https://github.com/OHIF/Viewers/issues/3663)) ([8a335bd](https://github.com/OHIF/Viewers/commit/8a335bd03d14ba87d65d7468d93f74040aa828d9)) + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + + +### Bug Fixes + +* **toggleOneUp:** fixed one up for main tmtv layout ([#3677](https://github.com/OHIF/Viewers/issues/3677)) ([86f54d0](https://github.com/OHIF/Viewers/commit/86f54d0d07042750a863ae876aa8dd5fb16029a5)) + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) +* **SidePanel:** new side panel tab look-and-feel ([#3657](https://github.com/OHIF/Viewers/issues/3657)) ([85c899b](https://github.com/OHIF/Viewers/commit/85c899b399e2521480724be145538993721b9378)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + + +### Bug Fixes + +* **DicomJson:** retrieve.series.metadata method should be async ([#3659](https://github.com/OHIF/Viewers/issues/3659)) ([2737903](https://github.com/OHIF/Viewers/commit/2737903386cf97399473e0fa64fe53ad14da155a)) + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + + +### Bug Fixes + +* **health imaging:** studies not loading from healthimaging if imagepositionpatient is missing ([#3646](https://github.com/OHIF/Viewers/issues/3646)) ([74e62a1](https://github.com/OHIF/Viewers/commit/74e62a176374f720080d4e777972f70e7f2d8b2b)) + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + + +### Bug Fixes + +* **suv:** import calculate-suv library version that prevents SUV calculation for a zero PatientWeight ([#3638](https://github.com/OHIF/Viewers/issues/3638)) ([0d10f46](https://github.com/OHIF/Viewers/commit/0d10f46b885fe54ec3dae1848134da658eb6280a)) + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + + +### Bug Fixes + +* **hotkeys:** preserve hotkeys if changed, and reduce re-rendering ([#3635](https://github.com/OHIF/Viewers/issues/3635)) ([94f7cfb](https://github.com/OHIF/Viewers/commit/94f7cfb08e3490488394efc42ef089ebe55e86be)) + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + + +### Features + +* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd)) + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + + +### Features + +* **data source UI config:** Popup the configuration dialogue whenever a data source is not fully configured ([#3620](https://github.com/OHIF/Viewers/issues/3620)) ([adedc8c](https://github.com/OHIF/Viewers/commit/adedc8c382e18a2e86a569e3d023cc55a157363f)) + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + + +### Bug Fixes + +* **PT Metadata:** Allow for PatientWeight to be missing from the metadata ([#3621](https://github.com/OHIF/Viewers/issues/3621)) ([44f101d](https://github.com/OHIF/Viewers/commit/44f101d3f2b3204b67e31f4e4939062e65a246ee)) + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-default + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + + +### Features + +* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1)) + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + +**Note:** Version bump only for package @ohif/extension-default diff --git a/extensions/default/babel.config.js b/extensions/default/babel.config.js new file mode 100644 index 0000000..325ca2a --- /dev/null +++ b/extensions/default/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/extensions/default/jest.config.js b/extensions/default/jest.config.js new file mode 100644 index 0000000..ba90c0c --- /dev/null +++ b/extensions/default/jest.config.js @@ -0,0 +1,17 @@ +const base = require('../../jest.config.base.js'); +const pkg = require('./package'); + +module.exports = { + ...base, + name: pkg.name, + displayName: pkg.name, + moduleNameMapper: { + ...base.moduleNameMapper, + '@ohif/(.*)': '/../../platform/$1/src', + }, + // rootDir: "../.." + // testMatch: [ + // //`/platform/${pack.name}/**/*.spec.js` + // "/platform/app/**/*.test.js" + // ] +}; diff --git a/extensions/default/package.json b/extensions/default/package.json new file mode 100644 index 0000000..55a0704 --- /dev/null +++ b/extensions/default/package.json @@ -0,0 +1,53 @@ +{ + "name": "@ohif/extension-default", + "version": "3.10.0-beta.111", + "description": "Common/default features and functionality for basic image viewing", + "author": "OHIF Core Team", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/ohif-extension-default.umd.js", + "module": "src/index.ts", + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.18.0" + }, + "files": [ + "dist", + "README.md" + ], + "keywords": [ + "ohif-extension" + ], + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:dicom-pdf": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package-1": "yarn run build", + "start": "yarn run dev" + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.111", + "@ohif/i18n": "3.10.0-beta.111", + "dcmjs": "*", + "dicomweb-client": "^0.10.4", + "prop-types": "^15.6.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^12.2.2", + "react-window": "^1.8.9", + "webpack": "5.89.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "@cornerstonejs/calculate-suv": "^1.1.0", + "lodash.get": "^4.4.2", + "lodash.uniqby": "^4.7.0" + } +} diff --git a/extensions/default/src/Actions/createReportAsync.tsx b/extensions/default/src/Actions/createReportAsync.tsx new file mode 100644 index 0000000..44d7c7a --- /dev/null +++ b/extensions/default/src/Actions/createReportAsync.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { DicomMetadataStore } from '@ohif/core'; + +/** + * + * @param {*} servicesManager + */ +async function createReportAsync({ + servicesManager, + getReport, + reportType = 'measurement', +}: withAppTypes) { + const { displaySetService, uiNotificationService, uiDialogService } = servicesManager.services; + const loadingDialogId = uiDialogService.create({ + showOverlay: true, + isDraggable: false, + centralize: true, + content: Loading, + }); + + try { + const naturalizedReport = await getReport(); + + if (!naturalizedReport) return; + + // The "Mode" route listens for DicomMetadataStore changes + // When a new instance is added, it listens and + // automatically calls makeDisplaySets + DicomMetadataStore.addInstances([naturalizedReport], true); + + const displaySet = displaySetService.getMostRecentDisplaySet(); + + const displaySetInstanceUID = displaySet.displaySetInstanceUID; + + uiNotificationService.show({ + title: 'Create Report', + message: `${reportType} saved successfully`, + type: 'success', + }); + + return [displaySetInstanceUID]; + } catch (error) { + uiNotificationService.show({ + title: 'Create Report', + message: error.message || `Failed to store ${reportType}`, + type: 'error', + }); + throw new Error(`Failed to store ${reportType}. Error: ${error.message || 'Unknown error'}`); + } finally { + uiDialogService.dismiss({ id: loadingDialogId }); + } +} + +function Loading() { + return
Loading...
; +} + +export default createReportAsync; diff --git a/extensions/default/src/Components/DataSourceConfigurationComponent.tsx b/extensions/default/src/Components/DataSourceConfigurationComponent.tsx new file mode 100644 index 0000000..fcee035 --- /dev/null +++ b/extensions/default/src/Components/DataSourceConfigurationComponent.tsx @@ -0,0 +1,117 @@ +import React, { ReactElement, useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useModal } from '@ohif/ui'; +import { Icons } from '@ohif/ui-next'; +import { Types } from '@ohif/core'; +import DataSourceConfigurationModalComponent from './DataSourceConfigurationModalComponent'; + +function DataSourceConfigurationComponent({ + servicesManager, + extensionManager, +}: withAppTypes): ReactElement { + const { t } = useTranslation('DataSourceConfiguration'); + const { show, hide } = useModal(); + + const { customizationService } = servicesManager.services; + + const [configurationAPI, setConfigurationAPI] = useState(); + + const [configuredItems, setConfiguredItems] = + useState>(); + + useEffect(() => { + let shouldUpdate = true; + + const dataSourceChangedCallback = async () => { + const activeDataSourceDef = extensionManager.getActiveDataSourceDefinition(); + + if (!activeDataSourceDef.configuration.configurationAPI) { + return; + } + + const { factory: configurationAPIFactory } = customizationService.getCustomization( + activeDataSourceDef.configuration.configurationAPI + ) ?? { factory: () => null }; + + if (!configurationAPIFactory) { + return; + } + + const configAPI = configurationAPIFactory(activeDataSourceDef.sourceName); + setConfigurationAPI(configAPI); + + // New configuration API means that the existing configured items must be cleared. + setConfiguredItems(null); + + configAPI.getConfiguredItems().then(list => { + if (shouldUpdate) { + setConfiguredItems(list); + } + }); + }; + + const sub = extensionManager.subscribe( + extensionManager.EVENTS.ACTIVE_DATA_SOURCE_CHANGED, + dataSourceChangedCallback + ); + + dataSourceChangedCallback(); + + return () => { + shouldUpdate = false; + sub.unsubscribe(); + }; + }, []); + + const showConfigurationModal = useCallback(() => { + show({ + content: DataSourceConfigurationModalComponent, + title: t('Configure Data Source'), + contentProps: { + configurationAPI, + configuredItems, + onHide: hide, + }, + }); + }, [configurationAPI, configuredItems]); + + useEffect(() => { + if (!configurationAPI || !configuredItems) { + return; + } + + if (configuredItems.length !== configurationAPI.getItemLabels().length) { + // Not the correct number of configured items, so show the modal to configure the data source. + showConfigurationModal(); + } + }, [configurationAPI, configuredItems, showConfigurationModal]); + + return configuredItems ? ( +
+ + {configuredItems.map((item, itemIndex) => { + return ( +
+
+ {item.name} +
+ {itemIndex !== configuredItems.length - 1 &&
|
} +
+ ); + })} +
+ ) : ( + <> + ); +} + +export default DataSourceConfigurationComponent; diff --git a/extensions/default/src/Components/DataSourceConfigurationModalComponent.tsx b/extensions/default/src/Components/DataSourceConfigurationModalComponent.tsx new file mode 100644 index 0000000..2517bac --- /dev/null +++ b/extensions/default/src/Components/DataSourceConfigurationModalComponent.tsx @@ -0,0 +1,195 @@ +import classNames from 'classnames'; +import React, { ReactElement, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Icons } from '@ohif/ui-next'; +import { Types } from '@ohif/core'; +import ItemListComponent from './ItemListComponent'; + +const NO_WRAP_ELLIPSIS_CLASS_NAMES = 'text-ellipsis whitespace-nowrap overflow-hidden'; + +type DataSourceConfigurationModalComponentProps = { + configurationAPI: Types.BaseDataSourceConfigurationAPI; + configuredItems: Array; + onHide: () => void; +}; + +function DataSourceConfigurationModalComponent({ + configurationAPI, + configuredItems, + onHide, +}: DataSourceConfigurationModalComponentProps) { + const { t } = useTranslation('DataSourceConfiguration'); + + const [itemList, setItemList] = useState>(); + + const [selectedItems, setSelectedItems] = useState(configuredItems); + + const [errorMessage, setErrorMessage] = useState(); + + const [itemLabels] = useState(configurationAPI.getItemLabels()); + + // Determines whether to show the full/existing configuration for the data source. + // A full or complete configuration is one where the data source (path) has the + // maximum/required number of path items. Anything less is considered not complete and + // the configuration starts from scratch (i.e. as if no items are configured at all). + // TODO: consider configuration starting from a partial (i.e. non-empty) configuration + const [showFullConfig, setShowFullConfig] = useState( + itemLabels.length === configuredItems.length + ); + + /** + * The index of the selected item that is considered current and for which + * its sub-items should be displayed in the items list component. When the + * full/existing configuration for a data source is to be shown, the current + * selected item is the second to last in the `selectedItems` list. + */ + const currentSelectedItemIndex = showFullConfig + ? selectedItems.length - 2 + : selectedItems.length - 1; + + useEffect(() => { + let shouldUpdate = true; + + setErrorMessage(null); + + // Clear out the former/old list while we fetch the next sub item list. + setItemList(null); + + if (selectedItems.length === 0) { + configurationAPI + .initialize() + .then(items => { + if (shouldUpdate) { + setItemList(items); + } + }) + .catch(error => setErrorMessage(error.message)); + } else if (!showFullConfig && selectedItems.length === itemLabels.length) { + // The last item to configure the data source (path) has been selected. + configurationAPI.setCurrentItem(selectedItems[selectedItems.length - 1]); + // We can hide the modal dialog now. + onHide(); + } else { + configurationAPI + .setCurrentItem(selectedItems[currentSelectedItemIndex]) + .then(items => { + if (shouldUpdate) { + setItemList(items); + } + }) + .catch(error => setErrorMessage(error.message)); + } + + return () => { + shouldUpdate = false; + }; + }, [ + selectedItems, + configurationAPI, + onHide, + itemLabels, + showFullConfig, + currentSelectedItemIndex, + ]); + + const getSelectedItemCursorClasses = itemIndex => + itemIndex !== itemLabels.length - 1 && itemIndex < selectedItems.length + ? 'cursor-pointer' + : 'cursor-auto'; + + const getSelectedItemBackgroundClasses = itemIndex => + itemIndex < selectedItems.length + ? classNames( + 'bg-black/[.4]', + itemIndex !== itemLabels.length - 1 ? 'hover:bg-transparent active:bg-secondary-dark' : '' + ) + : 'bg-transparent'; + + const getSelectedItemBorderClasses = itemIndex => + itemIndex === currentSelectedItemIndex + 1 + ? classNames('border-2', 'border-solid', 'border-primary-light') + : itemIndex < selectedItems.length + ? 'border border-solid border-primary-active hover:border-primary-light active:border-white' + : 'border border-dashed border-secondary-light'; + + const getSelectedItemTextClasses = itemIndex => + itemIndex <= selectedItems.length ? 'text-primary-light' : 'text-primary-active'; + + const getErrorComponent = (): ReactElement => { + return ( +
+
+ {t(`Error fetching ${itemLabels[selectedItems.length]} list`)} +
+
{errorMessage}
+
+ ); + }; + + const getSelectedItemsComponent = (): ReactElement => { + return ( +
+ {itemLabels.map((itemLabel, itemLabelIndex) => { + return ( +
{ + setShowFullConfig(false); + setSelectedItems(theList => theList.slice(0, itemLabelIndex)); + } + : undefined + } + > +
+ {itemLabelIndex < selectedItems.length ? ( + + ) : ( + + )} +
{t(itemLabel)}
+
+ {itemLabelIndex < selectedItems.length ? ( +
+ {selectedItems[itemLabelIndex].name} +
+ ) : ( +

+ )} +
+ ); + })} +
+ ); + }; + + return ( +
+ {getSelectedItemsComponent()} +
+ {errorMessage ? ( + getErrorComponent() + ) : ( + { + setShowFullConfig(false); + setSelectedItems(theList => [...theList.slice(0, currentSelectedItemIndex + 1), item]); + }} + > + )} +
+ ); +} + +export default DataSourceConfigurationModalComponent; diff --git a/extensions/default/src/Components/ItemListComponent.tsx b/extensions/default/src/Components/ItemListComponent.tsx new file mode 100644 index 0000000..796ad76 --- /dev/null +++ b/extensions/default/src/Components/ItemListComponent.tsx @@ -0,0 +1,90 @@ +import classNames from 'classnames'; +import React, { ReactElement, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSystem } from '@ohif/core'; +import { Button, InputFilterText } from '@ohif/ui'; +import { Icons } from '@ohif/ui-next'; +import { Types } from '@ohif/core'; + +type ItemListComponentProps = { + itemLabel: string; + itemList: Array; + onItemClicked: (item: Types.BaseDataSourceConfigurationAPIItem) => void; +}; + +function ItemListComponent({ + itemLabel, + itemList, + onItemClicked, +}: ItemListComponentProps): ReactElement { + const { servicesManager } = useSystem(); + const { t } = useTranslation('DataSourceConfiguration'); + const [filterValue, setFilterValue] = useState(''); + + useEffect(() => { + setFilterValue(''); + }, [itemList]); + + const LoadingIndicatorProgress = servicesManager.services.customizationService.getCustomization( + 'ui.loadingIndicatorProgress' + ); + + return ( +
+
+
{t(`Select ${itemLabel}`)}
+ +
+
+ {itemList == null ? ( + + ) : itemList.length === 0 ? ( +
+ + {t(`No ${itemLabel} available`)} +
+ ) : ( + <> +
{t(itemLabel)}
+
+ {itemList + .filter( + item => + !filterValue || item.name.toLowerCase().includes(filterValue.toLowerCase()) + ) + .map(item => { + const border = + 'rounded border-transparent border-b-secondary-light border-[1px] hover:border-primary-light'; + return ( +
+
{item.name}
+ +
+ ); + })} +
+ + )} +
+
+ ); +} + +export default ItemListComponent; diff --git a/extensions/default/src/Components/LineChartViewport/LineChartViewport.tsx b/extensions/default/src/Components/LineChartViewport/LineChartViewport.tsx new file mode 100644 index 0000000..4f61156 --- /dev/null +++ b/extensions/default/src/Components/LineChartViewport/LineChartViewport.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { LineChart } from '@ohif/ui'; + +const LineChartViewport = ({ displaySets }) => { + const displaySet = displaySets[0]; + const { axis: chartAxis, series: chartSeries } = displaySet.instance.chartData; + + return ( + + ); +}; + +export { LineChartViewport as default }; diff --git a/extensions/default/src/Components/LineChartViewport/index.ts b/extensions/default/src/Components/LineChartViewport/index.ts new file mode 100644 index 0000000..0871906 --- /dev/null +++ b/extensions/default/src/Components/LineChartViewport/index.ts @@ -0,0 +1 @@ +export { default } from './LineChartViewport'; diff --git a/extensions/default/src/Components/MoreDropdownMenu.tsx b/extensions/default/src/Components/MoreDropdownMenu.tsx new file mode 100644 index 0000000..66cd5f8 --- /dev/null +++ b/extensions/default/src/Components/MoreDropdownMenu.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, + DropdownMenuItem, + Icons, + Button, +} from '@ohif/ui-next'; + +/** + * The default sub-menu appearance and setup is defined here, but this can be + * replaced by + */ +const getMenuItemsDefault = ({ + commandsManager, + items, + servicesManager, + ...props +}: withAppTypes) => { + const { customizationService } = servicesManager.services; + + // This allows replacing the default child item for menus, whereas the entire + // getMenuItems can also be replaced by providing it to the MoreDropdownMenu + const menuContent = customizationService.getCustomization('ohif.menuContent'); + + // Default menu item component if none is provided through customization + + const DefaultMenuItem = ({ + item, + }: { + item: { + id: string; + label: string; + iconName: string; + onClick: ({ commandsManager, ...props }: withAppTypes) => () => void; + }; + }) => ( + item.onClick({ commandsManager, ...props })}> +
+ {item.iconName && } + {item.label} +
+
+ ); + + const MenuItemComponent = menuContent?.content || DefaultMenuItem; + + return ( + { + e.stopPropagation(); + e.preventDefault(); + }} + > + {items?.map((item, index) => ( + + ))} + + ); +}; + +/** + * The component provides a ... sub-menu for various components which appears + * on hover over the main component. + * + * @param bindProps - properties to define the sub-menu + * @returns Component bound to the bindProps + */ +export default function MoreDropdownMenu(bindProps) { + const { + menuItemsKey, + getMenuItems = getMenuItemsDefault, + commandsManager, + servicesManager, + } = bindProps; + const { customizationService } = servicesManager.services; + + const items = customizationService.getCustomization(menuItemsKey); + + if (!items?.length) { + return null; + } + + function BoundMoreDropdownMenu(props) { + return ( + + + + + {getMenuItems({ + ...props, + commandsManager: commandsManager, + servicesManager: servicesManager, + items, + })} + + ); + } + return BoundMoreDropdownMenu; +} diff --git a/extensions/default/src/Components/ProgressDropdownWithService.tsx b/extensions/default/src/Components/ProgressDropdownWithService.tsx new file mode 100644 index 0000000..d8720b4 --- /dev/null +++ b/extensions/default/src/Components/ProgressDropdownWithService.tsx @@ -0,0 +1,102 @@ +import React, { useEffect, useState, useCallback, ReactElement } from 'react'; +import { ProgressDropdown } from '@ohif/ui'; + +const workflowStepsToDropdownOptions = (steps = []) => + steps.map(step => ({ + label: step.name, + value: step.id, + info: step.info, + activated: false, + completed: false, + })); + +export function ProgressDropdownWithService({ servicesManager }: withAppTypes): ReactElement { + const { workflowStepsService } = servicesManager.services; + const [activeStepId, setActiveStepId] = useState(workflowStepsService.activeWorkflowStep?.id); + + const [dropdownOptions, setDropdownOptions] = useState( + workflowStepsToDropdownOptions(workflowStepsService.workflowSteps) + ); + + const setCurrentAndPreviousOptionsAsCompleted = useCallback(currentOption => { + if (currentOption.completed) { + return; + } + + setDropdownOptions(prevOptions => { + const newOptionsState = [...prevOptions]; + const startIndex = newOptionsState.findIndex(option => option.value === currentOption.value); + + for (let i = startIndex; i >= 0; i--) { + const option = newOptionsState[i]; + + if (option.completed) { + break; + } + + newOptionsState[i] = { + ...option, + completed: true, + }; + } + + return newOptionsState; + }); + }, []); + + const handleDropdownChange = useCallback( + ({ selectedOption }) => { + if (!selectedOption) { + return; + } + + // TODO: Steps should be marked as completed after user has + // completed some action when required (not implemented) + setCurrentAndPreviousOptionsAsCompleted(selectedOption); + setActiveStepId(selectedOption.value); + }, + [setCurrentAndPreviousOptionsAsCompleted] + ); + + useEffect(() => { + let timeoutId; + + if (activeStepId) { + // We've used setTimeout to give it more time to update the UI since + // create3DFilterableFromDataArray from Texture.js may take 600+ ms to run + // when there is a new series to load in the next step but that resulted + // in the followed React error when updating the content from left/right panels + // and all component states were being lost: + // Error: Can't perform a React state update on an unmounted component + workflowStepsService.setActiveWorkflowStep(activeStepId); + } + + return () => clearTimeout(timeoutId); + }, [activeStepId, workflowStepsService]); + + useEffect(() => { + const { unsubscribe: unsubStepsChanged } = workflowStepsService.subscribe( + workflowStepsService.EVENTS.STEPS_CHANGED, + () => setDropdownOptions(workflowStepsToDropdownOptions(workflowStepsService.workflowSteps)) + ); + + const { unsubscribe: unsubActiveStepChanged } = workflowStepsService.subscribe( + workflowStepsService.EVENTS.ACTIVE_STEP_CHANGED, + + () => setActiveStepId(workflowStepsService.activeWorkflowStep.id) + ); + + return () => { + unsubStepsChanged(); + unsubActiveStepChanged(); + }; + }, [servicesManager, workflowStepsService]); + + return ( + + ); +} diff --git a/extensions/default/src/Components/SidePanelWithServices.tsx b/extensions/default/src/Components/SidePanelWithServices.tsx new file mode 100644 index 0000000..298f5ae --- /dev/null +++ b/extensions/default/src/Components/SidePanelWithServices.tsx @@ -0,0 +1,116 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { SidePanel } from '@ohif/ui-next'; +import { Types } from '@ohif/core'; + +export type SidePanelWithServicesProps = { + servicesManager: AppTypes.ServicesManager; + side: 'left' | 'right'; + className?: string; + activeTabIndex: number; + tabs?: any; + expandedWidth?: number; + onClose: () => void; + onOpen: () => void; + isExpanded: boolean; + collapsedWidth?: number; + expandedInsideBorderSize?: number; + collapsedInsideBorderSize?: number; + collapsedOutsideBorderSize?: number; +}; + +const SidePanelWithServices = ({ + servicesManager, + side, + activeTabIndex: activeTabIndexProp, + isExpanded, + tabs: tabsProp, + onOpen, + onClose, + ...props +}: SidePanelWithServicesProps) => { + const panelService = servicesManager?.services?.panelService; + + // Tracks whether this SidePanel has been opened at least once since this SidePanel was inserted into the DOM. + // Thus going to the Study List page and back to the viewer resets this flag for a SidePanel. + const [sidePanelExpanded, setSidePanelExpanded] = useState(isExpanded); + const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp ?? 0); + const [closedManually, setClosedManually] = useState(false); + const [tabs, setTabs] = useState(tabsProp ?? panelService.getPanels(side)); + + const handleActiveTabIndexChange = useCallback(({ activeTabIndex }) => { + setActiveTabIndex(activeTabIndex); + }, []); + + const handleOpen = useCallback(() => { + setSidePanelExpanded(true); + onOpen?.(); + }, [onOpen]); + + const handleClose = useCallback(() => { + setSidePanelExpanded(false); + setClosedManually(true); + onClose?.(); + }, [onClose]); + + useEffect(() => { + setSidePanelExpanded(isExpanded); + }, [isExpanded]); + + /** update the active tab index from outside */ + useEffect(() => { + setActiveTabIndex(activeTabIndexProp ?? 0); + }, [activeTabIndexProp]); + + useEffect(() => { + const { unsubscribe } = panelService.subscribe( + panelService.EVENTS.PANELS_CHANGED, + panelChangedEvent => { + if (panelChangedEvent.position !== side) { + return; + } + + setTabs(panelService.getPanels(side)); + } + ); + + return () => { + unsubscribe(); + }; + }, [panelService, side]); + + useEffect(() => { + const activatePanelSubscription = panelService.subscribe( + panelService.EVENTS.ACTIVATE_PANEL, + (activatePanelEvent: Types.ActivatePanelEvent) => { + if (sidePanelExpanded || activatePanelEvent.forceActive) { + const tabIndex = tabs.findIndex(tab => tab.id === activatePanelEvent.panelId); + if (tabIndex !== -1) { + if (!closedManually) { + setSidePanelExpanded(true); + } + setActiveTabIndex(tabIndex); + } + } + } + ); + + return () => { + activatePanelSubscription.unsubscribe(); + }; + }, [tabs, sidePanelExpanded, panelService, closedManually]); + + return ( + + ); +}; + +export default SidePanelWithServices; diff --git a/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx b/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx new file mode 100644 index 0000000..50303a4 --- /dev/null +++ b/extensions/default/src/CustomizableContextMenu/ContextMenuController.tsx @@ -0,0 +1,217 @@ +import * as ContextMenuItemsBuilder from './ContextMenuItemsBuilder'; +import { CommandsManager } from '@ohif/core'; +import { annotation as CsAnnotation } from '@cornerstonejs/tools'; +import { Menu, MenuItem, Point, ContextMenuProps } from './types'; + +/** + * The context menu controller is a helper class that knows how + * to manage context menus based on the UI Customization Service. + * There are a few parts to this: + * 1. Basic controls to manage displaying and hiding context menus + * 2. Menu selection services, which use the UI customization service + * to choose which menu to display + * 3. Menu item adapter services to convert menu items into displayable and actionable items. + * + * The format for a menu is defined in the exported type MenuItem + */ +export default class ContextMenuController { + commandsManager: CommandsManager; + services: AppTypes.Services; + menuItems: Menu[] | MenuItem[]; + + constructor(servicesManager: AppTypes.ServicesManager, commandsManager: CommandsManager) { + this.services = servicesManager.services; + this.commandsManager = commandsManager; + } + + closeContextMenu() { + this.services.uiDialogService.dismiss({ id: 'context-menu' }); + } + + /** + * Figures out which context menu is appropriate to display and shows it. + * + * @param contextMenuProps - the context menu properties, see ./types.ts + * @param viewportElement - the DOM element this context menu is related to + * @param defaultPointsPosition - a default position to show the context menu + */ + showContextMenu( + contextMenuProps: ContextMenuProps, + viewportElement, + defaultPointsPosition + ): void { + if (!this.services.uiDialogService) { + console.warn('Unable to show dialog; no UI Dialog Service available.'); + return; + } + + const { event, subMenu, menuId, menus, selectorProps } = contextMenuProps; + if (!menus) { + console.warn('No menus found for', menuId); + return; + } + + const { locking, visibility } = CsAnnotation; + const targetAnnotationId = selectorProps?.nearbyToolData?.annotationUID as string; + + if (targetAnnotationId) { + const isLocked = locking.isAnnotationLocked(targetAnnotationId); + const isVisible = visibility.isAnnotationVisible(targetAnnotationId); + + if (isLocked || !isVisible) { + console.warn(`Annotation is ${isLocked ? 'locked' : 'not visible'}.`); + return; + } + } + + const items = ContextMenuItemsBuilder.getMenuItems( + selectorProps || contextMenuProps, + event, + menus, + menuId + ); + + const ContextMenu = this.services.customizationService.getCustomization('ui.contextMenu'); + + this.services.uiDialogService.dismiss({ id: 'context-menu' }); + this.services.uiDialogService.create({ + id: 'context-menu', + isDraggable: false, + preservePosition: false, + preventCutOf: true, + defaultPosition: ContextMenuController._getDefaultPosition( + defaultPointsPosition, + event?.detail || event, + viewportElement + ), + event, + content: ContextMenu, + + // This naming is part of the uiDialogService convention + // Clicking outside simply closes the dialog box. + onClickOutside: () => this.services.uiDialogService.dismiss({ id: 'context-menu' }), + + contentProps: { + items, + selectorProps, + menus, + event, + subMenu, + eventData: event?.detail || event, + + onClose: () => { + this.services.uiDialogService.dismiss({ id: 'context-menu' }); + }, + + /** + * Displays a sub-menu, removing this menu + * @param {*} item + * @param {*} itemRef + * @param {*} subProps + */ + onShowSubMenu: (item, itemRef, subProps) => { + if (!itemRef.subMenu) { + console.warn('No submenu defined for', item, itemRef, subProps); + return; + } + this.showContextMenu( + { + ...contextMenuProps, + menuId: itemRef.subMenu, + }, + viewportElement, + defaultPointsPosition + ); + }, + + // Default is to run the specified commands. + onDefault: (item, itemRef, subProps) => { + this.commandsManager.run(item, { + ...selectorProps, + ...itemRef, + subProps, + }); + }, + }, + }); + } + + static getDefaultPosition = (): Point => { + return { + x: 0, + y: 0, + }; + }; + + static _getEventDefaultPosition = eventDetail => ({ + x: eventDetail?.currentPoints?.client[0] ?? eventDetail?.pageX, + y: eventDetail?.currentPoints?.client[1] ?? eventDetail?.pageY, + }); + + static _getElementDefaultPosition = element => { + if (element) { + const boundingClientRect = element.getBoundingClientRect(); + return { + x: boundingClientRect.x, + y: boundingClientRect.y, + }; + } + + return { + x: undefined, + y: undefined, + }; + }; + + static _getCanvasPointsPosition = (points = [], element) => { + const viewerPos = ContextMenuController._getElementDefaultPosition(element); + + for (let pointIndex = 0; pointIndex < points.length; pointIndex++) { + const point = { + x: points[pointIndex][0] || points[pointIndex]['x'], + y: points[pointIndex][1] || points[pointIndex]['y'], + }; + if ( + ContextMenuController._isValidPosition(point) && + ContextMenuController._isValidPosition(viewerPos) + ) { + return { + x: point.x + viewerPos.x, + y: point.y + viewerPos.y, + }; + } + } + }; + + static _isValidPosition = (source): boolean => { + return source && typeof source.x === 'number' && typeof source.y === 'number'; + }; + + /** + * Returns the context menu default position. It look for the positions of: canvasPoints (got from selected), event that triggers it, current viewport element + */ + static _getDefaultPosition = (canvasPoints, eventDetail, viewerElement) => { + function* getPositionIterator() { + yield ContextMenuController._getCanvasPointsPosition(canvasPoints, viewerElement); + yield ContextMenuController._getEventDefaultPosition(eventDetail); + yield ContextMenuController._getElementDefaultPosition(viewerElement); + yield ContextMenuController.getDefaultPosition(); + } + + const positionIterator = getPositionIterator(); + + let current = positionIterator.next(); + let position = current.value; + + while (!current.done) { + position = current.value; + + if (ContextMenuController._isValidPosition(position)) { + positionIterator.return(); + } + current = positionIterator.next(); + } + + return position; + }; +} diff --git a/extensions/default/src/CustomizableContextMenu/ContextMenuItemsBuilder.test.js b/extensions/default/src/CustomizableContextMenu/ContextMenuItemsBuilder.test.js new file mode 100644 index 0000000..2231f75 --- /dev/null +++ b/extensions/default/src/CustomizableContextMenu/ContextMenuItemsBuilder.test.js @@ -0,0 +1,29 @@ +import * as ContextMenuItemsBuilder from './ContextMenuItemsBuilder'; + +const menus = [ + { + id: 'one', + selector: ({ value } = {}) => value === 'one', + items: [], + }, + { + id: 'two', + selector: ({ value } = {}) => value === 'two', + items: [], + }, + { + id: 'default', + items: [], + }, +]; + +describe('ContextMenuItemsBuilder', () => { + test('findMenuDefault', () => { + expect(ContextMenuItemsBuilder.findMenuDefault(menus, {})).toBe(menus[2]); + expect( + ContextMenuItemsBuilder.findMenuDefault(menus, { selectorProps: { value: 'two' } }) + ).toBe(menus[1]); + expect(ContextMenuItemsBuilder.findMenuDefault([], {})).toBeUndefined(); + expect(ContextMenuItemsBuilder.findMenuDefault(undefined, undefined)).toBeNull(); + }); +}); diff --git a/extensions/default/src/CustomizableContextMenu/ContextMenuItemsBuilder.ts b/extensions/default/src/CustomizableContextMenu/ContextMenuItemsBuilder.ts new file mode 100644 index 0000000..dc3162f --- /dev/null +++ b/extensions/default/src/CustomizableContextMenu/ContextMenuItemsBuilder.ts @@ -0,0 +1,176 @@ +import { Types } from '@ohif/ui'; +import { Menu, SelectorProps, MenuItem, ContextMenuProps } from './types'; + +type ContextMenuItem = Types.ContextMenuItem; + +/** + * Finds menu by menu id + * + * @returns Menu having the menuId + */ +export function findMenuById(menus: Menu[], menuId?: string): Menu { + if (!menuId) { + return; + } + + return menus.find(menu => menu.id === menuId); +} + +/** + * Default finding menu method. This method will go through + * the list of menus until it finds the first one which + * has no selector, OR has the selector, when applied to the + * check props, return true. + * The selectorProps are a set of provided properties which can be + * passed into the selector function to determine when to display a menu. + * For example, a selector function of: + * `({displayset}) => displaySet?.SeriesDescription?.indexOf?.('Left')!==-1 + * would match series descriptions containing 'Left'. + * + * @param {Object[]} menus List of menus + * @param {*} subProps + * @returns + */ +export function findMenuDefault(menus: Menu[], subProps: Record): Menu { + if (!menus) { + return null; + } + return menus.find(menu => !menu.selector || menu.selector(subProps.selectorProps)); +} + +/** + * Finds the menu to be used for different scenarios: + * This will first look for a subMenu with the specified subMenuId + * Next it will look for the first menu whose selector returns true. + * + * @param menus - List of menus + * @param props - root props + * @param menuIdFilter - menu id identifier (to be considered on selection) + * This is intended to support other types of filtering in the future. + */ +export function findMenu(menus: Menu[], props?: Types.IProps, menuIdFilter?: string) { + const { subMenu } = props; + + function* findMenuIterator() { + yield findMenuById(menus, menuIdFilter || subMenu); + yield findMenuDefault(menus, props); + } + + const findIt = findMenuIterator(); + + let current = findIt.next(); + let menu = current.value; + + while (!current.done) { + menu = current.value; + + if (menu) { + findIt.return(); + } + current = findIt.next(); + } + + return menu; +} + +/** + * Returns the menu from a list of possible menus, based on the actual state of component props and tool data nearby. + * This uses the findMenu command above to first find the appropriate + * menu, and then it chooses the actual contents of that menu. + * A menu item can be optional by implementing the 'selector', + * which will be called with the selectorProps, and if it does not return true, + * then the item is excluded. + * + * Other menus can be delegated to by setting the delegating value to + * a string id for another menu. That menu's content will replace the + * current menu item (only if the item would be included). + * + * This allows single id menus to be chosen by id, but have varying contents + * based on the delegated menus. + * + * Finally, for each item, the adaptItem call is made. This allows + * items to modify themselves before being displayed, such as + * incorporating additional information from translation sources. + * See the `test-mode` examples for details. + * + * @param selectorProps + * @param {*} event event that originates the context menu + * @param {*} menus List of menus + * @param {*} menuIdFilter + * @returns + */ +export function getMenuItems( + selectorProps: SelectorProps, + event: Event, + menus: Menu[], + menuIdFilter?: string +): MenuItem[] | void { + // Include both the check props and the ...check props as one is used + // by the child menu and the other used by the selector function + const subProps = { selectorProps, event }; + + const menu = findMenu(menus, subProps, menuIdFilter); + + if (!menu) { + return undefined; + } + + if (!menu.items) { + console.warn('Must define items in menu', menu); + return []; + } + + let menuItems = []; + menu.items.forEach(item => { + const { delegating, selector, subMenu } = item; + + if (!selector || selector(selectorProps)) { + if (delegating) { + menuItems = [...menuItems, ...getMenuItems(selectorProps, event, menus, subMenu)]; + } else { + const toAdd = adaptItem(item, subProps); + menuItems.push(toAdd); + } + } + }); + + return menuItems; +} + +/** + * Returns item adapted to be consumed by ContextMenu component + * and then goes through the item to add action behaviour for clicking the item, + * making it compatible with the default ContextMenu display. + * + * @param {Object} item + * @param {Object} subProps + * @returns a MenuItem that is compatible with the base ContextMenu + * This requires having a label and set of actions to be called. + */ +export function adaptItem(item: MenuItem, subProps: ContextMenuProps): ContextMenuItem { + const newItem: ContextMenuItem = { + ...item, + value: subProps.selectorProps?.value, + }; + + if (item.actionType === 'ShowSubMenu' && !newItem.iconRight) { + newItem.iconRight = 'chevron-down'; + } + if (!item.action) { + newItem.action = (itemRef, componentProps) => { + const { event = {} } = componentProps; + const { detail = {} } = event; + newItem.element = detail.element; + + componentProps.onClose(); + const action = componentProps[`on${itemRef.actionType || 'Default'}`]; + if (action) { + action.call(componentProps, newItem, itemRef, subProps); + } else { + console.warn('No action defined for', itemRef); + } + }; + } + + return newItem; +} diff --git a/extensions/default/src/CustomizableContextMenu/index.ts b/extensions/default/src/CustomizableContextMenu/index.ts new file mode 100644 index 0000000..500af07 --- /dev/null +++ b/extensions/default/src/CustomizableContextMenu/index.ts @@ -0,0 +1,5 @@ +import ContextMenuController from './ContextMenuController'; +import * as ContextMenuItemsBuilder from './ContextMenuItemsBuilder'; +import * as CustomizableContextMenuTypes from './types'; + +export { ContextMenuController, CustomizableContextMenuTypes, ContextMenuItemsBuilder }; diff --git a/extensions/default/src/CustomizableContextMenu/types.ts b/extensions/default/src/CustomizableContextMenu/types.ts new file mode 100644 index 0000000..e86d1a8 --- /dev/null +++ b/extensions/default/src/CustomizableContextMenu/types.ts @@ -0,0 +1,126 @@ +import { Types } from '@ohif/core'; + +/** + * SelectorProps are properties used to decide whether to select a menu or + * menu item for display. + * An instance of SelectorProps is provided to the selector functions, which + * return true to include the item or false to exclude it. + * The point of this is to allow more specific context menus which hide + * non-relevant menu options, optimizing the speed of selection of menus + */ +export interface SelectorProps { + // If the context menu is invoked in the context of a measurement, then it + // will contain the nearby tool data. + nearbyToolData?: Record; + + // The tool name for the nearby tool + toolName?: string; + + // An annotation UID - this will be present if nearbyToolData is present. + uid?: string; + + // If the context menu is invoked on an active viewport, then it will contain + // the first display set. + displaySet?: Record; + + // The triggering event - can be used to determine key modifiers + event?: Event; + + // Any other properties + [propertyName: string]: unknown; +} + +/** + * The type of item actually required for the ContextMenu UI display + */ +export type UIMenuItem = { + label: string; + // Called when the item is selected + action?: (itemRef, componentProps) => void; +}; + +/** + * A MenuItem is a single line item within a menu, and specifies a selectable + * value for the menu. + */ +export interface MenuItem { + id?: string; + /** + * The customization type is used to apply preset values to this item + * when registered with the customization service. + */ + inheritsFrom?: string; + + // The label is the value to show in the menu for this item + label?: string; + + // Delegating items are used to include other sub-menus inline within + // this menu. That allows sharing part of the menu structure, but also, + // more importantly to use a single selector function to include/exclude + // and entire section of sub-menu. + // See the `siteSelectionSubMenu` within the example `findingsMenu` + // for an example + delegating?: boolean; + + // A sub-menu is shown when this item is selected or is delegating. + // This item gives the name of the sub-menu. + subMenu?: string; + + // The selector is used to determine if this menu entry will be shown + // or more importantly, if the delegating subMenu will be included. + selector?: (props: SelectorProps) => boolean; + + /** Adapts the item by filling in additional properties as required */ + adaptItem?: (item: MenuItem, props: ContextMenuProps) => UIMenuItem; + + /** List of commands to run when this item's action is taken. */ + commands?: Types.Command[]; +} + +/** + * A menu is a list of menu items, plus a selector. + * The selector is used to determine whether the menu should be displayed + * in a given context. The parameters passed to the selector come from + * the 'selectorProps' value in the options, and are intended to be context + * specific values containing things like the selected object, the currently + * displayed study etc so that the context menu can dynamically choose which + * view to show. + */ +export interface Menu { + id: string; + + /** The customization type is used to apply preset values to this item + * when registered with the customization service. + */ + inheritsFrom?: string; + + // Choose whether this menu applies. + selector?: Types.Predicate; + + items: MenuItem[]; +} + +export type Point = { + x: number; + y: number; +}; + +/** + * ContextMenuProps is the top level argument used to invoke the context menu + * itself. It contains the menus available for display, as well as the event + * and selector props used to decide the menu. + */ +export type ContextMenuProps = { + event?: EventTarget; + menuCustomizationId?: string; + menuId: string; + element?: HTMLElement; + + /** A set of menus to choose from for this context menu */ + menus: Menu[]; + + /** The properties used to decide the menu type */ + selectorProps: SelectorProps; + + defaultPointsPosition?: [number, number] | []; +}; diff --git a/extensions/default/src/DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI.ts b/extensions/default/src/DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI.ts new file mode 100644 index 0000000..1be638d --- /dev/null +++ b/extensions/default/src/DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI.ts @@ -0,0 +1,236 @@ +import { ExtensionManager, Types } from '@ohif/core'; + +/** + * This file contains the implementations of BaseDataSourceConfigurationAPIItem + * and BaseDataSourceConfigurationAPI for the Google cloud healthcare API. To + * better understand this implementation and/or to implement custom implementations, + * see the platform\core\src\types\DataSourceConfigurationAPI.ts and its JS doc + * comments as a guide. + */ + +/** + * The various Google Cloud Healthcare path item types. + */ +enum ItemType { + projects = 0, + locations = 1, + datasets = 2, + dicomStores = 3, +} + +interface NamedItem { + name: string; +} +interface Project extends NamedItem { + projectId: string; +} + +const initialUrl = 'https://cloudresourcemanager.googleapis.com/v1'; +const baseHealthcareUrl = 'https://healthcare.googleapis.com/v1'; + +class GoogleCloudDataSourceConfigurationAPIItem + implements Types.BaseDataSourceConfigurationAPIItem +{ + id: string; + name: string; + url: string; + itemType: ItemType; +} + +class GoogleCloudDataSourceConfigurationAPI implements Types.BaseDataSourceConfigurationAPI { + private _extensionManager: ExtensionManager; + private _fetchOptions: { method: string; headers: unknown }; + private _dataSourceName: string; + + constructor(dataSourceName, servicesManager: AppTypes.ServicesManager, extensionManager) { + this._dataSourceName = dataSourceName; + this._extensionManager = extensionManager; + const userAuthenticationService = servicesManager.services.userAuthenticationService; + this._fetchOptions = { + method: 'GET', + headers: userAuthenticationService.getAuthorizationHeader(), + }; + } + + getItemLabels = () => ['Project', 'Location', 'Data set', 'DICOM store']; + + async initialize(): Promise { + const url = `${initialUrl}/projects`; + + const projects = (await GoogleCloudDataSourceConfigurationAPI._doFetch( + url, + ItemType.projects, + this._fetchOptions + )) as Array; + + if (!projects?.length) { + return []; + } + + const projectItems = projects.map(project => { + return { + id: project.projectId, + name: project.name, + itemType: ItemType.projects, + url: `${baseHealthcareUrl}/projects/${project.projectId}`, + }; + }); + + return projectItems; + } + + async setCurrentItem( + anItem: Types.BaseDataSourceConfigurationAPIItem + ): Promise { + const googleCloudItem = anItem as GoogleCloudDataSourceConfigurationAPIItem; + + if (googleCloudItem.itemType === ItemType.dicomStores) { + // Last configurable item, so update the data source configuration. + const url = `${googleCloudItem.url}/dicomWeb`; + const dataSourceDefCopy = JSON.parse( + JSON.stringify(this._extensionManager.getDataSourceDefinition(this._dataSourceName)) + ); + dataSourceDefCopy.configuration = { + ...dataSourceDefCopy.configuration, + wadoUriRoot: url, + qidoRoot: url, + wadoRoot: url, + }; + + this._extensionManager.updateDataSourceConfiguration( + dataSourceDefCopy.sourceName, + dataSourceDefCopy.configuration + ); + + return []; + } + + const subItemType = googleCloudItem.itemType + 1; + const subItemField = `${ItemType[subItemType]}`; + + const url = `${googleCloudItem.url}/${subItemField}`; + + const fetchedSubItems = await GoogleCloudDataSourceConfigurationAPI._doFetch( + url, + subItemType, + this._fetchOptions + ); + + if (!fetchedSubItems?.length) { + return []; + } + + const subItems = fetchedSubItems.map(subItem => { + const nameSplit = subItem.name.split('/'); + return { + id: subItem.name, + name: nameSplit[nameSplit.length - 1], + itemType: subItemType, + url: `${baseHealthcareUrl}/${subItem.name}`, + }; + }); + + return subItems; + } + + async getConfiguredItems(): Promise> { + const dataSourceDefinition = this._extensionManager.getDataSourceDefinition( + this._dataSourceName + ); + + const url = dataSourceDefinition.configuration.wadoUriRoot; + const projectsIndex = url.indexOf('projects'); + // Split the configured URL into (essentially) pairs (i.e. item type followed by item) + // Explicitly: ['projects','aProject','locations','aLocation','datasets','aDataSet','dicomStores','aDicomStore'] + // Note that a partial configuration will have a subset of the above. + const urlSplit = url.substring(projectsIndex).split('/'); + + const configuredItems = []; + + for ( + let itemType = 0; + // the number of configured items is either the max (4) or the number extracted from the url split + itemType < 4 && (itemType + 1) * 2 < urlSplit.length; + itemType += 1 + ) { + if (itemType === ItemType.projects) { + const projectId = urlSplit[1]; + const projectUrl = `${initialUrl}/projects/${projectId}`; + const data = await GoogleCloudDataSourceConfigurationAPI._doFetch( + projectUrl, + ItemType.projects, + this._fetchOptions + ); + const project = data[0] as Project; + configuredItems.push({ + id: project.projectId, + name: project.name, + itemType: itemType, + url: `${baseHealthcareUrl}/projects/${project.projectId}`, + }); + } else { + const relativePath = urlSplit.slice(0, itemType * 2 + 2).join('/'); + configuredItems.push({ + id: relativePath, + name: urlSplit[itemType * 2 + 1], + itemType: itemType, + url: `${baseHealthcareUrl}/${relativePath}`, + }); + } + } + + return configuredItems; + } + + /** + * Fetches an array of items the specified item type. + * @param urlStr the fetch url + * @param fetchItemType the type to fetch + * @param fetchOptions the header options for the fetch (e.g. authorization header) + * @param fetchSearchParams any search query params; currently only used for paging results + * @returns an array of items of the specified type + */ + private static async _doFetch( + urlStr: string, + fetchItemType: ItemType, + fetchOptions = {}, + fetchSearchParams: Record = {} + ): Promise | Array> { + try { + const url = new URL(urlStr); + url.search = new URLSearchParams(fetchSearchParams).toString(); + + const response = await fetch(url, fetchOptions); + const data = await response.json(); + if (response.status >= 200 && response.status < 300 && data != null) { + if (data.nextPageToken != null) { + fetchSearchParams.pageToken = data.nextPageToken; + const subPageData = await this._doFetch( + urlStr, + fetchItemType, + fetchOptions, + fetchSearchParams + ); + data[ItemType[fetchItemType]] = data[ItemType[fetchItemType]].concat(subPageData); + } + if (data[ItemType[fetchItemType]]) { + return data[ItemType[fetchItemType]]; + } else if (data.name) { + return [data]; + } else { + return []; + } + } else { + const message = + data?.error?.message || + `Error returned from Google Cloud Healthcare: ${response.status} - ${response.statusText}`; + throw new Error(message); + } + } catch (err) { + const message = err?.message || 'Error occurred during fetch request.'; + throw new Error(message); + } + } +} + +export { GoogleCloudDataSourceConfigurationAPI }; diff --git a/extensions/default/src/DicomJSONDataSource/index.js b/extensions/default/src/DicomJSONDataSource/index.js new file mode 100644 index 0000000..fa4fd60 --- /dev/null +++ b/extensions/default/src/DicomJSONDataSource/index.js @@ -0,0 +1,304 @@ +import { DicomMetadataStore, IWebApiDataSource } from '@ohif/core'; +import OHIF from '@ohif/core'; +import qs from 'query-string'; + +import getImageId from '../DicomWebDataSource/utils/getImageId'; +import getDirectURL from '../utils/getDirectURL'; + +const metadataProvider = OHIF.classes.MetadataProvider; + +const mappings = { + studyInstanceUid: 'StudyInstanceUID', + patientId: 'PatientID', +}; + +let _store = { + urls: [], + studyInstanceUIDMap: new Map(), // map of urls to array of study instance UIDs + // { + // url: url1 + // studies: [Study1, Study2], // if multiple studies + // } + // { + // url: url2 + // studies: [Study1], + // } + // } +}; + +function wrapSequences(obj) { + return Object.keys(obj).reduce( + (acc, key) => { + if (typeof obj[key] === 'object' && obj[key] !== null) { + // Recursively wrap sequences for nested objects + acc[key] = wrapSequences(obj[key]); + } else { + acc[key] = obj[key]; + } + if (key.endsWith('Sequence')) { + acc[key] = OHIF.utils.addAccessors(acc[key]); + } + return acc; + }, + Array.isArray(obj) ? [] : {} + ); +} +const getMetaDataByURL = url => { + return _store.urls.find(metaData => metaData.url === url); +}; + +const findStudies = (key, value) => { + let studies = []; + _store.urls.map(metaData => { + metaData.studies.map(aStudy => { + if (aStudy[key] === value) { + studies.push(aStudy); + } + }); + }); + return studies; +}; + +function createDicomJSONApi(dicomJsonConfig) { + const implementation = { + initialize: async ({ query, url }) => { + if (!url) { + url = query.get('url'); + } + let metaData = getMetaDataByURL(url); + + // if we have already cached the data from this specific url + // We are only handling one StudyInstanceUID to run; however, + // all studies for patientID will be put in the correct tab + if (metaData) { + return metaData.studies.map(aStudy => { + return aStudy.StudyInstanceUID; + }); + } + + const response = await fetch(url); + const data = await response.json(); + + let StudyInstanceUID; + let SeriesInstanceUID; + data.studies.forEach(study => { + StudyInstanceUID = study.StudyInstanceUID; + + study.series.forEach(series => { + SeriesInstanceUID = series.SeriesInstanceUID; + + series.instances.forEach(instance => { + const { metadata: naturalizedDicom } = instance; + const imageId = getImageId({ instance, config: dicomJsonConfig }); + + const { query } = qs.parseUrl(instance.url); + + // Add imageId specific mapping to this data as the URL isn't necessarily WADO-URI. + metadataProvider.addImageIdToUIDs(imageId, { + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID: naturalizedDicom.SOPInstanceUID, + frameNumber: query.frame ? parseInt(query.frame) : undefined, + }); + }); + }); + }); + + _store.urls.push({ + url, + studies: [...data.studies], + }); + _store.studyInstanceUIDMap.set( + url, + data.studies.map(study => study.StudyInstanceUID) + ); + }, + query: { + studies: { + mapParams: () => {}, + search: async param => { + const [key, value] = Object.entries(param)[0]; + const mappedParam = mappings[key]; + + // todo: should fetch from dicomMetadataStore + const studies = findStudies(mappedParam, value); + + return studies.map(aStudy => { + return { + accession: aStudy.AccessionNumber, + date: aStudy.StudyDate, + description: aStudy.StudyDescription, + instances: aStudy.NumInstances, + modalities: aStudy.Modalities, + mrn: aStudy.PatientID, + patientName: aStudy.PatientName, + studyInstanceUid: aStudy.StudyInstanceUID, + NumInstances: aStudy.NumInstances, + time: aStudy.StudyTime, + }; + }); + }, + processResults: () => { + console.warn(' DICOMJson QUERY processResults not implemented'); + }, + }, + series: { + // mapParams: mapParams.bind(), + search: () => { + console.warn(' DICOMJson QUERY SERIES SEARCH not implemented'); + }, + }, + instances: { + search: () => { + console.warn(' DICOMJson QUERY instances SEARCH not implemented'); + }, + }, + }, + retrieve: { + /** + * Generates a URL that can be used for direct retrieve of the bulkdata + * + * @param {object} params + * @param {string} params.tag is the tag name of the URL to retrieve + * @param {string} params.defaultPath path for the pixel data url + * @param {object} params.instance is the instance object that the tag is in + * @param {string} params.defaultType is the mime type of the response + * @param {string} params.singlepart is the type of the part to retrieve + * @param {string} params.fetchPart unknown? + * @returns an absolute URL to the resource, if the absolute URL can be retrieved as singlepart, + * or is already retrieved, or a promise to a URL for such use if a BulkDataURI + */ + directURL: params => { + return getDirectURL(dicomJsonConfig, params); + }, + series: { + metadata: async ({ filters, StudyInstanceUID, madeInClient = false, customSort } = {}) => { + if (!StudyInstanceUID) { + throw new Error('Unable to query for SeriesMetadata without StudyInstanceUID'); + } + + const study = findStudies('StudyInstanceUID', StudyInstanceUID)[0]; + let series; + + if (customSort) { + series = customSort(study.series); + } else { + series = study.series; + } + + const seriesKeys = [ + 'SeriesInstanceUID', + 'SeriesInstanceUIDs', + 'seriesInstanceUID', + 'seriesInstanceUIDs', + ]; + const seriesFilter = seriesKeys.find(key => filters[key]); + if (seriesFilter) { + const seriesUIDs = filters[seriesFilter]; + series = series.filter(s => seriesUIDs.includes(s.SeriesInstanceUID)); + } + + const seriesSummaryMetadata = series.map(series => { + const seriesSummary = { + StudyInstanceUID: study.StudyInstanceUID, + ...series, + }; + delete seriesSummary.instances; + return seriesSummary; + }); + + // Async load series, store as retrieved + function storeInstances(naturalizedInstances) { + DicomMetadataStore.addInstances(naturalizedInstances, madeInClient); + } + + DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient); + + function setSuccessFlag() { + const study = DicomMetadataStore.getStudy(StudyInstanceUID, madeInClient); + study.isLoaded = true; + } + + const numberOfSeries = series.length; + series.forEach((series, index) => { + const instances = series.instances.map(instance => { + // for instance.metadata if the key ends with sequence then + // we need to add a proxy to the first item in the sequence + // so that we can access the value of the sequence + // by using sequenceName.value + const modifiedMetadata = wrapSequences(instance.metadata); + + const obj = { + ...modifiedMetadata, + url: instance.url, + imageId: getImageId({ instance, config: dicomJsonConfig }), + ...series, + ...study, + }; + delete obj.instances; + delete obj.series; + return obj; + }); + storeInstances(instances); + if (index === numberOfSeries - 1) { + setSuccessFlag(); + } + }); + }, + }, + }, + store: { + dicom: () => { + console.warn(' DICOMJson store dicom not implemented'); + }, + }, + getImageIdsForDisplaySet(displaySet) { + const images = displaySet.images; + const imageIds = []; + + if (!images) { + return imageIds; + } + + const { StudyInstanceUID, SeriesInstanceUID } = displaySet; + const study = findStudies('StudyInstanceUID', StudyInstanceUID)[0]; + const series = study.series.find(s => s.SeriesInstanceUID === SeriesInstanceUID) || []; + + const instanceMap = new Map(); + series.instances.forEach(instance => { + if (instance?.metadata?.SOPInstanceUID) { + const { metadata, url } = instance; + const existingInstances = instanceMap.get(metadata.SOPInstanceUID) || []; + existingInstances.push({ ...metadata, url }); + instanceMap.set(metadata.SOPInstanceUID, existingInstances); + } + }); + + displaySet.images.forEach(instance => { + const NumberOfFrames = instance.NumberOfFrames || 1; + const instances = instanceMap.get(instance.SOPInstanceUID) || [instance]; + for (let i = 0; i < NumberOfFrames; i++) { + const imageId = getImageId({ + instance: instances[Math.min(i, instances.length - 1)], + frame: NumberOfFrames > 1 ? i : undefined, + config: dicomJsonConfig, + }); + imageIds.push(imageId); + } + }); + + return imageIds; + }, + getImageIdsForInstance({ instance, frame }) { + const imageIds = getImageId({ instance, frame }); + return imageIds; + }, + getStudyInstanceUIDs: ({ params, query }) => { + const url = query.get('url'); + return _store.studyInstanceUIDMap.get(url); + }, + }; + return IWebApiDataSource.create(implementation); +} + +export { createDicomJSONApi }; diff --git a/extensions/default/src/DicomLocalDataSource/index.js b/extensions/default/src/DicomLocalDataSource/index.js new file mode 100644 index 0000000..462fb87 --- /dev/null +++ b/extensions/default/src/DicomLocalDataSource/index.js @@ -0,0 +1,263 @@ +import { DicomMetadataStore, IWebApiDataSource, utils } from '@ohif/core'; +import OHIF from '@ohif/core'; +import dcmjs from 'dcmjs'; + +const metadataProvider = OHIF.classes.MetadataProvider; +const { EVENTS } = DicomMetadataStore; + +const END_MODALITIES = { + SR: true, + SEG: true, + DOC: true, +}; + +const compareValue = (v1, v2, def = 0) => { + if (v1 === v2) { + return def; + } + if (v1 < v2) { + return -1; + } + return 1; +}; + +// Sorting SR modalities to be at the end of series list +const customSort = (seriesA, seriesB) => { + const instanceA = seriesA.instances[0]; + const instanceB = seriesB.instances[0]; + const modalityA = instanceA.Modality; + const modalityB = instanceB.Modality; + + const isEndA = END_MODALITIES[modalityA]; + const isEndB = END_MODALITIES[modalityB]; + + if (isEndA && isEndB) { + // Compare by series date + return compareValue(instanceA.SeriesNumber, instanceB.SeriesNumber); + } + if (!isEndA && !isEndB) { + return compareValue(instanceB.SeriesNumber, instanceA.SeriesNumber); + } + return isEndA ? -1 : 1; +}; + +function createDicomLocalApi(dicomLocalConfig) { + const { name } = dicomLocalConfig; + + const implementation = { + initialize: ({ params, query }) => {}, + query: { + studies: { + mapParams: () => {}, + search: params => { + const studyUIDs = DicomMetadataStore.getStudyInstanceUIDs(); + + return studyUIDs.map(StudyInstanceUID => { + let numInstances = 0; + const modalities = new Set(); + + // Calculating the number of instances in the study and modalities + // present in the study + const study = DicomMetadataStore.getStudy(StudyInstanceUID); + study.series.forEach(aSeries => { + numInstances += aSeries.instances.length; + modalities.add(aSeries.instances[0].Modality); + }); + + // first instance in the first series + const firstInstance = study?.series[0]?.instances[0]; + + if (firstInstance) { + return { + accession: firstInstance.AccessionNumber, + date: firstInstance.StudyDate, + description: firstInstance.StudyDescription, + mrn: firstInstance.PatientID, + patientName: utils.formatPN(firstInstance.PatientName), + studyInstanceUid: firstInstance.StudyInstanceUID, + time: firstInstance.StudyTime, + // + instances: numInstances, + modalities: Array.from(modalities).join('/'), + NumInstances: numInstances, + }; + } + }); + }, + processResults: () => { + console.warn(' DICOMLocal QUERY processResults not implemented'); + }, + }, + series: { + search: studyInstanceUID => { + const study = DicomMetadataStore.getStudy(studyInstanceUID); + return study.series.map(aSeries => { + const firstInstance = aSeries?.instances[0]; + return { + studyInstanceUid: studyInstanceUID, + seriesInstanceUid: firstInstance.SeriesInstanceUID, + modality: firstInstance.Modality, + seriesNumber: firstInstance.SeriesNumber, + seriesDate: firstInstance.SeriesDate, + numSeriesInstances: aSeries.instances.length, + description: firstInstance.SeriesDescription, + }; + }); + }, + }, + instances: { + search: () => { + console.warn(' DICOMLocal QUERY instances SEARCH not implemented'); + }, + }, + }, + retrieve: { + directURL: params => { + const { instance, tag, defaultType } = params; + + const value = instance[tag]; + if (value instanceof Array && value[0] instanceof ArrayBuffer) { + return URL.createObjectURL( + new Blob([value[0]], { + type: defaultType, + }) + ); + } + }, + series: { + metadata: async ({ StudyInstanceUID, madeInClient = false } = {}) => { + if (!StudyInstanceUID) { + throw new Error('Unable to query for SeriesMetadata without StudyInstanceUID'); + } + + // Instances metadata already added via local upload + const study = DicomMetadataStore.getStudy(StudyInstanceUID, madeInClient); + + // Series metadata already added via local upload + DicomMetadataStore._broadcastEvent(EVENTS.SERIES_ADDED, { + StudyInstanceUID, + madeInClient, + }); + + study.series.forEach(aSeries => { + const { SeriesInstanceUID } = aSeries; + + const isMultiframe = aSeries.instances[0].NumberOfFrames > 1; + + aSeries.instances.forEach((instance, index) => { + const { + url: imageId, + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + } = instance; + + instance.imageId = imageId; + + // Add imageId specific mapping to this data as the URL isn't necessarily WADO-URI. + metadataProvider.addImageIdToUIDs(imageId, { + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + frameIndex: isMultiframe ? index : 1, + }); + }); + + DicomMetadataStore._broadcastEvent(EVENTS.INSTANCES_ADDED, { + StudyInstanceUID, + SeriesInstanceUID, + madeInClient, + }); + }); + }, + }, + }, + store: { + dicom: naturalizedReport => { + const reportBlob = dcmjs.data.datasetToBlob(naturalizedReport); + + //Create a URL for the binary. + var objectUrl = URL.createObjectURL(reportBlob); + window.location.assign(objectUrl); + }, + }, + getImageIdsForDisplaySet(displaySet) { + const images = displaySet.images; + const imageIds = []; + + if (!images) { + return imageIds; + } + + displaySet.images.forEach(instance => { + const NumberOfFrames = instance.NumberOfFrames; + if (NumberOfFrames > 1) { + // in multiframe we start at frame 1 + for (let i = 1; i <= NumberOfFrames; i++) { + const imageId = this.getImageIdsForInstance({ + instance, + frame: i, + }); + imageIds.push(imageId); + } + } else { + const imageId = this.getImageIdsForInstance({ instance }); + imageIds.push(imageId); + } + }); + + return imageIds; + }, + getImageIdsForInstance({ instance, frame }) { + // Important: Never use instance.imageId because it might be multiframe, + // which would make it an invalid imageId. + // if (instance.imageId) { + // return instance.imageId; + // } + + const { StudyInstanceUID, SeriesInstanceUID } = instance; + const SOPInstanceUID = instance.SOPInstanceUID || instance.SopInstanceUID; + const storedInstance = DicomMetadataStore.getInstance( + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID + ); + + let imageId = storedInstance.url; + + if (frame !== undefined) { + imageId += `&frame=${frame}`; + } + + return imageId; + }, + deleteStudyMetadataPromise() { + console.log('deleteStudyMetadataPromise not implemented'); + }, + getStudyInstanceUIDs: ({ params, query }) => { + const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params; + const queryStudyInstanceUIDs = query.getAll('StudyInstanceUIDs'); + + const StudyInstanceUIDs = queryStudyInstanceUIDs || paramsStudyInstanceUIDs; + const StudyInstanceUIDsAsArray = + StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs) + ? StudyInstanceUIDs + : [StudyInstanceUIDs]; + + // Put SRs at the end of series list to make sure images are loaded first + let isStudyInCache = false; + StudyInstanceUIDsAsArray.forEach(StudyInstanceUID => { + const study = DicomMetadataStore.getStudy(StudyInstanceUID); + if (study) { + study.series = study.series.sort(customSort); + isStudyInCache = true; + } + }); + + return isStudyInCache ? StudyInstanceUIDsAsArray : []; + }, + }; + return IWebApiDataSource.create(implementation); +} + +export { createDicomLocalApi }; diff --git a/extensions/default/src/DicomTagBrowser/DicomTagBrowser.css b/extensions/default/src/DicomTagBrowser/DicomTagBrowser.css new file mode 100644 index 0000000..2845f2f --- /dev/null +++ b/extensions/default/src/DicomTagBrowser/DicomTagBrowser.css @@ -0,0 +1,53 @@ +.dicom-tag-browser-table { + margin-right: auto; + margin-left: auto; +} + +.dicom-tag-browser-table-wrapper { + /* height: 500px;*/ + /*overflow-y: scroll;*/ + overflow-x: scroll; +} + +.dicom-tag-browser-table tr { + padding-left: 10px; + padding-right: 10px; + color: #ffffff; + border-top: 1px solid #ddd; + white-space: nowrap; +} + +.stick { + position: sticky; + overflow: clip; +} + +.dicom-tag-browser-content { + overflow: hidden; + width: 100%; + padding-bottom: 50px; + /*height: 500px;*/ +} + +.dicom-tag-browser-instance-range .range { + height: 20px; +} + +.dicom-tag-browser-instance-range { + padding: 20px 0 20px 0; +} + +.dicom-tag-browser-table td.dicom-tag-browser-table-center { + text-align: center; +} + +.dicom-tag-browser-table th { + padding-left: 10px; + padding-right: 10px; + text-align: center; + color: '#20A5D6'; +} + +.dicom-tag-browser-table th.dicom-tag-browser-table-left { + text-align: left; +} diff --git a/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx b/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx new file mode 100644 index 0000000..8ea351d --- /dev/null +++ b/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx @@ -0,0 +1,376 @@ +import dcmjs from 'dcmjs'; +import moment from 'moment'; +import React, { useState, useMemo, useCallback } from 'react'; +import { classes, Types } from '@ohif/core'; +import { InputFilterText } from '@ohif/ui'; +import { Select, SelectTrigger, SelectContent, SelectItem, Slider } from '@ohif/ui-next'; + +import DicomTagTable from './DicomTagTable'; +import './DicomTagBrowser.css'; + +export type Row = { + uid: string; + tag: string; + valueRepresentation: string; + keyword: string; + value: string; + isVisible: boolean; + depth: number; + parents?: string[]; + children?: string[]; + areChildrenVisible?: true; +}; + +let rowCounter = 0; +const generateRowId = () => `row_${++rowCounter}`; + +const { ImageSet } = classes; +const { DicomMetaDictionary } = dcmjs.data; +const { nameMap } = DicomMetaDictionary; + +const DicomTagBrowser = ({ + displaySets, + displaySetInstanceUID, +}: { + displaySets: Types.DisplaySet[]; + displaySetInstanceUID: string; +}) => { + const [selectedDisplaySetInstanceUID, setSelectedDisplaySetInstanceUID] = + useState(displaySetInstanceUID); + const [instanceNumber, setInstanceNumber] = useState(1); + const [shouldShowInstanceList, setShouldShowInstanceList] = useState(false); + const [filterValue, setFilterValue] = useState(''); + + const onSelectChange = value => { + setSelectedDisplaySetInstanceUID(value.value); + setInstanceNumber(1); + }; + + const activeDisplaySet = displaySets.find( + ds => ds.displaySetInstanceUID === selectedDisplaySetInstanceUID + ); + + const displaySetList = useMemo(() => { + displaySets.sort((a, b) => a.SeriesNumber - b.SeriesNumber); + return displaySets.map(displaySet => { + const { + displaySetInstanceUID, + SeriesDate, + SeriesTime, + SeriesNumber, + SeriesDescription, + Modality, + } = displaySet; + + /* Map to display representation */ + const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0]; + const date = moment(dateStr, 'YYYYMMDD:HHmmss'); + const displayDate = date.format('ddd, MMM Do YYYY'); + + return { + value: displaySetInstanceUID, + label: `${SeriesNumber} (${Modality}): ${SeriesDescription}`, + description: displayDate, + }; + }); + }, [displaySets]); + + const getMetadata = useCallback( + isImageStack => { + if (isImageStack) { + return activeDisplaySet.images[instanceNumber - 1]; + } + return activeDisplaySet.instance || activeDisplaySet; + }, + [activeDisplaySet, instanceNumber] + ); + + const rows = useMemo(() => { + const isImageStack = activeDisplaySet instanceof ImageSet; + const metadata = getMetadata(isImageStack); + + setShouldShowInstanceList(isImageStack && activeDisplaySet.images.length > 1); + const tags = getSortedTags(metadata); + const rows = getFormattedRowsFromTags({ tags, metadata, depth: 0 }); + return rows; + }, [getMetadata, activeDisplaySet]); + + const filteredRows = useMemo(() => { + if (!filterValue) { + return rows; + } + + const matchedRowIds = new Set(); + + const propertiesToCheck = ['tag', 'valueRepresentation', 'keyword', 'value']; + + const setIsMatched = row => { + const isDirectMatch = propertiesToCheck.some(propertyName => + row[propertyName]?.toLowerCase().includes(filterValueLowerCase) + ); + + if (!isDirectMatch) { + return; + } + + matchedRowIds.add(row.uid); + + [...(row.parents ?? []), ...(row.children ?? [])].forEach(uid => matchedRowIds.add(uid)); + }; + + const filterValueLowerCase = filterValue.toLowerCase(); + rows.forEach(setIsMatched); + return rows.filter(row => matchedRowIds.has(row.uid)); + }, [rows, filterValue]); + + return ( +
+
+
+
+ Series + +
+ {shouldShowInstanceList && ( +
+ + Instance Number ({instanceNumber} of {activeDisplaySet?.images?.length}) + + { + setInstanceNumber(value); + }} + min={1} + max={activeDisplaySet?.images?.length} + step={1} + className="pt-4" + /> +
+ )} +
+ + Search metadata + + +
+
+
+ +
+ ); +}; + +function getFormattedRowsFromTags({ tags, metadata, depth, parents }) { + const rows: Row[] = []; + + tags.forEach(tagInfo => { + const uid = generateRowId(); + if (tagInfo.vr === 'SQ') { + const children = tagInfo.values.flatMap(value => + getFormattedRowsFromTags({ + tags: value, + metadata, + depth: depth + 1, + parents: parents ? [...parents, uid] : [uid], + }) + ); + const row: Row = { + uid, + tag: tagInfo.tag, + valueRepresentation: tagInfo.vr, + keyword: tagInfo.keyword, + value: '', + depth, + isVisible: true, + areChildrenVisible: true, + children: children.map(child => child.uid), + parents, + }; + rows.push(row, ...children); + } else { + if (tagInfo.vr === 'xs') { + try { + const tag = dcmjs.data.Tag.fromPString(tagInfo.tag).toCleanString(); + const originalTagInfo = metadata[tag]; + tagInfo.vr = originalTagInfo.vr; + } catch (error) { + console.warn(`Failed to parse value representation for tag '${tagInfo.keyword}'`); + } + } + const row: Row = { + uid, + tag: tagInfo.tag, + valueRepresentation: tagInfo.vr, + keyword: tagInfo.keyword, + value: tagInfo.value, + depth, + isVisible: true, + parents, + }; + rows.push(row); + } + }); + + return rows; +} + +function getSortedTags(metadata) { + const tagList = getRows(metadata); + + // Sort top level tags, sequence groups are sorted when created. + _sortTagList(tagList); + + return tagList; +} + +function getRows(metadata, depth = 0) { + // Tag, Type, Value, Keyword + + const keywords = Object.keys(metadata); + + const rows = []; + for (let i = 0; i < keywords.length; i++) { + let keyword = keywords[i]; + + if (keyword === '_vrMap') { + continue; + } + + const tagInfo = nameMap[keyword]; + + let value = metadata[keyword]; + + if (tagInfo && tagInfo.vr === 'SQ') { + const sequenceAsArray = toArray(value); + + // Push line defining the sequence + + const sequence = { + tag: tagInfo.tag, + vr: tagInfo.vr, + keyword, + values: [], + }; + + rows.push(sequence); + + if (value === null) { + // Type 2 Sequence + continue; + } + + sequenceAsArray.forEach(item => { + const sequenceRows = getRows(item, depth + 1); + + if (sequenceRows.length) { + // Sort the sequence group. + _sortTagList(sequenceRows); + sequence.values.push(sequenceRows); + } + }); + + continue; + } + + if (Array.isArray(value)) { + if (value.length > 0 && typeof value[0] != 'object') { + value = value.join('\\'); + } + } + + if (typeof value === 'number') { + value = value.toString(); + } + + if (typeof value !== 'string') { + if (value === null) { + value = ' '; + } else { + if (typeof value === 'object') { + if (value.InlineBinary) { + value = 'Inline Binary'; + } else if (value.BulkDataURI) { + value = `Bulk Data URI`; //: ${value.BulkDataURI}`; + } else if (value.Alphabetic) { + value = value.Alphabetic; + } else { + console.warn(`Unrecognised Value: ${value} for ${keyword}:`); + console.warn(value); + value = ' '; + } + } else { + console.warn(`Unrecognised Value: ${value} for ${keyword}:`); + value = ' '; + } + } + } + + // tag / vr/ keyword/ value + + // Remove retired tags + keyword = keyword.replace('RETIRED_', ''); + if (tagInfo) { + rows.push({ + tag: tagInfo.tag, + vr: tagInfo.vr, + keyword, + value, + }); + } else { + // skip properties without hex tag numbers + const regex = /[0-9A-Fa-f]{6}/g; + if (keyword.match(regex)) { + const tag = `(${keyword.substring(0, 4)},${keyword.substring(4, 8)})`; + rows.push({ + tag, + vr: '', + keyword: 'Private Tag', + value, + }); + } + } + } + + return rows; +} + +function toArray(objectOrArray) { + return Array.isArray(objectOrArray) ? objectOrArray : [objectOrArray]; +} + +function _sortTagList(tagList) { + tagList.sort((a, b) => { + if (a.tag < b.tag) { + return -1; + } + + return 1; + }); +} + +export default DicomTagBrowser; diff --git a/extensions/default/src/DicomTagBrowser/DicomTagTable.tsx b/extensions/default/src/DicomTagBrowser/DicomTagTable.tsx new file mode 100644 index 0000000..ecafdb7 --- /dev/null +++ b/extensions/default/src/DicomTagBrowser/DicomTagTable.tsx @@ -0,0 +1,304 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { VariableSizeList as List } from 'react-window'; +import classNames from 'classnames'; +import debounce from 'lodash.debounce'; +import { Row } from './DicomTagBrowser'; +import { Icons } from '@ohif/ui-next'; + +const lineHeightPx = 20; +const lineHeightClassName = `leading-[${lineHeightPx}px]`; +const rowVerticalPaddingPx = 10; +const rowBottomBorderPx = 1; +const rowVerticalPaddingStyle = { padding: `${rowVerticalPaddingPx}px 0` }; +const rowStyle = { + borderBottomWidth: `${rowBottomBorderPx}px`, + ...rowVerticalPaddingStyle, +}; +const indentationPadding = 8; + +const RowComponent = ({ + row, + style, + keyPrefix, + onToggle, +}: { + row: Row; + style: any; + keyPrefix: string; + onToggle?: (areChildrenVisible: boolean) => void; +}) => { + const handleToggle = useCallback(() => { + onToggle(!row.areChildrenVisible); + }, [row.areChildrenVisible, onToggle]); + + const hasChildren = row.children && row.children.length > 0; + const isChildOrParent = hasChildren || row.depth > 0; + const padding = indentationPadding * (1 + 2 * row.depth); + + return ( +
+ {isChildOrParent && ( +
+ {row.areChildrenVisible ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+ )} +
{row.tag}
+
{row.valueRepresentation}
+
{row.keyword}
+
{row.value}
+
+ ); +}; + +function ColumnHeaders({ tagRef, vrRef, keywordRef, valueRef }) { + return ( +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ ); +} +function DicomTagTable({ rows }: { rows: Row[] }) { + const listRef = useRef(); + const canvasRef = useRef(); + + const [tagHeaderElem, setTagHeaderElem] = useState(null); + const [vrHeaderElem, setVrHeaderElem] = useState(null); + const [keywordHeaderElem, setKeywordHeaderElem] = useState(null); + const [valueHeaderElem, setValueHeaderElem] = useState(null); + const [internalRows, setInternalRows] = useState(rows); + + // Here the refs are inturn stored in state to trigger a render of the table. + // This virtualized table does NOT render until the header is rendered because the header column widths are used to determine the row heights in the table. + // Therefore whenever the refs change (in particular the first time the refs are set), we want to trigger a render of the table. + const tagRef = elem => { + if (elem) { + setTagHeaderElem(elem); + } + }; + const vrRef = elem => { + if (elem) { + setVrHeaderElem(elem); + } + }; + const keywordRef = elem => { + if (elem) { + setKeywordHeaderElem(elem); + } + }; + const valueRef = elem => { + if (elem) { + setValueHeaderElem(elem); + } + }; + + useEffect(() => { + setInternalRows(rows); + }, [rows]); + + const visibleRows = useMemo(() => { + return internalRows.filter(row => row.isVisible); + }, [internalRows]); + + /** + * When new rows are set, scroll to the top and reset the virtualization. + */ + useEffect(() => { + if (!listRef?.current) { + return; + } + + listRef.current.scrollTo(0); + listRef.current.resetAfterIndex(0); + }, [rows]); + + /** + * When the browser window resizes, update the row virtualization (i.e. row heights) + */ + useEffect(() => { + const debouncedResize = debounce(() => listRef.current.resetAfterIndex(0), 100); + + window.addEventListener('resize', debouncedResize); + + return () => { + debouncedResize.cancel(); + window.removeEventListener('resize', debouncedResize); + }; + }, []); + + const getOneRowHeight = useCallback( + row => { + const headerWidths = [ + tagHeaderElem.offsetWidth, + vrHeaderElem.offsetWidth, + keywordHeaderElem.offsetWidth, + valueHeaderElem.offsetWidth, + ]; + + const context = canvasRef.current.getContext('2d'); + context.font = getComputedStyle(canvasRef.current).font; + + const propertiesToCheck = ['tag', 'valueRepresentation', 'keyword', 'value']; + + return Object.entries(row) + .filter(([key]) => propertiesToCheck.includes(key)) + .map(([, colText], index) => { + const colOneLineWidth = context.measureText(colText).width; + const numLines = Math.ceil(colOneLineWidth / headerWidths[index]); + return numLines * lineHeightPx + 2 * rowVerticalPaddingPx + rowBottomBorderPx; + }) + .reduce((maxHeight, colHeight) => Math.max(maxHeight, colHeight), 0); + }, + [keywordHeaderElem, tagHeaderElem, valueHeaderElem, vrHeaderElem] + ); + + /** + * Get the item/row size. We use the header column widths to calculate the various row heights. + * @param index the row index + * @returns the row height + */ + const getItemSize = useCallback( + rows => index => { + const row = rows[index]; + const height = getOneRowHeight(row); + return height; + }, + [getOneRowHeight] + ); + + const onToggle = useCallback( + sourceRow => { + if (!sourceRow.children) { + return undefined; + } + + return areChildrenVisible => { + const newInternalRows = internalRows.map(internalRow => { + if (sourceRow.uid === internalRow.uid) { + return { ...internalRow, areChildrenVisible }; + } + if (sourceRow.children.includes(internalRow.uid)) { + return { ...internalRow, isVisible: areChildrenVisible, areChildrenVisible }; + } + return internalRow; + }); + setInternalRows(newInternalRows); + }; + }, + [internalRows] + ); + + const getRowComponent = useCallback( + ({ rows }: { rows: Row[] }) => + function RowList({ index, style }) { + const row = useMemo(() => rows[index], [index]); + + return ( + + ); + }, + [onToggle] + ); + + /** + * Whenever any one of the column headers is set, then the header is rendered. + * Here we chose the tag header. + */ + const isHeaderRendered = useCallback(() => tagHeaderElem !== null, [tagHeaderElem]); + + return ( +
+ + +
+ {isHeaderRendered() && ( + + {getRowComponent({ rows: visibleRows })} + + )} +
+
+ ); +} + +export default React.memo(DicomTagTable); diff --git a/extensions/default/src/DicomWebDataSource/dcm4cheeReject.js b/extensions/default/src/DicomWebDataSource/dcm4cheeReject.js new file mode 100644 index 0000000..979a682 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/dcm4cheeReject.js @@ -0,0 +1,41 @@ +export default function (wadoRoot, getAuthrorizationHeader) { + return { + series: (StudyInstanceUID, SeriesInstanceUID) => { + return new Promise((resolve, reject) => { + // Reject because of Quality. (Seems the most sensible out of the options) + const CodeValueAndCodeSchemeDesignator = `113001%5EDCM`; + + const url = `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/reject/${CodeValueAndCodeSchemeDesignator}`; + + const xhr = new XMLHttpRequest(); + xhr.open('POST', url, true); + + const headers = getAuthrorizationHeader(); + + for (const key in headers) { + xhr.setRequestHeader(key, headers[key]); + } + + //Send the proper header information along with the request + // TODO -> Auth when we re-add authorization. + + console.log(xhr); + + xhr.onreadystatechange = function () { + //Call a function when the state changes. + if (xhr.readyState == 4) { + switch (xhr.status) { + case 204: + resolve(xhr.responseText); + + break; + case 404: + reject('Your dataSource does not support reject functionality'); + } + } + }; + xhr.send(); + }); + }, + }; +} diff --git a/extensions/default/src/DicomWebDataSource/exampleInstances.js b/extensions/default/src/DicomWebDataSource/exampleInstances.js new file mode 100644 index 0000000..9e17979 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/exampleInstances.js @@ -0,0 +1,280 @@ +export default [ + { + '00080005': { vr: 'CS', Value: ['ISO_IR 100'] }, + '00080008': { vr: 'CS', Value: ['ORIGINAL', 'PRIMARY', 'LOCALIZER'] }, + '00080016': { vr: 'UI', Value: ['1.2.840.10008.5.1.4.1.1.2'] }, + '00080018': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.6'], + }, + '00080020': { vr: 'DA', Value: ['20141125'] }, + '00080021': { vr: 'DA', Value: ['20141125'] }, + '00080022': { vr: 'DA', Value: ['20141125'] }, + '00080023': { vr: 'DA', Value: ['20141125'] }, + '00080030': { vr: 'TM', Value: ['094528.000'] }, + '00080031': { vr: 'TM', Value: ['094604.688'] }, + '00080032': { vr: 'TM', Value: ['094623.600'] }, + '00080033': { vr: 'TM', Value: ['094623.600'] }, + '00080050': { vr: 'SH', Value: ['000092218'] }, + '00080060': { vr: 'CS', Value: ['CT'] }, + '00080070': { vr: 'LO', Value: ['TOSHIBA'] }, + '00080080': { vr: 'LO', Value: ['Precision Imaging Metrics'] }, + '00080090': { vr: 'PN' }, + '00081010': { vr: 'SH' }, + '00081030': { vr: 'LO', Value: ['DFCI CT CHEST W CONTRAST 6023'] }, + '00081032': { + vr: 'SQ', + Value: [ + { + '00080100': { vr: 'SH', Value: ['6023'] }, + '00080102': { vr: 'SH', Value: ['GEIIS'] }, + '00080103': { vr: 'SH', Value: ['0'] }, + '00080104': { vr: 'LO', Value: ['DFCI CT CHEST W CONTRAST 6023'] }, + }, + ], + }, + '0008103E': { vr: 'LO', Value: ['2.0'] }, + '00081040': { vr: 'LO' }, + '00081070': { vr: 'PN' }, + '00081090': { vr: 'LO', Value: ['Aquilion'] }, + '00081110': { + vr: 'SQ', + Value: [ + { + '00081150': { vr: 'UI', Value: ['1.2.840.100008.3.1.2.3.1'] }, + '00081155': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.4'], + }, + }, + ], + }, + '00100010': { vr: 'PN', Value: [{ Alphabetic: 'Venus' }] }, + '00100020': { vr: 'LO', Value: ['0000005'] }, + '00100021': { vr: 'LO', Value: ['001R74:20050625:205502036:195212'] }, + '00100030': { vr: 'DA' }, + '00100040': { vr: 'CS', Value: ['F'] }, + '00101000': { vr: 'LO' }, + '00101010': { vr: 'AS' }, + '00101020': { vr: 'DS' }, + '00101030': { vr: 'DS' }, + '00104000': { vr: 'LT' }, + '00180015': { vr: 'CS', Value: ['CHEST_TO_PELVIS'] }, + '00180022': { vr: 'CS', Value: ['SCANOSCOPE'] }, + '00180050': { vr: 'DS', Value: [2.0] }, + '00180060': { vr: 'DS', Value: [120.0] }, + '00180090': { vr: 'DS', Value: [1000.0] }, + '00181000': { vr: 'LO' }, + '00181020': { vr: 'LO', Value: ['V4.86ER003'] }, + '00181030': { vr: 'LO', Value: ['Chest / Abdomen/Pelvis 5mm'] }, + '00181100': { vr: 'DS', Value: [1000.0] }, + '00181120': { vr: 'DS', Value: [0.0] }, + '00181130': { vr: 'DS', Value: [102.0] }, + '00181140': { vr: 'CS', Value: ['CW'] }, + '00181150': { vr: 'IS', Value: [6840] }, + '00181151': { vr: 'IS', Value: [100] }, + '00181152': { vr: 'IS', Value: [600] }, + '00181160': { vr: 'SH', Value: ['LARGE'] }, + '00181170': { vr: 'IS', Value: [12] }, + '00181190': { vr: 'DS', Value: [1.6, 1.4] }, + '00181210': { vr: 'SH', Value: ['FL03'] }, + '00185100': { vr: 'CS', Value: ['FFS'] }, + '0020000D': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.1'], + }, + '0020000E': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.2'], + }, + '00200010': { vr: 'SH' }, + '00200011': { vr: 'IS', Value: [1] }, + '00200012': { vr: 'IS', Value: [2] }, + '00200013': { vr: 'IS', Value: [2] }, + '00200020': { vr: 'CS', Value: ['F', 'P'] }, + '00200032': { vr: 'DS', Value: [-1.7e-4, -512.0, 1925.0] }, + '00200037': { vr: 'DS', Value: [0.0, 0.0, -1.0, 0.0, 1.0, -0.0] }, + '00200052': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.5'], + }, + '00201040': { vr: 'LO' }, + '00201041': { vr: 'DS', Value: [342.0] }, + '00280002': { vr: 'US', Value: [1] }, + '00280004': { vr: 'CS', Value: ['MONOCHROME2'] }, + '00280010': { vr: 'US', Value: [512] }, + '00280011': { vr: 'US', Value: [512] }, + '00280030': { vr: 'DS', Value: [2.0, 2.0] }, + '00280100': { vr: 'US', Value: [16] }, + '00280101': { vr: 'US', Value: [16] }, + '00280102': { vr: 'US', Value: [15] }, + '00280103': { vr: 'US', Value: [1] }, + '00281050': { vr: 'DS', Value: [110.0] }, + '00281051': { vr: 'DS', Value: [320.0] }, + '00281052': { vr: 'DS', Value: [0.0] }, + '00281053': { vr: 'DS', Value: [1.0] }, + '00321033': { vr: 'LO', Value: ['OUTDFRAD'] }, + '00400002': { vr: 'DA', Value: ['20141125'] }, + '00400003': { vr: 'TM', Value: ['091000'] }, + '00400004': { vr: 'DA', Value: ['20141125'] }, + '00400005': { vr: 'TM', Value: ['094000.000'] }, + '00400244': { vr: 'DA', Value: ['20141125'] }, + '00400245': { vr: 'TM', Value: ['094528.000'] }, + '00400253': { vr: 'SH', Value: ['3708'] }, + '00400260': { + vr: 'SQ', + Value: [ + { + '00080100': { vr: 'SH', Value: ['6035'] }, + '00080102': { vr: 'SH', Value: ['CCG_CSTemp'] }, + '00080104': { vr: 'LO', Value: ['6035/DFCT2 CT 3-SITES W/OC'] }, + }, + ], + }, + '00402017': { vr: 'LO', Value: ['14159097'] }, + '7FE00010': { + vr: 'OW', + BulkDataURI: + 'http://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs/studies/1.3.6.1.4.1.25403.345050719074.3824.20170126082902.1/series/1.3.6.1.4.1.25403.345050719074.3824.20170126082902.2/instances/1.3.6.1.4.1.25403.345050719074.3824.20170126082902.6', + }, + }, + { + '00080005': { vr: 'CS', Value: ['ISO_IR 100'] }, + '00080008': { vr: 'CS', Value: ['ORIGINAL', 'PRIMARY', 'LOCALIZER'] }, + '00080016': { vr: 'UI', Value: ['1.2.840.10008.5.1.4.1.1.2'] }, + '00080018': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.3'], + }, + '00080020': { vr: 'DA', Value: ['20141125'] }, + '00080021': { vr: 'DA', Value: ['20141125'] }, + '00080022': { vr: 'DA', Value: ['20141125'] }, + '00080023': { vr: 'DA', Value: ['20141125'] }, + '00080030': { vr: 'TM', Value: ['094528.000'] }, + '00080031': { vr: 'TM', Value: ['094604.688'] }, + '00080032': { vr: 'TM', Value: ['094557.250'] }, + '00080033': { vr: 'TM', Value: ['094557.250'] }, + '00080050': { vr: 'SH', Value: ['000092218'] }, + '00080060': { vr: 'CS', Value: ['CT'] }, + '00080070': { vr: 'LO', Value: ['TOSHIBA'] }, + '00080080': { vr: 'LO', Value: ['Precision Imaging Metrics'] }, + '00080090': { vr: 'PN' }, + '00081010': { vr: 'SH' }, + '00081030': { vr: 'LO', Value: ['DFCI CT CHEST W CONTRAST 6023'] }, + '00081032': { + vr: 'SQ', + Value: [ + { + '00080100': { vr: 'SH', Value: ['6023'] }, + '00080102': { vr: 'SH', Value: ['GEIIS'] }, + '00080103': { vr: 'SH', Value: ['0'] }, + '00080104': { vr: 'LO', Value: ['DFCI CT CHEST W CONTRAST 6023'] }, + }, + ], + }, + '0008103E': { vr: 'LO', Value: ['2.0'] }, + '00081040': { vr: 'LO' }, + '00081070': { vr: 'PN' }, + '00081090': { vr: 'LO', Value: ['Aquilion'] }, + '00081110': { + vr: 'SQ', + Value: [ + { + '00081150': { vr: 'UI', Value: ['1.2.840.100008.3.1.2.3.1'] }, + '00081155': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.4'], + }, + }, + ], + }, + '00100010': { vr: 'PN', Value: [{ Alphabetic: 'Venus' }] }, + '00100020': { vr: 'LO', Value: ['0000005'] }, + '00100021': { vr: 'LO', Value: ['001R74:20050625:205502036:195212'] }, + '00100030': { vr: 'DA' }, + '00100040': { vr: 'CS', Value: ['F'] }, + '00101000': { vr: 'LO' }, + '00101010': { vr: 'AS' }, + '00101020': { vr: 'DS' }, + '00101030': { vr: 'DS' }, + '00104000': { vr: 'LT' }, + '00180015': { vr: 'CS', Value: ['CHEST_TO_PELVIS'] }, + '00180022': { vr: 'CS', Value: ['SCANOSCOPE'] }, + '00180050': { vr: 'DS', Value: [2.0] }, + '00180060': { vr: 'DS', Value: [120.0] }, + '00180090': { vr: 'DS', Value: [1000.0] }, + '00181000': { vr: 'LO' }, + '00181020': { vr: 'LO', Value: ['V4.86ER003'] }, + '00181030': { vr: 'LO', Value: ['Chest / Abdomen/Pelvis 5mm'] }, + '00181100': { vr: 'DS', Value: [1000.0] }, + '00181120': { vr: 'DS', Value: [0.0] }, + '00181130': { vr: 'DS', Value: [102.0] }, + '00181140': { vr: 'CS', Value: ['CW'] }, + '00181150': { vr: 'IS', Value: [6857] }, + '00181151': { vr: 'IS', Value: [50] }, + '00181152': { vr: 'IS', Value: [300] }, + '00181160': { vr: 'SH', Value: ['LARGE'] }, + '00181170': { vr: 'IS', Value: [6] }, + '00181190': { vr: 'DS', Value: [1.6, 1.4] }, + '00181210': { vr: 'SH', Value: ['FL03'] }, + '00185100': { vr: 'CS', Value: ['FFS'] }, + '0020000D': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.1'], + }, + '0020000E': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.2'], + }, + '00200010': { vr: 'SH' }, + '00200011': { vr: 'IS', Value: [1] }, + '00200012': { vr: 'IS', Value: [1] }, + '00200013': { vr: 'IS', Value: [1] }, + '00200020': { vr: 'CS', Value: ['L', 'F'] }, + '00200032': { vr: 'DS', Value: [-512.0, 1.7e-4, 1925.0] }, + '00200037': { vr: 'DS', Value: [1.0, 0.0, 0.0, 0.0, 0.0, -1.0] }, + '00200052': { + vr: 'UI', + Value: ['1.3.6.1.4.1.25403.345050719074.3824.20170126082902.5'], + }, + '00201040': { vr: 'LO' }, + '00201041': { vr: 'DS', Value: [342.0] }, + '00280002': { vr: 'US', Value: [1] }, + '00280004': { vr: 'CS', Value: ['MONOCHROME2'] }, + '00280010': { vr: 'US', Value: [512] }, + '00280011': { vr: 'US', Value: [512] }, + '00280030': { vr: 'DS', Value: [2.0, 2.0] }, + '00280100': { vr: 'US', Value: [16] }, + '00280101': { vr: 'US', Value: [16] }, + '00280102': { vr: 'US', Value: [15] }, + '00280103': { vr: 'US', Value: [1] }, + '00281050': { vr: 'DS', Value: [100.0] }, + '00281051': { vr: 'DS', Value: [230.0] }, + '00281052': { vr: 'DS', Value: [0.0] }, + '00281053': { vr: 'DS', Value: [1.0] }, + '00321033': { vr: 'LO', Value: ['OUTDFRAD'] }, + '00400002': { vr: 'DA', Value: ['20141125'] }, + '00400003': { vr: 'TM', Value: ['091000'] }, + '00400004': { vr: 'DA', Value: ['20141125'] }, + '00400005': { vr: 'TM', Value: ['094000.000'] }, + '00400244': { vr: 'DA', Value: ['20141125'] }, + '00400245': { vr: 'TM', Value: ['094528.000'] }, + '00400253': { vr: 'SH', Value: ['3708'] }, + '00400260': { + vr: 'SQ', + Value: [ + { + '00080100': { vr: 'SH', Value: ['6035'] }, + '00080102': { vr: 'SH', Value: ['CCG_CSTemp'] }, + '00080104': { vr: 'LO', Value: ['6035/DFCT2 CT 3-SITES W/OC'] }, + }, + ], + }, + '00402017': { vr: 'LO', Value: ['14159097'] }, + '7FE00010': { + vr: 'OW', + BulkDataURI: + 'http://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs/studies/1.3.6.1.4.1.25403.345050719074.3824.20170126082902.1/series/1.3.6.1.4.1.25403.345050719074.3824.20170126082902.2/instances/1.3.6.1.4.1.25403.345050719074.3824.20170126082902.3', + }, + }, +]; diff --git a/extensions/default/src/DicomWebDataSource/index.ts b/extensions/default/src/DicomWebDataSource/index.ts new file mode 100644 index 0000000..d9ee043 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/index.ts @@ -0,0 +1,641 @@ +import { api } from 'dicomweb-client'; +import { DicomMetadataStore, IWebApiDataSource, utils, errorHandler, classes } from '@ohif/core'; + +import { + mapParams, + search as qidoSearch, + seriesInStudy, + processResults, + processSeriesResults, +} from './qido.js'; +import dcm4cheeReject from './dcm4cheeReject.js'; + +import getImageId from './utils/getImageId.js'; +import dcmjs from 'dcmjs'; +import { retrieveStudyMetadata, deleteStudyMetadataPromise } from './retrieveStudyMetadata.js'; +import StaticWadoClient from './utils/StaticWadoClient'; +import getDirectURL from '../utils/getDirectURL'; +import { fixBulkDataURI } from './utils/fixBulkDataURI'; + +const { DicomMetaDictionary, DicomDict } = dcmjs.data; + +const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary; + +const ImplementationClassUID = '2.25.270695996825855179949881587723571202391.2.0.0'; +const ImplementationVersionName = 'OHIF-VIEWER-2.0.0'; +const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1'; + +const metadataProvider = classes.MetadataProvider; + +export type DicomWebConfig = { + /** Data source name */ + name: string; + // wadoUriRoot - Legacy? (potentially unused/replaced) + /** Base URL to use for QIDO requests */ + qidoRoot?: string; + wadoRoot?: string; // - Base URL to use for WADO requests + wadoUri?: string; // - Base URL to use for WADO URI requests + qidoSupportsIncludeField?: boolean; // - Whether QIDO supports the "Include" option to request additional fields in response + imageRendering?: string; // - wadors | ? (unsure of where/how this is used) + thumbnailRendering?: string; // - wadors | ? (unsure of where/how this is used) + /** Whether the server supports reject calls (i.e. DCM4CHEE) */ + supportsReject?: boolean; + /** Request series meta async instead of blocking */ + lazyLoadStudy?: boolean; + /** indicates if the retrieves can fetch singlepart. Options are bulkdata, video, image, or true */ + singlepart?: boolean | string; + /** Transfer syntax to request from the server */ + requestTransferSyntaxUID?: string; + acceptHeader?: string[]; // - Accept header to use for requests + /** Whether to omit quotation marks for multipart requests */ + omitQuotationForMultipartRequest?: boolean; + /** Whether the server supports fuzzy matching */ + supportsFuzzyMatching?: boolean; + /** Whether the server supports wildcard matching */ + supportsWildcard?: boolean; + /** Whether the server supports the native DICOM model */ + supportsNativeDICOMModel?: boolean; + /** Whether to enable request tag */ + enableRequestTag?: boolean; + /** Whether to enable study lazy loading */ + enableStudyLazyLoad?: boolean; + /** Whether to enable bulkDataURI */ + bulkDataURI?: BulkDataURIConfig; + /** Function that is called after the configuration is initialized */ + onConfiguration: (config: DicomWebConfig, params) => DicomWebConfig; + /** Whether to use the static WADO client */ + staticWado?: boolean; + /** User authentication service */ + userAuthenticationService: Record; +}; + +export type BulkDataURIConfig = { + /** Enable bulkdata uri configuration */ + enabled?: boolean; + /** + * Remove the startsWith string. + * This is used to correct reverse proxied URLs by removing the startsWith path + */ + startsWith?: string; + /** + * Adds this prefix path. Only used if the startsWith is defined and has + * been removed. This allows replacing the base path. + */ + prefixWith?: string; + /** Transform the bulkdata path. Used to replace a portion of the path */ + transform?: (uri: string) => string; + /** + * Adds relative resolution to the path handling. + * series is the default, as the metadata retrieved is series level. + */ + relativeResolution?: 'studies' | 'series'; +}; + +/** + * Creates a DICOM Web API based on the provided configuration. + * + * @param dicomWebConfig - Configuration for the DICOM Web API + * @returns DICOM Web API object + */ +function createDicomWebApi(dicomWebConfig: DicomWebConfig, servicesManager) { + const { userAuthenticationService } = servicesManager.services; + let dicomWebConfigCopy, + qidoConfig, + wadoConfig, + qidoDicomWebClient, + wadoDicomWebClient, + getAuthorizationHeader, + generateWadoHeader; + // Default to enabling bulk data retrieves, with no other customization as + // this is part of hte base standard. + dicomWebConfig.bulkDataURI ||= { enabled: true }; + + const implementation = { + initialize: ({ params, query }) => { + if (dicomWebConfig.onConfiguration && typeof dicomWebConfig.onConfiguration === 'function') { + dicomWebConfig = dicomWebConfig.onConfiguration(dicomWebConfig, { + params, + query, + }); + } + + dicomWebConfigCopy = JSON.parse(JSON.stringify(dicomWebConfig)); + + getAuthorizationHeader = () => { + const xhrRequestHeaders = {}; + const authHeaders = userAuthenticationService.getAuthorizationHeader(); + if (authHeaders && authHeaders.Authorization) { + xhrRequestHeaders.Authorization = authHeaders.Authorization; + } + return xhrRequestHeaders; + }; + + generateWadoHeader = () => { + const authorizationHeader = getAuthorizationHeader(); + //Generate accept header depending on config params + const formattedAcceptHeader = utils.generateAcceptHeader( + dicomWebConfig.acceptHeader, + dicomWebConfig.requestTransferSyntaxUID, + dicomWebConfig.omitQuotationForMultipartRequest + ); + + return { + ...authorizationHeader, + Accept: formattedAcceptHeader, + }; + }; + + qidoConfig = { + url: dicomWebConfig.qidoRoot, + staticWado: dicomWebConfig.staticWado, + singlepart: dicomWebConfig.singlepart, + headers: userAuthenticationService.getAuthorizationHeader(), + errorInterceptor: errorHandler.getHTTPErrorHandler(), + supportsFuzzyMatching: dicomWebConfig.supportsFuzzyMatching, + }; + + wadoConfig = { + url: dicomWebConfig.wadoRoot, + staticWado: dicomWebConfig.staticWado, + singlepart: dicomWebConfig.singlepart, + headers: userAuthenticationService.getAuthorizationHeader(), + errorInterceptor: errorHandler.getHTTPErrorHandler(), + supportsFuzzyMatching: dicomWebConfig.supportsFuzzyMatching, + }; + + // TODO -> Two clients sucks, but its better than 1000. + // TODO -> We'll need to merge auth later. + qidoDicomWebClient = dicomWebConfig.staticWado + ? new StaticWadoClient(qidoConfig) + : new api.DICOMwebClient(qidoConfig); + + wadoDicomWebClient = dicomWebConfig.staticWado + ? new StaticWadoClient(wadoConfig) + : new api.DICOMwebClient(wadoConfig); + }, + query: { + studies: { + mapParams: mapParams.bind(), + search: async function (origParams) { + qidoDicomWebClient.headers = getAuthorizationHeader(); + const { studyInstanceUid, seriesInstanceUid, ...mappedParams } = + mapParams(origParams, { + supportsFuzzyMatching: dicomWebConfig.supportsFuzzyMatching, + supportsWildcard: dicomWebConfig.supportsWildcard, + }) || {}; + + const results = await qidoSearch(qidoDicomWebClient, undefined, undefined, mappedParams); + + return processResults(results); + }, + processResults: processResults.bind(), + }, + series: { + // mapParams: mapParams.bind(), + search: async function (studyInstanceUid) { + qidoDicomWebClient.headers = getAuthorizationHeader(); + const results = await seriesInStudy(qidoDicomWebClient, studyInstanceUid); + + return processSeriesResults(results); + }, + // processResults: processResults.bind(), + }, + instances: { + search: (studyInstanceUid, queryParameters) => { + qidoDicomWebClient.headers = getAuthorizationHeader(); + return qidoSearch.call( + undefined, + qidoDicomWebClient, + studyInstanceUid, + null, + queryParameters + ); + }, + }, + }, + retrieve: { + /** + * Generates a URL that can be used for direct retrieve of the bulkdata + * + * @param {object} params + * @param {string} params.tag is the tag name of the URL to retrieve + * @param {object} params.instance is the instance object that the tag is in + * @param {string} params.defaultType is the mime type of the response + * @param {string} params.singlepart is the type of the part to retrieve + * @returns an absolute URL to the resource, if the absolute URL can be retrieved as singlepart, + * or is already retrieved, or a promise to a URL for such use if a BulkDataURI + */ + directURL: params => { + return getDirectURL( + { + wadoRoot: dicomWebConfig.wadoRoot, + singlepart: dicomWebConfig.singlepart, + }, + params + ); + }, + /** + * Provide direct access to the dicom web client for certain use cases + * where the dicom web client is used by an external library such as the + * microscopy viewer. + * Note this instance only needs to support the wado queries, and may not + * support any QIDO or STOW operations. + */ + getWadoDicomWebClient: () => wadoDicomWebClient, + + bulkDataURI: async ({ StudyInstanceUID, BulkDataURI }) => { + qidoDicomWebClient.headers = getAuthorizationHeader(); + const options = { + multipart: false, + BulkDataURI, + StudyInstanceUID, + }; + return qidoDicomWebClient.retrieveBulkData(options).then(val => { + const ret = (val && val[0]) || undefined; + return ret; + }); + }, + series: { + metadata: async ({ + StudyInstanceUID, + filters, + sortCriteria, + sortFunction, + madeInClient = false, + returnPromises = false, + } = {}) => { + if (!StudyInstanceUID) { + throw new Error('Unable to query for SeriesMetadata without StudyInstanceUID'); + } + + if (dicomWebConfig.enableStudyLazyLoad) { + return implementation._retrieveSeriesMetadataAsync( + StudyInstanceUID, + filters, + sortCriteria, + sortFunction, + madeInClient, + returnPromises + ); + } + + return implementation._retrieveSeriesMetadataSync( + StudyInstanceUID, + filters, + sortCriteria, + sortFunction, + madeInClient + ); + }, + }, + }, + + store: { + dicom: async (dataset, request, dicomDict) => { + wadoDicomWebClient.headers = getAuthorizationHeader(); + if (dataset instanceof ArrayBuffer) { + const options = { + datasets: [dataset], + request, + }; + await wadoDicomWebClient.storeInstances(options); + } else { + let effectiveDicomDict = dicomDict; + + if (!dicomDict) { + const meta = { + FileMetaInformationVersion: dataset._meta?.FileMetaInformationVersion?.Value, + MediaStorageSOPClassUID: dataset.SOPClassUID, + MediaStorageSOPInstanceUID: dataset.SOPInstanceUID, + TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN, + ImplementationClassUID, + ImplementationVersionName, + }; + + const denaturalized = denaturalizeDataset(meta); + const defaultDicomDict = new DicomDict(denaturalized); + defaultDicomDict.dict = denaturalizeDataset(dataset); + + effectiveDicomDict = defaultDicomDict; + } + + const part10Buffer = effectiveDicomDict.write(); + + const options = { + datasets: [part10Buffer], + request, + }; + + await wadoDicomWebClient.storeInstances(options); + } + }, + }, + + _retrieveSeriesMetadataSync: async ( + StudyInstanceUID, + filters, + sortCriteria, + sortFunction, + madeInClient + ) => { + const enableStudyLazyLoad = false; + wadoDicomWebClient.headers = generateWadoHeader(); + // data is all SOPInstanceUIDs + const data = await retrieveStudyMetadata( + wadoDicomWebClient, + StudyInstanceUID, + enableStudyLazyLoad, + filters, + sortCriteria, + sortFunction, + dicomWebConfig + ); + + // first naturalize the data + const naturalizedInstancesMetadata = data.map(naturalizeDataset); + + const seriesSummaryMetadata = {}; + const instancesPerSeries = {}; + + naturalizedInstancesMetadata.forEach(instance => { + if (!seriesSummaryMetadata[instance.SeriesInstanceUID]) { + seriesSummaryMetadata[instance.SeriesInstanceUID] = { + StudyInstanceUID: instance.StudyInstanceUID, + StudyDescription: instance.StudyDescription, + SeriesInstanceUID: instance.SeriesInstanceUID, + SeriesDescription: instance.SeriesDescription, + SeriesNumber: instance.SeriesNumber, + SeriesTime: instance.SeriesTime, + SOPClassUID: instance.SOPClassUID, + ProtocolName: instance.ProtocolName, + Modality: instance.Modality, + }; + } + + if (!instancesPerSeries[instance.SeriesInstanceUID]) { + instancesPerSeries[instance.SeriesInstanceUID] = []; + } + + const imageId = implementation.getImageIdsForInstance({ + instance, + }); + + instance.imageId = imageId; + instance.wadoRoot = dicomWebConfig.wadoRoot; + instance.wadoUri = dicomWebConfig.wadoUri; + + metadataProvider.addImageIdToUIDs(imageId, { + StudyInstanceUID, + SeriesInstanceUID: instance.SeriesInstanceUID, + SOPInstanceUID: instance.SOPInstanceUID, + }); + + instancesPerSeries[instance.SeriesInstanceUID].push(instance); + }); + + // grab all the series metadata + const seriesMetadata = Object.values(seriesSummaryMetadata); + DicomMetadataStore.addSeriesMetadata(seriesMetadata, madeInClient); + + Object.keys(instancesPerSeries).forEach(seriesInstanceUID => + DicomMetadataStore.addInstances(instancesPerSeries[seriesInstanceUID], madeInClient) + ); + + return seriesSummaryMetadata; + }, + + _retrieveSeriesMetadataAsync: async ( + StudyInstanceUID, + filters, + sortCriteria, + sortFunction, + madeInClient = false, + returnPromises = false + ) => { + const enableStudyLazyLoad = true; + wadoDicomWebClient.headers = generateWadoHeader(); + // Get Series + const { preLoadData: seriesSummaryMetadata, promises: seriesPromises } = + await retrieveStudyMetadata( + wadoDicomWebClient, + StudyInstanceUID, + enableStudyLazyLoad, + filters, + sortCriteria, + sortFunction, + dicomWebConfig + ); + + /** + * Adds the retrieve bulkdata function to naturalized DICOM data. + * This is done recursively, for sub-sequences. + */ + const addRetrieveBulkDataNaturalized = (naturalized, instance = naturalized) => { + for (const key of Object.keys(naturalized)) { + const value = naturalized[key]; + + if (Array.isArray(value) && typeof value[0] === 'object') { + // Fix recursive values + const validValues = value.filter(Boolean); + validValues.forEach(child => addRetrieveBulkDataNaturalized(child, instance)); + continue; + } + + // The value.Value will be set with the bulkdata read value + // in which case it isn't necessary to re-read this. + if (value && value.BulkDataURI && !value.Value) { + // handle the scenarios where bulkDataURI is relative path + fixBulkDataURI(value, instance, dicomWebConfig); + // Provide a method to fetch bulkdata + value.retrieveBulkData = retrieveBulkData.bind(qidoDicomWebClient, value); + } + } + return naturalized; + }; + + /** + * naturalizes the dataset, and adds a retrieve bulkdata method + * to any values containing BulkDataURI. + * @param {*} instance + * @returns naturalized dataset, with retrieveBulkData methods + */ + const addRetrieveBulkData = instance => { + const naturalized = naturalizeDataset(instance); + + // if we know the server doesn't use bulkDataURI, then don't + if (!dicomWebConfig.bulkDataURI?.enabled) { + return naturalized; + } + + return addRetrieveBulkDataNaturalized(naturalized); + }; + + // Async load series, store as retrieved + function storeInstances(instances) { + const naturalizedInstances = instances.map(addRetrieveBulkData); + + // Adding instanceMetadata to OHIF MetadataProvider + naturalizedInstances.forEach(instance => { + instance.wadoRoot = dicomWebConfig.wadoRoot; + instance.wadoUri = dicomWebConfig.wadoUri; + + const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance; + const numberOfFrames = instance.NumberOfFrames || 1; + // Process all frames consistently, whether single or multiframe + for (let i = 0; i < numberOfFrames; i++) { + const frameNumber = i + 1; + const frameImageId = implementation.getImageIdsForInstance({ + instance, + frame: frameNumber, + }); + // Add imageId specific mapping to this data as the URL isn't necessarily WADO-URI. + metadataProvider.addImageIdToUIDs(frameImageId, { + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + frameNumber: numberOfFrames > 1 ? frameNumber : undefined, + }); + } + + // Adding imageId to each instance + // Todo: This is not the best way I can think of to let external + // metadata handlers know about the imageId that is stored in the store + const imageId = implementation.getImageIdsForInstance({ + instance, + }); + instance.imageId = imageId; + }); + + DicomMetadataStore.addInstances(naturalizedInstances, madeInClient); + } + + function setSuccessFlag() { + const study = DicomMetadataStore.getStudy(StudyInstanceUID); + if (!study) { + return; + } + study.isLoaded = true; + } + + // Google Cloud Healthcare doesn't return StudyInstanceUID, so we need to add + // it manually here + seriesSummaryMetadata.forEach(aSeries => { + aSeries.StudyInstanceUID = StudyInstanceUID; + }); + + DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient); + + const seriesDeliveredPromises = seriesPromises.map(promise => { + if (!returnPromises) { + promise?.start(); + } + return promise.then(instances => { + storeInstances(instances); + }); + }); + + if (returnPromises) { + Promise.all(seriesDeliveredPromises).then(() => setSuccessFlag()); + return seriesPromises; + } else { + await Promise.all(seriesDeliveredPromises); + setSuccessFlag(); + } + + return seriesSummaryMetadata; + }, + deleteStudyMetadataPromise, + getImageIdsForDisplaySet(displaySet) { + const images = displaySet.images; + const imageIds = []; + + if (!images) { + return imageIds; + } + + displaySet.images.forEach(instance => { + const NumberOfFrames = instance.NumberOfFrames; + + if (NumberOfFrames > 1) { + for (let frame = 1; frame <= NumberOfFrames; frame++) { + const imageId = this.getImageIdsForInstance({ + instance, + frame, + }); + imageIds.push(imageId); + } + } else { + const imageId = this.getImageIdsForInstance({ instance }); + imageIds.push(imageId); + } + }); + + return imageIds; + }, + getImageIdsForInstance({ instance, frame = undefined }) { + const imageIds = getImageId({ + instance, + frame, + config: dicomWebConfig, + }); + return imageIds; + }, + getConfig() { + return dicomWebConfigCopy; + }, + getStudyInstanceUIDs({ params, query }) { + const paramsStudyInstanceUIDs = params.StudyInstanceUIDs || params.studyInstanceUIDs; + + const queryStudyInstanceUIDs = utils.splitComma( + query.getAll('StudyInstanceUIDs').concat(query.getAll('studyInstanceUIDs')) + ); + + const StudyInstanceUIDs = + (queryStudyInstanceUIDs.length && queryStudyInstanceUIDs) || paramsStudyInstanceUIDs; + const StudyInstanceUIDsAsArray = + StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs) + ? StudyInstanceUIDs + : [StudyInstanceUIDs]; + + return StudyInstanceUIDsAsArray; + }, + }; + + if (dicomWebConfig.supportsReject) { + implementation.reject = dcm4cheeReject(dicomWebConfig.wadoRoot, getAuthorizationHeader); + } + + return IWebApiDataSource.create(implementation); +} + +/** + * A bindable function that retrieves the bulk data against this as the + * dicomweb client, and on the given value element. + * + * @param value - a bind value that stores the retrieve value to short circuit the + * next retrieve instance. + * @param options - to allow specifying the content type. + */ +function retrieveBulkData(value, options = {}) { + const { mediaType } = options; + const useOptions = { + // The bulkdata fetches work with either multipart or + // singlepart, so set multipart to false to let the server + // decide which type to respond with. + multipart: false, + BulkDataURI: value.BulkDataURI, + mediaTypes: mediaType ? [{ mediaType }, { mediaType: 'application/octet-stream' }] : undefined, + ...options, + }; + return this.retrieveBulkData(useOptions).then(val => { + // There are DICOM PDF cases where the first ArrayBuffer in the array is + // the bulk data and DICOM video cases where the second ArrayBuffer is + // the bulk data. Here we play it safe and do a find. + const ret = + (val instanceof Array && val.find(arrayBuffer => arrayBuffer?.byteLength)) || undefined; + value.Value = ret; + return ret; + }); +} + +export { createDicomWebApi }; diff --git a/extensions/default/src/DicomWebDataSource/qido.js b/extensions/default/src/DicomWebDataSource/qido.js new file mode 100644 index 0000000..ee13fa8 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/qido.js @@ -0,0 +1,215 @@ +/** + * QIDO - Query based on ID for DICOM Objects + * search for studies, series and instances by patient ID, and receive their + * unique identifiers for further usage. + * + * Quick: https://www.dicomstandard.org/dicomweb/query-qido-rs/ + * Standard: http://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.6 + * + * Routes: + * ========== + * /studies? + * /studies/{studyInstanceUid}/series? + * /studies/{studyInstanceUid}/series/{seriesInstanceUid}/instances? + * + * Query Parameters: + * ================ + * | KEY | VALUE | + * |------------------|--------------------| + * | {attributeId} | {value} | + * | includeField | {attribute} or all | + * | fuzzymatching | true OR false | + * | limit | {number} | + * | offset | {number} | + */ +import { DICOMWeb, utils } from '@ohif/core'; +import { sortStudySeries } from '@ohif/core/src/utils/sortStudy'; + +const { getString, getName, getModalities } = DICOMWeb; + +/** + * Parses resulting data from a QIDO call into a set of Study MetaData + * + * @param {Array} qidoStudies - An array of study objects. Each object contains a keys for DICOM tags. + * @param {object} qidoStudies[0].qidoStudy - An object where each key is the DICOM Tag group+element + * @param {object} qidoStudies[0].qidoStudy[dicomTag] - Optional object that represents DICOM Tag + * @param {string} qidoStudies[0].qidoStudy[dicomTag].vr - Value Representation + * @param {string[]} qidoStudies[0].qidoStudy[dicomTag].Value - Optional string array representation of the DICOM Tag's value + * @returns {Array} An array of Study MetaData objects + */ +function processResults(qidoStudies) { + if (!qidoStudies || !qidoStudies.length) { + return []; + } + + const studies = []; + + qidoStudies.forEach(qidoStudy => + studies.push({ + studyInstanceUid: getString(qidoStudy['0020000D']), + date: getString(qidoStudy['00080020']), // YYYYMMDD + time: getString(qidoStudy['00080030']), // HHmmss.SSS (24-hour, minutes, seconds, fractional seconds) + accession: getString(qidoStudy['00080050']) || '', // short string, probably a number? + mrn: getString(qidoStudy['00100020']) || '', // medicalRecordNumber + patientName: utils.formatPN(getName(qidoStudy['00100010'])) || '', + instances: Number(getString(qidoStudy['00201208'])) || 0, // number + description: getString(qidoStudy['00081030']) || '', + modalities: getString(getModalities(qidoStudy['00080060'], qidoStudy['00080061'])) || '', + }) + ); + + return studies; +} + +/** + * Parses resulting data from a QIDO call into a set of Study MetaData + * + * @param {Array} qidoSeries - An array of study objects. Each object contains a keys for DICOM tags. + * @param {object} qidoSeries[0].qidoSeries - An object where each key is the DICOM Tag group+element + * @param {object} qidoSeries[0].qidoSeries[dicomTag] - Optional object that represents DICOM Tag + * @param {string} qidoSeries[0].qidoSeries[dicomTag].vr - Value Representation + * @param {string[]} qidoSeries[0].qidoSeries[dicomTag].Value - Optional string array representation of the DICOM Tag's value + * @returns {Array} An array of Study MetaData objects + */ +export function processSeriesResults(qidoSeries) { + const series = []; + + if (qidoSeries && qidoSeries.length) { + qidoSeries.forEach(qidoSeries => + series.push({ + studyInstanceUid: getString(qidoSeries['0020000D']), + seriesInstanceUid: getString(qidoSeries['0020000E']), + modality: getString(qidoSeries['00080060']), + seriesNumber: getString(qidoSeries['00200011']), + seriesDate: utils.formatDate(getString(qidoSeries['00080021'])), + numSeriesInstances: Number(getString(qidoSeries['00201209'])), + description: getString(qidoSeries['0008103E']), + }) + ); + } + + sortStudySeries(series); + + return series; +} + +/** + * + * @param {object} dicomWebClient - Client similar to what's provided by `dicomweb-client` library + * @param {function} dicomWebClient.searchForStudies - + * @param {string} [studyInstanceUid] + * @param {string} [seriesInstanceUid] + * @param {string} [queryParamaters] + * @returns {Promise} - Promise that resolves results + */ +async function search(dicomWebClient, studyInstanceUid, seriesInstanceUid, queryParameters) { + let searchResult = await dicomWebClient.searchForStudies({ + studyInstanceUid: undefined, + queryParams: queryParameters, + }); + + return searchResult; +} + +/** + * + * @param {string} studyInstanceUID - ID of study to return a list of series for + * @returns {Promise} - Resolves SeriesMetadata[] in study + */ +export function seriesInStudy(dicomWebClient, studyInstanceUID) { + // Series Description + // Already included? + const commaSeparatedFields = ['0008103E', '00080021'].join(','); + const queryParams = { + includefield: commaSeparatedFields, + }; + + return dicomWebClient.searchForSeries({ studyInstanceUID, queryParams }); +} + +export default function searchStudies(server, filter) { + const queryParams = getQIDOQueryParams(filter, server.qidoSupportsIncludeField); + const options = { + queryParams, + }; + + return dicomWeb.searchForStudies(options).then(resultDataToStudies); +} + +/** + * Produces a QIDO URL given server details and a set of specified search filter + * items + * + * @param filter + * @param serverSupportsQIDOIncludeField + * @returns {string} The URL with encoded filter query data + */ +function mapParams(params, options = {}) { + if (!params) { + return; + } + const commaSeparatedFields = [ + '00081030', // Study Description + '00080060', // Modality + // Add more fields here if you want them in the result + ].join(','); + + const useWildcard = + params?.disableWildcard !== undefined ? !params.disableWildcard : options.supportsWildcard; + + const withWildcard = value => { + return useWildcard && value ? `*${value}*` : value; + }; + + const parameters = { + // Named + PatientName: withWildcard(params.patientName), + //PatientID: withWildcard(params.patientId), + '00100020': withWildcard(params.patientId), // Temporarily to make the tests pass with dicomweb-server.. Apparently it's broken? + AccessionNumber: withWildcard(params.accessionNumber), + StudyDescription: withWildcard(params.studyDescription), + ModalitiesInStudy: params.modalitiesInStudy, + // Other + limit: params.limit || 101, + offset: params.offset || 0, + fuzzymatching: options.supportsFuzzyMatching === true, + includefield: commaSeparatedFields, // serverSupportsQIDOIncludeField ? commaSeparatedFields : 'all', + }; + + // build the StudyDate range parameter + if (params.startDate && params.endDate) { + parameters.StudyDate = `${params.startDate}-${params.endDate}`; + } else if (params.startDate) { + const today = new Date(); + const DD = String(today.getDate()).padStart(2, '0'); + const MM = String(today.getMonth() + 1).padStart(2, '0'); //January is 0! + const YYYY = today.getFullYear(); + const todayStr = `${YYYY}${MM}${DD}`; + + parameters.StudyDate = `${params.startDate}-${todayStr}`; + } else if (params.endDate) { + const oldDateStr = `19700102`; + + parameters.StudyDate = `${oldDateStr}-${params.endDate}`; + } + + // Build the StudyInstanceUID parameter + if (params.studyInstanceUid) { + let studyUids = params.studyInstanceUid; + studyUids = Array.isArray(studyUids) ? studyUids.join() : studyUids; + studyUids = studyUids.replace(/[^0-9.]+/g, '\\'); + parameters.StudyInstanceUID = studyUids; + } + + // Clean query params of undefined values. + const final = {}; + Object.keys(parameters).forEach(key => { + if (parameters[key] !== undefined && parameters[key] !== '') { + final[key] = parameters[key]; + } + }); + + return final; +} + +export { mapParams, search, processResults }; diff --git a/extensions/default/src/DicomWebDataSource/retrieveStudyMetadata.js b/extensions/default/src/DicomWebDataSource/retrieveStudyMetadata.js new file mode 100644 index 0000000..9c3c590 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/retrieveStudyMetadata.js @@ -0,0 +1,91 @@ +import retrieveMetadataFiltered from './utils/retrieveMetadataFiltered.js'; +import RetrieveMetadata from './wado/retrieveMetadata.js'; + +const moduleName = 'RetrieveStudyMetadata'; +// Cache for promises. Prevents unnecessary subsequent calls to the server +const StudyMetaDataPromises = new Map(); + +/** + * Retrieves study metadata. + * + * @param {Object} dicomWebClient The DICOMWebClient instance to be used for series load + * @param {string} StudyInstanceUID The UID of the Study to be retrieved + * @param {boolean} enableStudyLazyLoad Whether the study metadata should be loaded asynchronously. + * @param {Object} [filters] Object containing filters to be applied on retrieve metadata process + * @param {string} [filters.seriesInstanceUID] Series instance uid to filter results against + * @param {function} [sortCriteria] Sort criteria function + * @param {function} [sortFunction] Sort function + * + * @returns {Promise} that will be resolved with the metadata or rejected with the error + */ +export function retrieveStudyMetadata( + dicomWebClient, + StudyInstanceUID, + enableStudyLazyLoad, + filters, + sortCriteria, + sortFunction, + dicomWebConfig = {} +) { + // @TODO: Whenever a study metadata request has failed, its related promise will be rejected once and for all + // and further requests for that metadata will always fail. On failure, we probably need to remove the + // corresponding promise from the "StudyMetaDataPromises" map... + + if (!dicomWebClient) { + throw new Error(`${moduleName}: Required 'dicomWebClient' parameter not provided.`); + } + if (!StudyInstanceUID) { + throw new Error(`${moduleName}: Required 'StudyInstanceUID' parameter not provided.`); + } + + const promiseId = `${dicomWebConfig.name}:${StudyInstanceUID}`; + + // Already waiting on result? Return cached promise + if (StudyMetaDataPromises.has(promiseId)) { + return StudyMetaDataPromises.get(promiseId); + } + + let promise; + + if (filters && filters.seriesInstanceUID && Array.isArray(filters.seriesInstanceUID)) { + promise = retrieveMetadataFiltered( + dicomWebClient, + StudyInstanceUID, + enableStudyLazyLoad, + filters, + sortCriteria, + sortFunction + ); + } else { + // Create a promise to handle the data retrieval + promise = new Promise((resolve, reject) => { + RetrieveMetadata( + dicomWebClient, + StudyInstanceUID, + enableStudyLazyLoad, + filters, + sortCriteria, + sortFunction + ).then(function (data) { + resolve(data); + }, reject); + }); + } + + // Store the promise in cache + StudyMetaDataPromises.set(promiseId, promise); + + return promise; +} + +/** + * Delete the cached study metadata retrieval promise to ensure that the browser will + * re-retrieve the study metadata when it is next requested. + * + * @param {String} StudyInstanceUID The UID of the Study to be removed from cache + */ +export function deleteStudyMetadataPromise(StudyInstanceUID) { + if (StudyMetaDataPromises.has(StudyInstanceUID)) { + StudyMetaDataPromises.delete(StudyInstanceUID); + } +} diff --git a/extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.ts b/extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.ts new file mode 100644 index 0000000..a34cc9d --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.ts @@ -0,0 +1,272 @@ +import { api } from 'dicomweb-client'; +import fixMultipart from './fixMultipart'; + +const { DICOMwebClient } = api; + +const anyDicomwebClient = DICOMwebClient as any; + +// Ugly over-ride, but the internals aren't otherwise accessible. +if (!anyDicomwebClient._orig_buildMultipartAcceptHeaderFieldValue) { + anyDicomwebClient._orig_buildMultipartAcceptHeaderFieldValue = + anyDicomwebClient._buildMultipartAcceptHeaderFieldValue; + anyDicomwebClient._buildMultipartAcceptHeaderFieldValue = function (mediaTypes, acceptableTypes) { + if (mediaTypes.length === 1 && mediaTypes[0].mediaType.endsWith('/*')) { + return '*/*'; + } else { + return anyDicomwebClient._orig_buildMultipartAcceptHeaderFieldValue( + mediaTypes, + acceptableTypes + ); + } + }; +} + +/** + * An implementation of the static wado client, that fetches data from + * a static response rather than actually doing real queries. This allows + * fast encoding of test data, but because it is static, anything actually + * performing searches doesn't work. This version fixes the query issue + * by manually implementing a query option. + */ + +export default class StaticWadoClient extends api.DICOMwebClient { + static studyFilterKeys = { + studyinstanceuid: '0020000D', + patientname: '00100010', + '00100020': 'mrn', + studydescription: '00081030', + studydate: '00080020', + modalitiesinstudy: '00080061', + accessionnumber: '00080050', + }; + + static seriesFilterKeys = { + seriesinstanceuid: '0020000E', + seriesnumber: '00200011', + modality: '00080060', + }; + + protected config; + protected staticWado; + + constructor(config) { + super(config); + this.staticWado = config.staticWado; + this.config = config; + } + + /** + * Handle improperly specified multipart/related return type. + * Note if the response is SUPPOSED to be multipart encoded already, then this + * will double-decode it. + * + * @param options + * @returns De-multiparted response data. + * + */ + public retrieveBulkData(options): Promise { + const shouldFixMultipart = this.config.fixBulkdataMultipart !== false; + const useOptions = { + ...options, + }; + if (this.staticWado) { + useOptions.mediaTypes = [{ mediaType: 'application/*' }]; + } + return super + .retrieveBulkData(useOptions) + .then(result => (shouldFixMultipart ? fixMultipart(result) : result)); + } + + /** + * Retrieves instance frames using the image/* media type when configured + * to do so (static wado back end). + */ + public retrieveInstanceFrames(options) { + if (this.staticWado) { + return super.retrieveInstanceFrames({ + ...options, + mediaTypes: [{ mediaType: 'image/*' }], + }); + } else { + return super.retrieveInstanceFrames(options); + } + } + + /** + * Replace the search for studies remote query with a local version which + * retrieves a complete query list and then sub-selects from it locally. + * @param {*} options + * @returns + */ + async searchForStudies(options) { + if (!this.staticWado) { + return super.searchForStudies(options); + } + + const searchResult = await super.searchForStudies(options); + const { queryParams } = options; + + if (!queryParams) { + return searchResult; + } + + const lowerParams = this.toLowerParams(queryParams); + const filtered = searchResult.filter(study => { + for (const key of Object.keys(StaticWadoClient.studyFilterKeys)) { + if (!this.filterItem(key, lowerParams, study, StaticWadoClient.studyFilterKeys)) { + return false; + } + } + return true; + }); + return filtered; + } + + async searchForSeries(options) { + if (!this.staticWado) { + return super.searchForSeries(options); + } + + const searchResult = await super.searchForSeries(options); + const { queryParams } = options; + if (!queryParams) { + return searchResult; + } + const lowerParams = this.toLowerParams(queryParams); + + const filtered = searchResult.filter(series => { + for (const key of Object.keys(StaticWadoClient.seriesFilterKeys)) { + if (!this.filterItem(key, lowerParams, series, StaticWadoClient.seriesFilterKeys)) { + return false; + } + } + return true; + }); + + return filtered; + } + + /** + * Compares values, matching any instance of desired to any instance of + * actual by recursively go through the paired set of values. That is, + * this is O(m*n) where m is how many items in desired and n is the length of actual + * Then, at the individual item node, compares the Alphabetic name if present, + * and does a sub-string matching on string values, and otherwise does an + * exact match comparison. + * + * @param {*} desired + * @param {*} actual + * @param {*} options - fuzzyMatching: if true, then do a sub-string match + * @returns true if the values match + */ + compareValues(desired, actual, options) { + const { fuzzyMatching } = options; + + if (Array.isArray(desired)) { + return desired.find(item => this.compareValues(item, actual, options)); + } + if (Array.isArray(actual)) { + return actual.find(actualItem => this.compareValues(desired, actualItem, options)); + } + if (actual?.Alphabetic) { + actual = actual.Alphabetic; + } + + if (fuzzyMatching && typeof actual === 'string' && typeof desired === 'string') { + const normalizeValue = str => { + return str.toLowerCase(); + }; + + const normalizedDesired = normalizeValue(desired); + const normalizedActual = normalizeValue(actual); + + const tokenizeAndNormalize = str => str.split(/[\s^]+/).filter(Boolean); + + const desiredTokens = tokenizeAndNormalize(normalizedDesired); + const actualTokens = tokenizeAndNormalize(normalizedActual); + + return desiredTokens.every(desiredToken => + actualTokens.some(actualToken => actualToken.startsWith(desiredToken)) + ); + } + + if (typeof actual == 'string') { + if (actual.length === 0) { + return true; + } + if (desired.length === 0 || desired === '*') { + return true; + } + if (desired[0] === '*' && desired[desired.length - 1] === '*') { + // console.log(`Comparing ${actual} to ${desired.substring(1, desired.length - 1)}`) + return actual.indexOf(desired.substring(1, desired.length - 1)) != -1; + } else if (desired[desired.length - 1] === '*') { + return actual.indexOf(desired.substring(0, desired.length - 1)) != -1; + } else if (desired[0] === '*') { + return actual.indexOf(desired.substring(1)) === actual.length - desired.length + 1; + } + } + return desired === actual; + } + + /** Compares a pair of dates to see if the value is within the range */ + compareDateRange(range, value) { + if (!value) { + return true; + } + const dash = range.indexOf('-'); + if (dash === -1) { + return this.compareValues(range, value, {}); + } + const start = range.substring(0, dash); + const end = range.substring(dash + 1); + return (!start || value >= start) && (!end || value <= end); + } + + /** + * Filters the return list by the query parameters. + * + * @param anyCaseKey - a possible search key + * @param queryParams - + * @param {*} study + * @param {*} sourceFilterMap + * @returns + */ + filterItem(key: string, queryParams, study, sourceFilterMap) { + const isName = (key: string) => key.indexOf('name') !== -1; + + const { supportsFuzzyMatching = false } = this.config; + + const options = { + fuzzyMatching: isName(key) && supportsFuzzyMatching, + }; + + const altKey = sourceFilterMap[key] || key; + if (!queryParams) { + return true; + } + const testValue = queryParams[key] || queryParams[altKey]; + if (!testValue) { + return true; + } + const valueElem = study[key] || study[altKey]; + if (!valueElem) { + return false; + } + if (valueElem.vr === 'DA' && valueElem.Value?.[0]) { + return this.compareDateRange(testValue, valueElem.Value[0]); + } + const value = valueElem.Value; + + return this.compareValues(testValue, value, options); + } + + /** Converts the query parameters to lower case query parameters */ + toLowerParams(queryParams: Record): Record { + const lowerParams = {}; + Object.entries(queryParams).forEach(([key, value]) => { + lowerParams[key.toLowerCase()] = value; + }); + return lowerParams; + } +} diff --git a/extensions/default/src/DicomWebDataSource/utils/cleanDenaturalizedDataset.ts b/extensions/default/src/DicomWebDataSource/utils/cleanDenaturalizedDataset.ts new file mode 100644 index 0000000..6122799 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/utils/cleanDenaturalizedDataset.ts @@ -0,0 +1,78 @@ +import { fixBulkDataURI } from './fixBulkDataURI'; + +function isPrimitive(v: any) { + return !(typeof v == 'object' || Array.isArray(v)); +} + +const vrNumerics = new Set([ + 'DS', + 'FL', + 'FD', + 'IS', + 'OD', + 'OF', + 'OL', + 'OV', + 'SL', + 'SS', + 'SV', + 'UL', + 'US', + 'UV', +]); + +/** + * Specialized for DICOM JSON format dataset cleaning. + * @param obj + * @returns + */ +export function cleanDenaturalizedDataset( + obj: any, + options?: { + StudyInstanceUID: string; + SeriesInstanceUID: string; + dataSourceConfig: unknown; + } +): any { + if (Array.isArray(obj)) { + const newAry = obj.map(o => (isPrimitive(o) ? o : cleanDenaturalizedDataset(o, options))); + return newAry; + } + if (isPrimitive(obj)) { + return obj; + } + Object.keys(obj).forEach(key => { + if (obj[key].Value === null && obj[key].vr) { + delete obj[key].Value; + } else if (Array.isArray(obj[key].Value) && obj[key].vr) { + if (obj[key].Value.length === 1 && obj[key].Value[0].BulkDataURI) { + if (options?.dataSourceConfig) { + // Not needed unless data source is directly used for loading data. + fixBulkDataURI(obj[key].Value[0], options, options.dataSourceConfig); + } + + obj[key].BulkDataURI = obj[key].Value[0].BulkDataURI; + + // prevent mixed-content blockage + if (window.location.protocol === 'https:' && obj[key].BulkDataURI.startsWith('http:')) { + obj[key].BulkDataURI = obj[key].BulkDataURI.replace('http:', 'https:'); + } + delete obj[key].Value; + } else if (vrNumerics.has(obj[key].vr)) { + obj[key].Value = obj[key].Value.map(v => +v); + } else { + obj[key].Value = obj[key].Value.map(entry => cleanDenaturalizedDataset(entry, options)); + } + } + }); + return obj; +} + +/** + * This is required to make the denaturalized data transferrable when it has + * added proxy values. + */ +export function transferDenaturalizedDataset(dataset) { + const noNull = cleanDenaturalizedDataset(dataset); + return JSON.parse(JSON.stringify(noNull)); +} diff --git a/extensions/default/src/DicomWebDataSource/utils/findIndexOfString.ts b/extensions/default/src/DicomWebDataSource/utils/findIndexOfString.ts new file mode 100644 index 0000000..f5bbd35 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/utils/findIndexOfString.ts @@ -0,0 +1,47 @@ +function checkToken(token, data, dataOffset): boolean { + if (dataOffset + token.length > data.length) { + return false; + } + + let endIndex = dataOffset; + + for (let i = 0; i < token.length; i++) { + if (token[i] !== data[endIndex++]) { + return false; + } + } + + return true; +} + +function stringToUint8Array(str: string): Uint8Array { + const uint = new Uint8Array(str.length); + + for (let i = 0, j = str.length; i < j; i++) { + uint[i] = str.charCodeAt(i); + } + + return uint; +} + +function findIndexOfString( + data: Uint8Array, + str: string, + offset?: number +): number { + offset = offset || 0; + + const token = stringToUint8Array(str); + + for (let i = offset; i < data.length; i++) { + if (token[0] === data[i]) { + // console.log('match @', i); + if (checkToken(token, data, i)) { + return i; + } + } + } + + return -1; +} +export default findIndexOfString; diff --git a/extensions/default/src/DicomWebDataSource/utils/fixBulkDataURI.ts b/extensions/default/src/DicomWebDataSource/utils/fixBulkDataURI.ts new file mode 100644 index 0000000..68dab96 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/utils/fixBulkDataURI.ts @@ -0,0 +1,71 @@ +/** + * Modifies a bulkDataURI to ensure it is absolute based on the DICOMWeb configuration and + * instance data. The modification is in-place. + * + * If the bulkDataURI is relative to the series or study (according to the DICOM standard), + * it is made absolute by prepending the relevant paths. + * + * In scenarios where the bulkDataURI is a server-relative path (starting with '/'), the function + * handles two cases: + * + * 1. If the wado root is absolute (starts with 'http'), it prepends the wado root to the bulkDataURI. + * 2. If the wado root is relative, no changes are needed as the bulkDataURI is already correctly relative to the server root. + * + * @param value - The object containing BulkDataURI to be fixed. + * @param instance - The object (DICOM instance data) containing StudyInstanceUID and SeriesInstanceUID. + * @param dicomWebConfig - The DICOMWeb configuration object, containing wadoRoot and potentially bulkDataURI.relativeResolution. + * @returns The function modifies `value` in-place, it does not return a value. + */ +function fixBulkDataURI(value, instance, dicomWebConfig) { + // in case of the relative path, make it absolute. The current DICOM standard says + // the bulkdataURI is relative to the series. However, there are situations where + // it can be relative to the study too + let { BulkDataURI } = value; + const { bulkDataURI: uriConfig = {} } = dicomWebConfig; + + BulkDataURI = uriConfig.transform?.(BulkDataURI) || BulkDataURI; + + // Handle incorrectly prefixed origins + const { startsWith, prefixWith = '' } = uriConfig; + if (startsWith && BulkDataURI.startsWith(startsWith)) { + BulkDataURI = prefixWith + BulkDataURI.substring(startsWith.length); + value.BulkDataURI = BulkDataURI; + } + + if (!BulkDataURI.startsWith('http') && !value.BulkDataURI.startsWith('/')) { + const { StudyInstanceUID, SeriesInstanceUID } = instance; + const isInstanceStart = BulkDataURI.startsWith('instances/') || BulkDataURI.startsWith('../'); + if ( + BulkDataURI.startsWith('series/') || + BulkDataURI.startsWith('bulkdata/') || + (uriConfig.relativeResolution === 'studies' && !isInstanceStart) + ) { + value.BulkDataURI = `${dicomWebConfig.wadoRoot}/studies/${StudyInstanceUID}/${BulkDataURI}`; + } else if ( + isInstanceStart || + uriConfig.relativeResolution === 'series' || + !uriConfig.relativeResolution + ) { + value.BulkDataURI = `${dicomWebConfig.wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/${BulkDataURI}`; + } + return; + } + + // in case it is relative path but starts at the server (e.g., /bulk/1e, note the missing http + // in the beginning and the first character is /) There are two scenarios, whether the wado root + // is absolute or relative. In case of absolute, we need to prepend the wado root to the bulkdata + // uri (e.g., bulkData: /bulk/1e, wado root: http://myserver.com/dicomweb, output: http://myserver.com/bulk/1e) + // and in case of relative wado root, we need to prepend the bulkdata uri to the wado root (e.g,. bulkData: /bulk/1e + // wado root: /dicomweb, output: /bulk/1e) + if (BulkDataURI[0] === '/') { + if (dicomWebConfig.wadoRoot.startsWith('http')) { + // Absolute wado root + const url = new URL(dicomWebConfig.wadoRoot); + value.BulkDataURI = `${url.origin}${BulkDataURI}`; + } else { + // Relative wado root, we don't need to do anything, bulkdata uri is already correct + } + } +} + +export { fixBulkDataURI }; diff --git a/extensions/default/src/DicomWebDataSource/utils/fixMultiValueKeys.ts b/extensions/default/src/DicomWebDataSource/utils/fixMultiValueKeys.ts new file mode 100644 index 0000000..c0e462e --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/utils/fixMultiValueKeys.ts @@ -0,0 +1,12 @@ +/** + * Fix multi-valued keys so that those which are strings split by + * a backslash are returned as arrays. + */ +export function fixMultiValueKeys(naturalData, keys = ['ImageType']) { + for (const key of keys) { + if (typeof naturalData[key] === 'string') { + naturalData[key] = naturalData[key].split('\\'); + } + } + return naturalData; +} diff --git a/extensions/default/src/DicomWebDataSource/utils/fixMultipart.ts b/extensions/default/src/DicomWebDataSource/utils/fixMultipart.ts new file mode 100644 index 0000000..77e3071 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/utils/fixMultipart.ts @@ -0,0 +1,70 @@ +import findIndexOfString from './findIndexOfString'; + +/** + * Fix multipart data coming back from the retrieve bulkdata request, but + * incorrectly tagged as application/octet-stream. Some servers don't handle + * the response type correctly, and this method is relatively robust about + * detecting multipart data correctly. It will only extract one value. + */ +export default function fixMultipart(arrayData) { + const data = new Uint8Array(arrayData[0]); + // Don't know the exact minimum length, but it is at least 25 to encode multipart + if (data.length < 25) { + return arrayData; + } + const dashIndex = findIndexOfString(data, '--'); + if (dashIndex > 6) { + return arrayData; + } + const tokenIndex = findIndexOfString(data, '\r\n\r\n', dashIndex); + if (tokenIndex > 512) { + // Allow for 512 characters in the header - there is no apriori limit, but + // this seems ok for now as we only expect it to have content type in it. + return arrayData; + } + const header = uint8ArrayToString(data, 0, tokenIndex); + // Now find the boundary marker + const responseHeaders = header.split('\r\n'); + const boundary = findBoundary(responseHeaders); + + if (!boundary) { + return arrayData; + } + // Start of actual data is 4 characters after the token + const offset = tokenIndex + 4; + + const endIndex = findIndexOfString(data, boundary, offset); + if (endIndex === -1) { + return arrayData; + } + + return [data.slice(offset, endIndex - 2).buffer]; +} + +export function findBoundary(header: string[]): string { + for (let i = 0; i < header.length; i++) { + if (header[i].substr(0, 2) === '--') { + return header[i]; + } + } +} + +export function findContentType(header: string[]): string { + for (let i = 0; i < header.length; i++) { + if (header[i].substr(0, 13) === 'Content-Type:') { + return header[i].substr(13).trim(); + } + } +} + +export function uint8ArrayToString(data, offset, length) { + offset = offset || 0; + length = length || data.length - offset; + let str = ''; + + for (let i = offset; i < offset + length; i++) { + str += String.fromCharCode(data[i]); + } + + return str; +} diff --git a/extensions/default/src/DicomWebDataSource/utils/getImageId.js b/extensions/default/src/DicomWebDataSource/utils/getImageId.js new file mode 100644 index 0000000..b3dcef0 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/utils/getImageId.js @@ -0,0 +1,54 @@ +import getWADORSImageId from './getWADORSImageId'; + +function buildInstanceWadoUrl(config, instance) { + const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance; + const params = []; + + params.push('requestType=WADO'); + params.push(`studyUID=${StudyInstanceUID}`); + params.push(`seriesUID=${SeriesInstanceUID}`); + params.push(`objectUID=${SOPInstanceUID}`); + params.push('contentType=application/dicom'); + params.push('transferSyntax=*'); + + const paramString = params.join('&'); + + return `${config.wadoUriRoot}?${paramString}`; +} + +/** + * Obtain an imageId for Cornerstone from an image instance + * + * @param instance + * @param frame + * @param thumbnail + * @returns {string} The imageId to be used by Cornerstone + */ +export default function getImageId({ instance, frame, config, thumbnail = false }) { + if (!instance) { + return; + } + + if (instance.imageId && frame === undefined) { + return instance.imageId; + } + + if (instance.url) { + return instance.url; + } + + const renderingAttr = thumbnail ? 'thumbnailRendering' : 'imageRendering'; + + if (!config[renderingAttr] || config[renderingAttr] === 'wadouri') { + const wadouri = buildInstanceWadoUrl(config, instance); + + let imageId = 'dicomweb:' + wadouri; + if (frame !== undefined) { + imageId += '&frame=' + frame; + } + + return imageId; + } else { + return getWADORSImageId(instance, config, frame); // WADO-RS Retrieve Frame + } +} diff --git a/extensions/default/src/DicomWebDataSource/utils/getWADORSImageId.js b/extensions/default/src/DicomWebDataSource/utils/getWADORSImageId.js new file mode 100644 index 0000000..4276914 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/utils/getWADORSImageId.js @@ -0,0 +1,51 @@ +function buildInstanceWadoRsUri(instance, config) { + const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance; + return `${config.wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}`; +} + +function buildInstanceFrameWadoRsUri(instance, config, frame) { + const baseWadoRsUri = buildInstanceWadoRsUri(instance, config); + + frame = frame || 1; + + return `${baseWadoRsUri}/frames/${frame}`; +} + +// function getWADORSImageUrl(instance, frame) { +// const wadorsuri = buildInstanceFrameWadoRsUri(instance, config, frame); + +// if (!wadorsuri) { +// return; +// } + +// // Use null to obtain an imageId which represents the instance +// if (frame === null) { +// wadorsuri = wadorsuri.replace(/frames\/(\d+)/, ''); +// } else { +// // We need to sum 1 because WADO-RS frame number is 1-based +// frame = frame ? parseInt(frame) + 1 : 1; + +// // Replaces /frame/1 by /frame/{frame} +// wadorsuri = wadorsuri.replace(/frames\/(\d+)/, `frames/${frame}`); +// } + +// return wadorsuri; +// } + +/** + * Obtain an imageId for Cornerstone based on the WADO-RS scheme + * + * @param {object} instanceMetada metadata object (InstanceMetadata) + * @param {(string\|number)} [frame] the frame number + * @returns {string} The imageId to be used by Cornerstone + */ +export default function getWADORSImageId(instance, config, frame) { + //const uri = getWADORSImageUrl(instance, frame); + const uri = buildInstanceFrameWadoRsUri(instance, config, frame); + + if (!uri) { + return; + } + + return `wadors:${uri}`; +} diff --git a/extensions/default/src/DicomWebDataSource/utils/index.ts b/extensions/default/src/DicomWebDataSource/utils/index.ts new file mode 100644 index 0000000..3c34ea1 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/utils/index.ts @@ -0,0 +1,9 @@ +import { fixBulkDataURI } from './fixBulkDataURI'; +import { + cleanDenaturalizedDataset, + transferDenaturalizedDataset, +} from './cleanDenaturalizedDataset'; + +export { fixMultiValueKeys } from './fixMultiValueKeys'; + +export { fixBulkDataURI, cleanDenaturalizedDataset, transferDenaturalizedDataset }; diff --git a/extensions/default/src/DicomWebDataSource/utils/retrieveMetadataFiltered.js b/extensions/default/src/DicomWebDataSource/utils/retrieveMetadataFiltered.js new file mode 100644 index 0000000..5adc635 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/utils/retrieveMetadataFiltered.js @@ -0,0 +1,61 @@ +import RetrieveMetadata from '../wado/retrieveMetadata'; + +/** + * Retrieve metadata filtered. + * + * @param {*} dicomWebClient The DICOMWebClient instance to be used for series load + * @param {*} StudyInstanceUID The UID of the Study to be retrieved + * @param {*} enableStudyLazyLoad Whether the study metadata should be loaded asynchronously + * @param {object} filters Object containing filters to be applied on retrieve metadata process + * @param {string} [filters.seriesInstanceUID] Series instance uid to filter results against + * @param {function} [sortCriteria] Sort criteria function + * @param {function} [sortFunction] Sort function + * + * @returns + */ +function retrieveMetadataFiltered( + dicomWebClient, + StudyInstanceUID, + enableStudyLazyLoad, + filters, + sortCriteria, + sortFunction +) { + const { seriesInstanceUID } = filters; + + return new Promise((resolve, reject) => { + const promises = seriesInstanceUID.map(uid => { + const seriesSpecificFilters = Object.assign({}, filters, { + seriesInstanceUID: uid, + }); + + return RetrieveMetadata( + dicomWebClient, + StudyInstanceUID, + enableStudyLazyLoad, + seriesSpecificFilters, + sortCriteria, + sortFunction + ); + }); + + if (enableStudyLazyLoad === true) { + Promise.all(promises).then(results => { + const aggregatedResult = { preLoadData: [], promises: [] }; + + results.forEach(({ preLoadData, promises }) => { + aggregatedResult.preLoadData = aggregatedResult.preLoadData.concat(preLoadData); + aggregatedResult.promises = aggregatedResult.promises.concat(promises); + }); + + resolve(aggregatedResult); + }, reject); + } else { + Promise.all(promises).then(results => { + resolve(results.flat()); + }, reject); + } + }); +} + +export default retrieveMetadataFiltered; diff --git a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadata.js b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadata.js new file mode 100644 index 0000000..904b9ce --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadata.js @@ -0,0 +1,41 @@ +import RetrieveMetadataLoaderSync from './retrieveMetadataLoaderSync'; +import RetrieveMetadataLoaderAsync from './retrieveMetadataLoaderAsync'; + +/** + * Retrieve Study metadata from a DICOM server. If the server is configured to use lazy load, only the first series + * will be loaded and the property "studyLoader" will be set to let consumer load remaining series as needed. + * + * @param {*} dicomWebClient The DICOMWebClient instance to be used for series load + * @param {*} StudyInstanceUID The UID of the Study to be retrieved + * @param {*} enableStudyLazyLoad Whether the study metadata should be loaded asynchronously + * @param {object} filters Object containing filters to be applied on retrieve metadata process + * @param {string} [filters.seriesInstanceUID] Series instance uid to filter results against + * @param {function} [sortCriteria] Sort criteria function + * @param {function} [sortFunction] Sort function + * + * @returns {Promise} A promises that resolves the study descriptor object + */ +async function RetrieveMetadata( + dicomWebClient, + StudyInstanceUID, + enableStudyLazyLoad, + filters = {}, + sortCriteria, + sortFunction +) { + const RetrieveMetadataLoader = + enableStudyLazyLoad !== false ? RetrieveMetadataLoaderAsync : RetrieveMetadataLoaderSync; + + const retrieveMetadataLoader = new RetrieveMetadataLoader( + dicomWebClient, + StudyInstanceUID, + filters, + sortCriteria, + sortFunction + ); + const data = await retrieveMetadataLoader.execLoad(); + + return data; +} + +export default RetrieveMetadata; diff --git a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoader.js b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoader.js new file mode 100644 index 0000000..e014300 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoader.js @@ -0,0 +1,64 @@ +/** + * Class to define inheritance of load retrieve strategy. + * The process can be async load (lazy) or sync load + * + * There are methods that must be implemented at consumer level + * To retrieve study call execLoad + */ +export default class RetrieveMetadataLoader { + /** + * @constructor + * @param {Object} client The dicomweb-client. + * @param {Array} studyInstanceUID Study instance ui to be retrieved + * @param {Object} [filters] - Object containing filters to be applied on retrieve metadata process + * @param {string} [filters.seriesInstanceUID] - series instance uid to filter results against + * @param {Object} [sortCriteria] - Custom sort criteria used for series + * @param {Function} [sortFunction] - Custom sort function for series + */ + constructor( + client, + studyInstanceUID, + filters = {}, + sortCriteria = undefined, + sortFunction = undefined + ) { + this.client = client; + this.studyInstanceUID = studyInstanceUID; + this.filters = filters; + this.sortCriteria = sortCriteria; + this.sortFunction = sortFunction; + } + + async execLoad() { + const preLoadData = await this.preLoad(); + const loadData = await this.load(preLoadData); + const postLoadData = await this.posLoad(loadData); + return postLoadData; + } + + /** + * It iterates over given loaders running each one. Loaders parameters must be bind when getting it. + * @param {Array} loaders - array of loader to retrieve data. + */ + async runLoaders(loaders) { + let result; + for (const loader of loaders) { + result = await loader(); + if (result && result.length) { + break; // closes iterator in case data is retrieved successfully + } + } + + if (loaders.next().done && !result) { + throw new Error('RetrieveMetadataLoader failed'); + } + + return result; + } + + // Methods to be overwrite + async configLoad() {} + async preLoad() {} + async load(preLoadData) {} + async posLoad(loadData) {} +} diff --git a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js new file mode 100644 index 0000000..0c9a656 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js @@ -0,0 +1,159 @@ +import dcmjs from 'dcmjs'; +import { sortStudySeries } from '@ohif/core/src/utils/sortStudy'; +import RetrieveMetadataLoader from './retrieveMetadataLoader'; + +// Series Date, Series Time, Series Description and Series Number to be included +// in the series metadata query result +const includeField = ['00080021', '00080031', '0008103E', '00200011'].join(','); + +export class DeferredPromise { + metadata = undefined; + processFunction = undefined; + internalPromise = undefined; + thenFunction = undefined; + rejectFunction = undefined; + + setMetadata(metadata) { + this.metadata = metadata; + } + setProcessFunction(func) { + this.processFunction = func; + } + getPromise() { + return this.start(); + } + start() { + if (this.internalPromise) { + return this.internalPromise; + } + this.internalPromise = this.processFunction(); + // in case then and reject functions called before start + if (this.thenFunction) { + this.then(this.thenFunction); + this.thenFunction = undefined; + } + if (this.rejectFunction) { + this.reject(this.rejectFunction); + this.rejectFunction = undefined; + } + return this.internalPromise; + } + then(func) { + if (this.internalPromise) { + return this.internalPromise.then(func); + } else { + this.thenFunction = func; + } + } + reject(func) { + if (this.internalPromise) { + return this.internalPromise.reject(func); + } else { + this.rejectFunction = func; + } + } +} +/** + * Creates an immutable series loader object which loads each series sequentially using the iterator interface. + * + * @param {DICOMWebClient} dicomWebClient The DICOMWebClient instance to be used for series load + * @param {string} studyInstanceUID The Study Instance UID from which series will be loaded + * @param {Array} seriesInstanceUIDList A list of Series Instance UIDs + * + * @returns {Object} Returns an object which supports loading of instances from each of given Series Instance UID + */ +function makeSeriesAsyncLoader(client, studyInstanceUID, seriesInstanceUIDList) { + return Object.freeze({ + hasNext() { + return seriesInstanceUIDList.length > 0; + }, + next() { + const { seriesInstanceUID, metadata } = seriesInstanceUIDList.shift(); + const promise = new DeferredPromise(); + promise.setMetadata(metadata); + promise.setProcessFunction(() => { + return client.retrieveSeriesMetadata({ + studyInstanceUID, + seriesInstanceUID, + }); + }); + return promise; + }, + }); +} + +/** + * Class for async load of study metadata. + * It inherits from RetrieveMetadataLoader + * + * It loads the one series and then append to seriesLoader the others to be consumed/loaded + */ +export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader { + /** + * @returns {Array} Array of preLoaders. To be consumed as queue + */ + *getPreLoaders() { + const preLoaders = []; + const { studyInstanceUID, filters: { seriesInstanceUID } = {}, client } = this; + + // asking to include Series Date, Series Time, Series Description + // and Series Number in the series metadata returned to better sort series + // in preLoad function + let options = { + studyInstanceUID, + queryParams: { + includefield: includeField, + }, + }; + + if (seriesInstanceUID) { + options.queryParams.SeriesInstanceUID = seriesInstanceUID; + preLoaders.push(client.searchForSeries.bind(client, options)); + } + // Fallback preloader + preLoaders.push(client.searchForSeries.bind(client, options)); + + yield* preLoaders; + } + + async preLoad() { + const preLoaders = this.getPreLoaders(); + const result = await this.runLoaders(preLoaders); + const sortCriteria = this.sortCriteria; + const sortFunction = this.sortFunction; + + const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary; + const naturalized = result.map(naturalizeDataset); + + return sortStudySeries(naturalized, sortCriteria, sortFunction); + } + + async load(preLoadData) { + const { client, studyInstanceUID } = this; + + const seriesInstanceUIDs = preLoadData.map(seriesMetadata => { + return { seriesInstanceUID: seriesMetadata.SeriesInstanceUID, metadata: seriesMetadata }; + }); + + const seriesAsyncLoader = makeSeriesAsyncLoader(client, studyInstanceUID, seriesInstanceUIDs); + + const promises = []; + + while (seriesAsyncLoader.hasNext()) { + const promise = seriesAsyncLoader.next(); + promises.push(promise); + } + + return { + preLoadData, + promises, + }; + } + + async posLoad({ preLoadData, promises }) { + return { + preLoadData, + promises, + }; + } +} diff --git a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderSync.js b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderSync.js new file mode 100644 index 0000000..01d83e0 --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderSync.js @@ -0,0 +1,58 @@ +// import { api } from 'dicomweb-client'; +// import DICOMWeb from '../../../DICOMWeb/'; +import RetrieveMetadataLoader from './retrieveMetadataLoader'; + +/** + * Class for sync load of study metadata. + * It inherits from RetrieveMetadataLoader + * + * A list of loaders (getLoaders) can be created so, it will be applied a fallback load strategy. + * I.e Retrieve metadata using all loaders possibilities. + */ +export default class RetrieveMetadataLoaderSync extends RetrieveMetadataLoader { + getOptions() { + const { studyInstanceUID, filters } = this; + + const options = { + studyInstanceUID, + }; + + const { seriesInstanceUID } = filters; + if (seriesInstanceUID) { + options['seriesInstanceUID'] = seriesInstanceUID; + } + + return options; + } + + /** + * @returns {Array} Array of loaders. To be consumed as queue + */ + *getLoaders() { + const loaders = []; + const { studyInstanceUID, filters: { seriesInstanceUID } = {}, client } = this; + + if (seriesInstanceUID) { + loaders.push( + client.retrieveSeriesMetadata.bind(client, { + studyInstanceUID, + seriesInstanceUID, + }) + ); + } + + loaders.push(client.retrieveStudyMetadata.bind(client, { studyInstanceUID })); + + yield* loaders; + } + + async load(preLoadData) { + const loaders = this.getLoaders(); + const result = this.runLoaders(loaders); + return result; + } + + async posLoad(loadData) { + return loadData; + } +} diff --git a/extensions/default/src/DicomWebProxyDataSource/index.ts b/extensions/default/src/DicomWebProxyDataSource/index.ts new file mode 100644 index 0000000..8ab25f9 --- /dev/null +++ b/extensions/default/src/DicomWebProxyDataSource/index.ts @@ -0,0 +1,76 @@ +import { IWebApiDataSource } from '@ohif/core'; +import { createDicomWebApi } from '../DicomWebDataSource/index'; + +/** + * This datasource is initialized with a url that returns a JSON object with a + * dicomWeb datasource configuration array present in a "servers" object. + * + * Only the first array item is parsed, if there are multiple items in the + * dicomWeb configuration array + * + */ +function createDicomWebProxyApi(dicomWebProxyConfig, servicesManager: AppTypes.ServicesManager) { + const { name } = dicomWebProxyConfig; + let dicomWebDelegate = undefined; + + const implementation = { + initialize: async ({ params, query }) => { + const url = query.get('url'); + + if (!url) { + throw new Error(`No url for '${name}'`); + } else { + const response = await fetch(url); + const data = await response.json(); + if (!data.servers?.dicomWeb?.[0]) { + throw new Error('Invalid configuration returned by url'); + } + + dicomWebDelegate = createDicomWebApi( + data.servers.dicomWeb[0].configuration || data.servers.dicomWeb[0], + servicesManager + ); + dicomWebDelegate.initialize({ params, query }); + } + }, + query: { + studies: { + search: params => dicomWebDelegate.query.studies.search(params), + }, + series: { + search: (...args) => dicomWebDelegate.query.series.search(...args), + }, + instances: { + search: (studyInstanceUid, queryParameters) => + dicomWebDelegate.query.instances.search(studyInstanceUid, queryParameters), + }, + }, + retrieve: { + directURL: (...args) => dicomWebDelegate.retrieve.directURL(...args), + series: { + metadata: async (...args) => dicomWebDelegate.retrieve.series.metadata(...args), + }, + }, + store: { + dicom: (...args) => dicomWebDelegate.store(...args), + }, + deleteStudyMetadataPromise: (...args) => dicomWebDelegate.deleteStudyMetadataPromise(...args), + getImageIdsForDisplaySet: (...args) => dicomWebDelegate.getImageIdsForDisplaySet(...args), + getImageIdsForInstance: (...args) => dicomWebDelegate.getImageIdsForInstance(...args), + getStudyInstanceUIDs({ params, query }) { + let studyInstanceUIDs = []; + + // there seem to be a couple of variations of the case for this parameter + const queryStudyInstanceUIDs = + query.get('studyInstanceUIDs') || query.get('studyInstanceUids'); + if (!queryStudyInstanceUIDs) { + throw new Error(`No studyInstanceUids in request for '${name}'`); + } + studyInstanceUIDs = queryStudyInstanceUIDs.split(';'); + return studyInstanceUIDs; + }, + }; + return IWebApiDataSource.create(implementation); +} + +export { createDicomWebProxyApi }; diff --git a/extensions/default/src/MergeDataSource/index.test.js b/extensions/default/src/MergeDataSource/index.test.js new file mode 100644 index 0000000..2b915e3 --- /dev/null +++ b/extensions/default/src/MergeDataSource/index.test.js @@ -0,0 +1,203 @@ +import { DicomMetadataStore, IWebApiDataSource } from '@ohif/core'; +import { + mergeMap, + callForAllDataSourcesAsync, + callForAllDataSources, + callForDefaultDataSource, + callByRetrieveAETitle, + createMergeDataSourceApi, +} from './index'; + +jest.mock('@ohif/core'); + +describe('MergeDataSource', () => { + let path, + sourceName, + mergeConfig, + extensionManager, + series1, + series2, + series3, + series4, + mergeKey, + tagFunc, + dataSourceAndSeriesMap, + dataSourceAndUIDsMap, + dataSourceAndDSMap, + pathSync; + + beforeAll(() => { + path = 'query.series.search'; + pathSync = 'getImageIdsForInstance'; + tagFunc = jest.fn((data, sourceName) => + data.map(item => ({ ...item, RetrieveAETitle: sourceName })) + ); + sourceName = 'dicomweb1'; + mergeKey = 'seriesInstanceUid'; + series1 = { [mergeKey]: '123' }; + series2 = { [mergeKey]: '234' }; + series3 = { [mergeKey]: '345' }; + series4 = { [mergeKey]: '456' }; + mergeConfig = { + seriesMerge: { + dataSourceNames: ['dicomweb1', 'dicomweb2'], + defaultDataSourceName: 'dicomweb1', + }, + }; + dataSourceAndSeriesMap = { + dataSource1: series1, + dataSource2: series2, + dataSource3: series3, + }; + dataSourceAndUIDsMap = { + dataSource1: ['123'], + dataSource2: ['234'], + dataSource3: ['345'], + }; + dataSourceAndDSMap = { + dataSource1: { + displaySet: { + StudyInstanceUID: '123', + SeriesInstanceUID: '123', + }, + }, + dataSource2: { + displaySet: { + StudyInstanceUID: '234', + SeriesInstanceUID: '234', + }, + }, + dataSource3: { + displaySet: { + StudyInstanceUID: '345', + SeriesInstanceUID: '345', + }, + }, + }; + extensionManager = { + dataSourceDefs: { + dataSource1: { + sourceName: 'dataSource1', + configuration: {}, + }, + dataSource2: { + sourceName: 'dataSource2', + configuration: {}, + }, + dataSource3: { + sourceName: 'dataSource3', + configuration: {}, + }, + }, + getDataSources: jest.fn(dataSourceName => [ + { + [path]: jest.fn().mockResolvedValue([dataSourceAndSeriesMap[dataSourceName]]), + }, + ]), + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('callForAllDataSourcesAsync', () => { + it('should call the correct functions and return the merged data', async () => { + /** Arrange */ + extensionManager.getDataSources = jest.fn(dataSourceName => [ + { + [path]: jest.fn().mockResolvedValue([dataSourceAndSeriesMap[dataSourceName]]), + }, + ]); + + /** Act */ + const data = await callForAllDataSourcesAsync({ + mergeMap, + path, + args: [], + extensionManager, + dataSourceNames: ['dataSource1', 'dataSource2'], + }); + + /** Assert */ + expect(extensionManager.getDataSources).toHaveBeenCalledTimes(2); + expect(extensionManager.getDataSources).toHaveBeenCalledWith('dataSource1'); + expect(extensionManager.getDataSources).toHaveBeenCalledWith('dataSource2'); + expect(data).toEqual([series1, series2]); + }); + }); + + describe('callForAllDataSources', () => { + it('should call the correct functions and return the merged data', () => { + /** Arrange */ + extensionManager.getDataSources = jest.fn(dataSourceName => [ + { + [pathSync]: () => dataSourceAndUIDsMap[dataSourceName], + }, + ]); + + /** Act */ + const data = callForAllDataSources({ + path: pathSync, + args: [], + extensionManager, + dataSourceNames: ['dataSource2', 'dataSource3'], + }); + + /** Assert */ + expect(extensionManager.getDataSources).toHaveBeenCalledTimes(2); + expect(extensionManager.getDataSources).toHaveBeenCalledWith('dataSource2'); + expect(extensionManager.getDataSources).toHaveBeenCalledWith('dataSource3'); + expect(data).toEqual(['234', '345']); + }); + }); + + describe('callForDefaultDataSource', () => { + it('should call the correct function and return the data', () => { + /** Arrange */ + extensionManager.getDataSources = jest.fn(dataSourceName => [ + { + [pathSync]: () => dataSourceAndUIDsMap[dataSourceName], + }, + ]); + + /** Act */ + const data = callForDefaultDataSource({ + path: pathSync, + args: [], + extensionManager, + defaultDataSourceName: 'dataSource2', + }); + + /** Assert */ + expect(extensionManager.getDataSources).toHaveBeenCalledTimes(1); + expect(extensionManager.getDataSources).toHaveBeenCalledWith('dataSource2'); + expect(data).toEqual(['234']); + }); + }); + + describe('callByRetrieveAETitle', () => { + it('should call the correct function and return the data', () => { + /** Arrange */ + DicomMetadataStore.getSeries.mockImplementationOnce(() => [series2]); + extensionManager.getDataSources = jest.fn(dataSourceName => [ + { + [pathSync]: () => dataSourceAndUIDsMap[dataSourceName], + }, + ]); + + /** Act */ + const data = callByRetrieveAETitle({ + path: pathSync, + args: [dataSourceAndDSMap['dataSource2']], + extensionManager, + defaultDataSourceName: 'dataSource2', + }); + + /** Assert */ + expect(extensionManager.getDataSources).toHaveBeenCalledTimes(1); + expect(extensionManager.getDataSources).toHaveBeenCalledWith('dataSource2'); + expect(data).toEqual(['234']); + }); + }); +}); diff --git a/extensions/default/src/MergeDataSource/index.ts b/extensions/default/src/MergeDataSource/index.ts new file mode 100644 index 0000000..92f5415 --- /dev/null +++ b/extensions/default/src/MergeDataSource/index.ts @@ -0,0 +1,294 @@ +import { DicomMetadataStore, IWebApiDataSource } from '@ohif/core'; +import get from 'lodash.get'; +import uniqBy from 'lodash.uniqby'; +import { + MergeConfig, + CallForAllDataSourcesAsyncOptions, + CallForAllDataSourcesOptions, + CallForDefaultDataSourceOptions, + CallByRetrieveAETitleOptions, + MergeMap, +} from './types'; + +export const mergeMap: MergeMap = { + 'query.studies.search': { + mergeKey: 'studyInstanceUid', + tagFunc: x => x, + }, + 'query.series.search': { + mergeKey: 'seriesInstanceUid', + tagFunc: (series, sourceName) => { + series.forEach(series => { + series.RetrieveAETitle = sourceName; + DicomMetadataStore.updateSeriesMetadata(series); + }); + return series; + }, + }, +}; + +/** + * Calls all data sources asynchronously and merges the results. + * @param {CallForAllDataSourcesAsyncOptions} options - The options for calling all data sources. + * @param {string} options.path - The path to the function to be called on each data source. + * @param {unknown[]} options.args - The arguments to be passed to the function. + * @param {ExtensionManager} options.extensionManager - The extension manager. + * @param {string[]} options.dataSourceNames - The names of the data sources to be called. + * @param {string} options.defaultDataSourceName - The name of the default data source. + * @returns {Promise} - A promise that resolves to the merged data from all data sources. + */ +export const callForAllDataSourcesAsync = async ({ + mergeMap, + path, + args, + extensionManager, + dataSourceNames, + defaultDataSourceName, +}: CallForAllDataSourcesAsyncOptions) => { + const { mergeKey, tagFunc } = mergeMap[path] || { tagFunc: x => x }; + + /** Sort by default data source */ + const defs = Object.values(extensionManager.dataSourceDefs); + const defaultDataSourceDef = defs.find(def => def.sourceName === defaultDataSourceName); + const dataSourceDefs = defs.filter(def => def.sourceName !== defaultDataSourceName); + if (defaultDataSourceDef) { + dataSourceDefs.unshift(defaultDataSourceDef); + } + + const promises = []; + const sourceNames = []; + + for (const dataSourceDef of dataSourceDefs) { + const { configuration, sourceName } = dataSourceDef; + if (!!configuration && dataSourceNames.includes(sourceName)) { + const [dataSource] = extensionManager.getDataSources(sourceName); + const func = get(dataSource, path); + const promise = func.apply(dataSource, args); + promises.push(promise); + sourceNames.push(sourceName); + } + } + + const data = await Promise.allSettled(promises); + const mergedData = data.map((data, i) => tagFunc(data.value, sourceNames[i])); + + let results = []; + if (mergeKey) { + results = uniqBy(mergedData.flat(), obj => get(obj, mergeKey)); + } else { + results = mergedData.flat(); + } + + return results; +}; + +/** + * Calls all data sources that match the provided names and merges their data. + * @param options - The options for calling all data sources. + * @param options.path - The path to the function to be called on each data source. + * @param options.args - The arguments to be passed to the function. + * @param options.extensionManager - The extension manager instance. + * @param options.dataSourceNames - The names of the data sources to be called. + * @param options.defaultDataSourceName - The name of the default data source. + * @returns The merged data from all the matching data sources. + */ +export const callForAllDataSources = ({ + path, + args, + extensionManager, + dataSourceNames, + defaultDataSourceName, +}: CallForAllDataSourcesOptions) => { + /** Sort by default data source */ + const defs = Object.values(extensionManager.dataSourceDefs); + const defaultDataSourceDef = defs.find(def => def.sourceName === defaultDataSourceName); + const dataSourceDefs = defs.filter(def => def.sourceName !== defaultDataSourceName); + if (defaultDataSourceDef) { + dataSourceDefs.unshift(defaultDataSourceDef); + } + + const mergedData = []; + for (const dataSourceDef of dataSourceDefs) { + const { configuration, sourceName } = dataSourceDef; + if (!!configuration && dataSourceNames.includes(sourceName)) { + const [dataSource] = extensionManager.getDataSources(sourceName); + const func = get(dataSource, path); + const data = func.apply(dataSource, args); + mergedData.push(data); + } + } + + return mergedData.flat(); +}; + +/** + * Calls the default data source function specified by the given path with the provided arguments. + * @param {CallForDefaultDataSourceOptions} options - The options for calling the default data source. + * @param {string} options.path - The path to the function within the default data source. + * @param {unknown[]} options.args - The arguments to pass to the function. + * @param {string} options.defaultDataSourceName - The name of the default data source. + * @param {ExtensionManager} options.extensionManager - The extension manager instance. + * @returns {unknown} - The result of calling the default data source function. + */ +export const callForDefaultDataSource = ({ + path, + args, + defaultDataSourceName, + extensionManager, +}: CallForDefaultDataSourceOptions) => { + const [dataSource] = extensionManager.getDataSources(defaultDataSourceName); + const func = get(dataSource, path); + return func.apply(dataSource, args); +}; + +/** + * Calls the data source specified by the RetrieveAETitle of the given display set. + * @typedef {Object} CallByRetrieveAETitleOptions + * @property {string} path - The path of the method to call on the data source. + * @property {any[]} args - The arguments to pass to the method. + * @property {string} defaultDataSourceName - The name of the default data source. + * @property {ExtensionManager} extensionManager - The extension manager. + */ +export const callByRetrieveAETitle = ({ + path, + args, + defaultDataSourceName, + extensionManager, +}: CallByRetrieveAETitleOptions) => { + const [displaySet] = args; + const seriesMetadata = DicomMetadataStore.getSeries( + displaySet.StudyInstanceUID, + displaySet.SeriesInstanceUID + ); + const [dataSource] = extensionManager.getDataSources( + seriesMetadata.RetrieveAETitle || defaultDataSourceName + ); + return dataSource[path](...args); +}; + +function createMergeDataSourceApi( + mergeConfig: MergeConfig, + servicesManager: AppTypes.ServicesManager, + extensionManager +) { + const { seriesMerge } = mergeConfig; + const { dataSourceNames, defaultDataSourceName } = seriesMerge; + + const implementation = { + initialize: (...args: unknown[]) => + callForAllDataSources({ + path: 'initialize', + args, + extensionManager, + dataSourceNames, + defaultDataSourceName, + }), + query: { + studies: { + search: (...args: unknown[]) => + callForAllDataSourcesAsync({ + mergeMap, + path: 'query.studies.search', + args, + extensionManager, + dataSourceNames, + defaultDataSourceName, + }), + }, + series: { + search: (...args: unknown[]) => + callForAllDataSourcesAsync({ + mergeMap, + path: 'query.series.search', + args, + extensionManager, + dataSourceNames, + defaultDataSourceName, + }), + }, + instances: { + search: (...args: unknown[]) => + callForAllDataSourcesAsync({ + mergeMap, + path: 'query.instances.search', + args, + extensionManager, + dataSourceNames, + defaultDataSourceName, + }), + }, + }, + retrieve: { + bulkDataURI: (...args: unknown[]) => + callForAllDataSourcesAsync({ + mergeMap, + path: 'retrieve.bulkDataURI', + args, + extensionManager, + dataSourceNames, + defaultDataSourceName, + }), + directURL: (...args: unknown[]) => + callForDefaultDataSource({ + path: 'retrieve.directURL', + args, + defaultDataSourceName, + extensionManager, + }), + series: { + metadata: (...args: unknown[]) => + callForAllDataSourcesAsync({ + mergeMap, + path: 'retrieve.series.metadata', + args, + extensionManager, + dataSourceNames, + defaultDataSourceName, + }), + }, + }, + store: { + dicom: (...args: unknown[]) => + callForDefaultDataSource({ + path: 'store.dicom', + args, + defaultDataSourceName, + extensionManager, + }), + }, + deleteStudyMetadataPromise: (...args: unknown[]) => + callForAllDataSources({ + path: 'deleteStudyMetadataPromise', + args, + extensionManager, + dataSourceNames, + defaultDataSourceName, + }), + getImageIdsForDisplaySet: (...args: unknown[]) => + callByRetrieveAETitle({ + path: 'getImageIdsForDisplaySet', + args, + defaultDataSourceName, + extensionManager, + }), + getImageIdsForInstance: (...args: unknown[]) => + callByRetrieveAETitle({ + path: 'getImageIdsForDisplaySet', + args, + defaultDataSourceName, + extensionManager, + }), + getStudyInstanceUIDs: (...args: unknown[]) => + callForAllDataSources({ + path: 'getStudyInstanceUIDs', + args, + extensionManager, + dataSourceNames, + defaultDataSourceName, + }), + }; + + return IWebApiDataSource.create(implementation); +} + +export { createMergeDataSourceApi }; diff --git a/extensions/default/src/MergeDataSource/types.ts b/extensions/default/src/MergeDataSource/types.ts new file mode 100644 index 0000000..ef47b4e --- /dev/null +++ b/extensions/default/src/MergeDataSource/types.ts @@ -0,0 +1,46 @@ +import { ExtensionManager } from '@ohif/core'; + +export type MergeMap = { + [key: string]: { + mergeKey: string; + tagFunc: (data: unknown[], sourceName: string) => unknown[]; + }; +}; + +export type CallForAllDataSourcesAsyncOptions = { + mergeMap: object; + path: string; + args: unknown[]; + dataSourceNames: string[]; + extensionManager: ExtensionManager; + defaultDataSourceName: string; +}; + +export type CallForAllDataSourcesOptions = { + path: string; + args: unknown[]; + dataSourceNames: string[]; + extensionManager: ExtensionManager; + defaultDataSourceName: string; +}; + +export type CallForDefaultDataSourceOptions = { + path: string; + args: unknown[]; + defaultDataSourceName: string; + extensionManager: ExtensionManager; +}; + +export type CallByRetrieveAETitleOptions = { + path: string; + args: unknown[]; + defaultDataSourceName: string; + extensionManager: ExtensionManager; +}; + +export type MergeConfig = { + seriesMerge: { + dataSourceNames: string[]; + defaultDataSourceName: string; + }; +}; diff --git a/extensions/default/src/Panels/DataSourceSelector.tsx b/extensions/default/src/Panels/DataSourceSelector.tsx new file mode 100644 index 0000000..7c4e5d5 --- /dev/null +++ b/extensions/default/src/Panels/DataSourceSelector.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import classnames from 'classnames'; +import { useNavigate } from 'react-router-dom'; +import { useAppConfig } from '@state'; + +import { Button, ButtonEnums } from '@ohif/ui'; + +function DataSourceSelector() { + const [appConfig] = useAppConfig(); + const navigate = useNavigate(); + + // This is frowned upon, but the raw config is needed here to provide + // the selector + const dsConfigs = appConfig.dataSources; + + return ( +
+
+
+ OHIF +
+ {dsConfigs + .filter(it => it.sourceName !== 'dicomjson' && it.sourceName !== 'dicomlocal') + .map(ds => ( +
+

+ {ds.configuration?.friendlyName || ds.friendlyName} +

+ +
+
+ ))} +
+
+
+
+ ); +} + +export default DataSourceSelector; diff --git a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx new file mode 100644 index 0000000..f6ae367 --- /dev/null +++ b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx @@ -0,0 +1,399 @@ +import React, { useState, useEffect } from 'react'; +import { useImageViewer } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; +import { StudyBrowser } from '@ohif/ui-next'; +import { useSystem, utils } from '@ohif/core'; +import { useNavigate } from 'react-router-dom'; +import { Separator } from '@ohif/ui-next'; +import { PanelStudyBrowserHeader } from './PanelStudyBrowserHeader'; +import { defaultActionIcons } from './constants'; +import MoreDropdownMenu from '../../Components/MoreDropdownMenu'; + +const { sortStudyInstances, formatDate, createStudyBrowserTabs } = utils; + +/** + * + * @param {*} param0 + */ +function PanelStudyBrowser({ + getImageSrc, + getStudiesForPatientByMRN, + requestDisplaySetCreationForStudy, + dataSource, +}) { + const { servicesManager, commandsManager } = useSystem(); + const { hangingProtocolService, displaySetService, uiNotificationService, customizationService } = + servicesManager.services; + const navigate = useNavigate(); + + // Normally you nest the components so the tree isn't so deep, and the data + // doesn't have to have such an intense shape. This works well enough for now. + // Tabs --> Studies --> DisplaySets --> Thumbnails + const { StudyInstanceUIDs } = useImageViewer(); + const [{ activeViewportId, viewports, isHangingProtocolLayout }, viewportGridService] = + useViewportGrid(); + const [activeTabName, setActiveTabName] = useState('all'); + const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([ + ...StudyInstanceUIDs, + ]); + const [hasLoadedViewports, setHasLoadedViewports] = useState(false); + const [studyDisplayList, setStudyDisplayList] = useState([]); + const [displaySets, setDisplaySets] = useState([]); + const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({}); + + const [viewPresets, setViewPresets] = useState( + customizationService.getCustomization('studyBrowser.viewPresets') + ); + + const [actionIcons, setActionIcons] = useState(defaultActionIcons); + + // multiple can be true or false + const updateActionIconValue = actionIcon => { + actionIcon.value = !actionIcon.value; + const newActionIcons = [...actionIcons]; + setActionIcons(newActionIcons); + }; + + // only one is true at a time + const updateViewPresetValue = viewPreset => { + if (!viewPreset) { + return; + } + const newViewPresets = viewPresets.map(preset => { + preset.selected = preset.id === viewPreset.id; + return preset; + }); + setViewPresets(newViewPresets); + }; + + const onDoubleClickThumbnailHandler = displaySetInstanceUID => { + let updatedViewports = []; + const viewportId = activeViewportId; + try { + updatedViewports = hangingProtocolService.getViewportsRequireUpdate( + viewportId, + displaySetInstanceUID, + isHangingProtocolLayout + ); + } catch (error) { + console.warn(error); + uiNotificationService.show({ + title: 'Thumbnail Double Click', + message: 'The selected display sets could not be added to the viewport.', + type: 'error', + duration: 3000, + }); + } + + viewportGridService.setDisplaySetsForViewports(updatedViewports); + }; + + // ~~ studyDisplayList + useEffect(() => { + // Fetch all studies for the patient in each primary study + async function fetchStudiesForPatient(StudyInstanceUID) { + // current study qido + const qidoForStudyUID = await dataSource.query.studies.search({ + studyInstanceUid: StudyInstanceUID, + }); + + if (!qidoForStudyUID?.length) { + navigate('/notfoundstudy', '_self'); + throw new Error('Invalid study URL'); + } + + let qidoStudiesForPatient = qidoForStudyUID; + + // try to fetch the prior studies based on the patientID if the + // server can respond. + try { + qidoStudiesForPatient = await getStudiesForPatientByMRN(qidoForStudyUID); + } catch (error) { + console.warn(error); + } + + const mappedStudies = _mapDataSourceStudies(qidoStudiesForPatient); + const actuallyMappedStudies = mappedStudies.map(qidoStudy => { + return { + studyInstanceUid: qidoStudy.StudyInstanceUID, + date: formatDate(qidoStudy.StudyDate), + description: qidoStudy.StudyDescription, + modalities: qidoStudy.ModalitiesInStudy, + numInstances: qidoStudy.NumInstances, + }; + }); + + setStudyDisplayList(prevArray => { + const ret = [...prevArray]; + for (const study of actuallyMappedStudies) { + if (!prevArray.find(it => it.studyInstanceUid === study.studyInstanceUid)) { + ret.push(study); + } + } + return ret; + }); + } + + StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid)); + }, [StudyInstanceUIDs, dataSource, getStudiesForPatientByMRN, navigate]); + + // // ~~ Initial Thumbnails + useEffect(() => { + if (!hasLoadedViewports) { + if (activeViewportId) { + // Once there is an active viewport id, it means the layout is ready + // so wait a bit of time to allow the viewports preferential loading + // which improves user experience of responsiveness significantly on slower + // systems. + window.setTimeout(() => setHasLoadedViewports(true), 250); + } + + return; + } + + const currentDisplaySets = displaySetService.activeDisplaySets; + currentDisplaySets.forEach(async dSet => { + const newImageSrcEntry = {}; + const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID); + const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); + const imageId = imageIds[Math.floor(imageIds.length / 2)]; + + // TODO: Is it okay that imageIds are not returned here for SR displaySets? + if (!imageId || displaySet?.unsupported) { + return; + } + // When the image arrives, render it and store the result in the thumbnailImgSrcMap + newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(imageId); + + setThumbnailImageSrcMap(prevState => { + return { ...prevState, ...newImageSrcEntry }; + }); + }); + }, [ + StudyInstanceUIDs, + dataSource, + displaySetService, + getImageSrc, + hasLoadedViewports, + activeViewportId, + ]); + + // ~~ displaySets + useEffect(() => { + // TODO: Are we sure `activeDisplaySets` will always be accurate? + const currentDisplaySets = displaySetService.activeDisplaySets; + const mappedDisplaySets = _mapDisplaySets(currentDisplaySets, thumbnailImageSrcMap); + sortStudyInstances(mappedDisplaySets); + + setDisplaySets(mappedDisplaySets); + }, [StudyInstanceUIDs, thumbnailImageSrcMap, displaySetService]); + + // ~~ subscriptions --> displaySets + useEffect(() => { + // DISPLAY_SETS_ADDED returns an array of DisplaySets that were added + const SubscriptionDisplaySetsAdded = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SETS_ADDED, + data => { + // for some reason this breaks thumbnail loading + // if (!hasLoadedViewports) { + // return; + // } + const { displaySetsAdded } = data; + displaySetsAdded.forEach(async dSet => { + const newImageSrcEntry = {}; + const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID); + if (displaySet?.unsupported) { + return; + } + + const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); + const imageId = imageIds[Math.floor(imageIds.length / 2)]; + + // TODO: Is it okay that imageIds are not returned here for SR displaysets? + if (!imageId) { + return; + } + // When the image arrives, render it and store the result in the thumbnailImgSrcMap + newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc( + imageId, + dSet.initialViewport + ); + + setThumbnailImageSrcMap(prevState => { + return { ...prevState, ...newImageSrcEntry }; + }); + }); + } + ); + + return () => { + SubscriptionDisplaySetsAdded.unsubscribe(); + }; + }, [getImageSrc, dataSource, displaySetService]); + + useEffect(() => { + // TODO: Will this always hold _all_ the displaySets we care about? + // DISPLAY_SETS_CHANGED returns `DisplaySerService.activeDisplaySets` + const SubscriptionDisplaySetsChanged = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SETS_CHANGED, + changedDisplaySets => { + const mappedDisplaySets = _mapDisplaySets(changedDisplaySets, thumbnailImageSrcMap); + setDisplaySets(mappedDisplaySets); + } + ); + + const SubscriptionDisplaySetMetaDataInvalidated = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, + () => { + const mappedDisplaySets = _mapDisplaySets( + displaySetService.getActiveDisplaySets(), + thumbnailImageSrcMap + ); + + setDisplaySets(mappedDisplaySets); + } + ); + + return () => { + SubscriptionDisplaySetsChanged.unsubscribe(); + SubscriptionDisplaySetMetaDataInvalidated.unsubscribe(); + }; + }, [StudyInstanceUIDs, thumbnailImageSrcMap, displaySetService]); + + const tabs = createStudyBrowserTabs(StudyInstanceUIDs, studyDisplayList, displaySets); + + // TODO: Should not fire this on "close" + function _handleStudyClick(StudyInstanceUID) { + const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(StudyInstanceUID); + const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy + ? // eslint-disable-next-line prettier/prettier + [...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)] + : [...expandedStudyInstanceUIDs, StudyInstanceUID]; + + setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); + + if (!shouldCollapseStudy) { + const madeInClient = true; + requestDisplaySetCreationForStudy(displaySetService, StudyInstanceUID, madeInClient); + } + } + + const activeDisplaySetInstanceUIDs = viewports.get(activeViewportId)?.displaySetInstanceUIDs; + + return ( + <> + <> + + + + + { + setActiveTabName(clickedTabName); + }} + onClickThumbnail={() => {}} + onDoubleClickThumbnail={onDoubleClickThumbnailHandler} + activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs} + showSettings={actionIcons.find(icon => icon.id === 'settings').value} + viewPresets={viewPresets} + ThumbnailMenuItems={MoreDropdownMenu({ + commandsManager, + servicesManager, + menuItemsKey: 'studyBrowser.thumbnailMenuItems', + })} + StudyMenuItems={MoreDropdownMenu({ + commandsManager, + servicesManager, + menuItemsKey: 'studyBrowser.studyMenuItems', + })} + /> + + ); +} + +export default PanelStudyBrowser; + +/** + * Maps from the DataSource's format to a naturalized object + * + * @param {*} studies + */ +function _mapDataSourceStudies(studies) { + return studies.map(study => { + // TODO: Why does the data source return in this format? + return { + AccessionNumber: study.accession, + StudyDate: study.date, + StudyDescription: study.description, + NumInstances: study.instances, + ModalitiesInStudy: study.modalities, + PatientID: study.mrn, + PatientName: study.patientName, + StudyInstanceUID: study.studyInstanceUid, + StudyTime: study.time, + }; + }); +} + +function _mapDisplaySets(displaySets, thumbnailImageSrcMap) { + const thumbnailDisplaySets = []; + const thumbnailNoImageDisplaySets = []; + + displaySets + .filter(ds => !ds.excludeFromThumbnailBrowser) + .forEach(ds => { + const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID]; + const componentType = _getComponentType(ds); + + const array = + componentType === 'thumbnail' ? thumbnailDisplaySets : thumbnailNoImageDisplaySets; + + array.push({ + displaySetInstanceUID: ds.displaySetInstanceUID, + description: ds.SeriesDescription || '', + seriesNumber: ds.SeriesNumber, + modality: ds.Modality, + seriesDate: ds.SeriesDate, + seriesTime: ds.SeriesTime, + numInstances: ds.numImageFrames, + countIcon: ds.countIcon, + StudyInstanceUID: ds.StudyInstanceUID, + messages: ds.messages, + componentType, + imageSrc, + dragData: { + type: 'displayset', + displaySetInstanceUID: ds.displaySetInstanceUID, + // .. Any other data to pass + }, + isHydratedForDerivedDisplaySet: ds.isHydrated, + }); + }); + + return [...thumbnailDisplaySets, ...thumbnailNoImageDisplaySets]; +} + +const thumbnailNoImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE']; + +function _getComponentType(ds) { + if (thumbnailNoImageModalities.includes(ds.Modality) || ds?.unsupported) { + // TODO probably others. + return 'thumbnailNoImage'; + } + + return 'thumbnail'; +} diff --git a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowserHeader.tsx b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowserHeader.tsx new file mode 100644 index 0000000..0c55559 --- /dev/null +++ b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowserHeader.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { ToggleGroup, ToggleGroupItem } from '@ohif/ui-next'; +import { Icons } from '@ohif/ui-next'; +import { actionIcon, viewPreset } from './types'; + +function PanelStudyBrowserHeader({ + viewPresets, + updateViewPresetValue, + actionIcons, + updateActionIconValue, +}: { + viewPresets: viewPreset[]; + updateViewPresetValue: (viewPreset: viewPreset) => void; + actionIcons: actionIcon[]; + updateActionIconValue: (actionIcon: actionIcon) => void; +}) { + return ( + <> +
+
+
+
+
+ {actionIcons.map((icon: actionIcon, index) => + React.createElement(Icons[icon.iconName] || Icons.MissingIcon, { + key: index, + onClick: () => updateActionIconValue(icon), + className: `cursor-pointer`, + }) + )} +
+
+
+ preset.selected)[0].id} + onValueChange={value => { + const selectedViewPreset = viewPresets.find(preset => preset.id === value); + updateViewPresetValue(selectedViewPreset); + }} + > + {viewPresets.map((viewPreset: viewPreset, index) => ( + + {React.createElement(Icons[viewPreset.iconName] || Icons.MissingIcon)} + + ))} + +
+
+
+
+ + ); +} + +export { PanelStudyBrowserHeader }; diff --git a/extensions/default/src/Panels/StudyBrowser/constants/actionIcons.ts b/extensions/default/src/Panels/StudyBrowser/constants/actionIcons.ts new file mode 100644 index 0000000..f94a7a9 --- /dev/null +++ b/extensions/default/src/Panels/StudyBrowser/constants/actionIcons.ts @@ -0,0 +1,11 @@ +import type { actionIcon } from '../types/actionsIcon'; + +const defaultActionIcons = [ + { + id: 'settings', + iconName: 'Settings', + value: false, + }, +] as actionIcon[]; + +export { defaultActionIcons }; diff --git a/extensions/default/src/Panels/StudyBrowser/constants/index.ts b/extensions/default/src/Panels/StudyBrowser/constants/index.ts new file mode 100644 index 0000000..fb47a5e --- /dev/null +++ b/extensions/default/src/Panels/StudyBrowser/constants/index.ts @@ -0,0 +1,4 @@ +import { defaultActionIcons } from './actionIcons'; +import { defaultViewPresets } from './viewPresets'; + +export { defaultActionIcons, defaultViewPresets }; diff --git a/extensions/default/src/Panels/StudyBrowser/constants/viewPresets.ts b/extensions/default/src/Panels/StudyBrowser/constants/viewPresets.ts new file mode 100644 index 0000000..e0e391d --- /dev/null +++ b/extensions/default/src/Panels/StudyBrowser/constants/viewPresets.ts @@ -0,0 +1,16 @@ +import type { viewPreset } from '../types/viewPreset'; + +const defaultViewPresets = [ + { + id: 'list', + iconName: 'ListView', + selected: false, + }, + { + id: 'thumbnails', + iconName: 'ThumbnailView', + selected: true, + }, +] as viewPreset[]; + +export { defaultViewPresets }; diff --git a/extensions/default/src/Panels/StudyBrowser/types/actionIcon.ts b/extensions/default/src/Panels/StudyBrowser/types/actionIcon.ts new file mode 100644 index 0000000..0b522d5 --- /dev/null +++ b/extensions/default/src/Panels/StudyBrowser/types/actionIcon.ts @@ -0,0 +1,7 @@ +type actionIcon = { + id: string; + iconName: string; + value: boolean; +}; + +export type { actionIcon }; diff --git a/extensions/default/src/Panels/StudyBrowser/types/index.ts b/extensions/default/src/Panels/StudyBrowser/types/index.ts new file mode 100644 index 0000000..d2b31d4 --- /dev/null +++ b/extensions/default/src/Panels/StudyBrowser/types/index.ts @@ -0,0 +1,4 @@ +import type { actionIcon } from './actionIcon'; +import type { viewPreset } from './viewPreset'; + +export type { actionIcon, viewPreset }; diff --git a/extensions/default/src/Panels/StudyBrowser/types/viewPreset.ts b/extensions/default/src/Panels/StudyBrowser/types/viewPreset.ts new file mode 100644 index 0000000..ff7f5b4 --- /dev/null +++ b/extensions/default/src/Panels/StudyBrowser/types/viewPreset.ts @@ -0,0 +1,7 @@ +type viewPreset = { + id: string; + iconName: string; + selected: boolean; +}; + +export type { viewPreset }; diff --git a/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx b/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx new file mode 100644 index 0000000..35b32ec --- /dev/null +++ b/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx @@ -0,0 +1,64 @@ +import React, { useCallback } from 'react'; +import PropTypes from 'prop-types'; +// +import PanelStudyBrowser from './StudyBrowser/PanelStudyBrowser'; +import getImageSrcFromImageId from './getImageSrcFromImageId'; +import getStudiesForPatientByMRN from './getStudiesForPatientByMRN'; +import requestDisplaySetCreationForStudy from './requestDisplaySetCreationForStudy'; +import { useSystem } from '@ohif/core'; + +/** + * Wraps the PanelStudyBrowser and provides features afforded by managers/services + * + * @param {object} params + * @param {object} commandsManager + * @param {object} extensionManager + */ +function WrappedPanelStudyBrowser() { + const { extensionManager } = useSystem(); + // TODO: This should be made available a different way; route should have + // already determined our datasource + const [dataSource] = extensionManager.getActiveDataSource(); + const _getStudiesForPatientByMRN = getStudiesForPatientByMRN.bind(null, dataSource); + const _getImageSrcFromImageId = useCallback( + _createGetImageSrcFromImageIdFn(extensionManager), + [] + ); + const _requestDisplaySetCreationForStudy = requestDisplaySetCreationForStudy.bind( + null, + dataSource + ); + + return ( + + ); +} + +/** + * Grabs cornerstone library reference using a dependent command from + * the @ohif/extension-cornerstone extension. Then creates a helper function + * that can take an imageId and return an image src. + * + * @param {func} getCommand - CommandManager's getCommand method + * @returns {func} getImageSrcFromImageId - A utility function powered by + * cornerstone + */ +function _createGetImageSrcFromImageIdFn(extensionManager) { + const utilities = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.common' + ); + + try { + const { cornerstone } = utilities.exports.getCornerstoneLibraries(); + return getImageSrcFromImageId.bind(null, cornerstone); + } catch (ex) { + throw new Error('Required command not found'); + } +} + +export default WrappedPanelStudyBrowser; diff --git a/extensions/default/src/Panels/createReportDialogPrompt.tsx b/extensions/default/src/Panels/createReportDialogPrompt.tsx new file mode 100644 index 0000000..4e8dfa3 --- /dev/null +++ b/extensions/default/src/Panels/createReportDialogPrompt.tsx @@ -0,0 +1,134 @@ +import React from 'react'; + +import { ButtonEnums, Dialog, Input, Select } from '@ohif/ui'; +import PROMPT_RESPONSES from '../utils/_shared/PROMPT_RESPONSES'; + +export default function CreateReportDialogPrompt(uiDialogService, { extensionManager }) { + return new Promise(function (resolve, reject) { + let dialogId = undefined; + + const _handleClose = () => { + // Dismiss dialog + uiDialogService.dismiss({ id: dialogId }); + // Notify of cancel action + resolve({ + action: PROMPT_RESPONSES.CANCEL, + value: undefined, + dataSourceName: undefined, + }); + }; + + /** + * + * @param {string} param0.action - value of action performed + * @param {string} param0.value - value from input field + */ + const _handleFormSubmit = ({ action, value }) => { + uiDialogService.dismiss({ id: dialogId }); + switch (action.id) { + case 'save': + resolve({ + action: PROMPT_RESPONSES.CREATE_REPORT, + value: value.label, + dataSourceName: value.dataSourceName, + }); + break; + case 'cancel': + resolve({ + action: PROMPT_RESPONSES.CANCEL, + value: undefined, + dataSourceName: undefined, + }); + break; + } + }; + + const dataSourcesOpts = Object.keys(extensionManager.dataSourceMap) + .filter(ds => { + const configuration = extensionManager.dataSourceDefs[ds]?.configuration; + const supportsStow = configuration?.supportsStow ?? configuration?.wadoRoot; + return supportsStow; + }) + .map(ds => { + return { + value: ds, + label: ds, + placeHolder: ds, + }; + }); + + dialogId = uiDialogService.create({ + centralize: true, + isDraggable: false, + content: Dialog, + useLastPosition: false, + showOverlay: true, + contentProps: { + title: 'Create Report', + value: { + label: '', + dataSourceName: extensionManager.activeDataSource, + }, + noCloseButton: true, + onClose: _handleClose, + actions: [ + { id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary }, + { id: 'save', text: 'Save', type: ButtonEnums.type.primary }, + ], + // TODO: Should be on button press... + onSubmit: _handleFormSubmit, + body: ({ value, setValue }) => { + const onChangeHandler = event => { + event.persist(); + setValue(value => ({ ...value, label: event.target.value })); + }; + const onKeyPressHandler = event => { + if (event.key === 'Enter') { + uiDialogService.dismiss({ id: dialogId }); + resolve({ + action: PROMPT_RESPONSES.CREATE_REPORT, + value: value.label, + }); + } + }; + return ( + <> + {dataSourcesOpts.length > 1 && window.config?.allowMultiSelectExport && ( +
+ + +
+ + ); + }, + }, + }); + }); +} diff --git a/extensions/default/src/Panels/debounce.js b/extensions/default/src/Panels/debounce.js new file mode 100644 index 0000000..7a5b2bd --- /dev/null +++ b/extensions/default/src/Panels/debounce.js @@ -0,0 +1,25 @@ +// Returns a function, that, as long as it continues to be invoked, will not +// be triggered. The function will be called after it stops being called for +// N milliseconds. If `immediate` is passed, trigger the function on the +// leading edge, instead of the trailing. +function debounce(func, wait, immediate) { + var timeout; + return function () { + var context = this, + args = arguments; + var later = function () { + timeout = null; + if (!immediate) { + func.apply(context, args); + } + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + func.apply(context, args); + } + }; +} + +export default debounce; diff --git a/extensions/default/src/Panels/getImageSrcFromImageId.js b/extensions/default/src/Panels/getImageSrcFromImageId.js new file mode 100644 index 0000000..ae845b9 --- /dev/null +++ b/extensions/default/src/Panels/getImageSrcFromImageId.js @@ -0,0 +1,16 @@ +/** + * @param {*} cornerstone + * @param {*} imageId + */ +function getImageSrcFromImageId(cornerstone, imageId) { + return new Promise((resolve, reject) => { + const canvas = document.createElement('canvas'); + cornerstone.utilities + .loadImageToCanvas({ canvas, imageId, thumbnail: true }) + .then(imageId => { + resolve(canvas.toDataURL()); + }) + .catch(reject); + }); +} +export default getImageSrcFromImageId; diff --git a/extensions/default/src/Panels/getStudiesForPatientByMRN.js b/extensions/default/src/Panels/getStudiesForPatientByMRN.js new file mode 100644 index 0000000..be4ac15 --- /dev/null +++ b/extensions/default/src/Panels/getStudiesForPatientByMRN.js @@ -0,0 +1,12 @@ +async function getStudiesForPatientByMRN(dataSource, qidoForStudyUID) { + if (qidoForStudyUID && qidoForStudyUID.length && qidoForStudyUID[0].mrn) { + return dataSource.query.studies.search({ + patientId: qidoForStudyUID[0].mrn, + disableWildcard: true, + }); + } + console.log('No mrn found for', qidoForStudyUID); + return qidoForStudyUID; +} + +export default getStudiesForPatientByMRN; diff --git a/extensions/default/src/Panels/index.js b/extensions/default/src/Panels/index.js new file mode 100644 index 0000000..67f21c7 --- /dev/null +++ b/extensions/default/src/Panels/index.js @@ -0,0 +1,5 @@ +import PanelStudyBrowser from './StudyBrowser/PanelStudyBrowser'; +import WrappedPanelStudyBrowser from './WrappedPanelStudyBrowser'; +import createReportDialogPrompt from './createReportDialogPrompt'; + +export { PanelStudyBrowser, WrappedPanelStudyBrowser, createReportDialogPrompt }; diff --git a/extensions/default/src/Panels/requestDisplaySetCreationForStudy.js b/extensions/default/src/Panels/requestDisplaySetCreationForStudy.js new file mode 100644 index 0000000..9d13180 --- /dev/null +++ b/extensions/default/src/Panels/requestDisplaySetCreationForStudy.js @@ -0,0 +1,19 @@ +function requestDisplaySetCreationForStudy( + dataSource, + displaySetService, + StudyInstanceUID, + madeInClient +) { + // TODO: is this already short-circuited by the map of Retrieve promises? + if ( + displaySetService.activeDisplaySets.some( + displaySet => displaySet.StudyInstanceUID === StudyInstanceUID + ) + ) { + return; + } + + return dataSource.retrieve.series.metadata({ StudyInstanceUID, madeInClient }); +} + +export default requestDisplaySetCreationForStudy; diff --git a/extensions/default/src/SOPClassHandlers/chartSOPClassHandler.ts b/extensions/default/src/SOPClassHandlers/chartSOPClassHandler.ts new file mode 100644 index 0000000..58f561e --- /dev/null +++ b/extensions/default/src/SOPClassHandlers/chartSOPClassHandler.ts @@ -0,0 +1,96 @@ +import { Types, DisplaySetService, utils } from '@ohif/core'; + +import { id } from '../id'; + +type InstanceMetadata = Types.InstanceMetadata; + +const SOPClassHandlerName = 'chart'; + +const CHART_MODALITY = 'CHT'; + +// Private SOPClassUid for chart data +const ChartDataSOPClassUid = '1.9.451.13215.7.3.2.7.6.1'; + +const sopClassUids = [ChartDataSOPClassUid]; + +const makeChartDataDisplaySet = (instance, sopClassUids) => { + const { + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + SeriesDescription, + SeriesNumber, + SeriesDate, + SOPClassUID, + } = instance; + + return { + Modality: CHART_MODALITY, + loading: false, + isReconstructable: false, + displaySetInstanceUID: utils.guid(), + SeriesDescription, + SeriesNumber, + SeriesDate, + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + SOPClassHandlerId: `${id}.sopClassHandlerModule.${SOPClassHandlerName}`, + SOPClassUID, + isDerivedDisplaySet: true, + isLoaded: true, + sopClassUids, + instance, + instances: [instance], + + /** + * Adds instances to the chart displaySet, rather than creating a new one + * when user moves to a different workflow step and gets back to a step that + * recreates the chart + */ + addInstances: function (instances: InstanceMetadata[], _displaySetService: DisplaySetService) { + this.instances.push(...instances); + this.instance = this.instances[this.instances.length - 1]; + + return this; + }, + }; +}; + +function getSopClassUids(instances) { + const uniqueSopClassUidsInSeries = new Set(); + instances.forEach(instance => { + uniqueSopClassUidsInSeries.add(instance.SOPClassUID); + }); + const sopClassUids = Array.from(uniqueSopClassUidsInSeries); + + return sopClassUids; +} + +function _getDisplaySetsFromSeries(instances) { + // If the series has no instances, stop here + if (!instances || !instances.length) { + throw new Error('No instances were provided'); + } + + const sopClassUids = getSopClassUids(instances); + const displaySets = instances.map(instance => { + if (instance.Modality === CHART_MODALITY) { + return makeChartDataDisplaySet(instance, sopClassUids); + } + + throw new Error('Unsupported modality'); + }); + + return displaySets; +} + +const chartHandler = { + name: SOPClassHandlerName, + sopClassUids, + getDisplaySetsFromSeries: instances => { + return _getDisplaySetsFromSeries(instances); + }, +}; + +export { chartHandler }; diff --git a/extensions/default/src/Toolbar/LegacyLayoutSelector.tsx b/extensions/default/src/Toolbar/LegacyLayoutSelector.tsx new file mode 100644 index 0000000..e87b2e4 --- /dev/null +++ b/extensions/default/src/Toolbar/LegacyLayoutSelector.tsx @@ -0,0 +1,87 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import PropTypes from 'prop-types'; +import { LayoutSelector as OHIFLayoutSelector, ToolbarButton } from '@ohif/ui'; + +function LegacyLayoutSelectorWithServices({ + servicesManager, + rows = 3, + columns = 3, + onLayoutChange = () => {}, + ...props +}) { + const { toolbarService } = servicesManager.services; + + const onSelection = useCallback( + props => { + toolbarService.recordInteraction({ + interactionType: 'action', + commands: [ + { + commandName: 'setViewportGridLayout', + commandOptions: { ...props }, + context: 'DEFAULT', + }, + ], + }); + }, + [toolbarService] + ); + + return ( + + ); +} + +function LayoutSelector({ rows, columns, className, onSelection, ...rest }) { + const [isOpen, setIsOpen] = useState(false); + + const closeOnOutsideClick = () => { + if (isOpen) { + setIsOpen(false); + } + }; + + useEffect(() => { + window.addEventListener('click', closeOnOutsideClick); + return () => { + window.removeEventListener('click', closeOnOutsideClick); + }; + }, [isOpen]); + + const onInteractionHandler = () => setIsOpen(!isOpen); + const DropdownContent = isOpen ? OHIFLayoutSelector : null; + + return ( + + ) + } + isActive={isOpen} + type="toggle" + /> + ); +} + +LayoutSelector.propTypes = { + rows: PropTypes.number, + columns: PropTypes.number, + onLayoutChange: PropTypes.func, + servicesManager: PropTypes.object.isRequired, +}; + +export default LegacyLayoutSelectorWithServices; diff --git a/extensions/default/src/Toolbar/ToolBoxWrapper.tsx b/extensions/default/src/Toolbar/ToolBoxWrapper.tsx new file mode 100644 index 0000000..d40c901 --- /dev/null +++ b/extensions/default/src/Toolbar/ToolBoxWrapper.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import classNames from 'classnames'; +import { ToolButton } from '@ohif/ui-next'; + +/** + * Wraps the ToolButtonList component to handle the OHIF toolbar button structure + * @param props - Component props + * @returns Component + */ +export function ToolBoxButtonGroupWrapper({ groupId, items, onInteraction, ...props }) { + if (!items || !groupId) { + return null; + } + + return ( +
+ {items.map(item => ( + + onInteraction?.({ groupId, itemId: item.id, commands: item.commands }) + } + /> + ))} +
+ ); +} + +export function ToolBoxButtonWrapper({ onInteraction, ...props }) { + return ( +
+ onInteraction?.({ itemId: props.id, commands: props.commands })} + /> +
+ ); +} diff --git a/extensions/default/src/Toolbar/ToolButtonListWrapper.tsx b/extensions/default/src/Toolbar/ToolButtonListWrapper.tsx new file mode 100644 index 0000000..d1477f7 --- /dev/null +++ b/extensions/default/src/Toolbar/ToolButtonListWrapper.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { + ToolButtonList, + ToolButton, + ToolButtonListDefault, + ToolButtonListDropDown, + ToolButtonListItem, + ToolButtonListDivider, +} from '@ohif/ui-next'; + +interface ButtonItem { + id: string; + icon?: string; + label?: string; + tooltip?: string; + isActive?: boolean; + disabledText?: string; + commands?: Record; + disabled?: boolean; + className?: string; +} + +interface ToolButtonListWrapperProps { + groupId: string; + primary: ButtonItem; + items: ButtonItem[]; + onInteraction?: (details: { + groupId: string; + itemId: string; + commands?: Record; + }) => void; +} + +/** + * Wraps the ToolButtonList component to handle the OHIF toolbar button structure + * @param props - Component props + * @returns Component + * // test + */ +export default function ToolButtonListWrapper({ + groupId, + primary, + items, + onInteraction, +}: ToolButtonListWrapperProps) { + return ( + + +
+ + onInteraction?.({ groupId, itemId, commands: primary.commands }) + } + className={primary.className} + /> +
+
+ +
+ + {items.map(item => ( + + onInteraction?.({ groupId, itemId: item.id, commands: item.commands }) + } + > + {item.label || item.tooltip || item.id} + + ))} + +
+
+ ); +} diff --git a/extensions/default/src/Toolbar/Toolbar.tsx b/extensions/default/src/Toolbar/Toolbar.tsx new file mode 100644 index 0000000..967baf9 --- /dev/null +++ b/extensions/default/src/Toolbar/Toolbar.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { useToolbar } from '@ohif/core'; + +export function Toolbar({ servicesManager, buttonSection = 'primary' }) { + const { toolbarButtons, onInteraction } = useToolbar({ + servicesManager, + buttonSection, + }); + + if (!toolbarButtons.length) { + return null; + } + + return ( + <> + {toolbarButtons?.map(toolDef => { + if (!toolDef) { + return null; + } + + const { id, Component, componentProps } = toolDef; + const tool = ( + + ); + + return
{tool}
; + })} + + ); +} diff --git a/extensions/default/src/Toolbar/ToolbarButtonGroupWithServices.tsx b/extensions/default/src/Toolbar/ToolbarButtonGroupWithServices.tsx new file mode 100644 index 0000000..300270a --- /dev/null +++ b/extensions/default/src/Toolbar/ToolbarButtonGroupWithServices.tsx @@ -0,0 +1,36 @@ +import React, { useCallback } from 'react'; +import { ToolbarButton, ButtonGroup } from '@ohif/ui'; + +function ToolbarButtonGroupWithServices({ groupId, items, onInteraction, size }) { + const getSplitButtonItems = useCallback( + items => + items.map((item, index) => ( + { + onInteraction({ + groupId, + itemId: item.id, + commands: item.commands, + }); + }} + // Note: this is necessary since tooltip will add + // default styles to the tooltip container which + // we don't want for groups + toolTipClassName="" + /> + )), + [onInteraction, groupId] + ); + + return {getSplitButtonItems(items)}; +} + +export default ToolbarButtonGroupWithServices; diff --git a/extensions/default/src/Toolbar/ToolbarDivider.tsx b/extensions/default/src/Toolbar/ToolbarDivider.tsx new file mode 100644 index 0000000..30a7cff --- /dev/null +++ b/extensions/default/src/Toolbar/ToolbarDivider.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function ToolbarDivider() { + return ; +} diff --git a/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx b/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx new file mode 100644 index 0000000..8bd76d9 --- /dev/null +++ b/extensions/default/src/Toolbar/ToolbarLayoutSelector.tsx @@ -0,0 +1,169 @@ +import React, { useEffect, useState, useCallback, useRef } from 'react'; +import PropTypes from 'prop-types'; +import { LayoutSelector as OHIFLayoutSelector, ToolbarButton, LayoutPreset } from '@ohif/ui'; + +function ToolbarLayoutSelectorWithServices({ + commandsManager, + servicesManager, + ...props +}: withAppTypes) { + const [isDisabled, setIsDisabled] = useState(false); + + const handleMouseEnter = () => { + setIsDisabled(false); + }; + + const onSelection = useCallback(props => { + commandsManager.run({ + commandName: 'setViewportGridLayout', + commandOptions: { ...props }, + }); + setIsDisabled(true); + }, []); + + const onSelectionPreset = useCallback(props => { + commandsManager.run({ + commandName: 'setHangingProtocol', + commandOptions: { ...props }, + }); + setIsDisabled(true); + }, []); + + return ( +
+ +
+ ); +} + +function LayoutSelector({ + rows = 3, + columns = 4, + onLayoutChange = () => {}, + className, + onSelection, + onSelectionPreset, + servicesManager, + tooltipDisabled, + ...rest +}: withAppTypes) { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + const { customizationService } = servicesManager.services; + + const commonPresets = customizationService.getCustomization('layoutSelector.commonPresets'); + const advancedPresetsGenerator = customizationService.getCustomization( + 'layoutSelector.advancedPresetGenerator' + ); + + const advancedPresets = advancedPresetsGenerator({ servicesManager }); + + const closeOnOutsideClick = event => { + if (isOpen && dropdownRef.current) { + setIsOpen(false); + } + }; + + useEffect(() => { + if (!isOpen) { + return; + } + + setTimeout(() => { + window.addEventListener('click', closeOnOutsideClick); + }, 0); + return () => { + window.removeEventListener('click', closeOnOutsideClick); + dropdownRef.current = null; + }; + }, [isOpen]); + + const onInteractionHandler = () => { + setIsOpen(!isOpen); + }; + const DropdownContent = isOpen ? OHIFLayoutSelector : null; + + return ( + +
+
Common
+ +
+ {commonPresets.map((preset, index) => ( + + ))} +
+ +
+ +
Advanced
+ +
+ {advancedPresets.map((preset, index) => ( + + ))} +
+
+ +
+
Custom
+ +

+ Hover to select

rows and columns

Click to apply +

+
+ + ) + } + isActive={isOpen} + type="toggle" + /> + ); +} + +LayoutSelector.propTypes = { + rows: PropTypes.number, + columns: PropTypes.number, + onLayoutChange: PropTypes.func, + servicesManager: PropTypes.object.isRequired, +}; + +export default ToolbarLayoutSelectorWithServices; diff --git a/extensions/default/src/Toolbar/ToolbarSplitButtonWithServices.tsx b/extensions/default/src/Toolbar/ToolbarSplitButtonWithServices.tsx new file mode 100644 index 0000000..ccd3870 --- /dev/null +++ b/extensions/default/src/Toolbar/ToolbarSplitButtonWithServices.tsx @@ -0,0 +1,89 @@ +import { SplitButton, ToolbarButton } from '@ohif/ui'; +import React, { useCallback } from 'react'; +import PropTypes from 'prop-types'; + +function ToolbarSplitButtonWithServices({ + groupId, + primary, + secondary, + items, + renderer, + onInteraction, + servicesManager, +}: withAppTypes) { + const { toolbarService } = servicesManager?.services; + + /* Bubbles up individual item clicks */ + const getSplitButtonItems = useCallback( + items => + items.map((item, index) => ({ + ...item, + index, + onClick: () => { + onInteraction({ + groupId, + itemId: item.id, + commands: item.commands, + }); + }, + })), + [groupId, onInteraction] + ); + + const PrimaryButtonComponent = + toolbarService?.getButtonComponentForUIType(primary.uiType) ?? ToolbarButton; + + const listItemRenderer = renderer; + + return ( + ( + + )} + /> + ); +} + +ToolbarSplitButtonWithServices.propTypes = { + groupId: PropTypes.string, + primary: PropTypes.shape({ + id: PropTypes.string.isRequired, + uiType: PropTypes.string, + }), + secondary: PropTypes.shape({ + id: PropTypes.string, + icon: PropTypes.string.isRequired, + label: PropTypes.string, + tooltip: PropTypes.string.isRequired, + disabled: PropTypes.bool, + className: PropTypes.string, + }), + items: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + icon: PropTypes.string, + label: PropTypes.string, + tooltip: PropTypes.string, + disabled: PropTypes.bool, + className: PropTypes.string, + }) + ), + renderer: PropTypes.func, + onInteraction: PropTypes.func.isRequired, + servicesManager: PropTypes.shape({ + services: PropTypes.shape({ + toolbarService: PropTypes.object, + }), + }), +}; + +export default ToolbarSplitButtonWithServices; diff --git a/extensions/default/src/ViewerLayout/HeaderPatientInfo/HeaderPatientInfo.tsx b/extensions/default/src/ViewerLayout/HeaderPatientInfo/HeaderPatientInfo.tsx new file mode 100644 index 0000000..cac66d2 --- /dev/null +++ b/extensions/default/src/ViewerLayout/HeaderPatientInfo/HeaderPatientInfo.tsx @@ -0,0 +1,74 @@ +import React, { useState, useEffect } from 'react'; +import usePatientInfo from '../../hooks/usePatientInfo'; +import { Icons } from '@ohif/ui-next'; + +export enum PatientInfoVisibility { + VISIBLE = 'visible', + VISIBLE_COLLAPSED = 'visibleCollapsed', + DISABLED = 'disabled', + VISIBLE_READONLY = 'visibleReadOnly', +} + +const formatWithEllipsis = (str, maxLength) => { + if (str?.length > maxLength) { + return str.substring(0, maxLength) + '...'; + } + return str; +}; + +function HeaderPatientInfo({ servicesManager, appConfig }: withAppTypes) { + const initialExpandedState = + appConfig.showPatientInfo === PatientInfoVisibility.VISIBLE || + appConfig.showPatientInfo === PatientInfoVisibility.VISIBLE_READONLY; + const [expanded, setExpanded] = useState(initialExpandedState); + const { patientInfo, isMixedPatients } = usePatientInfo(servicesManager); + + useEffect(() => { + if (isMixedPatients && expanded) { + setExpanded(false); + } + }, [isMixedPatients, expanded]); + + const handleOnClick = () => { + if (!isMixedPatients && appConfig.showPatientInfo !== PatientInfoVisibility.VISIBLE_READONLY) { + setExpanded(!expanded); + } + }; + + const formattedPatientName = formatWithEllipsis(patientInfo.PatientName, 27); + const formattedPatientID = formatWithEllipsis(patientInfo.PatientID, 15); + + return ( +
+ {isMixedPatients ? ( + + ) : ( + + )} +
+ {expanded ? ( + <> +
+ {formattedPatientName} +
+
+
{formattedPatientID}
+
{patientInfo.PatientSex}
+
{patientInfo.PatientDOB}
+
+ + ) : ( +
+ {isMixedPatients ? 'Multiple Patients' : 'Patient'} +
+ )} +
+ +
+ ); +} + +export default HeaderPatientInfo; diff --git a/extensions/default/src/ViewerLayout/HeaderPatientInfo/index.js b/extensions/default/src/ViewerLayout/HeaderPatientInfo/index.js new file mode 100644 index 0000000..cc989ba --- /dev/null +++ b/extensions/default/src/ViewerLayout/HeaderPatientInfo/index.js @@ -0,0 +1,3 @@ +import HeaderPatientInfo from './HeaderPatientInfo'; + +export default HeaderPatientInfo; diff --git a/extensions/default/src/ViewerLayout/ResizablePanelsHook.tsx b/extensions/default/src/ViewerLayout/ResizablePanelsHook.tsx new file mode 100644 index 0000000..60dad54 --- /dev/null +++ b/extensions/default/src/ViewerLayout/ResizablePanelsHook.tsx @@ -0,0 +1,316 @@ +import { useState, useCallback, useLayoutEffect, useRef } from 'react'; +import { getPanelElement, getPanelGroupElement } from 'react-resizable-panels'; +import { panelGroupDefinition } from './constants/panels'; + +/** + * Set the minimum and maximum css style width attributes for the given element. + * The two style attributes are cleared whenever the width + * argument is undefined. + *

+ * This utility is used as part of a HACK throughout the ViewerLayout component as + * the means of restricting the side panel widths during the resizing of the + * browser window. In general, the widths are always set unless the resize + * handle for either side panel is being dragged (i.e. a side panel is being resized). + * + * @param elem the element + * @param width the max and min width to set on the element + */ +const setMinMaxWidth = (elem, width?) => { + elem.style.minWidth = width === undefined ? '' : `${width}px`; + elem.style.maxWidth = elem.style.minWidth; +}; + +const useResizablePanels = ( + leftPanelClosed, + setLeftPanelClosed, + rightPanelClosed, + setRightPanelClosed +) => { + const [leftPanelExpandedWidth, setLeftPanelExpandedWidth] = useState( + panelGroupDefinition.left.initialExpandedWidth + ); + const [rightPanelExpandedWidth, setRightPanelExpandedWidth] = useState( + panelGroupDefinition.right.initialExpandedWidth + ); + const [leftResizablePanelMinimumSize, setLeftResizablePanelMinimumSize] = useState(0); + const [rightResizablePanelMinimumSize, setRightResizablePanelMinimumSize] = useState(0); + const [leftResizablePanelCollapsedSize, setLeftResizePanelCollapsedSize] = useState(0); + const [rightResizePanelCollapsedSize, setRightResizePanelCollapsedSize] = useState(0); + + const resizablePanelGroupElemRef = useRef(null); + const resizableLeftPanelElemRef = useRef(null); + const resizableRightPanelElemRef = useRef(null); + const resizableLeftPanelAPIRef = useRef(null); + const resizableRightPanelAPIRef = useRef(null); + const isResizableHandleDraggingRef = useRef(false); + + // The total width of both handles. + const resizableHandlesWidth = useRef(null); + + // This useLayoutEffect is used to... + // - Grab a reference to the various resizable panel elements needed for + // converting between percentages and pixels in various callbacks. + // - Expand those panels that are initially expanded. + useLayoutEffect(() => { + const panelGroupElem = getPanelGroupElement(panelGroupDefinition.groupId); + resizablePanelGroupElemRef.current = panelGroupElem; + + const leftPanelElem = getPanelElement(panelGroupDefinition.left.panelId); + resizableLeftPanelElemRef.current = leftPanelElem; + + const rightPanelElem = getPanelElement(panelGroupDefinition.right.panelId); + resizableRightPanelElemRef.current = rightPanelElem; + + // Calculate and set the width of both handles combined. + const resizeHandles = document.querySelectorAll('[data-panel-resize-handle-id]'); + resizableHandlesWidth.current = 0; + resizeHandles.forEach(resizeHandle => { + resizableHandlesWidth.current += resizeHandle.offsetWidth; + }); + + // Since both resizable panels are collapsed by default (i.e. their default size is zero), + // on the very first render check if either/both side panels should be expanded. + // we use the initialExpandedOffsetWidth on the first render incase the panel has min width but we want the initial state to be larger than that + + if (!leftPanelClosed) { + const leftResizablePanelExpandedSize = getPercentageSize( + panelGroupDefinition.left.initialExpandedOffsetWidth + ); + resizableLeftPanelAPIRef?.current?.expand(leftResizablePanelExpandedSize); + setMinMaxWidth(leftPanelElem, panelGroupDefinition.left.initialExpandedOffsetWidth); + } + + if (!rightPanelClosed) { + const rightResizablePanelExpandedSize = getPercentageSize( + panelGroupDefinition.right.initialExpandedOffsetWidth + ); + resizableRightPanelAPIRef?.current?.expand(rightResizablePanelExpandedSize); + setMinMaxWidth(rightPanelElem, panelGroupDefinition.right.initialExpandedOffsetWidth); + } + }, []); // no dependencies because this useLayoutEffect is only needed on the very first render + + // This useLayoutEffect follows the pattern prescribed by the react-resizable-panels + // readme for converting between pixel values and percentages. An example of + // the pattern can be found here: + // https://github.com/bvaughn/react-resizable-panels/issues/46#issuecomment-1368108416 + // This useLayoutEffect is used to... + // - Ensure that the percentage size is up-to-date with the pixel sizes + // - Add a resize observer to the resizable panel group to reset various state + // values whenever the resizable panel group is resized (e.g. whenever the + // browser window is resized). + useLayoutEffect(() => { + // Ensure the side panels' percentage size is in synch with the pixel width of the + // expanded side panels. In general the two get out-of-sync during a browser + // window resize. Note that this code is here and NOT in the ResizeObserver + // because it has to be done AFTER the minimum percentage size for a panel is + // updated which occurs only AFTER the render following a browser window resize. + // And by virtue of the dependency on the minimum size state variables, this code + // is executed on the render following an update of the minimum percentage sizes + // for a panel. + if (!resizableLeftPanelAPIRef.current?.isCollapsed()) { + const leftSize = getPercentageSize( + leftPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize + ); + resizableLeftPanelAPIRef.current?.resize(leftSize); + } + + if (!resizableRightPanelAPIRef?.current?.isCollapsed()) { + const rightSize = getPercentageSize( + rightPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize + ); + resizableRightPanelAPIRef?.current?.resize(rightSize); + } + + // This observer kicks in when the ViewportLayout resizable panel group + // component is resized. This typically occurs when the browser window resizes. + const observer = new ResizeObserver(() => { + const minimumLeftSize = getPercentageSize( + panelGroupDefinition.left.minimumExpandedOffsetWidth + ); + const minimumRightSize = getPercentageSize( + panelGroupDefinition.right.minimumExpandedOffsetWidth + ); + + // Set the new minimum and collapsed resizable panel sizes. + setLeftResizablePanelMinimumSize(minimumLeftSize); + setRightResizablePanelMinimumSize(minimumRightSize); + setLeftResizePanelCollapsedSize( + getPercentageSize(panelGroupDefinition.left.collapsedOffsetWidth) + ); + setRightResizePanelCollapsedSize( + getPercentageSize(panelGroupDefinition.right.collapsedOffsetWidth) + ); + }); + + observer.observe(resizablePanelGroupElemRef.current); + + return () => { + observer.disconnect(); + }; + }, [ + leftPanelExpandedWidth, + rightPanelExpandedWidth, + leftResizablePanelMinimumSize, + rightResizablePanelMinimumSize, + ]); + + /** + * Handles dragging of either side panel resize handle. + */ + const onHandleDragging = useCallback( + isStartDrag => { + if (isStartDrag) { + isResizableHandleDraggingRef.current = true; + + setMinMaxWidth(resizableLeftPanelElemRef.current); + setMinMaxWidth(resizableRightPanelElemRef.current); + } else { + isResizableHandleDraggingRef.current = false; + + if (resizableLeftPanelAPIRef?.current?.isExpanded()) { + setMinMaxWidth( + resizableLeftPanelElemRef.current, + leftPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize + ); + } + + if (resizableRightPanelAPIRef?.current?.isExpanded()) { + setMinMaxWidth( + resizableRightPanelElemRef.current, + rightPanelExpandedWidth + panelGroupDefinition.shared.expandedInsideBorderSize + ); + } + } + }, + [leftPanelExpandedWidth, rightPanelExpandedWidth] + ); + + const onLeftPanelClose = useCallback(() => { + setLeftPanelClosed(true); + setMinMaxWidth(resizableLeftPanelElemRef.current); + resizableLeftPanelAPIRef?.current?.collapse(); + }, [setLeftPanelClosed]); + + const onLeftPanelOpen = useCallback(() => { + resizableLeftPanelAPIRef?.current?.expand( + getPercentageSize(panelGroupDefinition.left.initialExpandedOffsetWidth) + ); + setLeftPanelClosed(false); + }, [setLeftPanelClosed]); + + const onLeftPanelResize = useCallback(size => { + if (!resizablePanelGroupElemRef?.current || resizableLeftPanelAPIRef.current?.isCollapsed()) { + return; + } + + const newExpandedWidth = getExpandedPixelWidth(size); + setLeftPanelExpandedWidth(newExpandedWidth); + + if (!isResizableHandleDraggingRef.current) { + // This typically gets executed when the left panel is expanded via one of the UI + // buttons. It is done here instead of in the onLeftPanelOpen method + // because here we know the size of the expanded panel. + setMinMaxWidth(resizableLeftPanelElemRef.current, newExpandedWidth); + } + }, []); + + const onRightPanelClose = useCallback(() => { + setRightPanelClosed(true); + setMinMaxWidth(resizableRightPanelElemRef.current); + resizableRightPanelAPIRef?.current?.collapse(); + }, [setRightPanelClosed]); + + const onRightPanelOpen = useCallback(() => { + resizableRightPanelAPIRef?.current?.expand( + getPercentageSize(panelGroupDefinition.right.initialExpandedOffsetWidth) + ); + setRightPanelClosed(false); + }, [setRightPanelClosed]); + + const onRightPanelResize = useCallback(size => { + if (!resizablePanelGroupElemRef?.current || resizableRightPanelAPIRef?.current?.isCollapsed()) { + return; + } + + const newExpandedWidth = getExpandedPixelWidth(size); + setRightPanelExpandedWidth(newExpandedWidth); + + if (!isResizableHandleDraggingRef.current) { + // This typically gets executed when the right panel is expanded via one of the UI + // buttons. It is done here instead of in the onRightPanelOpen method + // because here we know the size of the expanded panel. + setMinMaxWidth(resizableRightPanelElemRef.current, newExpandedWidth); + } + }, []); + + /** + * Gets the percentage size corresponding to the given pixel size. + * Note that the width attributed to the handles must be taken into account. + */ + const getPercentageSize = pixelSize => { + const { width: panelGroupWidth } = resizablePanelGroupElemRef.current?.getBoundingClientRect(); + return (pixelSize / (panelGroupWidth - resizableHandlesWidth.current)) * 100; + }; + + /** + * Gets the width in pixels for an expanded panel given its percentage size/width. + * Note that the width attributed to the handles must be taken into account. + */ + const getExpandedPixelWidth = percentageSize => { + const { width: panelGroupWidth } = resizablePanelGroupElemRef.current?.getBoundingClientRect(); + const expandedWidth = + (percentageSize / 100) * (panelGroupWidth - resizableHandlesWidth.current) - + panelGroupDefinition.shared.expandedInsideBorderSize; + return expandedWidth; + }; + + return [ + { + expandedWidth: leftPanelExpandedWidth, + collapsedWidth: panelGroupDefinition.shared.collapsedWidth, + collapsedInsideBorderSize: panelGroupDefinition.shared.collapsedInsideBorderSize, + collapsedOutsideBorderSize: panelGroupDefinition.shared.collapsedOutsideBorderSize, + expandedInsideBorderSize: panelGroupDefinition.shared.expandedInsideBorderSize, + onClose: onLeftPanelClose, + onOpen: onLeftPanelOpen, + }, + { + expandedWidth: rightPanelExpandedWidth, + collapsedWidth: panelGroupDefinition.shared.collapsedWidth, + collapsedInsideBorderSize: panelGroupDefinition.shared.collapsedInsideBorderSize, + collapsedOutsideBorderSize: panelGroupDefinition.shared.collapsedOutsideBorderSize, + expandedInsideBorderSize: panelGroupDefinition.shared.expandedInsideBorderSize, + onClose: onRightPanelClose, + onOpen: onRightPanelOpen, + }, + { direction: 'horizontal', id: panelGroupDefinition.groupId }, + { + defaultSize: leftResizablePanelMinimumSize, + minSize: leftResizablePanelMinimumSize, + onResize: onLeftPanelResize, + collapsible: true, + collapsedSize: leftResizablePanelCollapsedSize, + onCollapse: () => setLeftPanelClosed(true), + onExpand: () => setLeftPanelClosed(false), + ref: resizableLeftPanelAPIRef, + order: 0, + id: panelGroupDefinition.left.panelId, + }, + { order: 1, id: 'viewerLayoutResizableViewportGridPanel' }, + { + defaultSize: rightResizablePanelMinimumSize, + minSize: rightResizablePanelMinimumSize, + onResize: onRightPanelResize, + collapsible: true, + collapsedSize: rightResizePanelCollapsedSize, + onCollapse: () => setRightPanelClosed(true), + onExpand: () => setRightPanelClosed(false), + ref: resizableRightPanelAPIRef, + order: 2, + id: panelGroupDefinition.right.panelId, + }, + onHandleDragging, + ]; +}; + +export default useResizablePanels; diff --git a/extensions/default/src/ViewerLayout/ToolbarButtonNestedMenu.tsx b/extensions/default/src/ViewerLayout/ToolbarButtonNestedMenu.tsx new file mode 100644 index 0000000..40ef819 --- /dev/null +++ b/extensions/default/src/ViewerLayout/ToolbarButtonNestedMenu.tsx @@ -0,0 +1,42 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { ToolbarButton } from '@ohif/ui'; + +function NestedMenu({ children, label = 'More', icon = 'tool-more-menu', isActive }) { + const [isOpen, setIsOpen] = useState(false); + + const toggleNestedMenu = () => setIsOpen(!isOpen); + + const closeNestedMenu = () => { + if (isOpen) { + setIsOpen(false); + } + }; + + useEffect(() => { + window.addEventListener('click', closeNestedMenu); + return () => { + window.removeEventListener('click', closeNestedMenu); + }; + }, [isOpen]); + + return ( + + ); +} + +NestedMenu.propTypes = { + children: PropTypes.any.isRequired, + icon: PropTypes.string, + label: PropTypes.string, +}; + +export default NestedMenu; diff --git a/extensions/default/src/ViewerLayout/ViewerHeader.tsx b/extensions/default/src/ViewerLayout/ViewerHeader.tsx new file mode 100644 index 0000000..2cbc8c1 --- /dev/null +++ b/extensions/default/src/ViewerLayout/ViewerHeader.tsx @@ -0,0 +1,133 @@ +import React from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; + +import { UserPreferences, AboutModal, useModal } from '@ohif/ui'; +import { Header } from '@ohif/ui-next'; +import i18n from '@ohif/i18n'; +import { hotkeys } from '@ohif/core'; +import { Toolbar } from '../Toolbar/Toolbar'; +import HeaderPatientInfo from './HeaderPatientInfo'; +import { PatientInfoVisibility } from './HeaderPatientInfo/HeaderPatientInfo'; +import { preserveQueryParameters, publicUrl } from '@ohif/app'; + +const { availableLanguages, defaultLanguage, currentLanguage } = i18n; + +function ViewerHeader({ + hotkeysManager, + extensionManager, + servicesManager, + appConfig, +}: withAppTypes<{ appConfig: AppTypes.Config }>) { + const navigate = useNavigate(); + const location = useLocation(); + + const onClickReturnButton = () => { + const { pathname } = location; + const dataSourceIdx = pathname.indexOf('/', 1); + + const dataSourceName = pathname.substring(dataSourceIdx + 1); + const existingDataSource = extensionManager.getDataSources(dataSourceName); + + const searchQuery = new URLSearchParams(); + if (dataSourceIdx !== -1 && existingDataSource) { + searchQuery.append('datasources', pathname.substring(dataSourceIdx + 1)); + } + preserveQueryParameters(searchQuery); + + navigate({ + pathname: publicUrl, + search: decodeURIComponent(searchQuery.toString()), + }); + }; + + const { t } = useTranslation(); + const { show, hide } = useModal(); + const { hotkeyDefinitions, hotkeyDefaults } = hotkeysManager; + const versionNumber = process.env.VERSION_NUMBER; + const commitHash = process.env.COMMIT_HASH; + + const menuOptions = [ + { + title: t('Header:About'), + icon: 'info', + onClick: () => + show({ + content: AboutModal, + title: t('AboutModal:About OHIF Viewer'), + contentProps: { versionNumber, commitHash }, + containerDimensions: 'max-w-4xl max-h-4xl', + }), + }, + { + title: t('Header:Preferences'), + icon: 'settings', + onClick: () => + show({ + title: t('UserPreferencesModal:User preferences'), + content: UserPreferences, + containerDimensions: 'w-[70%] max-w-[900px]', + contentProps: { + hotkeyDefaults: hotkeysManager.getValidHotkeyDefinitions(hotkeyDefaults), + hotkeyDefinitions, + currentLanguage: currentLanguage(), + availableLanguages, + defaultLanguage, + onCancel: () => { + hotkeys.stopRecord(); + hotkeys.unpause(); + hide(); + }, + onSubmit: ({ hotkeyDefinitions, language }) => { + if (language.value !== currentLanguage().value) { + i18n.changeLanguage(language.value); + } + hotkeysManager.setHotkeys(hotkeyDefinitions); + hide(); + }, + onReset: () => hotkeysManager.restoreDefaultBindings(), + hotkeysModule: hotkeys, + }, + }), + }, + ]; + + if (appConfig.oidc) { + menuOptions.push({ + title: t('Header:Logout'), + icon: 'power-off', + onClick: async () => { + navigate(`/logout?redirect_uri=${encodeURIComponent(window.location.href)}`); + }, + }); + } + + return ( +

+ } + PatientInfo={ + appConfig.showPatientInfo !== PatientInfoVisibility.DISABLED && ( + + ) + } + > +
+ +
+
+ ); +} + +export default ViewerHeader; diff --git a/extensions/default/src/ViewerLayout/constants/panels.ts b/extensions/default/src/ViewerLayout/constants/panels.ts new file mode 100644 index 0000000..3219e6c --- /dev/null +++ b/extensions/default/src/ViewerLayout/constants/panels.ts @@ -0,0 +1,38 @@ +const expandedInsideBorderSize = 0; +const collapsedInsideBorderSize = 4; +const collapsedOutsideBorderSize = 4; +const collapsedWidth = 25; + +const rightPanelInitialExpandedWidth = 280; +const leftPanelInitialExpandedWidth = 282; + +const panelGroupDefinition = { + groupId: 'viewerLayoutResizablePanelGroup', + shared: { + expandedInsideBorderSize, + collapsedInsideBorderSize, + collapsedOutsideBorderSize, + collapsedWidth, + }, + left: { + // id + panelId: 'viewerLayoutResizableLeftPanel', + // expanded width + initialExpandedWidth: leftPanelInitialExpandedWidth, + // expanded width + expanded inside border + minimumExpandedOffsetWidth: 145 + expandedInsideBorderSize, + // initial expanded width + initialExpandedOffsetWidth: leftPanelInitialExpandedWidth + expandedInsideBorderSize, + // collapsed width + collapsed inside border + collapsed outside border + collapsedOffsetWidth: collapsedWidth + collapsedInsideBorderSize + collapsedOutsideBorderSize, + }, + right: { + panelId: 'viewerLayoutResizableRightPanel', + initialExpandedWidth: rightPanelInitialExpandedWidth, + minimumExpandedOffsetWidth: rightPanelInitialExpandedWidth + expandedInsideBorderSize, + initialExpandedOffsetWidth: rightPanelInitialExpandedWidth + expandedInsideBorderSize, + collapsedOffsetWidth: collapsedWidth + collapsedInsideBorderSize + collapsedOutsideBorderSize, + }, +}; + +export { panelGroupDefinition }; diff --git a/extensions/default/src/ViewerLayout/index.tsx b/extensions/default/src/ViewerLayout/index.tsx new file mode 100644 index 0000000..7646900 --- /dev/null +++ b/extensions/default/src/ViewerLayout/index.tsx @@ -0,0 +1,225 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import PropTypes from 'prop-types'; + +import { InvestigationalUseDialog } from '@ohif/ui'; +import { HangingProtocolService, CommandsManager } from '@ohif/core'; +import { useAppConfig } from '@state'; +import ViewerHeader from './ViewerHeader'; +import SidePanelWithServices from '../Components/SidePanelWithServices'; +import { Onboarding, ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@ohif/ui-next'; +import useResizablePanels from './ResizablePanelsHook'; + +const resizableHandleClassName = 'mt-[1px] bg-black'; + +function ViewerLayout({ + // From Extension Module Params + extensionManager, + servicesManager, + hotkeysManager, + commandsManager, + // From Modes + viewports, + ViewportGridComp, + leftPanelClosed = false, + rightPanelClosed = false, + leftPanelResizable = false, + rightPanelResizable = false, +}: withAppTypes): React.FunctionComponent { + const [appConfig] = useAppConfig(); + + const { panelService, hangingProtocolService, customizationService } = servicesManager.services; + const [showLoadingIndicator, setShowLoadingIndicator] = useState(appConfig.showLoadingIndicator); + + const hasPanels = useCallback( + (side): boolean => !!panelService.getPanels(side).length, + [panelService] + ); + + const [hasRightPanels, setHasRightPanels] = useState(hasPanels('right')); + const [hasLeftPanels, setHasLeftPanels] = useState(hasPanels('left')); + const [leftPanelClosedState, setLeftPanelClosed] = useState(leftPanelClosed); + const [rightPanelClosedState, setRightPanelClosed] = useState(rightPanelClosed); + + const [ + leftPanelProps, + rightPanelProps, + resizablePanelGroupProps, + resizableLeftPanelProps, + resizableViewportGridPanelProps, + resizableRightPanelProps, + onHandleDragging, + ] = useResizablePanels( + leftPanelClosed, + setLeftPanelClosed, + rightPanelClosed, + setRightPanelClosed + ); + + const LoadingIndicatorProgress = customizationService.getCustomization( + 'ui.loadingIndicatorProgress' + ); + + /** + * Set body classes (tailwindcss) that don't allow vertical + * or horizontal overflow (no scrolling). Also guarantee window + * is sized to our viewport. + */ + useEffect(() => { + document.body.classList.add('bg-black'); + document.body.classList.add('overflow-hidden'); + return () => { + document.body.classList.remove('bg-black'); + document.body.classList.remove('overflow-hidden'); + }; + }, []); + + const getComponent = id => { + const entry = extensionManager.getModuleEntry(id); + + if (!entry || !entry.component) { + throw new Error( + `${id} is not valid for an extension module or no component found from extension ${id}. Please verify your configuration or ensure that the extension is properly registered. It's also possible that your mode is utilizing a module from an extension that hasn't been included in its dependencies (add the extension to the "extensionDependencies" array in your mode's index.js file). Check the reference string to the extension in your Mode configuration` + ); + } + + return { entry, content: entry.component }; + }; + + useEffect(() => { + const { unsubscribe } = hangingProtocolService.subscribe( + HangingProtocolService.EVENTS.PROTOCOL_CHANGED, + + // Todo: right now to set the loading indicator to false, we need to wait for the + // hangingProtocolService to finish applying the viewport matching to each viewport, + // however, this might not be the only approach to set the loading indicator to false. we need to explore this further. + () => { + setShowLoadingIndicator(false); + } + ); + + return () => { + unsubscribe(); + }; + }, [hangingProtocolService]); + + const getViewportComponentData = viewportComponent => { + const { entry } = getComponent(viewportComponent.namespace); + + return { + component: entry.component, + displaySetsToDisplay: viewportComponent.displaySetsToDisplay, + }; + }; + + useEffect(() => { + const { unsubscribe } = panelService.subscribe( + panelService.EVENTS.PANELS_CHANGED, + ({ options }) => { + setHasLeftPanels(hasPanels('left')); + setHasRightPanels(hasPanels('right')); + if (options?.leftPanelClosed !== undefined) { + setLeftPanelClosed(options.leftPanelClosed); + } + if (options?.rightPanelClosed !== undefined) { + setRightPanelClosed(options.rightPanelClosed); + } + } + ); + + return () => { + unsubscribe(); + }; + }, [panelService, hasPanels]); + + const viewportComponents = viewports.map(getViewportComponentData); + + return ( +
+ +
+ + {showLoadingIndicator && } + + {/* LEFT SIDEPANELS */} + + {hasLeftPanels ? ( + <> + + + + + + ) : null} + {/* TOOLBAR + GRID */} + +
+
+ +
+
+
+ {hasRightPanels ? ( + <> + + + + + + ) : null} +
+
+
+ + +
+ ); +} + +ViewerLayout.propTypes = { + // From extension module params + extensionManager: PropTypes.shape({ + getModuleEntry: PropTypes.func.isRequired, + }).isRequired, + commandsManager: PropTypes.instanceOf(CommandsManager), + servicesManager: PropTypes.object.isRequired, + // From modes + leftPanels: PropTypes.array, + rightPanels: PropTypes.array, + leftPanelClosed: PropTypes.bool.isRequired, + rightPanelClosed: PropTypes.bool.isRequired, + /** Responsible for rendering our grid of viewports; provided by consuming application */ + children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, + viewports: PropTypes.array, +}; + +export default ViewerLayout; diff --git a/extensions/default/src/commandsModule.ts b/extensions/default/src/commandsModule.ts new file mode 100644 index 0000000..494258c --- /dev/null +++ b/extensions/default/src/commandsModule.ts @@ -0,0 +1,654 @@ +import { Types, DicomMetadataStore } from '@ohif/core'; + +import { ContextMenuController } from './CustomizableContextMenu'; +import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser'; +import reuseCachedLayouts from './utils/reuseCachedLayouts'; +import findViewportsByPosition, { + findOrCreateViewport as layoutFindOrCreate, +} from './findViewportsByPosition'; + +import { ContextMenuProps } from './CustomizableContextMenu/types'; +import { NavigateHistory } from './types/commandModuleTypes'; +import { history } from '@ohif/app'; +import { useViewportGridStore } from './stores/useViewportGridStore'; +import { useDisplaySetSelectorStore } from './stores/useDisplaySetSelectorStore'; +import { useHangingProtocolStageIndexStore } from './stores/useHangingProtocolStageIndexStore'; +import { useToggleHangingProtocolStore } from './stores/useToggleHangingProtocolStore'; +import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore'; +import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore'; +import requestDisplaySetCreationForStudy from './Panels/requestDisplaySetCreationForStudy'; + +export type HangingProtocolParams = { + protocolId?: string; + stageIndex?: number; + activeStudyUID?: string; + stageId?: string; + reset?: false; +}; + +export type UpdateViewportDisplaySetParams = { + direction: number; + excludeNonImageModalities?: boolean; +}; + +const commandsModule = ({ + servicesManager, + commandsManager, + extensionManager, +}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => { + const { + customizationService, + measurementService, + hangingProtocolService, + uiNotificationService, + viewportGridService, + displaySetService, + multiMonitorService, + } = servicesManager.services; + + // Define a context menu controller for use with any context menus + const contextMenuController = new ContextMenuController(servicesManager, commandsManager); + + const actions = { + /** + * Runs a command in multi-monitor mode. No-op if not multi-monitor. + */ + multimonitor: async options => { + const { screenDelta, StudyInstanceUID, commands, hashParams } = options; + if (multiMonitorService.numberOfScreens < 2) { + return options.fallback?.(options); + } + + const newWindow = await multiMonitorService.launchWindow( + StudyInstanceUID, + screenDelta, + hashParams + ); + + // Only run commands if we successfully got a window with a commands manager + if (newWindow && commands) { + // Todo: fix this properly, but it takes time for the new window to load + // and then the commandsManager is available for it + setTimeout(() => { + multiMonitorService.run(screenDelta, commands, options); + }, 1000); + } + }, + + /** + * Ensures that the specified study is available for display + * Then, if commands is specified, runs the given commands list/instance + */ + loadStudy: async options => { + const { StudyInstanceUID } = options; + const displaySets = displaySetService.getActiveDisplaySets(); + const isActive = displaySets.find(ds => ds.StudyInstanceUID === StudyInstanceUID); + if (isActive) { + return; + } + const [dataSource] = extensionManager.getActiveDataSource(); + await requestDisplaySetCreationForStudy(dataSource, displaySetService, StudyInstanceUID); + + const study = DicomMetadataStore.getStudy(StudyInstanceUID); + hangingProtocolService.addStudy(study); + }, + + /** + * Show the context menu. + * @param options.menuId defines the menu name to lookup, from customizationService + * @param options.defaultMenu contains the default menu set to use + * @param options.element is the element to show the menu within + * @param options.event is the event that caused the context menu + * @param options.selectorProps is the set of selection properties to use + */ + showContextMenu: (options: ContextMenuProps) => { + const { + menuCustomizationId, + element, + event, + selectorProps, + defaultPointsPosition = [], + } = options; + + const optionsToUse = { ...options }; + + if (menuCustomizationId) { + Object.assign(optionsToUse, customizationService.getCustomization(menuCustomizationId)); + } + + // TODO - make the selectorProps richer by including the study metadata and display set. + const { protocol, stage } = hangingProtocolService.getActiveProtocol(); + optionsToUse.selectorProps = { + event, + protocol, + stage, + ...selectorProps, + }; + + contextMenuController.showContextMenu(optionsToUse, element, defaultPointsPosition); + }, + + /** Close a context menu currently displayed */ + closeContextMenu: () => { + contextMenuController.closeContextMenu(); + }, + + displayNotification: ({ text, title, type }) => { + uiNotificationService.show({ + title: title, + message: text, + type: type, + }); + }, + clearMeasurements: () => { + measurementService.clearMeasurements(); + }, + + /** + * Sets the specified protocol + * 1. Records any existing state using the viewport grid service + * 2. Finds the destination state - this can be one of: + * a. The specified protocol stage + * b. An alternate (toggled or restored) protocol stage + * c. A restored custom layout + * 3. Finds the parameters for the specified state + * a. Gets the displaySetSelectorMap + * b. Gets the map by position + * c. Gets any toggle mapping to map position to/from current view + * 4. If restore, then sets layout + * a. Maps viewport position by currently displayed viewport map id + * b. Uses toggle information to map display set id + * 5. Else applies the hanging protocol + * a. HP Service is provided displaySetSelectorMap + * b. HP Service will throw an exception if it isn't applicable + * @param options - contains information on the HP to apply + * @param options.activeStudyUID - the updated study to apply the HP to + * @param options.protocolId - the protocol ID to change to + * @param options.stageId - the stageId to apply + * @param options.stageIndex - the index of the stage to go to. + * @param options.reset - flag to indicate if the HP should be reset to its original and not restored to a previous state + * + * commandsManager.run('setHangingProtocol', { + * activeStudyUID: '1.2.3', + * protocolId: 'myProtocol', + * stageId: 'myStage', + * stageIndex: 0, + * reset: false, + * }); + */ + setHangingProtocol: ({ + activeStudyUID = '', + StudyInstanceUID = '', + protocolId, + stageId, + stageIndex, + reset = false, + }: HangingProtocolParams): boolean => { + const toUseStudyInstanceUID = activeStudyUID || StudyInstanceUID; + try { + // Stores in the state the display set selector id to displaySetUID mapping + // Pass in viewportId for the active viewport. This item will get set as + // the activeViewportId + const state = viewportGridService.getState(); + const hpInfo = hangingProtocolService.getState(); + reuseCachedLayouts(state, hangingProtocolService); + const { hangingProtocolStageIndexMap } = useHangingProtocolStageIndexStore.getState(); + const { displaySetSelectorMap } = useDisplaySetSelectorStore.getState(); + + if (!protocolId) { + // Reuse the previous protocol id, and optionally stage + protocolId = hpInfo.protocolId; + if (stageId === undefined && stageIndex === undefined) { + stageIndex = hpInfo.stageIndex; + } + } else if (stageIndex === undefined && stageId === undefined) { + // Re-set the same stage as was previously used + const hangingId = `${toUseStudyInstanceUID || hpInfo.activeStudyUID}:${protocolId}`; + stageIndex = hangingProtocolStageIndexMap[hangingId]?.stageIndex; + } + + const useStageIdx = + stageIndex ?? + hangingProtocolService.getStageIndex(protocolId, { + stageId, + stageIndex, + }); + + const activeStudyChanged = hangingProtocolService.setActiveStudyUID(toUseStudyInstanceUID); + + const storedHanging = `${toUseStudyInstanceUID || hangingProtocolService.getState().activeStudyUID}:${protocolId}:${ + useStageIdx || 0 + }`; + + const { viewportGridState } = useViewportGridStore.getState(); + const restoreProtocol = !reset && viewportGridState[storedHanging]; + + if ( + reset || + (activeStudyChanged && + !viewportGridState[storedHanging] && + stageIndex === undefined && + stageId === undefined) + ) { + // Run the hanging protocol fresh, re-using the existing study data + // This is done on reset or when the study changes and we haven't yet + // applied it, and don't specify exact stage to use. + const displaySets = displaySetService.getActiveDisplaySets(); + const activeStudy = { + StudyInstanceUID: toUseStudyInstanceUID, + displaySets, + }; + hangingProtocolService.run(activeStudy, protocolId); + } else if ( + protocolId === hpInfo.protocolId && + useStageIdx === hpInfo.stageIndex && + !toUseStudyInstanceUID + ) { + // Clear the HP setting to reset them + hangingProtocolService.setProtocol(protocolId, { + stageId, + stageIndex: useStageIdx, + }); + } else { + hangingProtocolService.setProtocol(protocolId, { + displaySetSelectorMap, + stageId, + stageIndex: useStageIdx, + restoreProtocol, + }); + if (restoreProtocol) { + viewportGridService.set(viewportGridState[storedHanging]); + } + } + // Do this after successfully applying the update + const { setDisplaySetSelector } = useDisplaySetSelectorStore.getState(); + setDisplaySetSelector( + `${toUseStudyInstanceUID || hpInfo.activeStudyUID}:activeDisplaySet:0`, + null + ); + return true; + } catch (e) { + console.error(e); + uiNotificationService.show({ + title: 'Apply Hanging Protocol', + message: 'The hanging protocol could not be applied.', + type: 'error', + duration: 3000, + }); + return false; + } + }, + + toggleHangingProtocol: ({ protocolId, stageIndex }: HangingProtocolParams): boolean => { + const { + protocol, + stageIndex: desiredStageIndex, + activeStudy, + } = hangingProtocolService.getActiveProtocol(); + const { toggleHangingProtocol, setToggleHangingProtocol } = + useToggleHangingProtocolStore.getState(); + const storedHanging = `${activeStudy.StudyInstanceUID}:${protocolId}:${stageIndex | 0}`; + if ( + protocol.id === protocolId && + (stageIndex === undefined || stageIndex === desiredStageIndex) + ) { + // Toggling off - restore to previous state + const previousState = toggleHangingProtocol[storedHanging] || { + protocolId: 'default', + }; + return actions.setHangingProtocol(previousState); + } else { + setToggleHangingProtocol(storedHanging, { + protocolId: protocol.id, + stageIndex: desiredStageIndex, + }); + return actions.setHangingProtocol({ + protocolId, + stageIndex, + reset: true, + }); + } + }, + + deltaStage: ({ direction }) => { + const { protocolId, stageIndex: oldStageIndex } = hangingProtocolService.getState(); + const { protocol } = hangingProtocolService.getActiveProtocol(); + for ( + let stageIndex = oldStageIndex + direction; + stageIndex >= 0 && stageIndex < protocol.stages.length; + stageIndex += direction + ) { + if (protocol.stages[stageIndex].status !== 'disabled') { + return actions.setHangingProtocol({ + protocolId, + stageIndex, + }); + } + } + uiNotificationService.show({ + title: 'Change Stage', + message: 'The hanging protocol has no more applicable stages', + type: 'info', + duration: 3000, + }); + }, + + /** + * Changes the viewport grid layout in terms of the MxN layout. + */ + setViewportGridLayout: ({ numRows, numCols, isHangingProtocolLayout = false }) => { + const { protocol } = hangingProtocolService.getActiveProtocol(); + const onLayoutChange = protocol.callbacks?.onLayoutChange; + if (commandsManager.run(onLayoutChange, { numRows, numCols }) === false) { + console.log('setViewportGridLayout running', onLayoutChange, numRows, numCols); + // Don't apply the layout if the run command returns false + return; + } + + const completeLayout = () => { + const state = viewportGridService.getState(); + findViewportsByPosition(state, { numRows, numCols }); + + const { viewportsByPosition, initialInDisplay } = useViewportsByPositionStore.getState(); + + const findOrCreateViewport = layoutFindOrCreate.bind( + null, + hangingProtocolService, + isHangingProtocolLayout, + { ...viewportsByPosition, initialInDisplay } + ); + + viewportGridService.setLayout({ + numRows, + numCols, + findOrCreateViewport, + isHangingProtocolLayout, + }); + }; + // Need to finish any work in the callback + window.setTimeout(completeLayout, 0); + }, + + toggleOneUp() { + const viewportGridState = viewportGridService.getState(); + const { activeViewportId, viewports, layout, isHangingProtocolLayout } = viewportGridState; + const { displaySetInstanceUIDs, displaySetOptions, viewportOptions } = + viewports.get(activeViewportId); + + if (layout.numCols === 1 && layout.numRows === 1) { + // The viewer is in one-up. Check if there is a state to restore/toggle back to. + const { toggleOneUpViewportGridStore } = useToggleOneUpViewportGridStore.getState(); + + if (!toggleOneUpViewportGridStore) { + return; + } + // There is a state to toggle back to. The viewport that was + // originally toggled to one up was the former active viewport. + const viewportIdToUpdate = toggleOneUpViewportGridStore.activeViewportId; + + // We are restoring the previous layout but taking into the account that + // the current one up viewport might have a new displaySet dragged and dropped on it. + // updatedViewportsViaHP below contains the viewports applicable to the HP that existed + // prior to the toggle to one-up - including the updated viewports if a display + // set swap were to have occurred. + const updatedViewportsViaHP = + displaySetInstanceUIDs.length > 1 + ? [] + : displaySetInstanceUIDs + .map(displaySetInstanceUID => + hangingProtocolService.getViewportsRequireUpdate( + viewportIdToUpdate, + displaySetInstanceUID, + isHangingProtocolLayout + ) + ) + .flat(); + + // findOrCreateViewport returns either one of the updatedViewportsViaHP + // returned from the HP service OR if there is not one from the HP service then + // simply returns what was in the previous state for a given position in the layout. + const findOrCreateViewport = (position: number, positionId: string) => { + // Find the viewport for the given position prior to the toggle to one-up. + const preOneUpViewport = Array.from(toggleOneUpViewportGridStore.viewports.values()).find( + viewport => viewport.positionId === positionId + ); + + // Use the viewport id from before the toggle to one-up to find any updates to the viewport. + const viewport = updatedViewportsViaHP.find( + viewport => viewport.viewportId === preOneUpViewport.viewportId + ); + + return viewport + ? // Use the applicable viewport from the HP updated viewports + { viewportOptions, displaySetOptions, ...viewport } + : // Use the previous viewport for the given position + preOneUpViewport; + }; + + const layoutOptions = viewportGridService.getLayoutOptionsFromState( + toggleOneUpViewportGridStore + ); + + // Restore the previous layout including the active viewport. + viewportGridService.setLayout({ + numRows: toggleOneUpViewportGridStore.layout.numRows, + numCols: toggleOneUpViewportGridStore.layout.numCols, + activeViewportId: viewportIdToUpdate, + layoutOptions, + findOrCreateViewport, + isHangingProtocolLayout: true, + }); + + // Reset crosshairs after restoring the layout + setTimeout(() => { + commandsManager.runCommand('resetCrosshairs'); + }, 0); + } else { + // We are not in one-up, so toggle to one up. + + // Store the current viewport grid state so we can toggle it back later. + const { setToggleOneUpViewportGridStore } = useToggleOneUpViewportGridStore.getState(); + setToggleOneUpViewportGridStore(viewportGridState); + + // one being toggled to one up. + const findOrCreateViewport = () => { + return { + displaySetInstanceUIDs, + displaySetOptions, + viewportOptions, + }; + }; + + // Set the layout to be 1x1/one-up. + viewportGridService.setLayout({ + numRows: 1, + numCols: 1, + findOrCreateViewport, + isHangingProtocolLayout: true, + }); + } + }, + + /** + * Exposes the browser history navigation used by OHIF. This command can be used to either replace or + * push a new entry into the browser history. For example, the following will replace the current + * browser history entry with the specified relative URL which changes the study displayed to the + * study with study instance UID 1.2.3. Note that as a result of using `options.replace = true`, the + * page prior to invoking this command cannot be returned to via the browser back button. + * + * navigateHistory({ + * to: 'viewer?StudyInstanceUIDs=1.2.3', + * options: { replace: true }, + * }); + * + * @param historyArgs - arguments for the history function; + * the `to` property is the URL; + * the `options.replace` is a boolean indicating if the current browser history entry + * should be replaced or a new entry pushed onto the history (stack); the default value + * for `replace` is false + */ + navigateHistory(historyArgs: NavigateHistory) { + history.navigate(historyArgs.to, historyArgs.options); + }, + + openDICOMTagViewer({ displaySetInstanceUID }: { displaySetInstanceUID?: string }) { + const { activeViewportId, viewports } = viewportGridService.getState(); + const activeViewportSpecificData = viewports.get(activeViewportId); + const { displaySetInstanceUIDs } = activeViewportSpecificData; + + const displaySets = displaySetService.activeDisplaySets; + const { UIModalService } = servicesManager.services; + + const defaultDisplaySetInstanceUID = displaySetInstanceUID || displaySetInstanceUIDs[0]; + UIModalService.show({ + content: DicomTagBrowser, + contentProps: { + displaySets, + displaySetInstanceUID: defaultDisplaySetInstanceUID, + onClose: UIModalService.hide, + }, + containerDimensions: 'w-[70%] max-w-[900px]', + title: 'DICOM Tag Browser', + }); + }, + + /** + * Toggle viewport overlay (the information panel shown on the four corners + * of the viewport) + * @see ViewportOverlay and CustomizableViewportOverlay components + */ + toggleOverlays: () => { + const overlays = document.getElementsByClassName('viewport-overlay'); + for (let i = 0; i < overlays.length; i++) { + overlays.item(i).classList.toggle('hidden'); + } + }, + + scrollActiveThumbnailIntoView: () => { + const { activeViewportId, viewports } = viewportGridService.getState(); + + const activeViewport = viewports.get(activeViewportId); + const activeDisplaySetInstanceUID = activeViewport.displaySetInstanceUIDs[0]; + + const thumbnailList = document.querySelector('#ohif-thumbnail-list'); + + if (!thumbnailList) { + return; + } + + const thumbnailListBounds = thumbnailList.getBoundingClientRect(); + + const thumbnail = document.querySelector(`#thumbnail-${activeDisplaySetInstanceUID}`); + + if (!thumbnail) { + return; + } + + const thumbnailBounds = thumbnail.getBoundingClientRect(); + + // This only handles a vertical thumbnail list. + if ( + thumbnailBounds.top >= thumbnailListBounds.top && + thumbnailBounds.top <= thumbnailListBounds.bottom + ) { + return; + } + + thumbnail.scrollIntoView({ behavior: 'smooth' }); + }, + + updateViewportDisplaySet: ({ + direction, + excludeNonImageModalities, + }: UpdateViewportDisplaySetParams) => { + const nonImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE']; + + const currentDisplaySets = [...displaySetService.activeDisplaySets]; + + const { activeViewportId, viewports, isHangingProtocolLayout } = + viewportGridService.getState(); + + const { displaySetInstanceUIDs } = viewports.get(activeViewportId); + + const activeDisplaySetIndex = currentDisplaySets.findIndex(displaySet => + displaySetInstanceUIDs.includes(displaySet.displaySetInstanceUID) + ); + + let displaySetIndexToShow: number; + + for ( + displaySetIndexToShow = activeDisplaySetIndex + direction; + displaySetIndexToShow > -1 && displaySetIndexToShow < currentDisplaySets.length; + displaySetIndexToShow += direction + ) { + if ( + !excludeNonImageModalities || + !nonImageModalities.includes(currentDisplaySets[displaySetIndexToShow].Modality) + ) { + break; + } + } + + if (displaySetIndexToShow < 0 || displaySetIndexToShow >= currentDisplaySets.length) { + return; + } + + const { displaySetInstanceUID } = currentDisplaySets[displaySetIndexToShow]; + + let updatedViewports = []; + + try { + updatedViewports = hangingProtocolService.getViewportsRequireUpdate( + activeViewportId, + displaySetInstanceUID, + isHangingProtocolLayout + ); + } catch (error) { + console.warn(error); + uiNotificationService.show({ + title: 'Navigate Viewport Display Set', + message: + 'The requested display sets could not be added to the viewport due to a mismatch in the Hanging Protocol rules.', + type: 'info', + duration: 3000, + }); + } + + viewportGridService.setDisplaySetsForViewports(updatedViewports); + + setTimeout(() => actions.scrollActiveThumbnailIntoView(), 0); + }, + }; + + const definitions = { + multimonitor: actions.multimonitor, + loadStudy: actions.loadStudy, + showContextMenu: actions.showContextMenu, + closeContextMenu: actions.closeContextMenu, + clearMeasurements: actions.clearMeasurements, + displayNotification: actions.displayNotification, + setHangingProtocol: actions.setHangingProtocol, + toggleHangingProtocol: actions.toggleHangingProtocol, + navigateHistory: actions.navigateHistory, + nextStage: { + commandFn: actions.deltaStage, + options: { direction: 1 }, + }, + previousStage: { + commandFn: actions.deltaStage, + options: { direction: -1 }, + }, + setViewportGridLayout: actions.setViewportGridLayout, + toggleOneUp: actions.toggleOneUp, + openDICOMTagViewer: actions.openDICOMTagViewer, + updateViewportDisplaySet: actions.updateViewportDisplaySet, + }; + + return { + actions, + definitions, + defaultContext: 'DEFAULT', + }; +}; + +export default commandsModule; diff --git a/extensions/default/src/customizations/contextMenuCustomization.ts b/extensions/default/src/customizations/contextMenuCustomization.ts new file mode 100644 index 0000000..b19232b --- /dev/null +++ b/extensions/default/src/customizations/contextMenuCustomization.ts @@ -0,0 +1,25 @@ +import { CustomizationService } from '@ohif/core'; + +export default { + 'ohif.contextMenu': { + $transform: function (customizationService: CustomizationService) { + /** + * Applies the inheritsFrom to all the menu items. + * This function clones the object and child objects to prevent + * changes to the original customization object. + */ + // Don't modify the children, as those are copied by reference + const clonedObject = { ...this }; + clonedObject.menus = this.menus.map(menu => ({ ...menu })); + + for (const menu of clonedObject.menus) { + const { items: originalItems } = menu; + menu.items = []; + for (const item of originalItems) { + menu.items.push(customizationService.transform(item)); + } + } + return clonedObject; + }, + }, +}; diff --git a/extensions/default/src/customizations/contextMenuUICustomization.ts b/extensions/default/src/customizations/contextMenuUICustomization.ts new file mode 100644 index 0000000..4366df8 --- /dev/null +++ b/extensions/default/src/customizations/contextMenuUICustomization.ts @@ -0,0 +1,5 @@ +import { ContextMenu } from '@ohif/ui'; + +export default { + 'ui.contextMenu': ContextMenu, +}; diff --git a/extensions/default/src/customizations/customRoutesCustomization.ts b/extensions/default/src/customizations/customRoutesCustomization.ts new file mode 100644 index 0000000..c2195ce --- /dev/null +++ b/extensions/default/src/customizations/customRoutesCustomization.ts @@ -0,0 +1,6 @@ +export default { + 'routes.customRoutes': { + routes: [], + notFoundRoute: null, + }, +}; diff --git a/extensions/default/src/customizations/dataSourceConfigurationCustomization.ts b/extensions/default/src/customizations/dataSourceConfigurationCustomization.ts new file mode 100644 index 0000000..0edc17b --- /dev/null +++ b/extensions/default/src/customizations/dataSourceConfigurationCustomization.ts @@ -0,0 +1,19 @@ +import DataSourceConfigurationComponent from '../Components/DataSourceConfigurationComponent'; +import { GoogleCloudDataSourceConfigurationAPI } from '../DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI'; + +export default function getDataSourceConfigurationCustomization({ + servicesManager, + extensionManager, +}) { + return { + // the generic GUI component to configure a data source using an instance of a BaseDataSourceConfigurationAPI + 'ohif.dataSourceConfigurationComponent': DataSourceConfigurationComponent.bind(null, { + servicesManager, + extensionManager, + }), + + // The factory for creating an instance of a BaseDataSourceConfigurationAPI for Google Cloud Healthcare + 'ohif.dataSourceConfigurationAPI.google': (dataSourceName: string) => + new GoogleCloudDataSourceConfigurationAPI(dataSourceName, servicesManager, extensionManager), + }; +} diff --git a/extensions/default/src/customizations/datasourcesCustomization.tsx b/extensions/default/src/customizations/datasourcesCustomization.tsx new file mode 100644 index 0000000..ad59ac5 --- /dev/null +++ b/extensions/default/src/customizations/datasourcesCustomization.tsx @@ -0,0 +1,14 @@ +import DataSourceSelector from '../Panels/DataSourceSelector'; + +export default { + 'routes.customRoutes': { + routes: { + $push: [ + { + path: '/datasources', + children: DataSourceSelector, + }, + ], + }, + }, +}; diff --git a/extensions/default/src/customizations/defaultContextMenuCustomization.ts b/extensions/default/src/customizations/defaultContextMenuCustomization.ts new file mode 100644 index 0000000..daa9090 --- /dev/null +++ b/extensions/default/src/customizations/defaultContextMenuCustomization.ts @@ -0,0 +1,22 @@ +export default { + measurementsContextMenu: { + inheritsFrom: 'ohif.contextMenu', + menus: [ + // Get the items from the UI Customization for the menu name (and have a custom name) + { + id: 'forExistingMeasurement', + selector: ({ nearbyToolData }) => !!nearbyToolData, + items: [ + { + label: 'Delete measurement', + commands: 'deleteMeasurement', + }, + { + label: 'Add Label', + commands: 'setMeasurementLabel', + }, + ], + }, + ], + }, +}; diff --git a/extensions/default/src/customizations/helloPageCustomization.tsx b/extensions/default/src/customizations/helloPageCustomization.tsx new file mode 100644 index 0000000..2eb32a9 --- /dev/null +++ b/extensions/default/src/customizations/helloPageCustomization.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +export default { + 'routes.customRoutes': { + routes: { + $push: [ + { + path: '/custom', + children: () =>

Hello Custom Route

, + }, + ], + }, + }, +}; diff --git a/extensions/default/src/customizations/hotkeyBindingsCustomization.ts b/extensions/default/src/customizations/hotkeyBindingsCustomization.ts new file mode 100644 index 0000000..a077956 --- /dev/null +++ b/extensions/default/src/customizations/hotkeyBindingsCustomization.ts @@ -0,0 +1,5 @@ +import { defaults } from '@ohif/core'; + +export default { + 'ohif.hotkeyBindings': defaults.hotkeyBindings, +}; diff --git a/extensions/default/src/customizations/labellingFlowCustomization.tsx b/extensions/default/src/customizations/labellingFlowCustomization.tsx new file mode 100644 index 0000000..00f97f8 --- /dev/null +++ b/extensions/default/src/customizations/labellingFlowCustomization.tsx @@ -0,0 +1,5 @@ +import { LabellingFlow } from '@ohif/ui'; + +export default { + 'ui.labellingComponent': LabellingFlow, +}; diff --git a/extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx b/extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx new file mode 100644 index 0000000..9bba0cc --- /dev/null +++ b/extensions/default/src/customizations/loadingIndicatorProgressCustomization.tsx @@ -0,0 +1,5 @@ +import { LoadingIndicatorProgress } from '@ohif/ui'; + +export default { + 'ui.loadingIndicatorProgress': LoadingIndicatorProgress, +}; diff --git a/extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx b/extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx new file mode 100644 index 0000000..368834f --- /dev/null +++ b/extensions/default/src/customizations/loadingIndicatorTotalPercentCustomization.tsx @@ -0,0 +1,5 @@ +import { LoadingIndicatorTotalPercent } from '@ohif/ui'; + +export default { + 'ui.loadingIndicatorTotalPercent': LoadingIndicatorTotalPercent, +}; diff --git a/extensions/default/src/customizations/menuContentCustomization.tsx b/extensions/default/src/customizations/menuContentCustomization.tsx new file mode 100644 index 0000000..bec11be --- /dev/null +++ b/extensions/default/src/customizations/menuContentCustomization.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuPortal, + DropdownMenuSubContent, + DropdownMenuItem, + Icons, +} from '@ohif/ui-next'; + +export default { + 'ohif.menuContent': function (props) { + const { item: topLevelItem, commandsManager, servicesManager, ...rest } = props; + + const content = function (subProps) { + const { item: subItem } = subProps; + + // Regular menu item + const isDisabled = subItem.selector && !subItem.selector({ servicesManager }); + + return ( + { + commandsManager.runAsync(subItem.commands, { + ...subItem.commandOptions, + ...rest, + }); + }} + className="gap-[6px]" + > + {subItem.iconName && ( + + )} + {subItem.label} + + ); + }; + + // If item has sub-items, render a submenu + if (topLevelItem.items) { + return ( + + + {topLevelItem.iconName && ( + + )} + {topLevelItem.label} + + + + {topLevelItem.items.map(subItem => content({ ...props, item: subItem }))} + + + + ); + } + + return content({ ...props, item: topLevelItem }); + }, +}; diff --git a/extensions/default/src/customizations/multimonitorCustomization.ts b/extensions/default/src/customizations/multimonitorCustomization.ts new file mode 100644 index 0000000..3fb0af3 --- /dev/null +++ b/extensions/default/src/customizations/multimonitorCustomization.ts @@ -0,0 +1,63 @@ +export default { + 'studyBrowser.studyMenuItems': { + $push: [ + { + id: 'applyHangingProtocol', + label: 'Apply Hanging Protocol', + iconName: 'ViewportViews', + items: [ + { + id: 'applyDefaultProtocol', + label: 'Default', + commands: [ + 'loadStudy', + { + commandName: 'setHangingProtocol', + commandOptions: { + protocolId: 'default', + }, + }, + ], + }, + { + id: 'applyMPRProtocol', + label: '2x2 Grid', + commands: [ + 'loadStudy', + { + commandName: 'setHangingProtocol', + commandOptions: { + protocolId: '@ohif/mnGrid', + }, + }, + ], + }, + ], + }, + { + id: 'showInOtherMonitor', + label: 'Launch On Second Monitor', + iconName: 'DicomTagBrowser', + selector: ({ servicesManager }) => { + const { multiMonitorService } = servicesManager.services; + return multiMonitorService.isMultimonitor; + }, + commands: { + commandName: 'multimonitor', + commandOptions: { + hashParams: '&hangingProtocolId=@ohif/mnGrid8', + commands: [ + 'loadStudy', + { + commandName: 'setHangingProtocol', + commandOptions: { + protocolId: '@ohif/mnGrid8', + }, + }, + ], + }, + }, + }, + ], + }, +}; diff --git a/extensions/default/src/customizations/notificationCustomization.ts b/extensions/default/src/customizations/notificationCustomization.ts new file mode 100644 index 0000000..6bff2de --- /dev/null +++ b/extensions/default/src/customizations/notificationCustomization.ts @@ -0,0 +1,5 @@ +import { Notification } from '@ohif/ui'; + +export default { + 'ui.notificationComponent': Notification, +}; diff --git a/extensions/default/src/customizations/onDropHandlerCustomization.ts b/extensions/default/src/customizations/onDropHandlerCustomization.ts new file mode 100644 index 0000000..42a83bc --- /dev/null +++ b/extensions/default/src/customizations/onDropHandlerCustomization.ts @@ -0,0 +1,5 @@ +export default { + customOnDropHandler: () => { + return Promise.resolve({ handled: false }); + }, +}; diff --git a/extensions/default/src/customizations/onboardingCustomization.ts b/extensions/default/src/customizations/onboardingCustomization.ts new file mode 100644 index 0000000..225743f --- /dev/null +++ b/extensions/default/src/customizations/onboardingCustomization.ts @@ -0,0 +1,210 @@ +function waitForElement(selector, maxAttempts = 20, interval = 25) { + return new Promise(resolve => { + let attempts = 0; + + const checkForElement = setInterval(() => { + const element = document.querySelector(selector); + + if (element || attempts >= maxAttempts) { + clearInterval(checkForElement); + resolve(); + } + + attempts++; + }, interval); + }); +} + +export default { + 'ohif.tours': [ + { + id: 'basicViewerTour', + route: '/viewer', + steps: [ + { + id: 'scroll', + title: 'Scrolling Through Images', + text: 'You can scroll through the images using the mouse wheel or scrollbar.', + attachTo: { + element: '.viewport-element', + on: 'top', + }, + advanceOn: { + selector: '.cornerstone-viewport-element', + event: 'CORNERSTONE_TOOLS_MOUSE_WHEEL', + }, + beforeShowPromise: () => waitForElement('.viewport-element'), + }, + { + id: 'zoom', + title: 'Zooming In and Out', + text: 'You can zoom the images using the right click.', + attachTo: { + element: '.viewport-element', + on: 'left', + }, + advanceOn: { + selector: '.cornerstone-viewport-element', + event: 'CORNERSTONE_TOOLS_MOUSE_UP', + }, + beforeShowPromise: () => waitForElement('.viewport-element'), + }, + { + id: 'pan', + title: 'Panning the Image', + text: 'You can pan the images using the middle click.', + attachTo: { + element: '.viewport-element', + on: 'top', + }, + advanceOn: { + selector: '.cornerstone-viewport-element', + event: 'CORNERSTONE_TOOLS_MOUSE_UP', + }, + beforeShowPromise: () => waitForElement('.viewport-element'), + }, + { + id: 'windowing', + title: 'Adjusting Window Level', + text: 'You can modify the window level using the left click.', + attachTo: { + element: '.viewport-element', + on: 'left', + }, + advanceOn: { + selector: '.cornerstone-viewport-element', + event: 'CORNERSTONE_TOOLS_MOUSE_UP', + }, + beforeShowPromise: () => waitForElement('.viewport-element'), + }, + { + id: 'length', + title: 'Using the Measurement Tools', + text: 'You can measure the length of a region using the Length tool.', + attachTo: { + element: '[data-cy="MeasurementTools-split-button-primary"]', + on: 'bottom', + }, + advanceOn: { + selector: '[data-cy="MeasurementTools-split-button-primary"]', + event: 'click', + }, + beforeShowPromise: () => + waitForElement('[data-cy="MeasurementTools-split-button-primary]'), + }, + { + id: 'drawAnnotation', + title: 'Drawing Length Annotations', + text: 'Use the length tool on the viewport to measure the length of a region.', + attachTo: { + element: '.viewport-element', + on: 'right', + }, + advanceOn: { + selector: 'body', + event: 'event::measurement_added', + }, + beforeShowPromise: () => waitForElement('.viewport-element'), + }, + { + id: 'trackMeasurement', + title: 'Tracking Measurements in the Panel', + text: 'Click yes to track the measurements in the measurement panel.', + attachTo: { + element: '[data-cy="prompt-begin-tracking-yes-btn"]', + on: 'bottom', + }, + advanceOn: { + selector: '[data-cy="prompt-begin-tracking-yes-btn"]', + event: 'click', + }, + beforeShowPromise: () => waitForElement('[data-cy="prompt-begin-tracking-yes-btn"]'), + }, + { + id: 'openMeasurementPanel', + title: 'Opening the Measurements Panel', + text: 'Click the measurements button to open the measurements panel.', + attachTo: { + element: '#trackedMeasurements-btn', + on: 'left-start', + }, + advanceOn: { + selector: '#trackedMeasurements-btn', + event: 'click', + }, + beforeShowPromise: () => waitForElement('#trackedMeasurements-btn'), + }, + { + id: 'scrollAwayFromMeasurement', + title: 'Scrolling Away from a Measurement', + text: 'Scroll the images using the mouse wheel away from the measurement.', + attachTo: { + element: '.viewport-element', + on: 'left', + }, + advanceOn: { + selector: '.cornerstone-viewport-element', + event: 'CORNERSTONE_TOOLS_MOUSE_WHEEL', + }, + beforeShowPromise: () => waitForElement('.viewport-element'), + }, + { + id: 'jumpToMeasurement', + title: 'Jumping to Measurements in the Panel', + text: 'Click the measurement in the measurement panel to jump to it.', + attachTo: { + element: '[data-cy="data-row"]', + on: 'left-start', + }, + advanceOn: { + selector: '[data-cy="data-row"]', + event: 'click', + }, + beforeShowPromise: () => waitForElement('[data-cy="data-row"]'), + }, + { + id: 'changeLayout', + title: 'Changing Layout', + text: 'You can change the layout of the viewer using the layout button.', + attachTo: { + element: '[data-cy="Layout"]', + on: 'bottom', + }, + advanceOn: { + selector: '[data-cy="Layout"]', + event: 'click', + }, + beforeShowPromise: () => waitForElement('[data-cy="Layout"]'), + }, + { + id: 'selectLayout', + title: 'Selecting the MPR Layout', + text: 'Select the MPR layout to view the images in MPR mode.', + attachTo: { + element: '[data-cy="MPR"]', + on: 'left-start', + }, + advanceOn: { + selector: '[data-cy="MPR"]', + event: 'click', + }, + beforeShowPromise: () => waitForElement('[data-cy="MPR"]'), + }, + ], + tourOptions: { + useModalOverlay: true, + defaultStepOptions: { + buttons: [ + { + text: 'Skip all', + action() { + this.complete(); + }, + secondary: true, + }, + ], + }, + }, + }, + ], +}; diff --git a/extensions/default/src/customizations/overlayItemCustomization.tsx b/extensions/default/src/customizations/overlayItemCustomization.tsx new file mode 100644 index 0000000..6e72827 --- /dev/null +++ b/extensions/default/src/customizations/overlayItemCustomization.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +export default { + 'ohif.overlayItem': function (props) { + if (this.condition && !this.condition(props)) { + return null; + } + + const { instance } = props; + const value = + instance && this.attribute + ? instance[this.attribute] + : this.contentF && typeof this.contentF === 'function' + ? this.contentF(props) + : null; + if (!value) { + return null; + } + + return ( + + {this.label && {this.label}} + {value} + + ); + }, +}; diff --git a/extensions/default/src/customizations/progressDropdownCustomization.ts b/extensions/default/src/customizations/progressDropdownCustomization.ts new file mode 100644 index 0000000..5f0d8e2 --- /dev/null +++ b/extensions/default/src/customizations/progressDropdownCustomization.ts @@ -0,0 +1,5 @@ +import { ProgressDropdownWithService } from '../Components/ProgressDropdownWithService'; + +export default { + progressDropdownWithServiceComponent: ProgressDropdownWithService, +}; diff --git a/extensions/default/src/customizations/progressLoadingBarCustomization.tsx b/extensions/default/src/customizations/progressLoadingBarCustomization.tsx new file mode 100644 index 0000000..d4de509 --- /dev/null +++ b/extensions/default/src/customizations/progressLoadingBarCustomization.tsx @@ -0,0 +1,5 @@ +import { ProgressLoadingBar } from '@ohif/ui'; + +export default { + 'ui.progressLoadingBar': ProgressLoadingBar, +}; diff --git a/extensions/default/src/customizations/sortingCriteriaCustomization.ts b/extensions/default/src/customizations/sortingCriteriaCustomization.ts new file mode 100644 index 0000000..b42ac03 --- /dev/null +++ b/extensions/default/src/customizations/sortingCriteriaCustomization.ts @@ -0,0 +1,7 @@ +import { utils } from '@ohif/core'; + +const { sortingCriteria } = utils; + +export default { + sortingCriteria: sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria, +}; diff --git a/extensions/default/src/customizations/studyBrowserCustomization.ts b/extensions/default/src/customizations/studyBrowserCustomization.ts new file mode 100644 index 0000000..238cc28 --- /dev/null +++ b/extensions/default/src/customizations/studyBrowserCustomization.ts @@ -0,0 +1,47 @@ +import { utils } from '@ohif/core'; +const { formatDate } = utils; + +export default { + 'studyBrowser.studyMenuItems': [], + 'studyBrowser.thumbnailMenuItems': [ + { + id: 'tagBrowser', + label: 'Tag Browser', + iconName: 'DicomTagBrowser', + onClick: ({ commandsManager, displaySetInstanceUID }: withAppTypes) => { + commandsManager.runCommand('openDICOMTagViewer', { + displaySetInstanceUID, + }); + }, + }, + ], + 'studyBrowser.sortFunctions': [ + { + label: 'Series Number', + sortFunction: (a, b) => { + return a?.SeriesNumber - b?.SeriesNumber; + }, + }, + { + label: 'Series Date', + sortFunction: (a, b) => { + const dateA = new Date(formatDate(a?.SeriesDate)); + const dateB = new Date(formatDate(b?.SeriesDate)); + return dateB.getTime() - dateA.getTime(); + }, + }, + ], + 'studyBrowser.viewPresets': [ + { + id: 'list', + iconName: 'ListView', + selected: false, + }, + { + id: 'thumbnails', + iconName: 'ThumbnailView', + selected: true, + }, + ], + 'studyBrowser.studyMode': 'all', +}; diff --git a/extensions/default/src/customizations/viewportActionCornersCustomization.ts b/extensions/default/src/customizations/viewportActionCornersCustomization.ts new file mode 100644 index 0000000..49b28bb --- /dev/null +++ b/extensions/default/src/customizations/viewportActionCornersCustomization.ts @@ -0,0 +1,5 @@ +import { ViewportActionCorners } from '@ohif/ui'; + +export default { + 'ui.viewportActionCorner': ViewportActionCorners, +}; diff --git a/extensions/default/src/findViewportsByPosition.ts b/extensions/default/src/findViewportsByPosition.ts new file mode 100644 index 0000000..87ff955 --- /dev/null +++ b/extensions/default/src/findViewportsByPosition.ts @@ -0,0 +1,102 @@ +import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore'; + +/** + * This find or create viewport is paired with the reduce results from + * below, and the action of this viewport is to look for previously filled + * viewports, and to reuse by position id. If there is no filled viewport, + * then one can be re-used from the display set if it isn't going to be displayed. + * @param hangingProtocolService - bound parameter supplied before using this + * @param viewportsByPosition - bound parameter supplied before using this + * @param position - the position in the grid to retrieve + * @param positionId - the current position on screen to retrieve + * @param options - the set of options used, so that subsequent calls can + * store state that is reset by the setLayout. + * This class uses the options to store the already viewed + * display sets, filling it initially with the pre-existing viewports. + */ +export const findOrCreateViewport = ( + hangingProtocolService, + isHangingProtocolLayout, + viewportsByPosition, + position: number, + positionId: string, + options: Record +) => { + const byPositionViewport = viewportsByPosition?.[positionId]; + if (byPositionViewport) { + return { ...byPositionViewport }; + } + const { protocolId, stageIndex } = hangingProtocolService.getState(); + + // Setup the initial in display correctly for initial view/select + if (!options.inDisplay) { + options.inDisplay = [...viewportsByPosition.initialInDisplay]; + } + + // See if there is a default viewport for new views + const missing = hangingProtocolService.getMissingViewport( + isHangingProtocolLayout ? protocolId : 'default', + stageIndex, + options + ); + if (missing) { + const displaySetInstanceUIDs = missing.displaySetsInfo.map(it => it.displaySetInstanceUID); + options.inDisplay.push(...displaySetInstanceUIDs); + return { + displaySetInstanceUIDs, + displaySetOptions: missing.displaySetsInfo.map(it => it.displaySetOptions), + viewportOptions: { + ...missing.viewportOptions, + }, + }; + } + + // and lastly if there is no default viewport, then we see if we can grab the + // viewportsByPosition at the position index and use that + // const candidate = Object.values(viewportsByPosition)[position]; + + // // if it has something to display, then we can use it + // return candidate?.displaySetInstanceUIDs ? candidate : {}; + return {}; +}; + +/** + * Records the information on what viewports are displayed in which position. + * Also records what instances from the existing positions are going to be in + * view initially. + * @param state is the viewport grid state + * @param syncService is the state sync service to use for getting existing state + * @returns Set of states that can be applied to the state sync to remember + * the current view state. + */ +const findViewportsByPosition = (state, { numRows, numCols }) => { + const { viewports } = state; + const { setViewportsByPosition, addInitialInDisplay } = useViewportsByPositionStore.getState(); + const initialInDisplay = []; + + const viewportsByPosition = {}; + viewports.forEach(viewport => { + if (viewport.positionId) { + const storedViewport = { + ...viewport, + viewportOptions: { ...viewport.viewportOptions }, + }; + viewportsByPosition[viewport.positionId] = storedViewport; + setViewportsByPosition(viewport.positionId, storedViewport); + } + }); + + for (let row = 0; row < numRows; row++) { + for (let col = 0; col < numCols; col++) { + const positionId = `${col}-${row}`; + const viewport = viewportsByPosition[positionId]; + if (viewport?.displaySetInstanceUIDs) { + initialInDisplay.push(...viewport.displaySetInstanceUIDs); + } + } + } + + initialInDisplay.forEach(displaySetInstanceUID => addInitialInDisplay(displaySetInstanceUID)); +}; + +export default findViewportsByPosition; diff --git a/extensions/default/src/getCustomizationModule.tsx b/extensions/default/src/getCustomizationModule.tsx new file mode 100644 index 0000000..b202ece --- /dev/null +++ b/extensions/default/src/getCustomizationModule.tsx @@ -0,0 +1,71 @@ +import defaultContextMenuCustomization from './customizations/defaultContextMenuCustomization'; +import helloPageCustomization from './customizations/helloPageCustomization'; +import datasourcesCustomization from './customizations/datasourcesCustomization'; +import multimonitorCustomization from './customizations/multimonitorCustomization'; +import customRoutesCustomization from './customizations/customRoutesCustomization'; +import studyBrowserCustomization from './customizations/studyBrowserCustomization'; +import overlayItemCustomization from './customizations/overlayItemCustomization'; +import contextMenuCustomization from './customizations/contextMenuCustomization'; +import contextMenuUICustomization from './customizations/contextMenuUICustomization'; +import menuContentCustomization from './customizations/menuContentCustomization'; +import getDataSourceConfigurationCustomization from './customizations/dataSourceConfigurationCustomization'; +import progressDropdownCustomization from './customizations/progressDropdownCustomization'; +import sortingCriteriaCustomization from './customizations/sortingCriteriaCustomization'; +import onDropHandlerCustomization from './customizations/onDropHandlerCustomization'; +import loadingIndicatorProgressCustomization from './customizations/loadingIndicatorProgressCustomization'; +import loadingIndicatorTotalPercentCustomization from './customizations/loadingIndicatorTotalPercentCustomization'; +import progressLoadingBarCustomization from './customizations/progressLoadingBarCustomization'; +import viewportActionCornersCustomization from './customizations/viewportActionCornersCustomization'; +import labellingFlowCustomization from './customizations/labellingFlowCustomization'; +import viewportNotificationCustomization from './customizations/notificationCustomization'; +import hotkeyBindingsCustomization from './customizations/hotkeyBindingsCustomization'; +import onboardingCustomization from './customizations/onboardingCustomization'; +/** + * + * Note: this is an example of how the customization module can be used + * using the customization module. Below, we are adding a new custom route + * to the application at the path /custom and rendering a custom component + * Real world use cases of the having a custom route would be to add a + * custom page for the user to view their profile, or to add a custom + * page for login etc. + */ +export default function getCustomizationModule({ servicesManager, extensionManager }) { + return [ + { + name: 'helloPage', + value: helloPageCustomization, + }, + { + name: 'datasources', + value: datasourcesCustomization, + }, + { + name: 'multimonitor', + value: multimonitorCustomization, + }, + { + name: 'default', + value: { + ...customRoutesCustomization, + ...studyBrowserCustomization, + ...overlayItemCustomization, + ...contextMenuCustomization, + ...menuContentCustomization, + ...getDataSourceConfigurationCustomization({ servicesManager, extensionManager }), + ...progressDropdownCustomization, + ...sortingCriteriaCustomization, + ...defaultContextMenuCustomization, + ...onDropHandlerCustomization, + ...loadingIndicatorProgressCustomization, + ...loadingIndicatorTotalPercentCustomization, + ...progressLoadingBarCustomization, + ...viewportActionCornersCustomization, + ...labellingFlowCustomization, + ...contextMenuUICustomization, + ...viewportNotificationCustomization, + ...hotkeyBindingsCustomization, + ...onboardingCustomization, + }, + }, + ]; +} diff --git a/extensions/default/src/getDataSourcesModule.js b/extensions/default/src/getDataSourcesModule.js new file mode 100644 index 0000000..85ae239 --- /dev/null +++ b/extensions/default/src/getDataSourcesModule.js @@ -0,0 +1,44 @@ +// TODO: Pull in IWebClientApi from @ohif/core +// TODO: Use constructor to create an instance of IWebClientApi +// TODO: Use existing DICOMWeb configuration (previously, appConfig, to configure instance) + +import { createDicomWebApi } from './DicomWebDataSource/index'; +import { createDicomJSONApi } from './DicomJSONDataSource/index'; +import { createDicomLocalApi } from './DicomLocalDataSource/index'; +import { createDicomWebProxyApi } from './DicomWebProxyDataSource/index'; +import { createMergeDataSourceApi } from './MergeDataSource/index'; + +/** + * + */ +function getDataSourcesModule() { + return [ + { + name: 'dicomweb', + type: 'webApi', + createDataSource: createDicomWebApi, + }, + { + name: 'dicomwebproxy', + type: 'webApi', + createDataSource: createDicomWebProxyApi, + }, + { + name: 'dicomjson', + type: 'jsonApi', + createDataSource: createDicomJSONApi, + }, + { + name: 'dicomlocal', + type: 'localApi', + createDataSource: createDicomLocalApi, + }, + { + name: 'merge', + type: 'mergeApi', + createDataSource: createMergeDataSourceApi, + }, + ]; +} + +export default getDataSourcesModule; diff --git a/extensions/default/src/getDisplaySetMessages.ts b/extensions/default/src/getDisplaySetMessages.ts new file mode 100644 index 0000000..80bffe0 --- /dev/null +++ b/extensions/default/src/getDisplaySetMessages.ts @@ -0,0 +1,54 @@ +import sortInstancesByPosition from '@ohif/core/src/utils/sortInstancesByPosition'; +import { constructableModalities } from '@ohif/core/src/utils/isDisplaySetReconstructable'; +import { DisplaySetMessage, DisplaySetMessageList } from '@ohif/core'; +import checkMultiFrame from './utils/validations/checkMultiframe'; +import checkSingleFrames from './utils/validations/checkSingleFrames'; +/** + * Checks if a series is reconstructable to a 3D volume. + * + * @param {Object[]} instances An array of `OHIFInstanceMetadata` objects. + */ +export default function getDisplaySetMessages( + instances: Array, + isReconstructable: boolean, + isDynamicVolume: boolean +): DisplaySetMessageList { + const messages = new DisplaySetMessageList(); + + if (isDynamicVolume) { + return messages; + } + + if (!instances.length) { + messages.addMessage(DisplaySetMessage.CODES.NO_VALID_INSTANCES); + return; + } + + const firstInstance = instances[0]; + const { Modality, ImageType, NumberOfFrames } = firstInstance; + // Due to current requirements, LOCALIZER series doesn't have any messages + if (ImageType?.includes('LOCALIZER')) { + return messages; + } + + if (!constructableModalities.includes(Modality)) { + return messages; + } + + const isMultiframe = NumberOfFrames > 1; + // Can't reconstruct if all instances don't have the ImagePositionPatient. + if (!isMultiframe && !instances.every(instance => instance.ImagePositionPatient)) { + messages.addMessage(DisplaySetMessage.CODES.NO_POSITION_INFORMATION); + } + + const sortedInstances = sortInstancesByPosition(instances); + + isMultiframe + ? checkMultiFrame(sortedInstances[0], messages) + : checkSingleFrames(sortedInstances, messages); + + if (!isReconstructable) { + messages.addMessage(DisplaySetMessage.CODES.NOT_RECONSTRUCTABLE); + } + return messages; +} diff --git a/extensions/default/src/getDisplaySetsFromUnsupportedSeries.js b/extensions/default/src/getDisplaySetsFromUnsupportedSeries.js new file mode 100644 index 0000000..d2da6f5 --- /dev/null +++ b/extensions/default/src/getDisplaySetsFromUnsupportedSeries.js @@ -0,0 +1,30 @@ +import ImageSet from '@ohif/core/src/classes/ImageSet'; +import { DisplaySetMessage, DisplaySetMessageList } from '@ohif/core'; +/** + * Default handler for a instance list with an unsupported sopClassUID + */ +export default function getDisplaySetsFromUnsupportedSeries(instances) { + const imageSet = new ImageSet(instances); + const messages = new DisplaySetMessageList(); + messages.addMessage(DisplaySetMessage.CODES.UNSUPPORTED_DISPLAYSET); + const instance = instances[0]; + + imageSet.setAttributes({ + displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID + SeriesDate: instance.SeriesDate, + SeriesTime: instance.SeriesTime, + SeriesInstanceUID: instance.SeriesInstanceUID, + StudyInstanceUID: instance.StudyInstanceUID, + SeriesNumber: instance.SeriesNumber || 0, + FrameRate: instance.FrameTime, + SOPClassUID: instance.SOPClassUID, + SeriesDescription: instance.SeriesDescription || '', + Modality: instance.Modality, + numImageFrames: instances.length, + unsupported: true, + SOPClassHandlerId: 'unsupported', + isReconstructable: false, + messages, + }); + return [imageSet]; +} diff --git a/extensions/default/src/getHangingProtocolModule.js b/extensions/default/src/getHangingProtocolModule.js new file mode 100644 index 0000000..4ccd6e2 --- /dev/null +++ b/extensions/default/src/getHangingProtocolModule.js @@ -0,0 +1,155 @@ +import { hpMN, hpMN8 } from './hangingprotocols/hpMNGrid'; +import hpMNCompare from './hangingprotocols/hpCompare'; +import hpMammography from './hangingprotocols/hpMammo'; +import hpScale from './hangingprotocols/hpScale'; + +const defaultProtocol = { + id: 'default', + locked: true, + // Don't store this hanging protocol as it applies to the currently active + // display set by default + // cacheId: null, + name: 'Default', + createdDate: '2021-02-23T19:22:08.894Z', + modifiedDate: '2023-04-01', + availableTo: {}, + editableBy: {}, + protocolMatchingRules: [], + toolGroupIds: ['default'], + // -1 would be used to indicate active only, whereas other values are + // the number of required priors referenced - so 0 means active with + // 0 or more priors. + numberOfPriorsReferenced: 0, + // Default viewport is used to define the viewport when + // additional viewports are added using the layout tool + defaultViewport: { + viewportOptions: { + viewportType: 'stack', + toolGroupId: 'default', + allowUnmatchedView: true, + syncGroups: [ + { + type: 'hydrateseg', + id: 'sameFORId', + source: true, + target: true, + options: { + matchingRules: ['sameFOR'], + }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + matchedDisplaySetsIndex: -1, + }, + ], + }, + displaySetSelectors: { + defaultDisplaySetId: { + // Matches displaysets, NOT series + seriesMatchingRules: [ + // Try to match series with images by default, to prevent weird display + // on SEG/SR containing studies + { + weight: 10, + attribute: 'numImageFrames', + constraint: { + greaterThan: { value: 0 }, + }, + }, + // This display set will select the specified items by preference + // It has no affect if nothing is specified in the URL. + { + attribute: 'isDisplaySetFromUrl', + weight: 20, + constraint: { + equals: true, + }, + }, + ], + }, + }, + stages: [ + { + name: 'default', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 1, + }, + }, + viewports: [ + { + viewportOptions: { + viewportType: 'stack', + viewportId: 'default', + toolGroupId: 'default', + // This will specify the initial image options index if it matches in the URL + // and will otherwise not specify anything. + initialImageOptions: { + custom: 'sopInstanceLocation', + }, + // Other options for initialImageOptions, which can be included in the default + // custom attribute, or can be provided directly. + // index: 180, + // preset: 'middle', // 'first', 'last', 'middle' + // }, + syncGroups: [ + { + type: 'hydrateseg', + id: 'sameFORId', + source: true, + target: true, + // options: { + // matchingRules: ['sameFOR'], + // }, + }, + ], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + ], + createdDate: '2021-02-23T18:32:42.850Z', + }, + ], +}; + +function getHangingProtocolModule() { + return [ + { + name: defaultProtocol.id, + protocol: defaultProtocol, + }, + // Create a MxN comparison hanging protocol available by default + { + name: hpMNCompare.id, + protocol: hpMNCompare, + }, + { + name: hpMammography.id, + protocol: hpMammography, + }, + { + name: hpScale.id, + protocol: hpScale, + }, + // Create a MxN hanging protocol available by default + { + name: hpMN.id, + protocol: hpMN, + }, + { + name: hpMN8.id, + protocol: hpMN8, + }, + ]; +} + +export default getHangingProtocolModule; diff --git a/extensions/default/src/getLayoutTemplateModule.js b/extensions/default/src/getLayoutTemplateModule.js new file mode 100644 index 0000000..df89b10 --- /dev/null +++ b/extensions/default/src/getLayoutTemplateModule.js @@ -0,0 +1,28 @@ +import ViewerLayout from './ViewerLayout'; +/* +- Define layout for the viewer in mode configuration. +- Pass in the viewport types that can populate the viewer. +- Init layout based on the displaySets and the objects. +*/ + +export default function ({ servicesManager, extensionManager, commandsManager, hotkeysManager }) { + function ViewerLayoutWithServices(props) { + return ViewerLayout({ + servicesManager, + extensionManager, + commandsManager, + hotkeysManager, + ...props, + }); + } + + return [ + // Layout Template Definition + // TODO: this is weird naming + { + name: 'viewerLayout', + id: 'viewerLayout', + component: ViewerLayoutWithServices, + }, + ]; +} diff --git a/extensions/default/src/getPTImageIdInstanceMetadata.ts b/extensions/default/src/getPTImageIdInstanceMetadata.ts new file mode 100644 index 0000000..4a9e114 --- /dev/null +++ b/extensions/default/src/getPTImageIdInstanceMetadata.ts @@ -0,0 +1,109 @@ +import OHIF from '@ohif/core'; + +import { InstanceMetadata, PhilipsPETPrivateGroup } from '@cornerstonejs/calculate-suv/src/types'; + +const metadataProvider = OHIF.classes.MetadataProvider; + +export default function getPTImageIdInstanceMetadata(imageId: string): InstanceMetadata { + const dicomMetaData = metadataProvider.get('instance', imageId); + + if (!dicomMetaData) { + throw new Error('dicom metadata are required'); + } + + if ( + dicomMetaData.SeriesDate === undefined || + dicomMetaData.SeriesTime === undefined || + dicomMetaData.CorrectedImage === undefined || + dicomMetaData.Units === undefined || + !dicomMetaData.RadiopharmaceuticalInformationSequence || + dicomMetaData.RadiopharmaceuticalInformationSequence.RadionuclideHalfLife === undefined || + dicomMetaData.RadiopharmaceuticalInformationSequence.RadionuclideTotalDose === undefined || + dicomMetaData.DecayCorrection === undefined || + dicomMetaData.AcquisitionDate === undefined || + dicomMetaData.AcquisitionTime === undefined || + (dicomMetaData.RadiopharmaceuticalInformationSequence.RadiopharmaceuticalStartDateTime === + undefined && + dicomMetaData.RadiopharmaceuticalInformationSequence.RadiopharmaceuticalStartTime === + undefined) + ) { + throw new Error('required metadata are missing'); + } + + if (dicomMetaData.PatientWeight === undefined) { + console.warn('PatientWeight missing from PT instance metadata'); + } + + const instanceMetadata: InstanceMetadata = { + CorrectedImage: dicomMetaData.CorrectedImage, + Units: dicomMetaData.Units, + RadionuclideHalfLife: dicomMetaData.RadiopharmaceuticalInformationSequence.RadionuclideHalfLife, + RadionuclideTotalDose: + dicomMetaData.RadiopharmaceuticalInformationSequence.RadionuclideTotalDose, + RadiopharmaceuticalStartDateTime: + dicomMetaData.RadiopharmaceuticalInformationSequence.RadiopharmaceuticalStartDateTime, + RadiopharmaceuticalStartTime: + dicomMetaData.RadiopharmaceuticalInformationSequence.RadiopharmaceuticalStartTime, + DecayCorrection: dicomMetaData.DecayCorrection, + PatientWeight: dicomMetaData.PatientWeight, + SeriesDate: dicomMetaData.SeriesDate, + SeriesTime: dicomMetaData.SeriesTime, + AcquisitionDate: dicomMetaData.AcquisitionDate, + AcquisitionTime: dicomMetaData.AcquisitionTime, + }; + + if ( + dicomMetaData['70531000'] || + dicomMetaData['70531000'] !== undefined || + dicomMetaData['70531009'] || + dicomMetaData['70531009'] !== undefined + ) { + const philipsPETPrivateGroup: PhilipsPETPrivateGroup = { + SUVScaleFactor: dicomMetaData['70531000'], + ActivityConcentrationScaleFactor: dicomMetaData['70531009'], + }; + instanceMetadata.PhilipsPETPrivateGroup = philipsPETPrivateGroup; + } + + if (dicomMetaData['0009100d'] && dicomMetaData['0009100d'] !== undefined) { + instanceMetadata.GEPrivatePostInjectionDateTime = dicomMetaData['0009100d']; + } + + if (dicomMetaData.FrameReferenceTime && dicomMetaData.FrameReferenceTime !== undefined) { + instanceMetadata.FrameReferenceTime = dicomMetaData.FrameReferenceTime; + } + + if (dicomMetaData.ActualFrameDuration && dicomMetaData.ActualFrameDuration !== undefined) { + instanceMetadata.ActualFrameDuration = dicomMetaData.ActualFrameDuration; + } + + if (dicomMetaData.PatientSex && dicomMetaData.PatientSex !== undefined) { + instanceMetadata.PatientSex = dicomMetaData.PatientSex; + } + + if (dicomMetaData.PatientSize && dicomMetaData.PatientSize !== undefined) { + instanceMetadata.PatientSize = dicomMetaData.PatientSize; + } + + return instanceMetadata; +} + +function convertInterfaceTimeToString(time): string { + const hours = `${time.hours || '00'}`.padStart(2, '0'); + const minutes = `${time.minutes || '00'}`.padStart(2, '0'); + const seconds = `${time.seconds || '00'}`.padStart(2, '0'); + + const fractionalSeconds = `${time.fractionalSeconds || '000000'}`.padEnd(6, '0'); + + const timeString = `${hours}${minutes}${seconds}.${fractionalSeconds}`; + return timeString; +} + +function convertInterfaceDateToString(date): string { + const month = `${date.month}`.padStart(2, '0'); + const day = `${date.day}`.padStart(2, '0'); + const dateString = `${date.year}${month}${day}`; + return dateString; +} + +export { getPTImageIdInstanceMetadata }; diff --git a/extensions/default/src/getPanelModule.tsx b/extensions/default/src/getPanelModule.tsx new file mode 100644 index 0000000..22d77e8 --- /dev/null +++ b/extensions/default/src/getPanelModule.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { WrappedPanelStudyBrowser } from './Panels'; +import i18n from 'i18next'; + +// TODO: +// - No loading UI exists yet +// - cancel promises when component is destroyed +// - show errors in UI for thumbnails if promise fails + +function getPanelModule({ commandsManager, extensionManager, servicesManager }) { + return [ + { + name: 'seriesList', + iconName: 'tab-studies', + iconLabel: 'Studies', + label: i18n.t('SidePanel:Studies'), + component: props => ( + + ), + }, + ]; +} + +export default getPanelModule; diff --git a/extensions/default/src/getSopClassHandlerModule.js b/extensions/default/src/getSopClassHandlerModule.js new file mode 100644 index 0000000..74f8cea --- /dev/null +++ b/extensions/default/src/getSopClassHandlerModule.js @@ -0,0 +1,287 @@ +import { utils, classes } from '@ohif/core'; +import { id } from './id'; +import getDisplaySetMessages from './getDisplaySetMessages'; +import getDisplaySetsFromUnsupportedSeries from './getDisplaySetsFromUnsupportedSeries'; +import { chartHandler } from './SOPClassHandlers/chartSOPClassHandler'; + +const { isImage, sopClassDictionary, isDisplaySetReconstructable } = utils; +const { ImageSet } = classes; + +const DEFAULT_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume'; +const DYNAMIC_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingDynamicImageVolume'; +const sopClassHandlerName = 'stack'; +let appContext = {}; + +const getDynamicVolumeInfo = instances => { + const { extensionManager } = appContext; + + if (!extensionManager) { + throw new Error('extensionManager is not available'); + } + + const imageIds = instances.map(({ imageId }) => imageId); + const volumeLoaderUtility = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.volumeLoader' + ); + const { getDynamicVolumeInfo: csGetDynamicVolumeInfo } = volumeLoaderUtility.exports; + + return csGetDynamicVolumeInfo(imageIds); +}; + +const isMultiFrame = instance => { + return instance.NumberOfFrames > 1; +}; + +function getDisplaySetInfo(instances) { + const dynamicVolumeInfo = getDynamicVolumeInfo(instances); + const { isDynamicVolume, timePoints } = dynamicVolumeInfo; + let displaySetInfo; + + const { appConfig } = appContext; + + if (isDynamicVolume) { + const timePoint = timePoints[0]; + const instancesMap = new Map(); + + // O(n) to convert it into a map and O(1) to find each instance + instances.forEach(instance => instancesMap.set(instance.imageId, instance)); + + const firstTimePointInstances = timePoint.map(imageId => instancesMap.get(imageId)); + + displaySetInfo = isDisplaySetReconstructable(firstTimePointInstances, appConfig); + } else { + displaySetInfo = isDisplaySetReconstructable(instances, appConfig); + } + + return { + isDynamicVolume, + ...displaySetInfo, + dynamicVolumeInfo, + }; +} + +const makeDisplaySet = instances => { + const instance = instances[0]; + const imageSet = new ImageSet(instances); + + const { + isDynamicVolume, + value: isReconstructable, + averageSpacingBetweenFrames, + dynamicVolumeInfo, + } = getDisplaySetInfo(instances); + + const volumeLoaderSchema = isDynamicVolume + ? DYNAMIC_VOLUME_LOADER_SCHEME + : DEFAULT_VOLUME_LOADER_SCHEME; + + // set appropriate attributes to image set... + const messages = getDisplaySetMessages(instances, isReconstructable, isDynamicVolume); + + imageSet.setAttributes({ + volumeLoaderSchema, + displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID + SeriesDate: instance.SeriesDate, + SeriesTime: instance.SeriesTime, + SeriesInstanceUID: instance.SeriesInstanceUID, + StudyInstanceUID: instance.StudyInstanceUID, + SeriesNumber: instance.SeriesNumber || 0, + FrameRate: instance.FrameTime, + SOPClassUID: instance.SOPClassUID, + SeriesDescription: instance.SeriesDescription || '', + Modality: instance.Modality, + isMultiFrame: isMultiFrame(instance), + countIcon: isReconstructable ? 'icon-mpr' : undefined, + numImageFrames: instances.length, + SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`, + isReconstructable, + messages, + averageSpacingBetweenFrames: averageSpacingBetweenFrames || null, + isDynamicVolume, + dynamicVolumeInfo, + }); + + // Sort the images in this series if needed + const shallSort = true; //!OHIF.utils.ObjectPath.get(Meteor, 'settings.public.ui.sortSeriesByIncomingOrder'); + if (shallSort) { + imageSet.sortBy((a, b) => { + // Sort by InstanceNumber (0020,0013) + return (parseInt(a.InstanceNumber) || 0) - (parseInt(b.InstanceNumber) || 0); + }); + } + + // Include the first image instance number (after sorted) + /*imageSet.setAttribute( + 'instanceNumber', + imageSet.getImage(0).InstanceNumber + );*/ + + /*const isReconstructable = isDisplaySetReconstructable(series, instances); + + imageSet.isReconstructable = isReconstructable.value; + + if (isReconstructable.missingFrames) { + // TODO -> This is currently unused, but may be used for reconstructing + // Volumes with gaps later on. + imageSet.missingFrames = isReconstructable.missingFrames; + }*/ + + return imageSet; +}; + +const isSingleImageModality = modality => { + return modality === 'CR' || modality === 'MG' || modality === 'DX'; +}; + +function getSopClassUids(instances) { + const uniqueSopClassUidsInSeries = new Set(); + instances.forEach(instance => { + uniqueSopClassUidsInSeries.add(instance.SOPClassUID); + }); + const sopClassUids = Array.from(uniqueSopClassUidsInSeries); + + return sopClassUids; +} + +/** + * Basic SOPClassHandler: + * - For all Image types that are stackable, create + * a displaySet with a stack of images + * + * @param {SeriesMetadata} series The series metadata object from which the display sets will be created + * @returns {Array} The list of display sets created for the given series object + */ +function getDisplaySetsFromSeries(instances) { + // If the series has no instances, stop here + if (!instances || !instances.length) { + throw new Error('No instances were provided'); + } + + const displaySets = []; + const sopClassUids = getSopClassUids(instances); + + // Search through the instances (InstanceMetadata object) of this series + // Split Multi-frame instances and Single-image modalities + // into their own specific display sets. Place the rest of each + // series into another display set. + const stackableInstances = []; + instances.forEach(instance => { + // All imaging modalities must have a valid value for sopClassUid (x00080016) or rows (x00280010) + if (!isImage(instance.SOPClassUID) && !instance.Rows) { + return; + } + + let displaySet; + + if (isMultiFrame(instance)) { + displaySet = makeDisplaySet([instance]); + + displaySet.setAttributes({ + sopClassUids, + numImageFrames: instance.NumberOfFrames, + instanceNumber: instance.InstanceNumber, + acquisitionDatetime: instance.AcquisitionDateTime, + }); + displaySets.push(displaySet); + } else if (isSingleImageModality(instance.Modality)) { + displaySet = makeDisplaySet([instance]); + displaySet.setAttributes({ + sopClassUids, + instanceNumber: instance.InstanceNumber, + acquisitionDatetime: instance.AcquisitionDateTime, + }); + displaySets.push(displaySet); + } else { + stackableInstances.push(instance); + } + }); + + if (stackableInstances.length) { + const displaySet = makeDisplaySet(stackableInstances); + displaySet.setAttribute('studyInstanceUid', instances[0].StudyInstanceUID); + displaySet.setAttributes({ + sopClassUids, + }); + displaySets.push(displaySet); + } + + return displaySets; +} + +const sopClassUids = [ + sopClassDictionary.ComputedRadiographyImageStorage, + sopClassDictionary.DigitalXRayImageStorageForPresentation, + sopClassDictionary.DigitalXRayImageStorageForProcessing, + sopClassDictionary.DigitalMammographyXRayImageStorageForPresentation, + sopClassDictionary.DigitalMammographyXRayImageStorageForProcessing, + sopClassDictionary.DigitalIntraOralXRayImageStorageForPresentation, + sopClassDictionary.DigitalIntraOralXRayImageStorageForProcessing, + sopClassDictionary.CTImageStorage, + sopClassDictionary.EnhancedCTImageStorage, + sopClassDictionary.LegacyConvertedEnhancedCTImageStorage, + sopClassDictionary.UltrasoundMultiframeImageStorage, + sopClassDictionary.MRImageStorage, + sopClassDictionary.EnhancedMRImageStorage, + sopClassDictionary.EnhancedMRColorImageStorage, + sopClassDictionary.LegacyConvertedEnhancedMRImageStorage, + sopClassDictionary.UltrasoundImageStorage, + sopClassDictionary.UltrasoundImageStorageRET, + sopClassDictionary.SecondaryCaptureImageStorage, + sopClassDictionary.MultiframeSingleBitSecondaryCaptureImageStorage, + sopClassDictionary.MultiframeGrayscaleByteSecondaryCaptureImageStorage, + sopClassDictionary.MultiframeGrayscaleWordSecondaryCaptureImageStorage, + sopClassDictionary.MultiframeTrueColorSecondaryCaptureImageStorage, + sopClassDictionary.XRayAngiographicImageStorage, + sopClassDictionary.EnhancedXAImageStorage, + sopClassDictionary.XRayRadiofluoroscopicImageStorage, + sopClassDictionary.EnhancedXRFImageStorage, + sopClassDictionary.XRay3DAngiographicImageStorage, + sopClassDictionary.XRay3DCraniofacialImageStorage, + sopClassDictionary.BreastTomosynthesisImageStorage, + sopClassDictionary.BreastProjectionXRayImageStorageForPresentation, + sopClassDictionary.BreastProjectionXRayImageStorageForProcessing, + sopClassDictionary.IntravascularOpticalCoherenceTomographyImageStorageForPresentation, + sopClassDictionary.IntravascularOpticalCoherenceTomographyImageStorageForProcessing, + sopClassDictionary.NuclearMedicineImageStorage, + sopClassDictionary.VLEndoscopicImageStorage, + sopClassDictionary.VideoEndoscopicImageStorage, + sopClassDictionary.VLMicroscopicImageStorage, + sopClassDictionary.VideoMicroscopicImageStorage, + sopClassDictionary.VLSlideCoordinatesMicroscopicImageStorage, + sopClassDictionary.VLPhotographicImageStorage, + sopClassDictionary.VideoPhotographicImageStorage, + sopClassDictionary.OphthalmicPhotography8BitImageStorage, + sopClassDictionary.OphthalmicPhotography16BitImageStorage, + sopClassDictionary.OphthalmicTomographyImageStorage, + // Handled by another sop class module + // sopClassDictionary.VLWholeSlideMicroscopyImageStorage, + sopClassDictionary.PositronEmissionTomographyImageStorage, + sopClassDictionary.EnhancedPETImageStorage, + sopClassDictionary.LegacyConvertedEnhancedPETImageStorage, + sopClassDictionary.RTImageStorage, + sopClassDictionary.EnhancedUSVolumeStorage, +]; + +function getSopClassHandlerModule(appContextParam) { + appContext = appContextParam; + + return [ + { + name: sopClassHandlerName, + sopClassUids, + getDisplaySetsFromSeries, + }, + { + name: 'not-supported-display-sets-handler', + sopClassUids: [], + getDisplaySetsFromSeries: getDisplaySetsFromUnsupportedSeries, + }, + { + name: chartHandler.name, + sopClassUids: chartHandler.sopClassUids, + getDisplaySetsFromSeries: chartHandler.getDisplaySetsFromSeries, + }, + ]; +} + +export default getSopClassHandlerModule; diff --git a/extensions/default/src/getToolbarModule.tsx b/extensions/default/src/getToolbarModule.tsx new file mode 100644 index 0000000..92ada7e --- /dev/null +++ b/extensions/default/src/getToolbarModule.tsx @@ -0,0 +1,94 @@ +import { ToolbarButton as ToolbarButtonLegacy } from '@ohif/ui'; +import { ToolButton, utils } from '@ohif/ui-next'; + +import ToolbarLayoutSelectorWithServices from './Toolbar/ToolbarLayoutSelector'; + +// legacy +import ToolbarDividerLegacy from './Toolbar/ToolbarDivider'; +import ToolbarSplitButtonWithServicesLegacy from './Toolbar/ToolbarSplitButtonWithServices'; +import ToolbarButtonGroupWithServicesLegacy from './Toolbar/ToolbarButtonGroupWithServices'; +import { ProgressDropdownWithService } from './Components/ProgressDropdownWithService'; + +// new +import ToolButtonListWrapper from './Toolbar/ToolButtonListWrapper'; +import { ToolBoxButtonGroupWrapper, ToolBoxButtonWrapper } from './Toolbar/ToolBoxWrapper'; + +export default function getToolbarModule({ commandsManager, servicesManager }: withAppTypes) { + const { cineService } = servicesManager.services; + return [ + // new + { + name: 'ohif.toolButton', + defaultComponent: ToolButton, + }, + { + name: 'ohif.toolButtonList', + defaultComponent: ToolButtonListWrapper, + }, + { + name: 'ohif.toolBoxButtonGroup', + defaultComponent: ToolBoxButtonGroupWrapper, + }, + { + name: 'ohif.toolBoxButton', + defaultComponent: ToolBoxButtonWrapper, + }, + // legacy + { + name: 'ohif.radioGroup', + defaultComponent: ToolbarButtonLegacy, + }, + { + name: 'ohif.buttonGroup', + defaultComponent: ToolbarButtonGroupWithServicesLegacy, + }, + { + name: 'ohif.divider', + defaultComponent: ToolbarDividerLegacy, + }, + { + name: 'ohif.splitButton', + defaultComponent: ToolbarSplitButtonWithServicesLegacy, + }, + // others + { + name: 'ohif.layoutSelector', + defaultComponent: props => + ToolbarLayoutSelectorWithServices({ ...props, commandsManager, servicesManager }), + }, + { + name: 'ohif.progressDropdown', + defaultComponent: ProgressDropdownWithService, + }, + { + name: 'evaluate.group.promoteToPrimary', + evaluate: ({ viewportId, button, itemId }) => { + const { items } = button.props; + + if (!itemId) { + return { + primary: button.props.primary, + items, + }; + } + + // other wise we can move the clicked tool to the primary button + const clickedItemProps = items.find(item => item.id === itemId || item.itemId === itemId); + + return { + primary: clickedItemProps, + items, + }; + }, + }, + { + name: 'evaluate.cine', + evaluate: () => { + const isToggled = cineService.getState().isCineEnabled; + return { + className: utils.getToggledClassName(isToggled), + }; + }, + }, + ]; +} diff --git a/extensions/default/src/getViewportModule.tsx b/extensions/default/src/getViewportModule.tsx new file mode 100644 index 0000000..c3ca1ef --- /dev/null +++ b/extensions/default/src/getViewportModule.tsx @@ -0,0 +1,21 @@ +import { CommandsManager, ExtensionManager } from '@ohif/core'; +import LineChartViewport from './Components/LineChartViewport/index'; + +const getViewportModule = ({ + servicesManager, + commandsManager, + extensionManager, +}: { + servicesManager: AppTypes.ServicesManager; + commandsManager: CommandsManager; + extensionManager: ExtensionManager; +}) => { + return [ + { + name: 'chartViewport', + component: LineChartViewport, + }, + ]; +}; + +export { getViewportModule as default }; diff --git a/extensions/default/src/hangingprotocols/hpCompare.ts b/extensions/default/src/hangingprotocols/hpCompare.ts new file mode 100644 index 0000000..14cb51d --- /dev/null +++ b/extensions/default/src/hangingprotocols/hpCompare.ts @@ -0,0 +1,188 @@ +import { Types } from '@ohif/core'; + +const defaultDisplaySetSelector = { + studyMatchingRules: [ + { + // The priorInstance is a study counter that indicates what position this study is in + // and the value comes from the options parameter. + attribute: 'studyInstanceUIDsIndex', + from: 'options', + required: true, + constraint: { + equals: { value: 0 }, + }, + }, + ], + seriesMatchingRules: [ + { + attribute: 'numImageFrames', + constraint: { + greaterThan: { value: 0 }, + }, + }, + // This display set will select the specified items by preference + // It has no affect if nothing is specified in the URL. + { + attribute: 'isDisplaySetFromUrl', + weight: 20, + constraint: { + equals: true, + }, + }, + ], +}; + +const priorDisplaySetSelector = { + studyMatchingRules: [ + { + // The priorInstance is a study counter that indicates what position this study is in + // and the value comes from the options parameter. + attribute: 'studyInstanceUIDsIndex', + from: 'options', + required: true, + constraint: { + equals: { value: 1 }, + }, + }, + ], + seriesMatchingRules: [ + { + attribute: 'numImageFrames', + constraint: { + greaterThan: { value: 0 }, + }, + }, + // This display set will select the specified items by preference + // It has no affect if nothing is specified in the URL. + { + attribute: 'isDisplaySetFromUrl', + weight: 20, + constraint: { + equals: true, + }, + }, + ], +}; + +const currentDisplaySet = { + id: 'defaultDisplaySetId', +}; + +const priorDisplaySet = { + id: 'priorDisplaySetId', +}; + +const currentViewport0 = { + viewportOptions: { + toolGroupId: 'default', + allowUnmatchedView: true, + }, + displaySets: [currentDisplaySet], +}; + +const currentViewport1 = { + ...currentViewport0, + displaySets: [ + { + ...currentDisplaySet, + matchedDisplaySetsIndex: 1, + }, + ], +}; + +const priorViewport0 = { + ...currentViewport0, + displaySets: [priorDisplaySet], +}; + +const priorViewport1 = { + ...priorViewport0, + displaySets: [ + { + ...priorDisplaySet, + matchedDisplaySetsIndex: 1, + }, + ], +}; + +/** + * This hanging protocol can be activated on the primary mode by directly + * referencing it in a URL or by directly including it within a mode, e.g.: + * `&hangingProtocolId=@ohif/mnGrid` added to the viewer URL + * It is not included in the viewer mode by default. + */ +const hpMNCompare: Types.HangingProtocol.Protocol = { + id: '@ohif/hpCompare', + description: 'Compare two studies in various layouts', + name: 'Compare Two Studies', + numberOfPriorsReferenced: 1, + protocolMatchingRules: [ + { + id: 'Two Studies', + weight: 1000, + // is there a second study or in another work the attribute + // studyInstanceUIDsIndex that we get from prior should not be null + attribute: 'StudyInstanceUID', + from: 'prior', + required: true, + constraint: { + notNull: true, + }, + }, + ], + toolGroupIds: ['default'], + displaySetSelectors: { + defaultDisplaySetId: defaultDisplaySetSelector, + priorDisplaySetId: priorDisplaySetSelector, + }, + defaultViewport: { + viewportOptions: { + viewportType: 'stack', + toolGroupId: 'default', + allowUnmatchedView: true, + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + matchedDisplaySetsIndex: -1, + }, + ], + }, + stages: [ + { + name: '2x2', + stageActivation: { + enabled: { + minViewportsMatched: 4, + }, + }, + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 2, + columns: 2, + }, + }, + viewports: [currentViewport0, priorViewport0, currentViewport1, priorViewport1], + }, + + { + name: '2x1', + stageActivation: { + enabled: { + minViewportsMatched: 2, + }, + }, + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 2, + }, + }, + viewports: [currentViewport0, priorViewport0], + }, + ], +}; + +export default hpMNCompare; diff --git a/extensions/default/src/hangingprotocols/hpMNGrid.ts b/extensions/default/src/hangingprotocols/hpMNGrid.ts new file mode 100644 index 0000000..29fc174 --- /dev/null +++ b/extensions/default/src/hangingprotocols/hpMNGrid.ts @@ -0,0 +1,396 @@ +import { Types } from '@ohif/core'; +import { studyWithImages } from './utils/studySelectors'; +import { seriesWithImages } from './utils/seriesSelectors'; +import { viewportOptions } from './utils/viewportOptions'; + +/** + * Sync group configuration for hydrating segmentations across viewports + * that share the same frame of reference + * @type {Types.HangingProtocol.SyncGroup} + */ +export const HYDRATE_SEG_SYNC_GROUP = { + type: 'hydrateseg', + id: 'sameFORId', + source: true, + target: true, + options: { + matchingRules: ['sameFOR'], + }, +} as const; + +/** + * This hanging protocol can be activated on the primary mode by directly + * referencing it in a URL or by directly including it within a mode, e.g.: + * `&hangingProtocolId=@ohif/mnGrid` added to the viewer URL + * It is not included in the viewer mode by default. + */ +export const hpMN: Types.HangingProtocol.Protocol = { + id: '@ohif/mnGrid', + description: 'Has various hanging protocol grid layouts', + name: '2x2', + protocolMatchingRules: studyWithImages, + toolGroupIds: ['default'], + displaySetSelectors: { + defaultDisplaySetId: { + allowUnmatchedView: true, + seriesMatchingRules: seriesWithImages, + }, + }, + defaultViewport: { + viewportOptions: { + viewportType: 'stack', + toolGroupId: 'default', + syncGroups: [HYDRATE_SEG_SYNC_GROUP], + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + matchedDisplaySetsIndex: -1, + }, + ], + }, + stages: [ + { + id: '2x2', + name: '2x2', + stageActivation: { + enabled: { + minViewportsMatched: 4, + }, + }, + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 2, + columns: 2, + }, + }, + viewports: [ + { + viewportOptions, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions, + displaySets: [ + { + matchedDisplaySetsIndex: 1, + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions, + displaySets: [ + { + matchedDisplaySetsIndex: 2, + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions, + displaySets: [ + { + matchedDisplaySetsIndex: 3, + id: 'defaultDisplaySetId', + }, + ], + }, + ], + }, + + // 3x1 stage + { + name: '3x1', + stageActivation: { + enabled: { + minViewportsMatched: 3, + }, + }, + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 3, + }, + }, + viewports: [ + { + viewportOptions, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions, + displaySets: [ + { + id: 'defaultDisplaySetId', + matchedDisplaySetsIndex: 1, + }, + ], + }, + { + viewportOptions, + displaySets: [ + { + id: 'defaultDisplaySetId', + matchedDisplaySetsIndex: 2, + }, + ], + }, + ], + }, + + // A 2x1 stage + { + name: '2x1', + stageActivation: { + enabled: { + minViewportsMatched: 2, + }, + }, + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 2, + }, + }, + viewports: [ + { + viewportOptions, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions, + displaySets: [ + { + matchedDisplaySetsIndex: 1, + id: 'defaultDisplaySetId', + }, + ], + }, + ], + }, + + // A 1x1 stage - should be automatically activated if there is only 1 viewable instance + { + name: '1x1', + stageActivation: { + enabled: { + minViewportsMatched: 1, + }, + }, + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 1, + }, + }, + viewports: [ + { + viewportOptions, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + ], + }, + ], + numberOfPriorsReferenced: -1, +}; + +/** + * This hanging protocol can be activated on the primary mode by directly + * referencing it in a URL or by directly including it within a mode, e.g.: + * `&hangingProtocolId=@ohif/mnGrid8` added to the viewer URL + * It is not included in the viewer mode by default. + */ +export const hpMN8: Types.HangingProtocol.Protocol = { + ...hpMN, + id: '@ohif/mnGrid8', + description: 'Has various hanging protocol grid layouts up to 4x2', + name: '4x2', + stages: [ + { + id: '4x2', + name: '4x2', + stageActivation: { + enabled: { + minViewportsMatched: 7, + }, + }, + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 2, + columns: 4, + }, + }, + viewports: [ + { + viewportOptions, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions, + displaySets: [ + { + matchedDisplaySetsIndex: 1, + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions, + displaySets: [ + { + matchedDisplaySetsIndex: 2, + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions, + displaySets: [ + { + matchedDisplaySetsIndex: 3, + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions, + displaySets: [ + { + matchedDisplaySetsIndex: 4, + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions, + displaySets: [ + { + matchedDisplaySetsIndex: 5, + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions, + displaySets: [ + { + matchedDisplaySetsIndex: 6, + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions, + displaySets: [ + { + matchedDisplaySetsIndex: 7, + id: 'defaultDisplaySetId', + }, + ], + }, + ], + }, + + { + id: '3x2', + name: '3x2', + stageActivation: { + enabled: { + minViewportsMatched: 5, + }, + }, + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 2, + columns: 3, + }, + }, + viewports: [ + { + viewportOptions, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions, + displaySets: [ + { + matchedDisplaySetsIndex: 1, + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions, + displaySets: [ + { + matchedDisplaySetsIndex: 2, + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions, + displaySets: [ + { + matchedDisplaySetsIndex: 3, + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions, + displaySets: [ + { + matchedDisplaySetsIndex: 4, + id: 'defaultDisplaySetId', + }, + ], + }, + { + viewportOptions, + displaySets: [ + { + matchedDisplaySetsIndex: 5, + id: 'defaultDisplaySetId', + }, + ], + }, + ], + }, + + ...hpMN.stages, + ], +}; + +export default hpMN; diff --git a/extensions/default/src/hangingprotocols/hpMammo.ts b/extensions/default/src/hangingprotocols/hpMammo.ts new file mode 100644 index 0000000..4a97b04 --- /dev/null +++ b/extensions/default/src/hangingprotocols/hpMammo.ts @@ -0,0 +1,201 @@ +import { + RCC, + RMLO, + LCC, + LMLO, + RCCPrior, + LCCPrior, + RMLOPrior, + LMLOPrior, +} from './utils/mammoDisplaySetSelector'; + +const rightDisplayArea = { + storeAsInitialCamera: true, + imageArea: [0.8, 0.8], + imageCanvasPoint: { + imagePoint: [0, 0.5], + canvasPoint: [0, 0.5], + }, +}; + +const leftDisplayArea = { + storeAsInitialCamera: true, + imageArea: [0.8, 0.8], + imageCanvasPoint: { + imagePoint: [1, 0.5], + canvasPoint: [1, 0.5], + }, +}; + +const hpMammography = { + id: '@ohif/hpMammo', + hasUpdatedPriorsInformation: false, + name: 'Mammography Breast Screening', + protocolMatchingRules: [ + { + id: 'Mammography', + weight: 150, + attribute: 'ModalitiesInStudy', + constraint: { + contains: 'MG', + }, + required: true, + }, + { + id: 'numberOfImages', + attribute: 'numberOfDisplaySetsWithImages', + constraint: { + greaterThan: 2, + }, + required: true, + }, + ], + toolGroupIds: ['default'], + displaySetSelectors: { + RCC, + LCC, + RMLO, + LMLO, + RCCPrior, + LCCPrior, + RMLOPrior, + LMLOPrior, + }, + + stages: [ + { + name: 'CC/MLO', + viewportStructure: { + type: 'grid', + layoutType: 'grid', + properties: { + rows: 2, + columns: 2, + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'default', + displayArea: leftDisplayArea, + // flipHorizontal: true, + // rotation: 180, + allowUnmatchedView: true, + }, + displaySets: [ + { + id: 'RCC', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + // flipHorizontal: true, + displayArea: rightDisplayArea, + allowUnmatchedView: true, + }, + displaySets: [ + { + id: 'LCC', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + displayArea: leftDisplayArea, + // rotation: 180, + // flipHorizontal: true, + allowUnmatchedView: true, + }, + displaySets: [ + { + id: 'RMLO', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + displayArea: rightDisplayArea, + // flipHorizontal: true, + allowUnmatchedView: true, + }, + displaySets: [ + { + id: 'LMLO', + }, + ], + }, + ], + }, + + // Compare CC current/prior top/bottom + { + name: 'CC compare', + viewportStructure: { + type: 'grid', + layoutType: 'grid', + properties: { + rows: 2, + columns: 2, + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'default', + displayArea: leftDisplayArea, + flipHorizontal: true, + rotation: 180, + }, + displaySets: [ + { + id: 'RCC', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + flipHorizontal: true, + displayArea: rightDisplayArea, + }, + displaySets: [ + { + id: 'LCC', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + displayArea: leftDisplayArea, + flipHorizontal: true, + }, + displaySets: [ + { + id: 'RCCPrior', + }, + ], + }, + { + viewportOptions: { + toolGroupId: 'default', + displayArea: rightDisplayArea, + }, + displaySets: [ + { + id: 'LCCPrior', + }, + ], + }, + ], + }, + ], + // Indicates it is prior aware, but will work with no priors + numberOfPriorsReferenced: 0, +}; + +export default hpMammography; diff --git a/extensions/default/src/hangingprotocols/hpScale.ts b/extensions/default/src/hangingprotocols/hpScale.ts new file mode 100644 index 0000000..e76f5d5 --- /dev/null +++ b/extensions/default/src/hangingprotocols/hpScale.ts @@ -0,0 +1,132 @@ +import { Types } from '@ohif/core'; + +const displayAreaScale1: Types.HangingProtocol.DisplayArea = { + type: 'SCALE', + scale: 1, + storeAsInitialCamera: true, +}; +const displayAreaScale15: Types.HangingProtocol.DisplayArea = { ...displayAreaScale1, scale: 15 }; + +/** + * This hanging protocol can be activated on the primary mode by directly + * referencing it in a URL or by directly including it within a mode, e.g.: + * `&hangingProtocolId=@ohif/mnGrid` added to the viewer URL + * It is not included in the viewer mode by default. + */ +const hpScale: Types.HangingProtocol.Protocol = { + id: '@ohif/hpScale', + description: 'Has various hanging protocol grid layouts', + name: 'Scale Images', + protocolMatchingRules: [ + { + id: 'OneOrMoreSeries', + weight: 25, + attribute: 'numberOfDisplaySetsWithImages', + constraint: { + greaterThan: 0, + }, + }, + ], + toolGroupIds: ['default'], + displaySetSelectors: { + defaultDisplaySetId: { + seriesMatchingRules: [ + { + weight: 1, + attribute: 'numImageFrames', + constraint: { + greaterThan: { value: 0 }, + }, + required: true, + }, + // This display set will select the specified items by preference + // It has no affect if nothing is specified in the URL. + { + attribute: 'isDisplaySetFromUrl', + weight: 20, + constraint: { + equals: true, + }, + }, + ], + }, + }, + defaultViewport: { + viewportOptions: { + viewportType: 'stack', + toolGroupId: 'default', + displayArea: displayAreaScale1, + allowUnmatchedView: true, + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + matchedDisplaySetsIndex: -1, + }, + ], + }, + stages: [ + // A 1x1 stage - should be automatically activated if there is only 1 viewable instance + { + name: 'Scale 1:1', + stageActivation: { + enabled: { + minViewportsMatched: 1, + }, + }, + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 1, + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'default', + allowUnmatchedView: true, + displayArea: displayAreaScale1, + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + ], + }, + { + name: 'Scale 1:15', + stageActivation: { + enabled: { + minViewportsMatched: 1, + }, + }, + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 1, + }, + }, + viewports: [ + { + viewportOptions: { + toolGroupId: 'default', + allowUnmatchedView: true, + displayArea: displayAreaScale15, + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], + }, + ], + }, + ], + numberOfPriorsReferenced: -1, +}; + +export default hpScale; diff --git a/extensions/default/src/hangingprotocols/index.ts b/extensions/default/src/hangingprotocols/index.ts new file mode 100644 index 0000000..57d00c2 --- /dev/null +++ b/extensions/default/src/hangingprotocols/index.ts @@ -0,0 +1,16 @@ +import viewCodeAttribute from './utils/viewCode'; +import lateralityAttribute from './utils/laterality'; +import registerHangingProtocolAttributes from './utils/registerHangingProtocolAttributes'; +import hpMammography from './hpMammo'; +import hpMNGrid from './hpMNGrid'; +import hpCompare from './hpCompare'; +export * from './hpMNGrid'; + +export { + viewCodeAttribute, + lateralityAttribute, + hpMammography as hpMammo, + hpMNGrid, + hpCompare, + registerHangingProtocolAttributes, +}; diff --git a/extensions/default/src/hangingprotocols/utils/laterality.ts b/extensions/default/src/hangingprotocols/utils/laterality.ts new file mode 100644 index 0000000..59f8a4e --- /dev/null +++ b/extensions/default/src/hangingprotocols/utils/laterality.ts @@ -0,0 +1,9 @@ +export default displaySet => { + const frameAnatomy = + displaySet?.images?.[0]?.SharedFunctionalGroupsSequence?.[0]?.FrameAnatomySequence?.[0]; + if (!frameAnatomy) { + return undefined; + } + const laterality = frameAnatomy?.FrameLaterality; + return laterality; +}; diff --git a/extensions/default/src/hangingprotocols/utils/mammoDisplaySetSelector.ts b/extensions/default/src/hangingprotocols/utils/mammoDisplaySetSelector.ts new file mode 100644 index 0000000..720d5a1 --- /dev/null +++ b/extensions/default/src/hangingprotocols/utils/mammoDisplaySetSelector.ts @@ -0,0 +1,214 @@ +const priorStudyMatchingRules = [ + { + // The priorInstance is a study counter that indicates what position this study is in + // and the value comes from the options parameter. + attribute: 'studyInstanceUIDsIndex', + from: 'options', + required: true, + constraint: { + equals: { value: 1 }, + }, + }, +]; + +const currentStudyMatchingRules = [ + { + // The priorInstance is a study counter that indicates what position this study is in + // and the value comes from the options parameter. + attribute: 'studyInstanceUIDsIndex', + from: 'options', + required: true, + constraint: { + equals: { value: 0 }, + }, + }, +]; + +const LCCSeriesMatchingRules = [ + { + weight: 10, + attribute: 'ViewCode', + constraint: { + contains: 'SCT:399162004', + }, + }, + { + weight: 5, + attribute: 'PatientOrientation', + constraint: { + contains: 'L', + }, + }, + { + weight: 20, + attribute: 'SeriesDescription', + constraint: { + contains: 'L CC', + }, + }, +]; + +const RCCSeriesMatchingRules = [ + { + weight: 10, + attribute: 'ViewCode', + constraint: { + contains: 'SCT:399162004', + }, + }, + { + weight: 5, + attribute: 'PatientOrientation', + constraint: { + equals: ['P', 'L'], + }, + }, + { + attribute: 'PatientOrientation', + constraint: { + doesNotEqual: ['A', 'R'], + }, + required: true, + }, + { + weight: 20, + attribute: 'SeriesDescription', + constraint: { + contains: 'CC', + }, + }, +]; + +const LMLOSeriesMatchingRules = [ + { + weight: 10, + attribute: 'ViewCode', + constraint: { + contains: 'SCT:399368009', + }, + }, + { + weight: 0, + attribute: 'ViewCode', + constraint: { + doesNotEqual: 'SCT:399162004', + }, + required: true, + }, + { + weight: 5, + attribute: 'PatientOrientation', + constraint: { + equals: ['A', 'R'], + }, + }, + { + weight: 20, + attribute: 'SeriesDescription', + constraint: { + contains: 'L MLO', + }, + }, +]; + +const RMLOSeriesMatchingRules = [ + { + weight: 10, + attribute: 'ViewCode', + constraint: { + contains: 'SCT:399368009', + }, + }, + { + attribute: 'ViewCode', + constraint: { + doesNotEqual: 'SCT:399162004', + }, + required: true, + }, + { + attribute: 'PatientOrientation', + constraint: { + doesNotContain: ['P', 'FL'], + }, + required: true, + }, + { + weight: 5, + attribute: 'PatientOrientation', + constraint: { + equals: ['P', 'L'], + }, + }, + { + weight: 5, + attribute: 'PatientOrientation', + constraint: { + equals: ['A', 'FR'], + }, + }, + { + weight: 20, + attribute: 'SeriesDescription', + constraint: { + contains: 'R MLO', + }, + }, + { + attribute: 'SeriesDescription', + required: true, + constraint: { + doesNotContain: 'CC', + }, + }, + { + attribute: 'SeriesDescription', + required: true, + constraint: { + doesNotEqual: 'L MLO', + }, + required: true, + }, +]; + +const RCC = { + seriesMatchingRules: RCCSeriesMatchingRules, + studyMatchingRules: currentStudyMatchingRules, +}; + +const RCCPrior = { + seriesMatchingRules: RCCSeriesMatchingRules, + studyMatchingRules: priorStudyMatchingRules, +}; + +const LCC = { + seriesMatchingRules: LCCSeriesMatchingRules, + studyMatchingRules: currentStudyMatchingRules, +}; + +const LCCPrior = { + seriesMatchingRules: LCCSeriesMatchingRules, + studyMatchingRules: priorStudyMatchingRules, +}; + +const RMLO = { + seriesMatchingRules: RMLOSeriesMatchingRules, + studyMatchingRules: currentStudyMatchingRules, +}; + +const RMLOPrior = { + seriesMatchingRules: RMLOSeriesMatchingRules, + studyMatchingRules: priorStudyMatchingRules, +}; + +const LMLO = { + seriesMatchingRules: LMLOSeriesMatchingRules, + studyMatchingRules: currentStudyMatchingRules, +}; + +const LMLOPrior = { + seriesMatchingRules: LMLOSeriesMatchingRules, + studyMatchingRules: priorStudyMatchingRules, +}; + +export { RCC, LCC, RMLO, LMLO, RCCPrior, LCCPrior, RMLOPrior, LMLOPrior }; diff --git a/extensions/default/src/hangingprotocols/utils/registerHangingProtocolAttributes.ts b/extensions/default/src/hangingprotocols/utils/registerHangingProtocolAttributes.ts new file mode 100644 index 0000000..6c59800 --- /dev/null +++ b/extensions/default/src/hangingprotocols/utils/registerHangingProtocolAttributes.ts @@ -0,0 +1,8 @@ +import viewCode from './viewCode'; +import laterality from './laterality'; + +export default function registerHangingProtocolAttributes({ servicesManager }) { + const { hangingProtocolService } = servicesManager.services; + hangingProtocolService.addCustomAttribute('ViewCode', 'View Code Designator:Value', viewCode); + hangingProtocolService.addCustomAttribute('Laterality', 'Laterality of object', laterality); +} diff --git a/extensions/default/src/hangingprotocols/utils/seriesSelectors.ts b/extensions/default/src/hangingprotocols/utils/seriesSelectors.ts new file mode 100644 index 0000000..ea77a30 --- /dev/null +++ b/extensions/default/src/hangingprotocols/utils/seriesSelectors.ts @@ -0,0 +1,23 @@ +import { Types } from '@ohif/core'; + +type MatchingRule = Types.HangingProtocol.MatchingRule; + +export const seriesWithImages: MatchingRule[] = [ + { + attribute: 'numImageFrames', + constraint: { + greaterThan: { value: 0 }, + }, + weight: 1, + required: true, + }, + // This display set will select the specified items by preference + // It has no affect if nothing is specified in the URL. + { + attribute: 'isDisplaySetFromUrl', + weight: 20, + constraint: { + equals: true, + }, + }, +]; diff --git a/extensions/default/src/hangingprotocols/utils/studySelectors.ts b/extensions/default/src/hangingprotocols/utils/studySelectors.ts new file mode 100644 index 0000000..029a446 --- /dev/null +++ b/extensions/default/src/hangingprotocols/utils/studySelectors.ts @@ -0,0 +1,14 @@ +import { Types } from '@ohif/core'; + +type MatchingRule = Types.HangingProtocol.MatchingRule; + +export const studyWithImages: MatchingRule[] = [ + { + id: 'OneOrMoreSeries', + weight: 25, + attribute: 'numberOfDisplaySetsWithImages', + constraint: { + greaterThan: 0, + }, + }, +]; diff --git a/extensions/default/src/hangingprotocols/utils/viewCode.ts b/extensions/default/src/hangingprotocols/utils/viewCode.ts new file mode 100644 index 0000000..0f2251e --- /dev/null +++ b/extensions/default/src/hangingprotocols/utils/viewCode.ts @@ -0,0 +1,11 @@ +export default displaySet => { + const ViewCodeSequence = displaySet?.images[0]?.ViewCodeSequence[0]; + if (!ViewCodeSequence) { + return undefined; + } + const { CodingSchemeDesignator, CodeValue } = ViewCodeSequence; + if (!CodingSchemeDesignator || !CodeValue) { + return undefined; + } + return `${CodingSchemeDesignator}:${CodeValue}`; +}; diff --git a/extensions/default/src/hangingprotocols/utils/viewportOptions.ts b/extensions/default/src/hangingprotocols/utils/viewportOptions.ts new file mode 100644 index 0000000..5a3ab98 --- /dev/null +++ b/extensions/default/src/hangingprotocols/utils/viewportOptions.ts @@ -0,0 +1,18 @@ +/** A default viewport options */ +export const viewportOptions = { + toolGroupId: 'default', + allowUnmatchedView: true, + syncGroups: [ + { + type: 'hydrateseg', + id: 'sameFORId', + source: true, + target: true, + options: { + matchingRules: ['sameFOR'], + }, + }, + ], +}; + +export const hydrateSegDefault = viewportOptions; diff --git a/extensions/default/src/hooks/usePatientInfo.tsx b/extensions/default/src/hooks/usePatientInfo.tsx new file mode 100644 index 0000000..a28d94d --- /dev/null +++ b/extensions/default/src/hooks/usePatientInfo.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import { utils } from '@ohif/core'; + +const { formatPN, formatDate } = utils; + +function usePatientInfo(servicesManager: AppTypes.ServicesManager) { + const { displaySetService } = servicesManager.services; + + const [patientInfo, setPatientInfo] = useState({ + PatientName: '', + PatientID: '', + PatientSex: '', + PatientDOB: '', + }); + const [isMixedPatients, setIsMixedPatients] = useState(false); + + const checkMixedPatients = (PatientID: string) => { + const displaySets = displaySetService.getActiveDisplaySets(); + let isMixedPatients = false; + displaySets.forEach(displaySet => { + const instance = displaySet?.instances?.[0] || displaySet?.instance; + if (!instance) { + return; + } + if (instance.PatientID !== PatientID) { + isMixedPatients = true; + } + }); + setIsMixedPatients(isMixedPatients); + }; + + const updatePatientInfo = ({ displaySetsAdded }) => { + if (!displaySetsAdded.length) { + return; + } + const displaySet = displaySetsAdded[0]; + const instance = displaySet?.instances?.[0] || displaySet?.instance; + if (!instance) { + return; + } + + setPatientInfo({ + PatientID: instance.PatientID || null, + PatientName: instance.PatientName ? formatPN(instance.PatientName) : null, + PatientSex: instance.PatientSex || null, + PatientDOB: formatDate(instance.PatientBirthDate) || null, + }); + checkMixedPatients(instance.PatientID || null); + }; + + useEffect(() => { + const subscription = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SETS_ADDED, + props => updatePatientInfo(props) + ); + return () => subscription.unsubscribe(); + }, []); + + return { patientInfo, isMixedPatients }; +} + +export default usePatientInfo; diff --git a/extensions/default/src/id.js b/extensions/default/src/id.js new file mode 100644 index 0000000..ebe5acd --- /dev/null +++ b/extensions/default/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/extensions/default/src/index.ts b/extensions/default/src/index.ts new file mode 100644 index 0000000..6847833 --- /dev/null +++ b/extensions/default/src/index.ts @@ -0,0 +1,108 @@ +import { Types } from '@ohif/core'; + +import getDataSourcesModule from './getDataSourcesModule'; +import getLayoutTemplateModule from './getLayoutTemplateModule'; +import getPanelModule from './getPanelModule'; +import getSopClassHandlerModule from './getSopClassHandlerModule'; +import getToolbarModule from './getToolbarModule'; +import getCommandsModule from './commandsModule'; +import getHangingProtocolModule from './getHangingProtocolModule'; +import getStudiesForPatientByMRN from './Panels/getStudiesForPatientByMRN'; +import getCustomizationModule from './getCustomizationModule'; +import getViewportModule from './getViewportModule'; +import { id } from './id'; +import preRegistration from './init'; +import { ContextMenuController, CustomizableContextMenuTypes } from './CustomizableContextMenu'; +import * as dicomWebUtils from './DicomWebDataSource/utils'; +import { createReportDialogPrompt } from './Panels'; +import createReportAsync from './Actions/createReportAsync'; +import StaticWadoClient from './DicomWebDataSource/utils/StaticWadoClient'; +import { cleanDenaturalizedDataset } from './DicomWebDataSource/utils'; +import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore'; +import { useViewportGridStore } from './stores/useViewportGridStore'; +import { useUIStateStore } from './stores/useUIStateStore'; +import { useDisplaySetSelectorStore } from './stores/useDisplaySetSelectorStore'; +import { useHangingProtocolStageIndexStore } from './stores/useHangingProtocolStageIndexStore'; +import { useToggleHangingProtocolStore } from './stores/useToggleHangingProtocolStore'; +import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore'; +import { + callLabelAutocompleteDialog, + showLabelAnnotationPopup, + callInputDialog, +} from './utils/callInputDialog'; +import colorPickerDialog from './utils/colorPickerDialog'; + +import promptSaveReport from './utils/promptSaveReport'; +import promptLabelAnnotation from './utils/promptLabelAnnotation'; +import usePatientInfo from './hooks/usePatientInfo'; +import { PanelStudyBrowserHeader } from './Panels/StudyBrowser/PanelStudyBrowserHeader'; +import * as utils from './utils'; +import MoreDropdownMenu from './Components/MoreDropdownMenu'; +import requestDisplaySetCreationForStudy from './Panels/requestDisplaySetCreationForStudy'; +const defaultExtension: Types.Extensions.Extension = { + /** + * Only required property. Should be a unique value across all extensions. + */ + id, + preRegistration, + onModeExit() { + useViewportGridStore.getState().clearViewportGridState(); + useUIStateStore.getState().clearUIState(); + useDisplaySetSelectorStore.getState().clearDisplaySetSelectorMap(); + useHangingProtocolStageIndexStore.getState().clearHangingProtocolStageIndexMap(); + useToggleHangingProtocolStore.getState().clearToggleHangingProtocol(); + useViewportsByPositionStore.getState().clearViewportsByPosition(); + }, + getDataSourcesModule, + getViewportModule, + getLayoutTemplateModule, + getPanelModule, + getHangingProtocolModule, + getSopClassHandlerModule, + getToolbarModule, + getCommandsModule, + getUtilityModule({ servicesManager }) { + return [ + { + name: 'common', + exports: { + getStudiesForPatientByMRN, + }, + }, + ]; + }, + + getCustomizationModule, +}; + +export default defaultExtension; + +export { + ContextMenuController, + CustomizableContextMenuTypes, + getStudiesForPatientByMRN, + dicomWebUtils, + createReportDialogPrompt, + createReportAsync, + StaticWadoClient, + cleanDenaturalizedDataset, + // Export all stores + useDisplaySetSelectorStore, + useHangingProtocolStageIndexStore, + useToggleHangingProtocolStore, + useToggleOneUpViewportGridStore, + useUIStateStore, + useViewportGridStore, + useViewportsByPositionStore, + showLabelAnnotationPopup, + callLabelAutocompleteDialog, + callInputDialog, + promptSaveReport, + promptLabelAnnotation, + colorPickerDialog, + usePatientInfo, + PanelStudyBrowserHeader, + utils, + MoreDropdownMenu, + requestDisplaySetCreationForStudy, +}; diff --git a/extensions/default/src/init.ts b/extensions/default/src/init.ts new file mode 100644 index 0000000..ee5eee7 --- /dev/null +++ b/extensions/default/src/init.ts @@ -0,0 +1,113 @@ +import { DicomMetadataStore, classes } from '@ohif/core'; +import { calculateSUVScalingFactors } from '@cornerstonejs/calculate-suv'; + +import getPTImageIdInstanceMetadata from './getPTImageIdInstanceMetadata'; +import { registerHangingProtocolAttributes } from './hangingprotocols'; + +const metadataProvider = classes.MetadataProvider; + +/** + * + * @param {Object} servicesManager + * @param {Object} configuration + */ +export default function init({ + servicesManager, + configuration = {}, + commandsManager, +}: withAppTypes): void { + const { toolbarService, cineService, viewportGridService } = servicesManager.services; + + toolbarService.registerEventForToolbarUpdate(cineService, [ + cineService.EVENTS.CINE_STATE_CHANGED, + ]); + // Add + DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.INSTANCES_ADDED, handlePETImageMetadata); + + // If the metadata for PET has changed by the user (e.g. manually changing the PatientWeight) + // we need to recalculate the SUV Scaling Factors + DicomMetadataStore.subscribe(DicomMetadataStore.EVENTS.SERIES_UPDATED, handlePETImageMetadata); + + // Adds extra custom attributes for use by hanging protocols + registerHangingProtocolAttributes({ servicesManager }); + + // Function to process and subscribe to events for a given set of commands and listeners + const subscribeToEvents = listeners => { + Object.entries(listeners).forEach(([event, commands]) => { + const supportedEvents = [ + viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED, + viewportGridService.EVENTS.VIEWPORTS_READY, + ]; + + if (supportedEvents.includes(event)) { + viewportGridService.subscribe(event, eventData => { + const viewportId = eventData?.viewportId ?? viewportGridService.getActiveViewportId(); + + commandsManager.run(commands, { viewportId }); + }); + } + }); + }; + + toolbarService.subscribe(toolbarService.EVENTS.TOOL_BAR_MODIFIED, state => { + const { buttons } = state; + for (const [id, button] of Object.entries(buttons)) { + const { groupId, items, listeners } = button.props || {}; + + // Handle group items' listeners + if (groupId && items) { + items.forEach(item => { + if (item.listeners) { + subscribeToEvents(item.listeners); + } + }); + } + + // Handle button listeners + if (listeners) { + subscribeToEvents(listeners); + } + } + }); +} + +const handlePETImageMetadata = ({ SeriesInstanceUID, StudyInstanceUID }) => { + const { instances } = DicomMetadataStore.getSeries(StudyInstanceUID, SeriesInstanceUID); + + if (!instances?.length) { + return; + } + + const modality = instances[0].Modality; + + if (!modality || modality !== 'PT') { + return; + } + + const imageIds = instances.map(instance => instance.imageId); + const instanceMetadataArray = []; + // try except block to prevent errors when the metadata is not correct + try { + imageIds.forEach(imageId => { + const instanceMetadata = getPTImageIdInstanceMetadata(imageId); + if (instanceMetadata) { + instanceMetadataArray.push(instanceMetadata); + } + }); + + if (!instanceMetadataArray.length) { + return; + } + + const suvScalingFactors = calculateSUVScalingFactors(instanceMetadataArray); + instanceMetadataArray.forEach((instanceMetadata, index) => { + metadataProvider.addCustomMetadata( + imageIds[index], + 'scalingModule', + suvScalingFactors[index] + ); + }); + } catch (error) { + console.log(error); + } +}; diff --git a/extensions/default/src/stores/index.ts b/extensions/default/src/stores/index.ts new file mode 100644 index 0000000..9921324 --- /dev/null +++ b/extensions/default/src/stores/index.ts @@ -0,0 +1,7 @@ +export { useDisplaySetSelectorStore } from './useDisplaySetSelectorStore'; +export { useHangingProtocolStageIndexStore } from './useHangingProtocolStageIndexStore'; +export { useToggleHangingProtocolStore } from './useToggleHangingProtocolStore'; +export { useToggleOneUpViewportGridStore } from './useToggleOneUpViewportGridStore'; +export { useUIStateStore } from './useUIStateStore'; +export { useViewportGridStore } from './useViewportGridStore'; +export { useViewportsByPositionStore } from './useViewportsByPositionStore'; diff --git a/extensions/default/src/stores/useDisplaySetSelectorStore.ts b/extensions/default/src/stores/useDisplaySetSelectorStore.ts new file mode 100644 index 0000000..e5973ad --- /dev/null +++ b/extensions/default/src/stores/useDisplaySetSelectorStore.ts @@ -0,0 +1,83 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +/** + * Identifier for the display set selector store type. + */ +const PRESENTATION_TYPE_ID = 'displaySetSelectorId'; + +/** + * Flag to enable or disable debug mode for the store. + * Set to `true` to enable zustand devtools. + */ +const DEBUG_STORE = false; + +/** + * State shape for the Display Set Selector store. + */ +type DisplaySetSelectorState = { + /** + * Type identifier for the store. + */ + type: string; + + /** + * Stores a mapping from `::` to `displaySetInstanceUID`. + */ + displaySetSelectorMap: Record; + + /** + * Sets the display set selector for a given key. + * + * @param key - The key. + * @param value - The `displaySetInstanceUID` to associate with the key. + */ + setDisplaySetSelector: (key: string, value: string) => void; + + /** + * Clears the entire display set selector map. + */ + clearDisplaySetSelectorMap: () => void; +}; + +/** + * Creates the Display Set Selector store. + * + * @param set - The zustand set function. + * @returns The display set selector store state and actions. + */ +const createDisplaySetSelectorStore = (set): DisplaySetSelectorState => ({ + type: PRESENTATION_TYPE_ID, + displaySetSelectorMap: {}, + + /** + * Sets the display set selector for a given key. + */ + setDisplaySetSelector: (key: string, value: string) => + set( + state => ({ + displaySetSelectorMap: { + ...state.displaySetSelectorMap, + [key]: value, + }, + }), + false, + 'setDisplaySetSelector' + ), + + /** + * Clears the entire display set selector map. + */ + clearDisplaySetSelectorMap: () => + set({ displaySetSelectorMap: {} }, false, 'clearDisplaySetSelectorMap'), +}); + +/** + * Zustand store for managing display set selectors. + * Applies devtools middleware when DEBUG_STORE is enabled. + */ +export const useDisplaySetSelectorStore = create()( + DEBUG_STORE + ? devtools(createDisplaySetSelectorStore, { name: 'DisplaySetSelectorStore' }) + : createDisplaySetSelectorStore +); diff --git a/extensions/default/src/stores/useHangingProtocolStageIndexStore.ts b/extensions/default/src/stores/useHangingProtocolStageIndexStore.ts new file mode 100644 index 0000000..4a2b0b7 --- /dev/null +++ b/extensions/default/src/stores/useHangingProtocolStageIndexStore.ts @@ -0,0 +1,76 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { Types } from '@ohif/core'; + +const PRESENTATION_TYPE_ID = 'hangingProtocolStageIndexId'; +const DEBUG_STORE = false; + +/** + * Represents the state and actions for managing hanging protocol stage indexes. + */ +type HangingProtocolStageIndexState = { + /** + * Stores a mapping from key to `HPInfo`. + */ + hangingProtocolStageIndexMap: Record; + + /** + * Sets the hanging protocol stage index for a given key. + * + * @param key - The key. + * @param value - The `HPInfo` to associate with the key. + */ + setHangingProtocolStageIndex: (key: string, value: Types.HangingProtocol.HPInfo) => void; + + /** + * Clears all hanging protocol stage indexes. + */ + clearHangingProtocolStageIndexMap: () => void; + + /** + * Type identifier for the store. + */ + type: string; +}; + +/** + * Creates the Hanging Protocol Stage Index store. + * + * @param set - The zustand set function. + * @returns The hanging protocol stage index store state and actions. + */ +const createHangingProtocolStageIndexStore = (set): HangingProtocolStageIndexState => ({ + hangingProtocolStageIndexMap: {}, + type: PRESENTATION_TYPE_ID, + + /** + * Sets the hanging protocol stage index for a given key. + */ + setHangingProtocolStageIndex: (key, value) => + set( + state => ({ + hangingProtocolStageIndexMap: { + ...state.hangingProtocolStageIndexMap, + [key]: value, + }, + }), + false, + 'setHangingProtocolStageIndex' + ), + + /** + * Clears all hanging protocol stage indexes. + */ + clearHangingProtocolStageIndexMap: () => + set({ hangingProtocolStageIndexMap: {} }, false, 'clearHangingProtocolStageIndexMap'), +}); + +/** + * Zustand store for managing hanging protocol stage indexes. + * Applies devtools middleware when DEBUG_STORE is enabled. + */ +export const useHangingProtocolStageIndexStore = create()( + DEBUG_STORE + ? devtools(createHangingProtocolStageIndexStore, { name: 'HangingProtocolStageIndexStore' }) + : createHangingProtocolStageIndexStore +); diff --git a/extensions/default/src/stores/useToggleHangingProtocolStore.ts b/extensions/default/src/stores/useToggleHangingProtocolStore.ts new file mode 100644 index 0000000..e2863ff --- /dev/null +++ b/extensions/default/src/stores/useToggleHangingProtocolStore.ts @@ -0,0 +1,76 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { Types } from '@ohif/core'; + +const PRESENTATION_TYPE_ID = 'toggleHangingProtocolId'; +const DEBUG_STORE = false; + +/** + * Represents the state and actions for managing toggle hanging protocols. + */ +type ToggleHangingProtocolState = { + /** + * Stores a mapping from key to `HPInfo`. + */ + toggleHangingProtocol: Record; + + /** + * Sets the toggle hanging protocol for a given key. + * + * @param key - The key . + * @param value - The `HPInfo` to associate with the key. + */ + setToggleHangingProtocol: (key: string, value: Types.HangingProtocol.HPInfo) => void; + + /** + * Clears all toggle hanging protocols. + */ + clearToggleHangingProtocol: () => void; + + /** + * Type identifier for the store. + */ + type: string; +}; + +/** + * Creates the Toggle Hanging Protocol store. + * + * @param set - The zustand set function. + * @returns The toggle hanging protocol store state and actions. + */ +const createToggleHangingProtocolStore = (set): ToggleHangingProtocolState => ({ + toggleHangingProtocol: {}, + type: PRESENTATION_TYPE_ID, + + /** + * Sets the toggle hanging protocol for a given key. + */ + setToggleHangingProtocol: (key, value) => + set( + state => ({ + toggleHangingProtocol: { + ...state.toggleHangingProtocol, + [key]: value, + }, + }), + false, + 'setToggleHangingProtocol' + ), + + /** + * Clears all toggle hanging protocols. + */ + clearToggleHangingProtocol: () => + set({ toggleHangingProtocol: {} }, false, 'clearToggleHangingProtocol'), +}); + +/** + * Zustand store for managing toggle hanging protocols. + * Applies devtools middleware when DEBUG_STORE is enabled. + */ +export const useToggleHangingProtocolStore = create()( + DEBUG_STORE + ? devtools(createToggleHangingProtocolStore, { name: 'ToggleHangingProtocolStore' }) + : createToggleHangingProtocolStore +); diff --git a/extensions/default/src/stores/useToggleOneUpViewportGridStore.ts b/extensions/default/src/stores/useToggleOneUpViewportGridStore.ts new file mode 100644 index 0000000..d67c08d --- /dev/null +++ b/extensions/default/src/stores/useToggleOneUpViewportGridStore.ts @@ -0,0 +1,19 @@ +import { create } from 'zustand'; + +const PRESENTATION_TYPE_ID = 'toggleOneUpViewportGridId'; + +type ToggleOneUpViewportGridState = { + toggleOneUpViewportGridStore: any | null; + setToggleOneUpViewportGridStore: (state: any) => void; + clearToggleOneUpViewportGridStore: () => void; + type: string; +}; + +// Stores the entire ViewportGridService getState when toggling to one up +// (e.g. via a double click) so that it can be restored when toggling back. +export const useToggleOneUpViewportGridStore = create(set => ({ + toggleOneUpViewportGridStore: null, + type: PRESENTATION_TYPE_ID, + setToggleOneUpViewportGridStore: state => set({ toggleOneUpViewportGridStore: state }), + clearToggleOneUpViewportGridStore: () => set({ toggleOneUpViewportGridStore: null }), +})); diff --git a/extensions/default/src/stores/useUIStateStore.ts b/extensions/default/src/stores/useUIStateStore.ts new file mode 100644 index 0000000..708e7a7 --- /dev/null +++ b/extensions/default/src/stores/useUIStateStore.ts @@ -0,0 +1,87 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +/** + * Identifier for the UI State store type. + */ +const PRESENTATION_TYPE_ID = 'uiStateId'; + +/** + * Flag to enable or disable debug mode for the store. + * Set to `true` to enable zustand devtools. + */ +const DEBUG_STORE = false; + +/** + * Represents the UI state. + */ +type UIState = { + [key: string]: unknown; +}; + +/** + * State shape for the UI State store. + */ +type UIStateStore = { + /** + * Type identifier for the store. + */ + type: string; + + /** + * Stores the UI state as a key-value mapping. + */ + uiState: UIState; + + /** + * Sets the UI state for a given key. + * + * @param key - The key to set in the UI state. + * @param value - The value to associate with the key. + */ + setUIState: (key: string, value: unknown) => void; + + /** + * Clears all UI state. + */ + clearUIState: () => void; +}; + +/** + * Creates the UI State store. + * + * @param set - The zustand set function. + * @returns The UI State store state and actions. + */ +const createUIStateStore = (set): UIStateStore => ({ + type: PRESENTATION_TYPE_ID, + uiState: {}, + + /** + * Sets the UI state for a given key. + */ + setUIState: (key, value) => + set( + state => ({ + uiState: { + ...state.uiState, + [key]: value, + }, + }), + false, + 'setUIState' + ), + + /** + * Clears all UI state. + */ + clearUIState: () => set({ uiState: {} }, false, 'clearUIState'), +}); + +/** + * Zustand store for managing UI state. + * Applies devtools middleware when DEBUG_STORE is enabled. + */ +export const useUIStateStore = create()( + DEBUG_STORE ? devtools(createUIStateStore, { name: 'UIStateStore' }) : createUIStateStore +); diff --git a/extensions/default/src/stores/useViewportGridStore.ts b/extensions/default/src/stores/useViewportGridStore.ts new file mode 100644 index 0000000..77655c9 --- /dev/null +++ b/extensions/default/src/stores/useViewportGridStore.ts @@ -0,0 +1,89 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +/** + * Identifier for the viewport grid store type. + */ +const PRESENTATION_TYPE_ID = 'viewportGridId'; + +/** + * Flag to enable or disable debug mode for the store. + * Set to `true` to enable zustand devtools. + */ +const DEBUG_STORE = false; + +/** + * Represents the state of the viewport grid. + */ +type ViewportGridState = { + [key: string]: unknown; +}; + +/** + * State shape for the Viewport Grid store. + */ +type ViewportGridStore = { + /** + * Type identifier for the store. + */ + type: string; + + /** + * Stores the viewport grid state as a key-value mapping. + */ + viewportGridState: ViewportGridState; + + /** + * Sets the viewport grid state for a given key. + * + * @param key - The key to set in the viewport grid state. + * @param value - The value to associate with the key. + */ + setViewportGridState: (key: string, value: unknown) => void; + + /** + * Clears the entire viewport grid state. + */ + clearViewportGridState: () => void; +}; + +/** + * Creates the Viewport Grid store. + * + * @param set - The zustand set function. + * @returns The Viewport Grid store state and actions. + */ +const createViewportGridStore = (set): ViewportGridStore => ({ + type: PRESENTATION_TYPE_ID, + viewportGridState: {}, + + /** + * Sets the viewport grid state for a given key. + */ + setViewportGridState: (key, value) => + set( + state => ({ + viewportGridState: { + ...state.viewportGridState, + [key]: value, + }, + }), + false, + 'setViewportGridState' + ), + + /** + * Clears the entire viewport grid state. + */ + clearViewportGridState: () => set({ viewportGridState: {} }, false, 'clearViewportGridState'), +}); + +/** + * Zustand store for managing viewport grid state. + * Applies devtools middleware when DEBUG_STORE is enabled. + */ +export const useViewportGridStore = create()( + DEBUG_STORE + ? devtools(createViewportGridStore, { name: 'ViewportGridStore' }) + : createViewportGridStore +); diff --git a/extensions/default/src/stores/useViewportsByPositionStore.ts b/extensions/default/src/stores/useViewportsByPositionStore.ts new file mode 100644 index 0000000..1dadcac --- /dev/null +++ b/extensions/default/src/stores/useViewportsByPositionStore.ts @@ -0,0 +1,100 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +const PRESENTATION_TYPE_ID = 'viewportsByPositionId'; +const DEBUG_STORE = false; + +/** + * Represents the state and actions for managing viewports by position. + */ +type ViewportsByPositionState = { + /** + * Type identifier for the store. + */ + type: string; + + /** + * Stores viewports indexed by their position. + */ + viewportsByPosition: Record; + + /** + * Stores initial display viewports as an array of strings. + */ + initialInDisplay: string[]; + + /** + * Sets the viewport for a given key. + * + * @param key - The key identifying the viewport position. + * @param value - The viewport data to associate with the key. + */ + setViewportsByPosition: (key: string, value: unknown) => void; + + /** + * Clears all viewports by position. + */ + clearViewportsByPosition: () => void; + + /** + * Adds an initial display viewport. + * + * @param value - The viewport identifier to add. + */ + addInitialInDisplay: (value: string) => void; +}; + +/** + * Creates the Viewports By Position store. + * + * @param set - The zustand set function. + * @returns The Viewports By Position store state and actions. + */ +const createViewportsByPositionStore = (set): ViewportsByPositionState => ({ + type: PRESENTATION_TYPE_ID, + viewportsByPosition: {}, + initialInDisplay: [], + + /** + * Sets the viewport for a given key. + */ + setViewportsByPosition: (key, value) => + set( + state => ({ + viewportsByPosition: { + ...state.viewportsByPosition, + [key]: value, + }, + }), + false, + 'setViewportsByPosition' + ), + + /** + * Clears all viewports by position. + */ + clearViewportsByPosition: () => + set({ viewportsByPosition: {} }, false, 'clearViewportsByPosition'), + + /** + * Adds an initial display viewport. + */ + addInitialInDisplay: value => + set( + state => ({ + initialInDisplay: [...state.initialInDisplay, value], + }), + false, + 'addInitialInDisplay' + ), +}); + +/** + * Zustand store for managing viewports by position. + * Applies devtools middleware when DEBUG_STORE is enabled. + */ +export const useViewportsByPositionStore = create()( + DEBUG_STORE + ? devtools(createViewportsByPositionStore, { name: 'ViewportsByPositionStore' }) + : createViewportsByPositionStore +); diff --git a/extensions/default/src/types/commandModuleTypes.tsx b/extensions/default/src/types/commandModuleTypes.tsx new file mode 100644 index 0000000..d2cdde1 --- /dev/null +++ b/extensions/default/src/types/commandModuleTypes.tsx @@ -0,0 +1,6 @@ +export type NavigateHistory = { + to: string; // the URL to navigate to + options?: { + replace?: boolean; // replace or add/push to history? + }; +}; diff --git a/extensions/default/src/utils/_shared/PROMPT_RESPONSES.ts b/extensions/default/src/utils/_shared/PROMPT_RESPONSES.ts new file mode 100644 index 0000000..39e2e61 --- /dev/null +++ b/extensions/default/src/utils/_shared/PROMPT_RESPONSES.ts @@ -0,0 +1,10 @@ +const RESPONSE = { + NO_NEVER: -1, + CANCEL: 0, + CREATE_REPORT: 1, + ADD_SERIES: 2, + SET_STUDY_AND_SERIES: 3, + NO_NOT_FOR_SERIES: 4, +}; + +export default RESPONSE; diff --git a/extensions/default/src/utils/addIcon.ts b/extensions/default/src/utils/addIcon.ts new file mode 100644 index 0000000..03bbf2c --- /dev/null +++ b/extensions/default/src/utils/addIcon.ts @@ -0,0 +1,8 @@ +import { addIcon as addIconUI } from '@ohif/ui'; +import { Icons } from '@ohif/ui-next'; + +/** Adds the icon to both ui and ui-next */ +export function addIcon(name, icon) { + addIconUI(name, icon); + Icons.addIcon(name, icon); +} diff --git a/extensions/default/src/utils/calculateScanAxisNormal.ts b/extensions/default/src/utils/calculateScanAxisNormal.ts new file mode 100644 index 0000000..fe278f4 --- /dev/null +++ b/extensions/default/src/utils/calculateScanAxisNormal.ts @@ -0,0 +1,20 @@ +import { vec3 } from 'gl-matrix'; + +/** + * Calculates the scanAxisNormal based on a image orientation vector extract from a frame + * @param {*} imageOrientation + * @returns + */ +export default function calculateScanAxisNormal(imageOrientation) { + const rowCosineVec = vec3.fromValues( + imageOrientation[0], + imageOrientation[1], + imageOrientation[2] + ); + const colCosineVec = vec3.fromValues( + imageOrientation[3], + imageOrientation[4], + imageOrientation[5] + ); + return vec3.cross(vec3.create(), rowCosineVec, colCosineVec); +} diff --git a/extensions/default/src/utils/callInputDialog.tsx b/extensions/default/src/utils/callInputDialog.tsx new file mode 100644 index 0000000..dcd893f --- /dev/null +++ b/extensions/default/src/utils/callInputDialog.tsx @@ -0,0 +1,169 @@ +import React from 'react'; +import { Input, Dialog, ButtonEnums, LabellingFlow } from '@ohif/ui'; + +/** + * + * @param {*} data + * @param {*} data.text + * @param {*} data.label + * @param {*} event + * @param {*} callback + * @param {*} isArrowAnnotateInputDialog + * @param {*} dialogConfig + * @param {string?} dialogConfig.dialogTitle - title of the input dialog + * @param {string?} dialogConfig.inputLabel - show label above the input + */ + +export function callInputDialog( + uiDialogService, + data, + callback, + isArrowAnnotateInputDialog = true, + dialogConfig: any = {} +) { + const dialogId = 'dialog-enter-annotation'; + const label = data ? (isArrowAnnotateInputDialog ? data.text : data.label) : ''; + const { + dialogTitle = 'Annotation', + inputLabel = 'Enter your annotation', + validateFunc = value => true, + } = dialogConfig; + + const onSubmitHandler = ({ action, value }) => { + switch (action.id) { + case 'save': + if (typeof validateFunc === 'function' && !validateFunc(value.label)) { + return; + } + + callback(value.label, action.id); + break; + case 'cancel': + callback('', action.id); + break; + } + uiDialogService.dismiss({ id: dialogId }); + }; + + if (uiDialogService) { + uiDialogService.create({ + id: dialogId, + centralize: true, + isDraggable: false, + showOverlay: true, + content: Dialog, + contentProps: { + title: dialogTitle, + value: { label }, + noCloseButton: true, + onClose: () => uiDialogService.dismiss({ id: dialogId }), + actions: [ + { id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary }, + { id: 'save', text: 'Save', type: ButtonEnums.type.primary }, + ], + onSubmit: onSubmitHandler, + body: ({ value, setValue }) => { + return ( + { + event.persist(); + setValue(value => ({ ...value, label: event.target.value })); + }} + onKeyPress={event => { + if (event.key === 'Enter') { + onSubmitHandler({ value, action: { id: 'save' } }); + } + }} + /> + ); + }, + }, + }); + } +} + +export function callLabelAutocompleteDialog( + uiDialogService, + callback, + dialogConfig, + labelConfig, + renderContent = LabellingFlow +) { + const exclusive = labelConfig ? labelConfig.exclusive : false; + const dropDownItems = labelConfig ? labelConfig.items : []; + + const { validateFunc = value => true } = dialogConfig; + + const labellingDoneCallback = value => { + if (typeof value === 'string') { + if (typeof validateFunc === 'function' && !validateFunc(value)) { + return; + } + callback(value, 'save'); + } else { + callback('', 'cancel'); + } + uiDialogService.dismiss({ id: 'select-annotation' }); + }; + + uiDialogService.create({ + id: 'select-annotation', + centralize: true, + isDraggable: false, + showOverlay: true, + content: renderContent, + contentProps: { + labellingDoneCallback: labellingDoneCallback, + measurementData: { label: '' }, + componentClassName: {}, + labelData: dropDownItems, + exclusive: exclusive, + }, + }); +} + +export function showLabelAnnotationPopup( + measurement, + uiDialogService, + labelConfig, + renderContent = LabellingFlow +) { + const exclusive = labelConfig ? labelConfig.exclusive : false; + const dropDownItems = labelConfig ? labelConfig.items : []; + return new Promise>((resolve, reject) => { + const labellingDoneCallback = value => { + uiDialogService.dismiss({ id: 'select-annotation' }); + if (typeof value === 'string') { + measurement.label = value; + } + resolve(measurement); + }; + + uiDialogService.create({ + id: 'select-annotation', + isDraggable: false, + showOverlay: true, + content: renderContent, + defaultPosition: { + x: window.innerWidth / 2, + y: window.innerHeight / 2, + }, + contentProps: { + labellingDoneCallback: labellingDoneCallback, + measurementData: measurement, + componentClassName: {}, + labelData: dropDownItems, + exclusive: exclusive, + }, + }); + }); +} + +export default callInputDialog; diff --git a/extensions/default/src/utils/colorPickerDialog.css b/extensions/default/src/utils/colorPickerDialog.css new file mode 100644 index 0000000..1c6bb20 --- /dev/null +++ b/extensions/default/src/utils/colorPickerDialog.css @@ -0,0 +1,3 @@ +.chrome-picker { + background: #090c29 !important; +} diff --git a/extensions/default/src/utils/colorPickerDialog.tsx b/extensions/default/src/utils/colorPickerDialog.tsx new file mode 100644 index 0000000..9de59ee --- /dev/null +++ b/extensions/default/src/utils/colorPickerDialog.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { Dialog } from '@ohif/ui'; +import { ChromePicker } from 'react-color'; + +import './colorPickerDialog.css'; + +function colorPickerDialog(uiDialogService, rgbaColor, callback) { + const dialogId = 'pick-color'; + + const onSubmitHandler = ({ action, value }) => { + switch (action.id) { + case 'save': + callback(value.rgbaColor, action.id); + break; + case 'cancel': + callback('', action.id); + break; + } + uiDialogService.dismiss({ id: dialogId }); + }; + + if (uiDialogService) { + uiDialogService.create({ + id: dialogId, + centralize: true, + isDraggable: false, + showOverlay: true, + content: Dialog, + contentProps: { + title: 'Segment Color', + value: { rgbaColor }, + noCloseButton: true, + onClose: () => uiDialogService.dismiss({ id: dialogId }), + actions: [ + { id: 'cancel', text: 'Cancel', type: 'primary' }, + { id: 'save', text: 'Save', type: 'secondary' }, + ], + onSubmit: onSubmitHandler, + body: ({ value, setValue }) => { + const handleChange = color => { + setValue({ rgbaColor: color.rgb }); + }; + + return ( + + ); + }, + }, + }); + } +} + +export default colorPickerDialog; diff --git a/extensions/default/src/utils/createRenderedRetrieve.js b/extensions/default/src/utils/createRenderedRetrieve.js new file mode 100644 index 0000000..ff49040 --- /dev/null +++ b/extensions/default/src/utils/createRenderedRetrieve.js @@ -0,0 +1,32 @@ +/** + * Generates the rendered URL that can be used for direct retrieve of the pixel data binary stream. + * + * @param {object} config - The configuration object. + * @param {string} config.wadoRoot - The root URL for the WADO service. + * @param {object} params - The parameters object. + * @param {string} params.tag - The tag name of the URL to retrieve. + * @param {string} params.defaultPath - The path for the pixel data URL. + * @param {object} params.instance - The instance object that the tag is in. + * @param {string} params.defaultType - The mime type of the response. + * @param {string} params.singlepart - The type of the part to retrieve. + * @param {string} params.fetchPart - Unknown parameter. + * @param {string} params.url - Unknown parameter. + * @returns {string|Promise} - An absolute URL to the binary stream. + */ +const createRenderedRetrieve = (config, params) => { + const { wadoRoot } = config; + const { instance, tag = 'PixelData' } = params; + const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance; + const bulkDataURI = instance[tag]?.BulkDataURI ?? ''; + + if (bulkDataURI?.indexOf('?') !== -1) { + // The value instance has parameters, so it should not revert to the rendered + return; + } + + if (tag === 'PixelData' || tag === 'EncapsulatedDocument') { + return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/rendered`; + } +}; + +export default createRenderedRetrieve; diff --git a/extensions/default/src/utils/createRenderedRetrieve.test.js b/extensions/default/src/utils/createRenderedRetrieve.test.js new file mode 100644 index 0000000..f0d7bec --- /dev/null +++ b/extensions/default/src/utils/createRenderedRetrieve.test.js @@ -0,0 +1,46 @@ +import createRenderedRetrieve from './createRenderedRetrieve'; + +describe('createRenderedRetrieve', () => { + const config = { + wadoRoot: 'https://example.com/wado', + }; + + const params = { + instance: { + StudyInstanceUID: 'study-uid', + SeriesInstanceUID: 'series-uid', + SOPInstanceUID: 'sop-uid', + }, + }; + + it('should return the rendered URL for PixelData tag', () => { + const result = createRenderedRetrieve(config, { + ...params, + tag: 'PixelData', + }); + + expect(result).toBe( + 'https://example.com/wado/studies/study-uid/series/series-uid/instances/sop-uid/rendered' + ); + }); + + it('should return the rendered URL for EncapsulatedDocument tag', () => { + const result = createRenderedRetrieve(config, { + ...params, + tag: 'EncapsulatedDocument', + }); + + expect(result).toBe( + 'https://example.com/wado/studies/study-uid/series/series-uid/instances/sop-uid/rendered' + ); + }); + + it('should return undefined for unknown tag', () => { + const result = createRenderedRetrieve(config, { + ...params, + tag: 'UnknownTag', + }); + + expect(result).toBeUndefined(); + }); +}); diff --git a/extensions/default/src/utils/findSRWithSameSeriesDescription.ts b/extensions/default/src/utils/findSRWithSameSeriesDescription.ts new file mode 100644 index 0000000..af565c9 --- /dev/null +++ b/extensions/default/src/utils/findSRWithSameSeriesDescription.ts @@ -0,0 +1,41 @@ +import { DisplaySetService, Types } from '@ohif/core'; + +import getNextSRSeriesNumber from './getNextSRSeriesNumber'; + +/** + * Find an SR having the same series description. + * This is used by the store service in order to store DICOM SR's having the + * same Series Description into a single series under consecutive instance numbers + * That way, they are all organized as a set and could have tools to view + * "prior" SR instances. + * + * @param SeriesDescription - is the description to look for + * @param displaySetService - the display sets to search for DICOM SR in + * @returns SeriesMetadata from a DICOM SR having the same series description + */ +export default function findSRWithSameSeriesDescription( + SeriesDescription: string, + displaySetService: DisplaySetService +): Types.SeriesMetadata { + const activeDisplaySets = displaySetService.getActiveDisplaySets(); + const srDisplaySets = activeDisplaySets.filter(ds => ds.Modality === 'SR'); + const sameSeries = srDisplaySets.find(ds => ds.SeriesDescription === SeriesDescription); + if (sameSeries) { + console.log('Storing to same series', sameSeries); + const { instance } = sameSeries; + const { SeriesInstanceUID, SeriesDescription, SeriesDate, SeriesTime, SeriesNumber, Modality } = + instance; + return { + SeriesInstanceUID, + SeriesDescription, + SeriesDate, + SeriesTime, + SeriesNumber, + Modality, + InstanceNumber: sameSeries.instances.length + 1, + }; + } + + const SeriesNumber = getNextSRSeriesNumber(displaySetService); + return { SeriesDescription, SeriesNumber }; +} diff --git a/extensions/default/src/utils/getBulkdataValue.js b/extensions/default/src/utils/getBulkdataValue.js new file mode 100644 index 0000000..8857bab --- /dev/null +++ b/extensions/default/src/utils/getBulkdataValue.js @@ -0,0 +1,45 @@ +/** + * Generates a URL that can be used for direct retrieve of the bulkdata. + * + * @param {object} config - The configuration object. + * @param {object} params - The parameters object. + * @param {string} params.tag - The tag name of the URL to retrieve. + * @param {string} params.defaultPath - The path for the pixel data URL. + * @param {object} params.instance - The instance object that the tag is in. + * @param {string} params.defaultType - The mime type of the response. + * @param {string} params.singlepart - The type of the part to retrieve. + * @param {string} params.fetchPart - Unknown. + * @returns {string|Promise} - An absolute URL to the resource, if the absolute URL can be retrieved as singlepart, + * or is already retrieved, or a promise to a URL for such use if a BulkDataURI. + */ +const getBulkdataValue = (config, params) => { + const { + instance, + tag = 'PixelData', + defaultPath = '/pixeldata', + defaultType = 'video/mp4', + } = params; + + const value = instance[tag]; + + const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance; + + const BulkDataURI = + (value && value.BulkDataURI) || + `series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`; + const hasQuery = BulkDataURI.indexOf('?') !== -1; + const hasAccept = BulkDataURI.indexOf('accept=') !== -1; + const acceptUri = + BulkDataURI + (hasAccept ? '' : (hasQuery ? '&' : '?') + `accept=${defaultType}`); + + if (acceptUri.startsWith('series/')) { + const { wadoRoot } = config; + return `${wadoRoot}/studies/${StudyInstanceUID}/${acceptUri}`; + } + + // The DICOMweb standard states that the default is multipart related, and then + // separately states that the accept parameter is the URL parameter equivalent of the accept header. + return acceptUri; +}; + +export default getBulkdataValue; diff --git a/extensions/default/src/utils/getBulkdataValue.test.js b/extensions/default/src/utils/getBulkdataValue.test.js new file mode 100644 index 0000000..542358a --- /dev/null +++ b/extensions/default/src/utils/getBulkdataValue.test.js @@ -0,0 +1,105 @@ +import getBulkdataValue from './getBulkdataValue'; + +jest.mock('@ohif/core'); + +global.URL.createObjectURL = jest.fn(() => 'blob:'); + +describe('getBulkdataValue', () => { + const config = { + singlepart: true, + }; + + const params = { + instance: { + StudyInstanceUID: 'study-uid', + SeriesInstanceUID: 'series-uid', + SOPInstanceUID: 'sop-uid', + }, + }; + + it('should return the BulkDataURI with defaultType if singlepart is true without accept', () => { + const value = { + BulkDataURI: 'https://example.com/bulkdata', + retrieveBulkData: jest.fn().mockResolvedValueOnce(new Uint8Array([0, 1, 2])), + }; + + const result = getBulkdataValue(config, { + ...params, + tag: 'PixelData', + instance: { + ...params.instance, + PixelData: value, + }, + }); + + expect(result).toContain(value.BulkDataURI); + expect(result).toContain('accept=video/mp4'); + const acceptCount = result.match(/accept=video\/mp4/g)?.length || 0; + expect(acceptCount).toBe(1); + }); + + it('should return the BulkDataURI with accept', () => { + const value = { + BulkDataURI: 'https://example.com/bulkdata?accept=video/mp4', + }; + + const result = getBulkdataValue(config, { + ...params, + tag: 'PixelData', + instance: { + ...params.instance, + PixelData: value, + }, + }); + + expect(result).toContain(value.BulkDataURI); + expect(result).toContain('accept=video/mp4'); + const acceptCount = result.match(/accept=video\/mp4/g)?.length || 0; + expect(acceptCount).toBe(1); + }); + + it('should return the BulkDataURI with accept and query params', () => { + const value = { + BulkDataURI: 'https://example.com/bulkdata?test=123', + }; + + const result = getBulkdataValue(config, { + ...params, + tag: 'PixelData', + instance: { + ...params.instance, + PixelData: value, + }, + }); + + expect(result).toContain(value.BulkDataURI); + expect(result).toContain('accept=video/mp4'); + expect(result).toContain('test=123'); + const acceptCount = result.match(/accept=video\/mp4/g)?.length || 0; + expect(acceptCount).toBe(1); + }); + + it('should return default path with accept', () => { + const value = { + BulkDataURI: null, + }; + + const defaultPath = '/testing'; + const defaultURI = `series/${params.instance.SeriesInstanceUID}/instances/${params.instance.SOPInstanceUID}${defaultPath}`; + + const result = getBulkdataValue(config, { + ...params, + defaultPath, + tag: 'PixelData', + instance: { + ...params.instance, + PixelData: value, + }, + }); + + expect(result).toContain(defaultURI); + expect(result).toContain('accept=video/mp4'); + const acceptCount = result.match(/accept=video\/mp4/g)?.length || 0; + expect(acceptCount).toBe(1); + }); +}); diff --git a/extensions/default/src/utils/getDirectURL.test.js b/extensions/default/src/utils/getDirectURL.test.js new file mode 100644 index 0000000..eb13e5a --- /dev/null +++ b/extensions/default/src/utils/getDirectURL.test.js @@ -0,0 +1,169 @@ +import getDirectURL from './getDirectURL'; +import getBulkdataValue from './getBulkdataValue'; +import createRenderedRetrieve from './createRenderedRetrieve'; + +jest.mock('@ohif/core'); +jest.mock('./getBulkdataValue'); +jest.mock('./createRenderedRetrieve'); + +global.URL.createObjectURL = jest.fn(() => 'blob:'); + +describe('getDirectURL', () => { + const config = { + singlepart: true, + defaultType: 'video/mp4', + }; + + const params = { + tag: 'PixelData', + defaultPath: '/path/to/pixeldata', + instance: { + StudyInstanceUID: 'study-uid', + SeriesInstanceUID: 'series-uid', + SOPInstanceUID: 'sop-uid', + }, + }; + + it('should return the provided URL if it exists', () => { + const url = 'https://example.com/direct-retrieve'; + + const result = getDirectURL(config, { + ...params, + url: 'https://example.com/direct-retrieve', + }); + + expect(result).toBe(url); + }); + + it('should return the DirectRetrieveURL if it exists', () => { + const value = { + DirectRetrieveURL: 'https://example.com/direct-retrieve', + }; + + const result = getDirectURL(config, { + ...params, + tag: 'PixelData', + instance: { + ...params.instance, + PixelData: value, + }, + }); + + expect(result).toBe(value.DirectRetrieveURL); + }); + + it('should return the URL for InlineBinary', () => { + const value = { + InlineBinary: 'base64-encoded-data', + }; + + const result = getDirectURL(config, { + ...params, + tag: 'PixelData', + instance: { + ...params.instance, + PixelData: value, + }, + }); + + expect(result).toContain('blob:'); + }); + + it('should return the BulkDataURI with defaultType if singlepart is false and there is no retrieveBulkData', () => { + const value = { + BulkDataURI: 'https://example.com/bulkdata', + }; + + const result = getDirectURL( + { + ...config, + singlepart: false, + }, + { + ...params, + tag: 'PixelData', + instance: { + ...params.instance, + PixelData: value, + }, + } + ); + + expect(result).toBeUndefined(); + }); + + it('should return the BulkDataURI with defaultType if singlepart is false with retrieveBulkData', async () => { + const value = { + BulkDataURI: 'https://example.com/bulkdata', + retrieveBulkData: jest.fn().mockResolvedValueOnce(new Uint8Array([0, 1, 2])), + }; + + const result = await getDirectURL( + { + ...config, + singlepart: false, + }, + { + ...params, + tag: 'PixelData', + instance: { + ...params.instance, + PixelData: value, + }, + } + ); + + expect(result).toContain('blob:'); + }); + + it('should return the BulkDataURI with defaultType if singlepart does not include fetchPart', async () => { + const arr = new Uint8Array([0, 1, 2]); + + const value = { + BulkDataURI: 'https://example.com/bulkdata', + retrieveBulkData: jest.fn().mockResolvedValueOnce(arr), + }; + + const result = await getDirectURL( + { + ...config, + singlepart: ['audio'], + }, + { + ...params, + tag: 'PixelData', + instance: { + ...params.instance, + PixelData: value, + }, + } + ); + + expect(result).toContain('blob:'); + expect(URL.createObjectURL).toHaveBeenCalledWith(new Blob([arr], { type: 'accept=video/mp4' })); + }); + + it('should return the URL from getBulkdataValue if it exists', () => { + const bulkDataURL = 'https://example.com/bulkdata'; + + getBulkdataValue.mockReturnValueOnce(bulkDataURL); + + const result = getDirectURL(config, params); + + expect(getBulkdataValue).toHaveBeenCalledWith(config, params); + expect(result).toBe(bulkDataURL); + }); + + it('should return the URL from createRenderedRetrieve if getBulkdataValue returns falsy', () => { + const renderedRetrieveURL = 'https://example.com/rendered-retrieve'; + + getBulkdataValue.mockReturnValueOnce(null); + createRenderedRetrieve.mockReturnValueOnce(renderedRetrieveURL); + + const result = getDirectURL(config, params); + + expect(getBulkdataValue).toHaveBeenCalledWith(config, params); + expect(createRenderedRetrieve).toHaveBeenCalledWith(config, params); + expect(result).toBe(renderedRetrieveURL); + }); +}); diff --git a/extensions/default/src/utils/getDirectURL.ts b/extensions/default/src/utils/getDirectURL.ts new file mode 100644 index 0000000..54e7745 --- /dev/null +++ b/extensions/default/src/utils/getDirectURL.ts @@ -0,0 +1,65 @@ +import { utils } from '@ohif/core'; + +import getBulkdataValue from './getBulkdataValue'; +import createRenderedRetrieve from './createRenderedRetrieve'; + +/** + * Generates a URL that can be used for direct retrieve of the bulkdata + * + * @param {object} params + * @param {string} params.tag is the tag name of the URL to retrieve + * @param {string} params.defaultPath path for the pixel data url + * @param {object} params.instance is the instance object that the tag is in + * @param {string} params.defaultType is the mime type of the response + * @param {string} params.singlepart is the type of the part to retrieve + * @param {string} params.fetchPart unknown? + * @param {string} params.url unknown? + * @returns an absolute URL to the resource, if the absolute URL can be retrieved as singlepart, + * or is already retrieved, or a promise to a URL for such use if a BulkDataURI + */ +const getDirectURL = (config, params) => { + const { singlepart } = config; + const { + instance, + tag = 'PixelData', + defaultType = 'video/mp4', + singlepart: fetchPart = 'video', + url = null, + } = params; + + if (url) { + return url; + } + + const value = instance[tag]; + if (value) { + if (value.DirectRetrieveURL) { + return value.DirectRetrieveURL; + } + + if (value.InlineBinary) { + const blob = utils.b64toBlob(value.InlineBinary, defaultType); + value.DirectRetrieveURL = URL.createObjectURL(blob); + return value.DirectRetrieveURL; + } + + if (!singlepart || (singlepart !== true && singlepart.indexOf(fetchPart) === -1)) { + if (value.retrieveBulkData) { + // Try the specified retrieve type. + const options = { + mediaType: defaultType, + }; + return value.retrieveBulkData(options).then(arr => { + value.DirectRetrieveURL = URL.createObjectURL(new Blob([arr], { type: defaultType })); + return value.DirectRetrieveURL; + }); + } + console.warn('Unable to retrieve', tag, 'from', instance); + return undefined; + } + } + + return createRenderedRetrieve(config, params) || getBulkdataValue(config, params); +}; + +export default getDirectURL; diff --git a/extensions/default/src/utils/getNextSRSeriesNumber.js b/extensions/default/src/utils/getNextSRSeriesNumber.js new file mode 100644 index 0000000..c980aae --- /dev/null +++ b/extensions/default/src/utils/getNextSRSeriesNumber.js @@ -0,0 +1,10 @@ +const MIN_SR_SERIES_NUMBER = 4700; + +export default function getNextSRSeriesNumber(displaySetService) { + const activeDisplaySets = displaySetService.getActiveDisplaySets(); + const srDisplaySets = activeDisplaySets.filter(ds => ds.Modality === 'SR'); + const srSeriesNumbers = srDisplaySets.map(ds => ds.SeriesNumber); + const maxSeriesNumber = Math.max(...srSeriesNumbers, MIN_SR_SERIES_NUMBER); + + return maxSeriesNumber + 1; +} diff --git a/extensions/default/src/utils/index.ts b/extensions/default/src/utils/index.ts new file mode 100644 index 0000000..81118c6 --- /dev/null +++ b/extensions/default/src/utils/index.ts @@ -0,0 +1 @@ +export { addIcon } from './addIcon'; diff --git a/extensions/default/src/utils/promptLabelAnnotation.js b/extensions/default/src/utils/promptLabelAnnotation.js new file mode 100644 index 0000000..eb3cbaa --- /dev/null +++ b/extensions/default/src/utils/promptLabelAnnotation.js @@ -0,0 +1,43 @@ +import { showLabelAnnotationPopup } from './callInputDialog'; + +function promptLabelAnnotation({ servicesManager }, ctx, evt) { + const { measurementService, customizationService, toolGroupService } = servicesManager.services; + const { viewportId, StudyInstanceUID, SeriesInstanceUID, measurementId, toolName } = evt; + return new Promise(async function (resolve) { + const toolGroup = toolGroupService.getToolGroupForViewport(viewportId); + const activeToolOptions = toolGroup.getToolConfiguration(toolName); + if(activeToolOptions.getTextCallback) { + resolve({ + StudyInstanceUID, + SeriesInstanceUID, + viewportId, + }) + } else { + const labelConfig = customizationService.getCustomization('measurementLabels'); + const measurement = measurementService.getMeasurement(measurementId); + const renderContent = customizationService.getCustomization('ui.labellingComponent'); + const value = await showLabelAnnotationPopup( + measurement, + servicesManager.services.uiDialogService, + labelConfig, + renderContent + ); + + measurementService.update( + measurementId, + { + ...value, + }, + true + ); + + resolve({ + StudyInstanceUID, + SeriesInstanceUID, + viewportId, + }); + } + }); +} + +export default promptLabelAnnotation; diff --git a/extensions/default/src/utils/promptSaveReport.js b/extensions/default/src/utils/promptSaveReport.js new file mode 100644 index 0000000..ae5c53b --- /dev/null +++ b/extensions/default/src/utils/promptSaveReport.js @@ -0,0 +1,75 @@ +import createReportAsync from '../Actions/createReportAsync'; +import { createReportDialogPrompt } from '../Panels'; +import getNextSRSeriesNumber from './getNextSRSeriesNumber'; +import PROMPT_RESPONSES from './_shared/PROMPT_RESPONSES'; + +async function promptSaveReport({ servicesManager, commandsManager, extensionManager }, ctx, evt) { + const { uiDialogService, measurementService, displaySetService } = servicesManager.services; + const viewportId = evt.viewportId === undefined ? evt.data.viewportId : evt.viewportId; + const isBackupSave = evt.isBackupSave === undefined ? evt.data.isBackupSave : evt.isBackupSave; + const StudyInstanceUID = evt?.data?.StudyInstanceUID; + const SeriesInstanceUID = evt?.data?.SeriesInstanceUID; + + const { trackedStudy, trackedSeries } = ctx; + let displaySetInstanceUIDs; + + try { + const promptResult = await createReportDialogPrompt(uiDialogService, { + extensionManager, + }); + + if (promptResult.action === PROMPT_RESPONSES.CREATE_REPORT) { + const dataSources = extensionManager.getDataSources(); + const dataSource = dataSources[0]; + const measurements = measurementService.getMeasurements(); + const trackedMeasurements = measurements + .filter( + m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID) + ) + .filter(m => m.referencedImageId != null); + + const SeriesDescription = + // isUndefinedOrEmpty + promptResult.value === undefined || promptResult.value === '' + ? 'Research Derived Series' // default + : promptResult.value; // provided value + + const SeriesNumber = getNextSRSeriesNumber(displaySetService); + + const getReport = async () => { + return commandsManager.runCommand( + 'storeMeasurements', + { + measurementData: trackedMeasurements, + dataSource, + additionalFindingTypes: ['ArrowAnnotate'], + options: { + SeriesDescription, + SeriesNumber, + }, + }, + 'CORNERSTONE_STRUCTURED_REPORT' + ); + }; + displaySetInstanceUIDs = await createReportAsync({ + servicesManager, + getReport, + }); + } else if (promptResult.action === RESPONSE.CANCEL) { + // Do nothing + } + + return { + userResponse: promptResult.action, + createdDisplaySetInstanceUIDs: displaySetInstanceUIDs, + StudyInstanceUID, + SeriesInstanceUID, + viewportId, + isBackupSave, + }; + } catch (error) { + return null; + } +} + +export default promptSaveReport; diff --git a/extensions/default/src/utils/reuseCachedLayouts.ts b/extensions/default/src/utils/reuseCachedLayouts.ts new file mode 100644 index 0000000..3e6f728 --- /dev/null +++ b/extensions/default/src/utils/reuseCachedLayouts.ts @@ -0,0 +1,84 @@ +import { HangingProtocolService, Types } from '@ohif/core'; +import { useViewportGridStore } from '../stores/useViewportGridStore'; +import { useDisplaySetSelectorStore } from '../stores/useDisplaySetSelectorStore'; +import { useHangingProtocolStageIndexStore } from '../stores/useHangingProtocolStageIndexStore'; + +export type ReturnType = { + hangingProtocolStageIndexMap: Record; + viewportGridStore: Record; + displaySetSelectorMap: Record; +}; + +/** + * Calculates a set of state information for hanging protocols and viewport grid + * which defines the currently applied hanging protocol state. + * @param state is the viewport grid state + * @param syncService is the state sync service to use for getting existing state + * @returns Set of states that can be applied to the state sync to remember + * the current view state. + */ +const reuseCachedLayout = (state, hangingProtocolService: HangingProtocolService): ReturnType => { + const { activeViewportId } = state; + const { protocol } = hangingProtocolService.getActiveProtocol(); + + if (!protocol) { + return; + } + + const hpInfo = hangingProtocolService.getState(); + const { protocolId, stageIndex, activeStudyUID } = hpInfo; + + const { viewportGridState, setViewportGridState } = useViewportGridStore.getState(); + const { displaySetSelectorMap, setDisplaySetSelector } = useDisplaySetSelectorStore.getState(); + const { hangingProtocolStageIndexMap, setHangingProtocolStageIndex } = + useHangingProtocolStageIndexStore.getState(); + + const stage = protocol.stages[stageIndex]; + const storeId = `${activeStudyUID}:${protocolId}:${stageIndex}`; + const cacheId = `${activeStudyUID}:${protocolId}`; + const { rows, columns } = stage.viewportStructure.properties; + const custom = + stage.viewports.length !== state.viewports.size || + state.layout.numRows !== rows || + state.layout.numCols !== columns; + + hangingProtocolStageIndexMap[cacheId] = hpInfo; + + if (storeId && custom) { + setViewportGridState(storeId, { ...state }); + } + + state.viewports.forEach((viewport, viewportId) => { + const { displaySetOptions, displaySetInstanceUIDs } = viewport; + if (!displaySetOptions) { + return; + } + for (let i = 0; i < displaySetOptions.length; i++) { + const displaySetUID = displaySetInstanceUIDs[i]; + if (!displaySetUID) { + continue; + } + if (viewportId === activeViewportId && i === 0) { + setDisplaySetSelector(`${activeStudyUID}:activeDisplaySet:0`, displaySetUID); + } + if (displaySetOptions[i]?.id) { + setDisplaySetSelector( + `${activeStudyUID}:${displaySetOptions[i].id}:${ + displaySetOptions[i].matchedDisplaySetsIndex || 0 + }`, + displaySetUID + ); + } + } + }); + + setHangingProtocolStageIndex(cacheId, hpInfo); + + return { + hangingProtocolStageIndexMap, + viewportGridStore: viewportGridState, + displaySetSelectorMap, + }; +}; + +export default reuseCachedLayout; diff --git a/extensions/default/src/utils/validations/areAllImageComponentsEqual.ts b/extensions/default/src/utils/validations/areAllImageComponentsEqual.ts new file mode 100644 index 0000000..97040fb --- /dev/null +++ b/extensions/default/src/utils/validations/areAllImageComponentsEqual.ts @@ -0,0 +1,24 @@ +import toNumber from '@ohif/core/src/utils/toNumber'; + +/** + * Check if all voxels in series images has same number of components (samplesPerPixel) + * @param {*} instances + * @returns + */ +export default function areAllImageComponentsEqual(instances: Array): boolean { + if (!instances?.length) { + return false; + } + const firstImage = instances[0]; + const firstImageSamplesPerPixel = toNumber(firstImage.SamplesPerPixel); + + for (let i = 1; i < instances.length; i++) { + const instance = instances[i]; + const { SamplesPerPixel } = instance; + + if (SamplesPerPixel !== firstImageSamplesPerPixel) { + return false; + } + } + return true; +} diff --git a/extensions/default/src/utils/validations/areAllImageDimensionsEqual.test.ts b/extensions/default/src/utils/validations/areAllImageDimensionsEqual.test.ts new file mode 100644 index 0000000..4158037 --- /dev/null +++ b/extensions/default/src/utils/validations/areAllImageDimensionsEqual.test.ts @@ -0,0 +1,41 @@ +import areAllImageDimensionsEqual from './areAllImageDimensionsEqual'; + +describe('areAllImageDimensionsEqual', () => { + it('should return false when no instances are provided', () => { + expect(areAllImageDimensionsEqual([])).toBe(false); + expect(areAllImageDimensionsEqual([] as any)).toBe(false); + }); + + it('should return true when all instances have the same dimensions', () => { + const instances = [ + { Rows: '512', Columns: '512' }, + { Rows: '512', Columns: '512' }, + { Rows: '512', Columns: '512' } + ]; + expect(areAllImageDimensionsEqual(instances)).toBe(true); + }); + + it('should return true when comparing string and number dimensions of same value', () => { + const instances = [ + { Rows: 512, Columns: 512 }, + { Rows: '512', Columns: '512' } + ]; + expect(areAllImageDimensionsEqual(instances)).toBe(true); + }); + + it('should return false when instances have different dimensions', () => { + const instances = [ + { Rows: '512', Columns: '512' }, + { Rows: '256', Columns: '512' } + ]; + expect(areAllImageDimensionsEqual(instances)).toBe(false); + }); + + it('should return false when dimensions are invalid strings', () => { + const instances = [ + { Rows: '512', Columns: '512' }, + { Rows: 'invalid', Columns: '512' } + ]; + expect(areAllImageDimensionsEqual(instances)).toBe(false); + }); +}); diff --git a/extensions/default/src/utils/validations/areAllImageDimensionsEqual.ts b/extensions/default/src/utils/validations/areAllImageDimensionsEqual.ts new file mode 100644 index 0000000..2638ec2 --- /dev/null +++ b/extensions/default/src/utils/validations/areAllImageDimensionsEqual.ts @@ -0,0 +1,25 @@ +import toNumber from '@ohif/core/src/utils/toNumber'; + +/** + * Check if the frames in a series has different dimensions + * @param {*} instances + * @returns + */ +export default function areAllImageDimensionsEqual(instances: Array): boolean { + if (!instances?.length) { + return false; + } + const firstImage = instances[0]; + const firstImageRows = toNumber(firstImage.Rows); + const firstImageColumns = toNumber(firstImage.Columns); + + for (let i = 1; i < instances.length; i++) { + const instance = instances[i]; + const { Rows, Columns } = instance; + + if (toNumber(Rows) !== firstImageRows || toNumber(Columns) !== firstImageColumns) { + return false; + } + } + return true; +} diff --git a/extensions/default/src/utils/validations/areAllImageOrientationsEqual.ts b/extensions/default/src/utils/validations/areAllImageOrientationsEqual.ts new file mode 100644 index 0000000..310bd84 --- /dev/null +++ b/extensions/default/src/utils/validations/areAllImageOrientationsEqual.ts @@ -0,0 +1,25 @@ +import toNumber from '@ohif/core/src/utils/toNumber'; +import { _isSameOrientation } from '@ohif/core/src/utils/isDisplaySetReconstructable'; + +/** + * Check is the series has frames with different orientations + * @param {*} instances + * @returns + */ +export default function areAllImageOrientationsEqual(instances: Array): boolean { + if (!instances?.length) { + return false; + } + const firstImage = instances[0]; + const firstImageOrientationPatient = toNumber(firstImage.ImageOrientationPatient); + + for (let i = 1; i < instances.length; i++) { + const instance = instances[i]; + const imageOrientationPatient = toNumber(instance.ImageOrientationPatient); + + if (!_isSameOrientation(imageOrientationPatient, firstImageOrientationPatient)) { + return false; + } + } + return true; +} diff --git a/extensions/default/src/utils/validations/areAllImagePositionsEqual.ts b/extensions/default/src/utils/validations/areAllImagePositionsEqual.ts new file mode 100644 index 0000000..35b696b --- /dev/null +++ b/extensions/default/src/utils/validations/areAllImagePositionsEqual.ts @@ -0,0 +1,73 @@ +import { vec3 } from 'gl-matrix'; +import toNumber from '@ohif/core/src/utils/toNumber'; +import { _getPerpendicularDistance } from '@ohif/core/src/utils/isDisplaySetReconstructable'; +import calculateScanAxisNormal from '../calculateScanAxisNormal'; + +/** + * Checks if there is a position shift between consecutive frames + * @param {*} previousPosition + * @param {*} actualPosition + * @param {*} scanAxisNormal + * @param {*} averageSpacingBetweenFrames + * @returns + */ +function _checkSeriesPositionShift( + previousPosition, + actualPosition, + scanAxisNormal, + averageSpacingBetweenFrames +) { + // predicted position should be the previous position added by the multiplication + // of the scanAxisNormal and the average spacing between frames + const predictedPosition = vec3.scaleAndAdd( + vec3.create(), + previousPosition, + scanAxisNormal, + averageSpacingBetweenFrames + ); + return vec3.distance(actualPosition, predictedPosition) > averageSpacingBetweenFrames; +} + +/** + * Checks if a series has position shifts between consecutive frames + * @param {*} instances + * @returns + */ +export default function areAllImagePositionsEqual(instances: Array): boolean { + if (!instances?.length) { + return false; + } + const firstImageOrientationPatient = toNumber(instances[0].ImageOrientationPatient); + if (!firstImageOrientationPatient) { + return false; + } + const scanAxisNormal = calculateScanAxisNormal(firstImageOrientationPatient); + const firstImagePositionPatient = toNumber(instances[0].ImagePositionPatient); + const lastIpp = toNumber(instances[instances.length - 1].ImagePositionPatient); + + if (!firstImagePositionPatient || !lastIpp) { + return false; + } + + const averageSpacingBetweenFrames = + _getPerpendicularDistance(firstImagePositionPatient, lastIpp) / (instances.length - 1); + + let previousImagePositionPatient = firstImagePositionPatient; + for (let i = 1; i < instances.length; i++) { + const instance = instances[i]; + const imagePositionPatient = toNumber(instance.ImagePositionPatient); + + if ( + _checkSeriesPositionShift( + previousImagePositionPatient, + imagePositionPatient, + scanAxisNormal, + averageSpacingBetweenFrames + ) + ) { + return false; + } + previousImagePositionPatient = imagePositionPatient; + } + return true; +} diff --git a/extensions/default/src/utils/validations/areAllImageSpacingEqual.ts b/extensions/default/src/utils/validations/areAllImageSpacingEqual.ts new file mode 100644 index 0000000..49906c3 --- /dev/null +++ b/extensions/default/src/utils/validations/areAllImageSpacingEqual.ts @@ -0,0 +1,64 @@ +import { + _getPerpendicularDistance, + _getSpacingIssue, + reconstructionIssues, +} from '@ohif/core/src/utils/isDisplaySetReconstructable'; +import { DisplaySetMessage } from '@ohif/core'; +import toNumber from '@ohif/core/src/utils/toNumber'; +import { DisplaySetMessageList } from '@ohif/core'; + +/** + * Checks if series has spacing issues + * @param {*} instances + * @param {*} warnings + */ +export default function areAllImageSpacingEqual( + instances: Array, + messages: DisplaySetMessageList +): void { + if (!instances?.length) { + return; + } + const firstImagePositionPatient = toNumber(instances[0].ImagePositionPatient); + if (!firstImagePositionPatient) { + return; + } + const lastIpp = toNumber(instances[instances.length - 1].ImagePositionPatient); + + const averageSpacingBetweenFrames = + _getPerpendicularDistance(firstImagePositionPatient, lastIpp) / (instances.length - 1); + + let previousImagePositionPatient = firstImagePositionPatient; + + const issuesFound = []; + for (let i = 1; i < instances.length; i++) { + const instance = instances[i]; + const imagePositionPatient = toNumber(instance.ImagePositionPatient); + + const spacingBetweenFrames = _getPerpendicularDistance( + imagePositionPatient, + previousImagePositionPatient + ); + + const spacingIssue = _getSpacingIssue(spacingBetweenFrames, averageSpacingBetweenFrames); + + if (spacingIssue) { + const issue = spacingIssue.issue; + + // avoid multiple warning of the same thing + if (!issuesFound.includes(issue)) { + issuesFound.push(issue); + if (issue === reconstructionIssues.MISSING_FRAMES) { + messages.addMessage(DisplaySetMessage.CODES.MISSING_FRAMES); + } else if (issue === reconstructionIssues.IRREGULAR_SPACING) { + messages.addMessage(DisplaySetMessage.CODES.IRREGULAR_SPACING); + } + } + // we just want to find issues not how many + if (issuesFound.length > 1) { + break; + } + } + previousImagePositionPatient = imagePositionPatient; + } +} diff --git a/extensions/default/src/utils/validations/checkMultiframe.ts b/extensions/default/src/utils/validations/checkMultiframe.ts new file mode 100644 index 0000000..f4f4a20 --- /dev/null +++ b/extensions/default/src/utils/validations/checkMultiframe.ts @@ -0,0 +1,25 @@ +import { + hasPixelMeasurements, + hasOrientation, + hasPosition, +} from '@ohif/core/src/utils/isDisplaySetReconstructable'; +import { DisplaySetMessage, DisplaySetMessageList } from '@ohif/core'; + +/** + * Check various multi frame issues. It calls OHIF core functions + * @param {*} multiFrameInstance + * @param {*} warnings + */ +export default function checkMultiFrame(multiFrameInstance, messages: DisplaySetMessageList): void { + if (!hasPixelMeasurements(multiFrameInstance)) { + messages.addMessage(DisplaySetMessage.CODES.MULTIFRAME_NO_PIXEL_MEASUREMENTS); + } + + if (!hasOrientation(multiFrameInstance)) { + messages.addMessage(DisplaySetMessage.CODES.MULTIFRAME_NO_ORIENTATION); + } + + if (!hasPosition(multiFrameInstance)) { + messages.addMessage(DisplaySetMessage.CODES.MULTIFRAME_NO_POSITION_INFORMATION); + } +} diff --git a/extensions/default/src/utils/validations/checkSingleFrames.ts b/extensions/default/src/utils/validations/checkSingleFrames.ts new file mode 100644 index 0000000..677d66f --- /dev/null +++ b/extensions/default/src/utils/validations/checkSingleFrames.ts @@ -0,0 +1,35 @@ +import areAllImageDimensionsEqual from './areAllImageDimensionsEqual'; +import areAllImageComponentsEqual from './areAllImageComponentsEqual'; +import areAllImageOrientationsEqual from './areAllImageOrientationsEqual'; +import areAllImagePositionsEqual from './areAllImagePositionsEqual'; +import areAllImageSpacingEqual from './areAllImageSpacingEqual'; +import { DisplaySetMessage, DisplaySetMessageList } from '@ohif/core'; + +/** + * Runs various checks in a single frame series + * @param {*} instances + * @param {*} warnings + */ +export default function checkSingleFrames( + instances: Array, + messages: DisplaySetMessageList +): void { + if (instances.length > 2) { + if (!areAllImageDimensionsEqual(instances)) { + messages.addMessage(DisplaySetMessage.CODES.INCONSISTENT_DIMENSIONS); + } + + if (!areAllImageComponentsEqual(instances)) { + messages.addMessage(DisplaySetMessage.CODES.INCONSISTENT_COMPONENTS); + } + + if (!areAllImageOrientationsEqual(instances)) { + messages.addMessage(DisplaySetMessage.CODES.INCONSISTENT_ORIENTATIONS); + } + + if (!areAllImagePositionsEqual(instances)) { + messages.addMessage(DisplaySetMessage.CODES.INCONSISTENT_POSITION_INFORMATION); + } + areAllImageSpacingEqual(instances, messages); + } +} diff --git a/extensions/dicom-microscopy/.gitignore b/extensions/dicom-microscopy/.gitignore new file mode 100644 index 0000000..6704566 --- /dev/null +++ b/extensions/dicom-microscopy/.gitignore @@ -0,0 +1,104 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/extensions/dicom-microscopy/.prettierrc b/extensions/dicom-microscopy/.prettierrc new file mode 100644 index 0000000..ef83baa --- /dev/null +++ b/extensions/dicom-microscopy/.prettierrc @@ -0,0 +1,11 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"], + "trailingComma": "es5", + "printWidth": 100, + "proseWrap": "always", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/extensions/dicom-microscopy/.webpack/webpack.dev.js b/extensions/dicom-microscopy/.webpack/webpack.dev.js new file mode 100644 index 0000000..6aea859 --- /dev/null +++ b/extensions/dicom-microscopy/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/extensions/dicom-microscopy/.webpack/webpack.prod.js b/extensions/dicom-microscopy/.webpack/webpack.prod.js new file mode 100644 index 0000000..540fb81 --- /dev/null +++ b/extensions/dicom-microscopy/.webpack/webpack.prod.js @@ -0,0 +1,60 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +const pkg = require('./../package.json'); + +const ROOT_DIR = path.join(__dirname, '../'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +const outputName = `ohif-${pkg.name.split('/').pop()}`; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: true, + }, + output: { + path: ROOT_DIR, + library: 'ohif-extension-dicom-microscopy', + libraryTarget: 'umd', + filename: pkg.main, + }, + externals: [ + /\b(vtk.js)/, + /\b(dcmjs)/, + /\b(gl-matrix)/, + /^@ohif/, + /^@cornerstonejs/, + ], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + new MiniCssExtractPlugin({ + filename: `./dist/${outputName}.css`, + chunkFilename: `./dist/${outputName}.css`, + }), + ], + }); +}; diff --git a/extensions/dicom-microscopy/CHANGELOG.md b/extensions/dicom-microscopy/CHANGELOG.md new file mode 100644 index 0000000..b36f6e0 --- /dev/null +++ b/extensions/dicom-microscopy/CHANGELOG.md @@ -0,0 +1,3056 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + + +### Bug Fixes + +* lodash dependencies ([#4791](https://github.com/OHIF/Viewers/issues/4791)) ([4e16099](https://github.com/OHIF/Viewers/commit/4e16099ad3ab777b09f6ac8f181025cfd656ab6b)) + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + + +### Features + +* add viewport overlays to microscopy mode ([#4776](https://github.com/OHIF/Viewers/issues/4776)) ([084a10f](https://github.com/OHIF/Viewers/commit/084a10f7835acab6a851922850c474bc9c7b864b)) + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + + +### Bug Fixes + +* **core:** Address 3D reconstruction and Android compatibility issues and clean up 4D data mode ([#4762](https://github.com/OHIF/Viewers/issues/4762)) ([149d6d0](https://github.com/OHIF/Viewers/commit/149d6d049cd333b9e5846576b403ff387558a66f)) + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + + +### Features + +* Add customization support for more UI components ([#4634](https://github.com/OHIF/Viewers/issues/4634)) ([f15eb44](https://github.com/OHIF/Viewers/commit/f15eb44b4cf49de1b73a22512571cec02effaef3)) + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + + +### Bug Fixes + +* Inconsistent Handling of Patient Name Tag ([#4703](https://github.com/OHIF/Viewers/issues/4703)) ([8aedb2e](https://github.com/OHIF/Viewers/commit/8aedb2ec54a0ccf2550f745fed6f0b8aa184a860)) + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + + +### Features + +* **dev:** move to rsbuild for dev - faster ([#4674](https://github.com/OHIF/Viewers/issues/4674)) ([d4a4267](https://github.com/OHIF/Viewers/commit/d4a4267429c02916dd51f6aefb290d96dd1c3b04)) + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + + +### Features + +* **new-study-panel:** default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser ([#4417](https://github.com/OHIF/Viewers/issues/4417)) ([a7fd9fa](https://github.com/OHIF/Viewers/commit/a7fd9fa5bfff7a1b533d99cb96f7147a35fd528f)) + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + + +### Features + +* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3)) + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + + +### Features + +* **studies-panel:** New OHIF study panel - under experimental flag ([#4254](https://github.com/OHIF/Viewers/issues/4254)) ([7a96406](https://github.com/OHIF/Viewers/commit/7a96406a116e46e62c396855fa64f434e2984b58)) + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + + +### Features + +* Add CS3D WSI and Video Viewports and add annotation navigation for MPR ([#4182](https://github.com/OHIF/Viewers/issues/4182)) ([7599ec9](https://github.com/OHIF/Viewers/commit/7599ec9421129dcade94e6fa6ec7908424ab3134)) + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + + +### Bug Fixes + +* webpack import bugs showing warnings on import ([#4265](https://github.com/OHIF/Viewers/issues/4265)) ([24c511f](https://github.com/OHIF/Viewers/commit/24c511f4bc04c4143bbd3d0d48029f41f7f36014)) + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + + +### Bug Fixes + +* **bugs:** enhancements and bugs in several areas ([#4086](https://github.com/OHIF/Viewers/issues/4086)) ([730f434](https://github.com/OHIF/Viewers/commit/730f4349100f21b4489a21707dbb2dca9dbfbba2)) + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + + +### Features + +* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13)) + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + + +### Bug Fixes + +* Microscopy bulkdata and image retrieve ([#3894](https://github.com/OHIF/Viewers/issues/3894)) ([7fac49b](https://github.com/OHIF/Viewers/commit/7fac49b4492b4bd5e9ece8e2e2b0fa2faa840d7f)) + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + + +### Bug Fixes + +* **SM:** drag and drop is now fixed for SM ([#3813](https://github.com/OHIF/Viewers/issues/3813)) ([f1a6764](https://github.com/OHIF/Viewers/commit/f1a67647aed635437b188cea7cf5d5a8fb974bbe)) + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + + +### Bug Fixes + +* **measurement and microscopy:** various small fixes for measurement and microscopy side panel ([#3696](https://github.com/OHIF/Viewers/issues/3696)) ([c1d5ee7](https://github.com/OHIF/Viewers/commit/c1d5ee7e3f7f4c0c6bed9ae81eba5519741c5155)) + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + + +### Bug Fixes + +* **config:** support more values for the useSharedArrayBuffer ([#3688](https://github.com/OHIF/Viewers/issues/3688)) ([1129c15](https://github.com/OHIF/Viewers/commit/1129c155d2c7d46c98a5df7c09879aa3d459fa7e)) + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + + +### Features + +* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd)) + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + +**Note:** Version bump only for package @ohif/extension-dicom-microscopy diff --git a/extensions/dicom-microscopy/LICENSE b/extensions/dicom-microscopy/LICENSE new file mode 100644 index 0000000..effbc2d --- /dev/null +++ b/extensions/dicom-microscopy/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2023 dicom-microscopy (26860200+md-prog@users.noreply.github.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/extensions/dicom-microscopy/README.md b/extensions/dicom-microscopy/README.md new file mode 100644 index 0000000..cecf00a --- /dev/null +++ b/extensions/dicom-microscopy/README.md @@ -0,0 +1,11 @@ +# OHIF extension for microscopy +Adapter for *DICOM Microscopy Viewer* to get it integrated into OHIF Viewer. + +## Acknowledgements + +- [DICOM Microscopy Viewer](https://github.com/ImagingDataCommons/dicom-microscopy-viewer) is a Vanilla JS library for web-based visualization of DICOM VL Whole Slide Microscopy Image datasets and derived information. +- [SLIM Viewer](https://github.com/imagingdatacommons/slim) is a single-page application for interactive visualization and annotation of digital whole slide microscopy images and derived image analysis results in standard DICOM format. The application is based on the dicom-microscopy-viewer JavaScript library and runs fully client side without any custom server components. + + +## License +MIT diff --git a/extensions/dicom-microscopy/babel.config.js b/extensions/dicom-microscopy/babel.config.js new file mode 100644 index 0000000..a35080a --- /dev/null +++ b/extensions/dicom-microscopy/babel.config.js @@ -0,0 +1,43 @@ +module.exports = { + plugins: ['@babel/plugin-proposal-class-properties'], + env: { + test: { + presets: [ + [ + // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2 + '@babel/preset-env', + { + modules: 'commonjs', + debug: false, + }, + '@babel/preset-typescript', + ], + '@babel/preset-react', + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-regenerator', + '@babel/plugin-transform-runtime', + ], + }, + production: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + development: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + }, +}; diff --git a/extensions/dicom-microscopy/package.json b/extensions/dicom-microscopy/package.json new file mode 100644 index 0000000..8f8e50d --- /dev/null +++ b/extensions/dicom-microscopy/package.json @@ -0,0 +1,53 @@ +{ + "name": "@ohif/extension-dicom-microscopy", + "version": "3.10.0-beta.111", + "description": "OHIF extension for DICOM microscopy", + "author": "Bill Wallace, md-prog", + "license": "MIT", + "main": "dist/ohif-extension-dicom-microscopy.umd.js", + "files": [ + "dist/**", + "public/**", + "README.md" + ], + "repository": "OHIF/Viewers", + "keywords": [ + "ohif-extension" + ], + "module": "src/index.tsx", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.18.0" + }, + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:dicom-pdf": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package-1": "yarn run build", + "start": "yarn run dev" + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.111", + "@ohif/extension-default": "3.10.0-beta.111", + "@ohif/i18n": "3.10.0-beta.111", + "@ohif/ui": "3.10.0-beta.111", + "prop-types": "^15.6.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^10.11.0", + "react-router": "^6.23.1", + "react-router-dom": "^6.23.1" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "@cornerstonejs/codec-charls": "^1.2.3", + "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", + "@cornerstonejs/codec-openjpeg": "^1.2.4", + "colormap": "^2.3", + "lodash.debounce": "^4.0.8", + "mathjs": "^12.4.2" + } +} diff --git a/extensions/dicom-microscopy/src/DicomMicroscopySRSopClassHandler.js b/extensions/dicom-microscopy/src/DicomMicroscopySRSopClassHandler.js new file mode 100644 index 0000000..a195a80 --- /dev/null +++ b/extensions/dicom-microscopy/src/DicomMicroscopySRSopClassHandler.js @@ -0,0 +1,119 @@ +import OHIF, { DicomMetadataStore } from '@ohif/core'; +import loadSR from './utils/loadSR'; +import toArray from './utils/toArray'; +import DCM_CODE_VALUES from './utils/dcmCodeValues'; +import getSourceDisplaySet from './utils/getSourceDisplaySet'; + +const { utils } = OHIF; + +const SOP_CLASS_UIDS = { + COMPREHENSIVE_3D_SR: '1.2.840.10008.5.1.4.1.1.88.34', +}; + +const SOPClassHandlerId = + '@ohif/extension-dicom-microscopy.sopClassHandlerModule.DicomMicroscopySRSopClassHandler'; + +function _getReferencedFrameOfReferenceUID(naturalizedDataset) { + const { ContentSequence } = naturalizedDataset; + + const imagingMeasurementsContentItem = ContentSequence.find( + ci => ci.ConceptNameCodeSequence.CodeValue === DCM_CODE_VALUES.IMAGING_MEASUREMENTS + ); + + const firstMeasurementGroupContentItem = toArray( + imagingMeasurementsContentItem.ContentSequence + ).find(ci => ci.ConceptNameCodeSequence.CodeValue === DCM_CODE_VALUES.MEASUREMENT_GROUP); + + const imageRegionContentItem = toArray(firstMeasurementGroupContentItem.ContentSequence).find( + ci => ci.ConceptNameCodeSequence.CodeValue === DCM_CODE_VALUES.IMAGE_REGION + ); + + return imageRegionContentItem.ReferencedFrameOfReferenceUID; +} + +function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) { + // If the series has no instances, stop here + if (!instances || !instances.length) { + throw new Error('No instances were provided'); + } + + const { displaySetService, microscopyService } = servicesManager.services; + + const instance = instances[0]; + + // TODO ! Consumption of DICOMMicroscopySRSOPClassHandler to a derived dataset or normal dataset? + // TODO -> Easy to swap this to a "non-derived" displaySet, but unfortunately need to put it in a different extension. + const naturalizedDataset = DicomMetadataStore.getSeries( + instance.StudyInstanceUID, + instance.SeriesInstanceUID + ).instances[0]; + const ReferencedFrameOfReferenceUID = _getReferencedFrameOfReferenceUID(naturalizedDataset); + + const { + FrameOfReferenceUID, + SeriesDescription, + ContentDate, + ContentTime, + SeriesNumber, + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + SOPClassUID, + } = instance; + + const displaySet = { + plugin: 'microscopy', + Modality: 'SR', + altImageText: 'Microscopy SR', + displaySetInstanceUID: utils.guid(), + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + ReferencedFrameOfReferenceUID, + SOPClassHandlerId, + SOPClassUID, + SeriesDescription, + // Map the content date/time to the series date/time, these are only used for filtering. + SeriesDate: ContentDate, + SeriesTime: ContentTime, + SeriesNumber, + instance, + metadata: naturalizedDataset, + isDerived: true, + isLoading: false, + isLoaded: false, + loadError: false, + }; + + displaySet.load = function (referencedDisplaySet) { + return loadSR(microscopyService, displaySet, referencedDisplaySet).catch(error => { + displaySet.isLoaded = false; + displaySet.loadError = true; + throw new Error(error); + }); + }; + + displaySet.getSourceDisplaySet = function () { + let allDisplaySets = []; + const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID); + studyMetadata.series.forEach(series => { + const displaySets = displaySetService.getDisplaySetsForSeries(series.SeriesInstanceUID); + allDisplaySets = allDisplaySets.concat(displaySets); + }); + return getSourceDisplaySet(allDisplaySets, displaySet); + }; + + return [displaySet]; +} + +export default function getDicomMicroscopySRSopClassHandler({ servicesManager, extensionManager }) { + const getDisplaySetsFromSeries = instances => { + return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager); + }; + + return { + name: 'DicomMicroscopySRSopClassHandler', + sopClassUids: [SOP_CLASS_UIDS.COMPREHENSIVE_3D_SR], + getDisplaySetsFromSeries, + }; +} diff --git a/extensions/dicom-microscopy/src/DicomMicroscopyViewport.css b/extensions/dicom-microscopy/src/DicomMicroscopyViewport.css new file mode 100644 index 0000000..9ab3892 --- /dev/null +++ b/extensions/dicom-microscopy/src/DicomMicroscopyViewport.css @@ -0,0 +1,365 @@ +.DicomMicroscopyViewer { + --ol-partial-background-color: rgba(127, 127, 127, 0.7); + --ol-foreground-color: #000000; + --ol-subtle-foreground-color: #000; + --ol-subtle-background-color: rgba(78, 78, 78, 0.5); +} + +.DicomMicroscopyViewer .ol-box { + box-sizing: border-box; + border-radius: 2px; + border: 1.5px solid var(--ol-background-color); + background-color: var(--ol-partial-background-color); +} + +.DicomMicroscopyViewer .ol-mouse-position { + top: 8px; + right: 8px; + position: absolute; +} + +.DicomMicroscopyViewer .ol-scale-line { + background: var(--ol-partial-background-color); + border-radius: 4px; + bottom: 8px; + left: 8px; + padding: 2px; + position: absolute; +} + +.DicomMicroscopyViewer .ol-scale-line-inner { + border: 1px solid var(--ol-subtle-foreground-color); + border-top: none; + color: var(--ol-foreground-color); + font-size: 10px; + text-align: center; + margin: 1px; + will-change: contents, width; + transition: all 0.25s; +} + +.DicomMicroscopyViewer .ol-scale-bar { + position: absolute; + bottom: 8px; + left: 8px; +} + +.DicomMicroscopyViewer .ol-scale-bar-inner { + display: flex; +} + +.DicomMicroscopyViewer .ol-scale-step-marker { + width: 1px; + height: 15px; + background-color: var(--ol-foreground-color); + float: right; + z-index: 10; +} + +.DicomMicroscopyViewer .ol-scale-step-text { + position: absolute; + bottom: -5px; + font-size: 10px; + z-index: 11; + color: var(--ol-foreground-color); + text-shadow: + -1.5px 0 var(--ol-partial-background-color), + 0 1.5px var(--ol-partial-background-color), + 1.5px 0 var(--ol-partial-background-color), + 0 -1.5px var(--ol-partial-background-color); +} + +.DicomMicroscopyViewer .ol-scale-text { + position: absolute; + font-size: 12px; + text-align: center; + bottom: 25px; + color: var(--ol-foreground-color); + text-shadow: + -1.5px 0 var(--ol-partial-background-color), + 0 1.5px var(--ol-partial-background-color), + 1.5px 0 var(--ol-partial-background-color), + 0 -1.5px var(--ol-partial-background-color); +} + +.DicomMicroscopyViewer .ol-scale-singlebar { + position: relative; + height: 10px; + z-index: 9; + box-sizing: border-box; + border: 1px solid var(--ol-foreground-color); +} + +.DicomMicroscopyViewer .ol-scale-singlebar-even { + background-color: var(--ol-subtle-foreground-color); +} + +.DicomMicroscopyViewer .ol-scale-singlebar-odd { + background-color: var(--ol-background-color); +} + +.DicomMicroscopyViewer .ol-unsupported { + display: none; +} + +.DicomMicroscopyViewer .ol-viewport, +.DicomMicroscopyViewer .ol-unselectable { + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + -webkit-tap-highlight-color: transparent; +} + +.DicomMicroscopyViewer .ol-viewport canvas { + all: unset; +} + +.DicomMicroscopyViewer .ol-selectable { + -webkit-touch-callout: default; + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; +} + +.DicomMicroscopyViewer .ol-grabbing { + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + cursor: grabbing; +} + +.DicomMicroscopyViewer .ol-grab { + cursor: move; + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; +} + +.DicomMicroscopyViewer .ol-control { + position: absolute; + background-color: var(--ol-subtle-background-color); + border-radius: 4px; +} + +.DicomMicroscopyViewer .ol-zoom { + top: 0.5em; + left: 0.5em; +} + +.DicomMicroscopyViewer .ol-rotate { + top: 0.5em; + right: 0.5em; + transition: + opacity 0.25s linear, + visibility 0s linear; +} + +.DicomMicroscopyViewer .ol-rotate.ol-hidden { + opacity: 0; + visibility: hidden; + transition: + opacity 0.25s linear, + visibility 0s linear 0.25s; +} + +.DicomMicroscopyViewer .ol-zoom-extent { + top: 4.643em; + left: 0.5em; +} + +.DicomMicroscopyViewer .ol-full-screen { + right: 0.5em; + top: 0.5em; +} + +.DicomMicroscopyViewer .ol-control button { + display: block; + margin: 1px; + padding: 0; + color: var(--ol-subtle-foreground-color); + font-weight: bold; + text-decoration: none; + font-size: inherit; + text-align: center; + height: 1.375em; + width: 1.375em; + line-height: 0.4em; + background-color: var(--ol-background-color); + border: none; + border-radius: 2px; +} + +.DicomMicroscopyViewer .ol-control button::-moz-focus-inner { + border: none; + padding: 0; +} + +.DicomMicroscopyViewer .ol-zoom-extent button { + line-height: 1.4em; +} + +.DicomMicroscopyViewer .ol-compass { + display: block; + font-weight: normal; + will-change: transform; +} + +.DicomMicroscopyViewer .ol-touch .ol-control button { + font-size: 1.5em; +} + +.DicomMicroscopyViewer .ol-touch .ol-zoom-extent { + top: 5.5em; +} + +.DicomMicroscopyViewer .ol-control button:hover, +.DicomMicroscopyViewer .ol-control button:focus { + text-decoration: none; + outline: 1px solid var(--ol-subtle-foreground-color); + color: var(--ol-foreground-color); +} + +.DicomMicroscopyViewer .ol-zoom .ol-zoom-in { + border-radius: 2px 2px 0 0; +} + +.DicomMicroscopyViewer .ol-zoom .ol-zoom-out { + border-radius: 0 0 2px 2px; +} + +.DicomMicroscopyViewer .ol-attribution { + text-align: right; + bottom: 0.5em; + right: 0.5em; + max-width: calc(100% - 1.3em); + display: flex; + flex-flow: row-reverse; + align-items: center; +} + +.DicomMicroscopyViewer .ol-attribution a { + color: var(--ol-subtle-foreground-color); + text-decoration: none; +} + +.DicomMicroscopyViewer .ol-attribution ul { + margin: 0; + padding: 1px 0.5em; + color: var(--ol-foreground-color); + text-shadow: 0 0 2px var(--ol-background-color); + font-size: 12px; +} + +.DicomMicroscopyViewer .ol-attribution li { + display: inline; + list-style: none; +} + +.DicomMicroscopyViewer .ol-attribution li:not(:last-child):after { + content: ' '; +} + +.DicomMicroscopyViewer .ol-attribution img { + max-height: 2em; + max-width: inherit; + vertical-align: middle; +} + +.DicomMicroscopyViewer .ol-attribution button { + flex-shrink: 0; +} + +.DicomMicroscopyViewer .ol-attribution.ol-collapsed ul { + display: none; +} + +.DicomMicroscopyViewer .ol-attribution:not(.ol-collapsed) { + background: var(--ol-partial-background-color); +} + +.DicomMicroscopyViewer .ol-attribution.ol-uncollapsible { + bottom: 0; + right: 0; + border-radius: 4px 0 0; +} + +.DicomMicroscopyViewer .ol-attribution.ol-uncollapsible img { + margin-top: -0.2em; + max-height: 1.6em; +} + +.DicomMicroscopyViewer .ol-attribution.ol-uncollapsible button { + display: none; +} + +.DicomMicroscopyViewer .ol-zoomslider { + top: 4.5em; + left: 0.5em; + height: 200px; +} + +.DicomMicroscopyViewer .ol-zoomslider button { + position: relative; + height: 10px; +} + +.DicomMicroscopyViewer .ol-touch .ol-zoomslider { + top: 5.5em; +} + +.DicomMicroscopyViewer .ol-overviewmap { + left: 0.5em; + bottom: 0.5em; +} + +.DicomMicroscopyViewer .ol-overviewmap.ol-uncollapsible { + bottom: 0; + left: 0; + border-radius: 0 4px 0 0; +} + +.DicomMicroscopyViewer .ol-overviewmap .ol-overviewmap-map, +.DicomMicroscopyViewer .ol-overviewmap button { + display: block; +} + +.DicomMicroscopyViewer .ol-overviewmap .ol-overviewmap-map { + border: 1px solid var(--ol-subtle-foreground-color); + height: 150px; + width: 150px; +} + +.DicomMicroscopyViewer .ol-overviewmap:not(.ol-collapsed) button { + bottom: 0; + left: 0; + position: absolute; +} + +.DicomMicroscopyViewer .ol-overviewmap.ol-collapsed .ol-overviewmap-map, +.DicomMicroscopyViewer .ol-overviewmap.ol-uncollapsible button { + display: none; +} + +.DicomMicroscopyViewer .ol-overviewmap:not(.ol-collapsed) { + background: var(--ol-subtle-background-color); +} + +.DicomMicroscopyViewer .ol-overviewmap-box { + border: 0.5px dotted var(--ol-subtle-foreground-color); +} + +.DicomMicroscopyViewer .ol-overviewmap .ol-overviewmap-box:hover { + cursor: move; +} + +@layout-header-background: #007ea3; +@primary-color: #007ea3; +@processing-color: #8cb8c6; +@success-color: #3f9c35; +@warning-color: #eeaf30; +@error-color: #96172e; +@font-size-base: 14px; + +.DicomMicroscopyViewer .ol-tooltip { + font-size: 16px !important; +} diff --git a/extensions/dicom-microscopy/src/DicomMicroscopyViewport.tsx b/extensions/dicom-microscopy/src/DicomMicroscopyViewport.tsx new file mode 100644 index 0000000..8975a91 --- /dev/null +++ b/extensions/dicom-microscopy/src/DicomMicroscopyViewport.tsx @@ -0,0 +1,258 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { cleanDenaturalizedDataset } from '@ohif/extension-default'; + +import './DicomMicroscopyViewport.css'; +import ViewportOverlay from './components/ViewportOverlay'; +import getDicomWebClient from './utils/dicomWebClient'; +import dcmjs from 'dcmjs'; +import { useSystem } from '@ohif/core'; + +function DicomMicroscopyViewport({ + activeViewportId, + setViewportActive, + displaySets, + viewportId, + dataSource, + resizeRef, +}: { + activeViewportId: string; + setViewportActive: Function; + displaySets: any[]; + viewportId: string; + dataSource: any; + resizeRef: any; +}) { + const { servicesManager, extensionManager } = useSystem(); + const [isLoaded, setIsLoaded] = useState(false); + const [viewer, setViewer] = useState(null); + const [managedViewer, setManagedViewer] = useState(null); + const overlayElement = useRef(); + const container = useRef(); + const { microscopyService, customizationService } = servicesManager.services; + + const overlayData = customizationService.getCustomization('microscopyViewport.overlay'); + + // install the microscopy renderer into the web page. + // you should only do this once. + const installOpenLayersRenderer = useCallback( + async (container, displaySet) => { + const loadViewer = async metadata => { + const dicomMicroscopyModule = await microscopyService.importDicomMicroscopyViewer(); + const { viewer: DicomMicroscopyViewer, metadata: metadataUtils } = dicomMicroscopyModule; + + const microscopyViewer = DicomMicroscopyViewer.VolumeImageViewer; + + const client = getDicomWebClient({ + extensionManager, + servicesManager, + }); + + // Parse, format, and filter metadata + const volumeImages: any[] = []; + + /** + * This block of code is the original way of loading DICOM into dicom-microscopy-viewer + * as in their documentation. + * But we have the metadata already loaded by our loaders. + * As the metadata for microscopy DIOM files tends to be big and we don't + * want to double load it, below we have the mechanism to reconstruct the + * DICOM JSON structure (denaturalized) from naturalized metadata. + * (NOTE: Our loaders cache only naturalized metadata, not the denaturalized.) + */ + // { + // const retrieveOptions = { + // studyInstanceUID: metadata[0].StudyInstanceUID, + // seriesInstanceUID: metadata[0].SeriesInstanceUID, + // }; + // metadata = await client.retrieveSeriesMetadata(retrieveOptions); + // // Parse, format, and filter metadata + // metadata.forEach(m => { + // if ( + // volumeImages.length > 0 && + // m['00200052'].Value[0] != volumeImages[0].FrameOfReferenceUID + // ) { + // console.warn( + // 'Expected FrameOfReferenceUID of difference instances within a series to be the same, found multiple different values', + // m['00200052'].Value[0] + // ); + // m['00200052'].Value[0] = volumeImages[0].FrameOfReferenceUID; + // } + // NOTE: depending on different data source, image.ImageType sometimes + // is a string, not a string array. + // m['00080008'] = transformImageTypeUnnaturalized(m['00080008']); + + // const image = new metadataUtils.VLWholeSlideMicroscopyImage({ + // metadata: m, + // }); + // const imageFlavor = image.ImageType[2]; + // if (imageFlavor === 'VOLUME' || imageFlavor === 'THUMBNAIL') { + // volumeImages.push(image); + // } + // }); + // } + + metadata.forEach(m => { + // NOTE: depending on different data source, image.ImageType sometimes + // is a string, not a string array. + m.ImageType = typeof m.ImageType === 'string' ? m.ImageType.split('\\') : m.ImageType; + + const inst = cleanDenaturalizedDataset( + dcmjs.data.DicomMetaDictionary.denaturalizeDataset(m), + { + StudyInstanceUID: m.StudyInstanceUID, + SeriesInstanceUID: m.SeriesInstanceUID, + dataSourceConfig: dataSource.getConfig(), + } + ); + if (!inst['00480105']) { + // Optical Path Sequence, no OpticalPathIdentifier? + // NOTE: this is actually a not-well formatted DICOM VL Whole Slide Microscopy Image. + inst['00480105'] = { + vr: 'SQ', + Value: [ + { + '00480106': { + vr: 'SH', + Value: ['1'], + }, + }, + ], + }; + } + const image = new metadataUtils.VLWholeSlideMicroscopyImage({ + metadata: inst, + }); + + const imageFlavor = image.ImageType[2]; + if (imageFlavor === 'VOLUME' || imageFlavor === 'THUMBNAIL') { + volumeImages.push(image); + } + }); + + // format metadata for microscopy-viewer + const options = { + client, + metadata: volumeImages, + retrieveRendered: false, + controls: ['overview', 'position'], + }; + + const viewer = new microscopyViewer(options); + + if (overlayElement && overlayElement.current && viewer.addViewportOverlay) { + viewer.addViewportOverlay({ + element: overlayElement.current, + coordinates: [0, 0], // TODO: dicom-microscopy-viewer documentation says this can be false to be automatically, but it is not. + navigate: true, + className: 'OpenLayersOverlay', + }); + } + + viewer.render({ container }); + + const { StudyInstanceUID, SeriesInstanceUID } = displaySet; + + const managedViewer = microscopyService.addViewer( + viewer, + viewportId, + container, + StudyInstanceUID, + SeriesInstanceUID + ); + + managedViewer.addContextMenuCallback((event: Event) => { + // TODO: refactor this after Bill's changes on ContextMenu feature get merged + // const roiAnnotationNearBy = this.getNearbyROI(event); + }); + + setViewer(viewer); + setManagedViewer(managedViewer); + }; + + microscopyService.clearAnnotations(); + + let smDisplaySet = displaySet; + if (displaySet.Modality === 'SR') { + // for SR displaySet, let's load the actual image displaySet + smDisplaySet = displaySet.getSourceDisplaySet(); + } + console.log('Loading viewer metadata', smDisplaySet); + + await loadViewer(smDisplaySet.others); + + if (displaySet.Modality === 'SR') { + displaySet.load(smDisplaySet); + } + }, + [dataSource, extensionManager, microscopyService, servicesManager, viewportId] + ); + + useEffect(() => { + const displaySet = displaySets[0]; + installOpenLayersRenderer(container.current, displaySet).then(() => { + setIsLoaded(true); + }); + + return () => { + if (viewer) { + microscopyService.removeViewer(viewer); + } + }; + }, []); + + useEffect(() => { + const displaySet = displaySets[0]; + + microscopyService.clearAnnotations(); + + // loading SR + if (displaySet.Modality === 'SR') { + const referencedDisplaySet = displaySet.getSourceDisplaySet(); + displaySet.load(referencedDisplaySet); + } + }, [managedViewer, displaySets, microscopyService]); + + const style = { width: '100%', height: '100%' }; + const displaySet = displaySets[0]; + const firstInstance = displaySet.firstInstance || displaySet.instance; + const LoadingIndicatorProgress = customizationService.getCustomization( + 'ui.loadingIndicatorProgress' + ); + + return ( +
{ + if (viewportId !== activeViewportId) { + setViewportActive(viewportId); + } + }} + > +
+
+
+ {displaySet && firstInstance.imageId && ( + + )} +
+
+
+
{ + container.current = ref; + resizeRef.current = ref; + }} + /> + {isLoaded ? null : } +
+ ); +} + +export default DicomMicroscopyViewport; diff --git a/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx b/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx new file mode 100644 index 0000000..b3200d8 --- /dev/null +++ b/extensions/dicom-microscopy/src/components/MicroscopyPanel/MicroscopyPanel.tsx @@ -0,0 +1,351 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { ExtensionManager, CommandsManager, DicomMetadataStore } from '@ohif/core'; +import { MeasurementTable } from '@ohif/ui'; +import { withTranslation, WithTranslation } from 'react-i18next'; +import { EVENTS as MicroscopyEvents } from '../../services/MicroscopyService'; +import dcmjs from 'dcmjs'; +import { callInputDialog } from '@ohif/extension-default'; +import constructSR from '../../utils/constructSR'; +import { saveByteArray } from '../../utils/saveByteArray'; +import { Separator } from '@ohif/ui-next'; + +let saving = false; +const { datasetToBuffer } = dcmjs.data; + +const formatArea = area => { + let mult = 1; + let unit = 'mm'; + if (area > 1000000) { + unit = 'm'; + mult = 1 / 1000000; + } else if (area < 1) { + unit = 'ฮผm'; + mult = 1000000; + } + return `${(area * mult).toFixed(2)} ${unit}ยฒ`; +}; + +const formatLength = (length, unit) => { + let mult = 1; + if (unit == 'km' || (!unit && length > 1000000)) { + unit = 'km'; + mult = 1 / 1000000; + } else if (unit == 'm' || (!unit && length > 1000)) { + unit = 'm'; + mult = 1 / 1000; + } else if (unit == 'ฮผm' || (!unit && length < 1)) { + unit = 'ฮผm'; + mult = 1000; + } else if (unit && unit != 'mm') { + throw new Error(`Unknown length unit ${unit}`); + } else { + unit = 'mm'; + } + return `${(length * mult).toFixed(2)} ${unit}`; +}; + +interface IMicroscopyPanelProps extends WithTranslation { + viewports: PropTypes.array; + activeViewportId: PropTypes.string; + + // + onSaveComplete?: PropTypes.func; // callback when successfully saved annotations + onRejectComplete?: PropTypes.func; // callback when rejected annotations + + // + servicesManager: AppTypes.ServicesManager; + extensionManager: ExtensionManager; + commandsManager: CommandsManager; +} + +/** + * Microscopy Measurements Panel Component + * + * @param props + * @returns + */ +function MicroscopyPanel(props: IMicroscopyPanelProps) { + const { microscopyService } = props.servicesManager.services; + + const [studyInstanceUID, setStudyInstanceUID] = useState(null as string | null); + const [roiAnnotations, setRoiAnnotations] = useState([] as any[]); + const [selectedAnnotation, setSelectedAnnotation] = useState(null as any); + const { servicesManager, extensionManager } = props; + + const { uiDialogService, displaySetService } = servicesManager.services; + + useEffect(() => { + const viewport = props.viewports.get(props.activeViewportId); + if (viewport?.displaySetInstanceUIDs[0]) { + const displaySet = displaySetService.getDisplaySetByUID(viewport.displaySetInstanceUIDs[0]); + if (displaySet) { + setStudyInstanceUID(displaySet.StudyInstanceUID); + } + } + }, [props.viewports, props.activeViewportId]); + + useEffect(() => { + const onAnnotationUpdated = () => { + const roiAnnotations = microscopyService.getAnnotationsForStudy(studyInstanceUID); + setRoiAnnotations(roiAnnotations); + }; + + const onAnnotationSelected = () => { + const selectedAnnotation = microscopyService.getSelectedAnnotation(); + setSelectedAnnotation(selectedAnnotation); + }; + + const onAnnotationRemoved = () => { + onAnnotationUpdated(); + }; + + const { unsubscribe: unsubscribeAnnotationUpdated } = microscopyService.subscribe( + MicroscopyEvents.ANNOTATION_UPDATED, + onAnnotationUpdated + ); + const { unsubscribe: unsubscribeAnnotationSelected } = microscopyService.subscribe( + MicroscopyEvents.ANNOTATION_SELECTED, + onAnnotationSelected + ); + const { unsubscribe: unsubscribeAnnotationRemoved } = microscopyService.subscribe( + MicroscopyEvents.ANNOTATION_REMOVED, + onAnnotationRemoved + ); + onAnnotationUpdated(); + onAnnotationSelected(); + + // on unload unsubscribe from events + return () => { + unsubscribeAnnotationUpdated(); + unsubscribeAnnotationSelected(); + unsubscribeAnnotationRemoved(); + }; + }, [studyInstanceUID]); + + /** + * On clicking "Save Annotations" button, prompt an input modal for the + * new series' description, and continue to save. + * + * @returns + */ + const promptSave = () => { + const annotations = microscopyService.getAnnotationsForStudy(studyInstanceUID); + + if (!annotations || saving) { + return; + } + + callInputDialog({ + uiDialogService, + title: 'Enter description of the Series', + defaultValue: '', + callback: (value: string, action: string) => { + switch (action) { + case 'save': { + saveFunction(value); + } + } + }, + }); + }; + + const getAllDisplaySets = (studyMetadata: any) => { + let allDisplaySets = [] as any[]; + studyMetadata.series.forEach((series: any) => { + const displaySets = displaySetService.getDisplaySetsForSeries(series.SeriesInstanceUID); + allDisplaySets = allDisplaySets.concat(displaySets); + }); + return allDisplaySets; + }; + + /** + * Save annotations as a series + * + * @param SeriesDescription - series description + * @returns + */ + const saveFunction = async (SeriesDescription: string) => { + const dataSource = extensionManager.getActiveDataSource()[0]; + const { onSaveComplete } = props; + const annotations = microscopyService.getAnnotationsForStudy(studyInstanceUID); + + saving = true; + + // There is only one viewer possible for one study, + // Since once study contains multiple resolution levels (series) of one whole + // Slide image. + + const studyMetadata = DicomMetadataStore.getStudy(studyInstanceUID); + const displaySets = getAllDisplaySets(studyMetadata); + const smDisplaySet = displaySets.find(ds => ds.Modality === 'SM'); + + // Get the next available series number after 4700. + + const dsWithMetadata = displaySets.filter( + ds => ds.metadata && ds.metadata.SeriesNumber && typeof ds.metadata.SeriesNumber === 'number' + ); + + // Generate next series number + const seriesNumbers = dsWithMetadata.map(ds => ds.metadata.SeriesNumber); + const maxSeriesNumber = Math.max(...seriesNumbers, 4700); + const SeriesNumber = maxSeriesNumber + 1; + + const { instance: metadata } = smDisplaySet; + + // construct SR dataset + const dataset = constructSR(metadata, { SeriesDescription, SeriesNumber }, annotations); + + // Save in DICOM format + try { + if (dataSource) { + if (dataSource.wadoRoot == 'saveDicom') { + // download as DICOM file + const part10Buffer = datasetToBuffer(dataset); + saveByteArray(part10Buffer, `sr-microscopy.dcm`); + } else { + // Save into Web Data source + const { StudyInstanceUID } = dataset; + await dataSource.store.dicom(dataset); + if (StudyInstanceUID) { + dataSource.deleteStudyMetadataPromise(StudyInstanceUID); + } + } + onSaveComplete({ + title: 'SR Saved', + message: 'Measurements downloaded successfully', + type: 'success', + }); + } else { + console.error('Server unspecified'); + } + } catch (error) { + onSaveComplete({ + title: 'SR Save Failed', + message: error.message || error.toString(), + type: 'error', + }); + } finally { + saving = false; + } + }; + + /** + * On clicking "Reject annotations" button + */ + const onDeleteCurrentSRHandler = async () => { + try { + const activeViewport = props.viewports[props.activeViewportId]; + const { StudyInstanceUID } = activeViewport; + + // TODO: studies? + const study = DicomMetadataStore.getStudy(StudyInstanceUID); + + const lastDerivedDisplaySet = study.derivedDisplaySets.sort((ds1: any, ds2: any) => { + const dateTime1 = Number(`${ds1.SeriesDate}${ds1.SeriesTime}`); + const dateTime2 = Number(`${ds2.SeriesDate}${ds2.SeriesTime}`); + return dateTime1 > dateTime2; + })[study.derivedDisplaySets.length - 1]; + + // TODO: use dataSource.reject.dicom() + // await DICOMSR.rejectMeasurements( + // study.wadoRoot, + // lastDerivedDisplaySet.StudyInstanceUID, + // lastDerivedDisplaySet.SeriesInstanceUID + // ); + props.onRejectComplete({ + title: 'Report rejected', + message: 'Latest report rejected successfully', + type: 'success', + }); + } catch (error) { + props.onRejectComplete({ + title: 'Failed to reject report', + message: error.message, + type: 'error', + }); + } + }; + + /** + * Handler for clicking event of an annotation item. + * + * @param param0 + */ + const onMeasurementItemClickHandler = ({ uid }: { uid: string }) => { + const roiAnnotation = microscopyService.getAnnotation(uid); + microscopyService.selectAnnotation(roiAnnotation); + microscopyService.focusAnnotation(roiAnnotation, props.activeViewportId); + }; + + /** + * Handler for "Edit" action of an annotation item + * @param param0 + */ + const onMeasurementItemEditHandler = ({ uid, isActive }: { uid: string; isActive: boolean }) => { + props.commandsManager.runCommand('setLabel', { uid }, 'MICROSCOPY'); + }; + + const onMeasurementDeleteHandler = ({ uid, isActive }: { uid: string; isActive: boolean }) => { + const roiAnnotation = microscopyService.getAnnotation(uid); + microscopyService.removeAnnotation(roiAnnotation); + }; + + // Convert ROI annotations managed by microscopyService into our + // own format for display + const data = roiAnnotations.map((roiAnnotation, index) => { + const label = roiAnnotation.getDetailedLabel(); + const area = roiAnnotation.getArea(); + const length = roiAnnotation.getLength(); + const shortAxisLength = roiAnnotation.roiGraphic.properties.shortAxisLength; + const isSelected: boolean = selectedAnnotation === roiAnnotation; + + // other events + const { uid } = roiAnnotation; + + // display text + const displayText = []; + + if (area !== undefined) { + displayText.push(formatArea(area)); + } else if (length !== undefined) { + displayText.push( + shortAxisLength + ? `${formatLength(length, 'ฮผm')} x ${formatLength(shortAxisLength, 'ฮผm')}` + : `${formatLength(length, 'ฮผm')}` + ); + } + + // convert to measurementItem format compatible with component + return { + uid, + index, + label, + isActive: isSelected, + displayText, + roiAnnotation, + }; + }); + + return ( + <> +
+ +
+ + ); +} + +const connectedMicroscopyPanel = withTranslation(['MicroscopyTable', 'Common'])(MicroscopyPanel); + +export default connectedMicroscopyPanel; diff --git a/extensions/dicom-microscopy/src/components/ViewportOverlay/ViewportOverlay.css b/extensions/dicom-microscopy/src/components/ViewportOverlay/ViewportOverlay.css new file mode 100644 index 0000000..5e90b20 --- /dev/null +++ b/extensions/dicom-microscopy/src/components/ViewportOverlay/ViewportOverlay.css @@ -0,0 +1,87 @@ +.DicomMicroscopyViewer .OpenLayersOverlay { + height: 100%; + width: 100%; + display: block !important; + pointer-events: none !important; +} + +.DicomMicroscopyViewer .text-primary-light { + font-size: 14px; + color: yellow; + font-weight: normal; +} + +.DicomMicroscopyViewer .text-primary-light span { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + max-width: 300px; + /* text-shadow: 0px 1px 1px rgba(225, 225, 225, 0.6), + 0px 1px 1px rgba(225, 225, 225, 0.6), + 1px 1px 3px rgba(225, 225, 225, 0.9), + 1px 1px 3px rgba(225, 225, 225, 0.9), + 1px 1px 3px rgba(225, 225, 225, 0.9), + 1px 1px 3px rgba(225, 225, 225, 0.9); */ +} + +.DicomMicroscopyViewer .absolute { + position: absolute; +} + +.DicomMicroscopyViewer .flex { + display: flex; +} + +.DicomMicroscopyViewer .flex-row { + flex-direction: row; +} + +.DicomMicroscopyViewer .flex-col { + flex-direction: column; +} + +.DicomMicroscopyViewer .pointer-events-none { + pointer-events: none; +} + +.DicomMicroscopyViewer .left-viewport-scrollbar { + left: 0.5rem; +} + +.DicomMicroscopyViewer .right-viewport-scrollbar { + right: 1.3rem; +} + +.DicomMicroscopyViewer .top-viewport { + top: 0.5rem; +} + +.DicomMicroscopyViewer .bottom-viewport { + bottom: 0.5rem; +} + +.DicomMicroscopyViewer .bottom-viewport.left-viewport { + bottom: 0.5rem; + left: calc(0.5rem + 250px); +} + +.DicomMicroscopyViewer .right-viewport-scrollbar .flex { + justify-content: end; +} + +.DicomMicroscopyViewer .microscopy-viewport-overlay { + padding: 0.5rem 1rem; + background: rgba(0, 0, 0, 0.5); + max-width: 40%; +} + +.DicomMicroscopyViewer .microscopy-viewport-overlay .flex { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.DicomMicroscopyViewer .top-viewport .flex span:not(.font-light) { + flex-shrink: 0; +} diff --git a/extensions/dicom-microscopy/src/components/ViewportOverlay/index.tsx b/extensions/dicom-microscopy/src/components/ViewportOverlay/index.tsx new file mode 100644 index 0000000..7b8175b --- /dev/null +++ b/extensions/dicom-microscopy/src/components/ViewportOverlay/index.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import classnames from 'classnames'; + +import listComponentGenerator from './listComponentGenerator'; +import './ViewportOverlay.css'; +import { formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils'; +import { utils } from '@ohif/core'; + +const { formatPN } = utils; + +interface OverlayItem { + id: string; + title: string; + value?: (props: any) => string; + condition?: (props: any) => boolean; + contents?: (props: any) => { className: string; value: any }; + generator?: (props: any) => any; +} + +/** + * + * @param {*} config is a configuration object that defines four lists of elements, + * one topLeft, topRight, bottomLeft, bottomRight contents. + * @param {*} extensionManager is used to load the image data. + * @returns + */ +export const generateFromConfig = ({ config, overlayData, ...props }) => { + const { + topLeft = [], + topRight = [], + bottomLeft = [], + bottomRight = [], + }: { + topLeft?: OverlayItem[]; + topRight?: OverlayItem[]; + bottomLeft?: OverlayItem[]; + bottomRight?: OverlayItem[]; + } = overlayData ?? {}; + const topLeftClass = 'top-viewport left-viewport text-primary-light'; + const topRightClass = 'top-viewport right-viewport-scrollbar text-primary-light'; + const bottomRightClass = 'bottom-viewport right-viewport-scrollbar text-primary-light'; + const bottomLeftClass = 'bottom-viewport left-viewport text-primary-light'; + const overlay = 'absolute pointer-events-none microscopy-viewport-overlay'; + + return ( + <> + {topLeft && topLeft.length > 0 && ( +
+ {listComponentGenerator({ ...props, list: topLeft, itemGenerator })} +
+ )} + {topRight && topRight.length > 0 && ( +
+ {listComponentGenerator({ + ...props, + list: topRight, + itemGenerator, + })} +
+ )} + {bottomRight && bottomRight.length > 0 && ( +
+ {listComponentGenerator({ + ...props, + list: bottomRight, + itemGenerator, + })} +
+ )} + {bottomLeft && bottomLeft.length > 0 && ( +
+ {listComponentGenerator({ + ...props, + list: bottomLeft, + itemGenerator, + })} +
+ )} + + ); +}; + +const itemGenerator = (props: any) => { + const { item } = props; + const { title, value: valueFunc, condition, contents } = item; + props.image = { ...props.image, ...props.metadata }; + props.formatDate = formatDICOMDate; + props.formatTime = formatDICOMTime; + props.formatPN = formatPN; + props.formatNumberPrecision = formatNumberPrecision; + if (condition && !condition(props)) { + return null; + } + if (!contents && !valueFunc) { + return null; + } + const value = valueFunc && valueFunc(props); + const contentsValue = (contents && contents(props)) || [ + { className: 'mr-1', value: title }, + { classname: 'mr-1 font-light', value }, + ]; + + return ( +
+ {contentsValue.map((content, idx) => ( + + {content.value} + + ))} +
+ ); +}; + +export default generateFromConfig; diff --git a/extensions/dicom-microscopy/src/components/ViewportOverlay/listComponentGenerator.tsx b/extensions/dicom-microscopy/src/components/ViewportOverlay/listComponentGenerator.tsx new file mode 100644 index 0000000..451607a --- /dev/null +++ b/extensions/dicom-microscopy/src/components/ViewportOverlay/listComponentGenerator.tsx @@ -0,0 +1,18 @@ +const listComponentGenerator = props => { + const { list, itemGenerator } = props; + if (!list) { + return; + } + return list.map(item => { + if (!item) { + return; + } + const generator = item.generator || itemGenerator; + if (!generator) { + throw new Error(`No generator for ${item}`); + } + return generator({ ...props, item }); + }); +}; + +export default listComponentGenerator; diff --git a/extensions/dicom-microscopy/src/components/ViewportOverlay/utils.ts b/extensions/dicom-microscopy/src/components/ViewportOverlay/utils.ts new file mode 100644 index 0000000..d47dd14 --- /dev/null +++ b/extensions/dicom-microscopy/src/components/ViewportOverlay/utils.ts @@ -0,0 +1,73 @@ +import moment from 'moment'; +import * as cornerstone from '@cornerstonejs/core'; + +/** + * Checks if value is valid. + * + * @param {number} value + * @returns {boolean} is valid. + */ +export function isValidNumber(value) { + return typeof value === 'number' && !isNaN(value); +} + +/** + * Formats number precision. + * + * @param {number} number + * @param {number} precision + * @returns {number} formatted number. + */ +export function formatNumberPrecision(number, precision) { + if (number !== null) { + return parseFloat(number).toFixed(precision); + } +} + +/** + * Formats DICOM date. + * + * @param {string} date + * @param {string} strFormat + * @returns {string} formatted date. + */ +export function formatDICOMDate(date, strFormat = 'MMM D, YYYY') { + return moment(date, 'YYYYMMDD').format(strFormat); +} + +/** + * DICOM Time is stored as HHmmss.SSS, where: + * HH 24 hour time: + * m mm 0..59 Minutes + * s ss 0..59 Seconds + * S SS SSS 0..999 Fractional seconds + * + * Goal: '24:12:12' + * + * @param {*} time + * @param {string} strFormat + * @returns {string} formatted name. + */ +export function formatDICOMTime(time, strFormat = 'HH:mm:ss') { + return moment(time, 'HH:mm:ss').format(strFormat); +} + +/** + * Gets compression type + * + * @param {number} imageId + * @returns {string} compression type. + */ +export function getCompression(imageId) { + const generalImageModule = cornerstone.metaData.get('generalImageModule', imageId) || {}; + const { lossyImageCompression, lossyImageCompressionRatio, lossyImageCompressionMethod } = + generalImageModule; + + if (lossyImageCompression === '01' && lossyImageCompressionRatio !== '') { + const compressionMethod = lossyImageCompressionMethod || 'Lossy: '; + const compressionRatio = formatNumberPrecision(lossyImageCompressionRatio, 2); + return compressionMethod + compressionRatio + ' : 1'; + } + + return 'Lossless / Uncompressed'; +} diff --git a/extensions/dicom-microscopy/src/customizations/panelMeasurementItem.tsx b/extensions/dicom-microscopy/src/customizations/panelMeasurementItem.tsx new file mode 100644 index 0000000..e971d7a --- /dev/null +++ b/extensions/dicom-microscopy/src/customizations/panelMeasurementItem.tsx @@ -0,0 +1,5 @@ +import { MeasurementItem } from '@ohif/ui'; + +export default { + 'microscopyPanel.measurementItem': MeasurementItem, +}; diff --git a/extensions/dicom-microscopy/src/getCommandsModule.ts b/extensions/dicom-microscopy/src/getCommandsModule.ts new file mode 100644 index 0000000..5ed91a1 --- /dev/null +++ b/extensions/dicom-microscopy/src/getCommandsModule.ts @@ -0,0 +1,157 @@ +import { CommandsManager, ExtensionManager } from '@ohif/core'; +import { callInputDialog } from '@ohif/extension-default'; +import styles from './utils/styles'; + +export default function getCommandsModule({ + servicesManager, + commandsManager, + extensionManager, +}: { + servicesManager: AppTypes.ServicesManager; + commandsManager: CommandsManager; + extensionManager: ExtensionManager; +}) { + const { viewportGridService, uiDialogService, microscopyService } = servicesManager.services; + + const actions = { + // Measurement tool commands: + deleteMeasurement: ({ uid }) => { + if (uid) { + const roiAnnotation = microscopyService.getAnnotation(uid); + if (roiAnnotation) { + microscopyService.removeAnnotation(roiAnnotation); + } + } + }, + + setLabel: ({ uid }) => { + const roiAnnotation = microscopyService.getAnnotation(uid); + callInputDialog({ + uiDialogService, + defaultValue: '', + callback: (value: string, action: string) => { + switch (action) { + case 'save': { + roiAnnotation.setLabel(value); + microscopyService.triggerRelabel(roiAnnotation); + } + } + }, + }); + }, + + setToolActive: ({ toolName, toolGroupId = 'MICROSCOPY' }) => { + const dragPanOnMiddle = [ + 'dragPan', + { + bindings: { + mouseButtons: ['middle'], + }, + }, + ]; + const dragZoomOnRight = [ + 'dragZoom', + { + bindings: { + mouseButtons: ['right'], + }, + }, + ]; + if ( + ['line', 'box', 'circle', 'point', 'polygon', 'freehandpolygon', 'freehandline'].indexOf( + toolName + ) >= 0 + ) { + // TODO: read from configuration + const options = { + geometryType: toolName, + vertexEnabled: true, + styleOptions: styles.default, + bindings: { + mouseButtons: ['left'], + }, + } as any; + if ('line' === toolName) { + options.minPoints = 2; + options.maxPoints = 2; + } else if ('point' === toolName) { + delete options.styleOptions; + delete options.vertexEnabled; + } + + microscopyService.activateInteractions([ + ['draw', options], + dragPanOnMiddle, + dragZoomOnRight, + ]); + } else if (toolName == 'dragPan') { + microscopyService.activateInteractions([ + [ + 'dragPan', + { + bindings: { + mouseButtons: ['left', 'middle'], + }, + }, + ], + dragZoomOnRight, + ]); + } else { + microscopyService.activateInteractions([ + [ + toolName, + { + bindings: { + mouseButtons: ['left'], + }, + }, + ], + dragPanOnMiddle, + dragZoomOnRight, + ]); + } + }, + toggleOverlays: () => { + // overlay + const overlays = document.getElementsByClassName('microscopy-viewport-overlay'); + let onoff = false; // true if this will toggle on + for (let i = 0; i < overlays.length; i++) { + if (i === 0) { + onoff = overlays.item(0).classList.contains('hidden'); + } + overlays.item(i).classList.toggle('hidden'); + } + + // overview + const { activeViewportId } = viewportGridService.getState(); + microscopyService.toggleOverviewMap(activeViewportId); + }, + toggleAnnotations: () => { + microscopyService.toggleROIsVisibility(); + }, + }; + + const definitions = { + deleteMeasurement: { + commandFn: actions.deleteMeasurement, + }, + setLabel: { + commandFn: actions.setLabel, + }, + setToolActive: { + commandFn: actions.setToolActive, + }, + toggleOverlays: { + commandFn: actions.toggleOverlays, + }, + toggleAnnotations: { + commandFn: actions.toggleAnnotations, + }, + }; + + return { + actions, + definitions, + defaultContext: 'MICROSCOPY', + }; +} diff --git a/extensions/dicom-microscopy/src/getCustomizationModule.ts b/extensions/dicom-microscopy/src/getCustomizationModule.ts new file mode 100644 index 0000000..bee4fd0 --- /dev/null +++ b/extensions/dicom-microscopy/src/getCustomizationModule.ts @@ -0,0 +1,12 @@ +import panelMeasurementItem from './customizations/panelMeasurementItem'; + +export default function getCustomizationModule() { + return [ + { + name: 'default', + value: { + ...panelMeasurementItem, + }, + }, + ]; +} diff --git a/extensions/dicom-microscopy/src/getPanelModule.tsx b/extensions/dicom-microscopy/src/getPanelModule.tsx new file mode 100644 index 0000000..6500e8d --- /dev/null +++ b/extensions/dicom-microscopy/src/getPanelModule.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import type { Types } from '@ohif/core'; +import { useViewportGrid } from '@ohif/ui-next'; +import MicroscopyPanel from './components/MicroscopyPanel/MicroscopyPanel'; + +// TODO: +// - No loading UI exists yet +// - cancel promises when component is destroyed +// - show errors in UI for thumbnails if promise fails + +export default function getPanelModule({ + commandsManager, + extensionManager, + servicesManager, +}: Types.Extensions.ExtensionParams) { + const wrappedMeasurementPanel = ({}) => { + const [{ activeViewportId, viewports }] = useViewportGrid(); + + return ( + {}} + onRejectComplete={() => {}} + commandsManager={commandsManager} + servicesManager={servicesManager} + extensionManager={extensionManager} + /> + ); + }; + + return [ + { + name: 'measure', + iconName: 'tab-linear', + iconLabel: 'Measure', + label: 'Measurements', + secondaryLabel: 'Measurements', + component: wrappedMeasurementPanel, + }, + ]; +} diff --git a/extensions/dicom-microscopy/src/helpers/formatDICOMDate.js b/extensions/dicom-microscopy/src/helpers/formatDICOMDate.js new file mode 100644 index 0000000..c048df4 --- /dev/null +++ b/extensions/dicom-microscopy/src/helpers/formatDICOMDate.js @@ -0,0 +1,11 @@ +import moment from 'moment'; + +/** + * Formats DICOM date. + * + * @param {string} date + * @param {string} strFormat + */ +export default function formatDICOMDate(date, strFormat = 'MMM D, YYYY') { + return moment(date, 'YYYYMMDD').format(strFormat); +} diff --git a/extensions/dicom-microscopy/src/helpers/formatDICOMDate.test.js b/extensions/dicom-microscopy/src/helpers/formatDICOMDate.test.js new file mode 100644 index 0000000..abb9b2c --- /dev/null +++ b/extensions/dicom-microscopy/src/helpers/formatDICOMDate.test.js @@ -0,0 +1,9 @@ +import formatDICOMDate from './formatDICOMDate'; + +describe('formatDICOMDate', () => { + it('should format DICOM date string', () => { + const date = '20180916'; + const formattedDate = formatDICOMDate(date); + expect(formattedDate).toEqual('Sep 16, 2018'); + }); +}); diff --git a/extensions/dicom-microscopy/src/helpers/formatDICOMPatientName.js b/extensions/dicom-microscopy/src/helpers/formatDICOMPatientName.js new file mode 100644 index 0000000..c1b284a --- /dev/null +++ b/extensions/dicom-microscopy/src/helpers/formatDICOMPatientName.js @@ -0,0 +1,23 @@ +/** + * Formats a patient name for display purposes. + * + * @param {string} name DICOM patient name string + * @returns {string} formatted name + */ +export default function formatDICOMPatientName(name) { + if (typeof name !== 'string') { + return; + } + + /** + * Convert the first ^ to a ', '. String.replace() only affects + * the first appearance of the character. + */ + const commaBetweenFirstAndLast = name.replace('^', ', '); + + /** Replace any remaining '^' characters with spaces */ + const cleaned = commaBetweenFirstAndLast.replace(/\^/g, ' '); + + /** Trim any extraneous whitespace */ + return cleaned.trim(); +} diff --git a/extensions/dicom-microscopy/src/helpers/formatDICOMPatientName.test.js b/extensions/dicom-microscopy/src/helpers/formatDICOMPatientName.test.js new file mode 100644 index 0000000..efda1ed --- /dev/null +++ b/extensions/dicom-microscopy/src/helpers/formatDICOMPatientName.test.js @@ -0,0 +1,17 @@ +import formatDICOMPatientName from './formatDICOMPatientName'; + +describe('formatDICOMPatientName', () => { + it('should format DICOM patient name correctly', () => { + const patientName = 'Blackford^Test'; + const formattedPatientName = formatDICOMPatientName(patientName); + expect(formattedPatientName).toEqual('Blackford, Test'); + }); + + it('should return undefined it input is not a string', () => { + expect(formatDICOMPatientName(123)).toEqual(undefined); + expect(formatDICOMPatientName(null)).toEqual(undefined); + expect(formatDICOMPatientName(undefined)).toEqual(undefined); + expect(formatDICOMPatientName(false)).toEqual(undefined); + expect(formatDICOMPatientName([])).toEqual(undefined); + }); +}); diff --git a/extensions/dicom-microscopy/src/helpers/formatDICOMTime.js b/extensions/dicom-microscopy/src/helpers/formatDICOMTime.js new file mode 100644 index 0000000..ea2deb7 --- /dev/null +++ b/extensions/dicom-microscopy/src/helpers/formatDICOMTime.js @@ -0,0 +1,17 @@ +import moment from 'moment'; + +/** + * DICOM Time is stored as HHmmss.SSS, where: + * HH 24 hour time: + * m mm 0..59 Minutes + * s ss 0..59 Seconds + * S SS SSS 0..999 Fractional seconds + * + * Goal: '24:12:12' + * + * @param {*} time + * @param {string} strFormat + */ +export default function formatDICOMTime(time, strFormat = 'HH:mm:ss') { + return moment(time, 'HH:mm:ss').format(strFormat); +} diff --git a/extensions/dicom-microscopy/src/helpers/formatDICOMTime.test.js b/extensions/dicom-microscopy/src/helpers/formatDICOMTime.test.js new file mode 100644 index 0000000..a94d9bc --- /dev/null +++ b/extensions/dicom-microscopy/src/helpers/formatDICOMTime.test.js @@ -0,0 +1,9 @@ +import formatDICOMTime from './formatDICOMTime'; + +describe('formatDICOMTime', () => { + it('should format DICOM time string', () => { + const time = '101300.000'; + const formattedTime = formatDICOMTime(time); + expect(formattedTime).toEqual('10:13:00'); + }); +}); diff --git a/extensions/dicom-microscopy/src/helpers/formatNumberPrecision.js b/extensions/dicom-microscopy/src/helpers/formatNumberPrecision.js new file mode 100644 index 0000000..9aa152f --- /dev/null +++ b/extensions/dicom-microscopy/src/helpers/formatNumberPrecision.js @@ -0,0 +1,9 @@ +/** + * Formats a number to a fixed precision. + * + * @param {number} number + * @param {number} precision + */ +export default function formatNumberPrecision(number, precision) { + return Number(parseFloat(number).toFixed(precision)); +} diff --git a/extensions/dicom-microscopy/src/helpers/formatNumberPrecision.test.js b/extensions/dicom-microscopy/src/helpers/formatNumberPrecision.test.js new file mode 100644 index 0000000..0ccce4d --- /dev/null +++ b/extensions/dicom-microscopy/src/helpers/formatNumberPrecision.test.js @@ -0,0 +1,9 @@ +import formatNumberPrecision from './formatNumberPrecision'; + +describe('formatNumberPrecision', () => { + it('should format number precision', () => { + const number = 0.229387; + const formattedNumber = formatNumberPrecision(number, 2); + expect(formattedNumber).toEqual(0.23); + }); +}); diff --git a/extensions/dicom-microscopy/src/helpers/index.js b/extensions/dicom-microscopy/src/helpers/index.js new file mode 100644 index 0000000..5946898 --- /dev/null +++ b/extensions/dicom-microscopy/src/helpers/index.js @@ -0,0 +1,15 @@ +import formatDICOMPatientName from './formatDICOMPatientName'; +import formatDICOMDate from './formatDICOMDate'; +import formatDICOMTime from './formatDICOMTime'; +import formatNumberPrecision from './formatNumberPrecision'; +import isValidNumber from './isValidNumber'; + +const helpers = { + formatDICOMPatientName, + formatDICOMDate, + formatDICOMTime, + formatNumberPrecision, + isValidNumber, +}; + +export default helpers; diff --git a/extensions/dicom-microscopy/src/helpers/isValidNumber.js b/extensions/dicom-microscopy/src/helpers/isValidNumber.js new file mode 100644 index 0000000..086b1c0 --- /dev/null +++ b/extensions/dicom-microscopy/src/helpers/isValidNumber.js @@ -0,0 +1,3 @@ +export default function isValidNumber(value) { + return typeof value === 'number' && !isNaN(value); +} diff --git a/extensions/dicom-microscopy/src/id.js b/extensions/dicom-microscopy/src/id.js new file mode 100644 index 0000000..ebe5acd --- /dev/null +++ b/extensions/dicom-microscopy/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/extensions/dicom-microscopy/src/index.tsx b/extensions/dicom-microscopy/src/index.tsx new file mode 100644 index 0000000..4023222 --- /dev/null +++ b/extensions/dicom-microscopy/src/index.tsx @@ -0,0 +1,163 @@ +import { id } from './id'; +import React, { Suspense, useMemo } from 'react'; +import getPanelModule from './getPanelModule'; +import getCommandsModule from './getCommandsModule'; +import getCustomizationModule from './getCustomizationModule'; +import { Types } from '@ohif/core'; + +import { useViewportGrid } from '@ohif/ui-next'; +import getDicomMicroscopySRSopClassHandler from './DicomMicroscopySRSopClassHandler'; +import MicroscopyService from './services/MicroscopyService'; +import { useResizeDetector } from 'react-resize-detector'; +import debounce from 'lodash.debounce'; + +const Component = React.lazy(() => { + return import('./DicomMicroscopyViewport'); +}); + +const MicroscopyViewport = props => { + return ( + Loading...
}> + + + ); +}; + +/** + * You can remove any of the following modules if you don't need them. + */ +const extension: Types.Extensions.Extension = { + /** + * Only required property. Should be a unique value across all extensions. + * You ID can be anything you want, but it should be unique. + */ + id, + + async preRegistration({ servicesManager }) { + servicesManager.registerService(MicroscopyService.REGISTRATION(servicesManager)); + }, + + /** + * ViewportModule should provide a list of viewports that will be available in OHIF + * for Modes to consume and use in the viewports. Each viewport is defined by + * {name, component} object. Example of a viewport module is the CornerstoneViewport + * that is provided by the Cornerstone extension in OHIF. + */ + getViewportModule({ servicesManager, extensionManager, commandsManager }) { + /** + * + * @param props {*} + * @param props.displaySets + * @param props.viewportId + * @param props.viewportLabel + * @param props.dataSource + * @param props.viewportOptions + * @param props.displaySetOptions + * @returns + */ + const ExtendedMicroscopyViewport = props => { + const { viewportOptions } = props; + + const [viewportGrid, viewportGridService] = useViewportGrid(); + const { activeViewportId } = viewportGrid; + + const displaySetsKey = useMemo(() => { + return props.displaySets.map(ds => ds.displaySetInstanceUID).join('-'); + }, [props.displaySets]); + + const onResize = debounce(() => { + const { microscopyService } = servicesManager.services; + const managedViewer = microscopyService.getAllManagedViewers(); + + if (managedViewer && managedViewer.length > 0) { + managedViewer[0].viewer.resize(); + } + }, 100); + + const { ref: resizeRef } = useResizeDetector({ + onResize, + handleHeight: true, + handleWidth: true, + }); + + return ( + { + viewportGridService.setActiveViewportId(viewportId); + }} + viewportData={viewportOptions} + resizeRef={resizeRef} + {...props} + /> + ); + }; + + return [ + { + name: 'microscopy-dicom', + component: ExtendedMicroscopyViewport, + }, + ]; + }, + + getToolbarModule({ servicesManager }) { + return [ + { + name: 'evaluate.microscopyTool', + evaluate: ({ button }) => { + const { microscopyService } = servicesManager.services; + + const activeInteractions = microscopyService.getActiveInteractions(); + if (!activeInteractions) { + return false; + } + const isPrimaryActive = activeInteractions.find(interactions => { + const sameMouseButton = interactions[1].bindings.mouseButtons.includes('left'); + + if (!sameMouseButton) { + return false; + } + + const notDraw = interactions[0] !== 'draw'; + + // there seems to be a custom logic for draw tool for some reason + return notDraw + ? interactions[0] === button.id + : interactions[1].geometryType === button.id; + }); + + return { + disabled: false, + className: isPrimaryActive + ? '!text-black bg-primary-light' + : '!text-common-bright hover:!bg-primary-dark hover:!text-primary-light', + // Todo: isActive right now is used for nested buttons where the primary + // button needs to be fully rounded (vs partial rounded) when active + // otherwise it does not have any other use + isActive: isPrimaryActive, + }; + }, + }, + ]; + }, + + /** + * SopClassHandlerModule should provide a list of sop class handlers that will be + * available in OHIF for Modes to consume and use to create displaySets from Series. + * Each sop class handler is defined by a { name, sopClassUids, getDisplaySetsFromSeries}. + * Examples include the default sop class handler provided by the default extension + */ + getSopClassHandlerModule(params) { + return [getDicomMicroscopySRSopClassHandler(params)]; + }, + + getPanelModule, + + getCommandsModule, + + getCustomizationModule, +}; + +export default extension; diff --git a/extensions/dicom-microscopy/src/services/MicroscopyService.ts b/extensions/dicom-microscopy/src/services/MicroscopyService.ts new file mode 100644 index 0000000..e59f048 --- /dev/null +++ b/extensions/dicom-microscopy/src/services/MicroscopyService.ts @@ -0,0 +1,647 @@ +import ViewerManager, { EVENTS as ViewerEvents } from '../tools/viewerManager'; +import RoiAnnotation, { EVENTS as AnnotationEvents } from '../utils/RoiAnnotation'; +import styles from '../utils/styles'; +import { DicomMetadataStore, PubSubService } from '@ohif/core'; + +const EVENTS = { + ANNOTATION_UPDATED: 'annotationUpdated', + ANNOTATION_SELECTED: 'annotationSelected', + ANNOTATION_REMOVED: 'annotationRemoved', + RELABEL: 'relabel', + DELETE: 'delete', +}; + +/** + * MicroscopyService is responsible to manage multiple third-party API's + * microscopy viewers expose methods to manage the interaction with these + * viewers and handle their ROI graphics to create, remove and modify the + * ROI annotations relevant to the application + */ +export default class MicroscopyService extends PubSubService { + public static REGISTRATION = servicesManager => { + return { + name: 'microscopyService', + altName: 'MicroscopyService', + create: (props) => { + return new MicroscopyService(props); + }, + }; + }; + + servicesManager: any; + + managedViewers = new Set(); + roiUids = new Set(); + annotations = {}; + selectedAnnotation = null; + pendingFocus = false; + + constructor({ servicesManager, extensionManager }) { + super(EVENTS); + this.servicesManager = servicesManager; + this.peerImport = extensionManager.appConfig.peerImport; + this._onRoiAdded = this._onRoiAdded.bind(this); + this._onRoiModified = this._onRoiModified.bind(this); + this._onRoiRemoved = this._onRoiRemoved.bind(this); + this._onRoiUpdated = this._onRoiUpdated.bind(this); + this._onRoiSelected = this._onRoiSelected.bind(this); + this.isROIsVisible = true; + } + + /** + * Clears all the annotations and managed viewers, setting the manager state + * to its initial state + */ + clear() { + this.managedViewers.forEach(managedViewer => managedViewer.destroy()); + this.managedViewers.clear(); + for (const key in this.annotations) { + delete this.annotations[key]; + } + + this.roiUids.clear(); + this.selectedAnnotation = null; + this.pendingFocus = false; + } + + clearAnnotations() { + Object.keys(this.annotations).forEach(uid => { + this.removeAnnotation(this.annotations[uid]); + }); + } + + public importDicomMicroscopyViewer(): Promise { + return this.peerImport("dicom-microscopy-viewer"); + } + + /** + * Observes when a ROI graphic is added, creating the correspondent annotation + * with the current graphic and view state. + * Creates a subscription for label updating for the created annotation and + * publishes an ANNOTATION_UPDATED event when it happens. + * Also triggers the relabel process after the graphic is placed. + * + * @param {Object} data The published data + * @param {Object} data.roiGraphic The added ROI graphic object + * @param {ViewerManager} data.managedViewer The origin viewer for the event + */ + _onRoiAdded(data) { + const { roiGraphic, managedViewer, label } = data; + const { studyInstanceUID, seriesInstanceUID } = managedViewer; + const viewState = managedViewer.getViewState(); + + const roiAnnotation = new RoiAnnotation( + roiGraphic, + studyInstanceUID, + seriesInstanceUID, + '', + viewState + ); + + this.roiUids.add(roiGraphic.uid); + this.annotations[roiGraphic.uid] = roiAnnotation; + + roiAnnotation.subscribe(AnnotationEvents.LABEL_UPDATED, () => { + this._broadcastEvent(EVENTS.ANNOTATION_UPDATED, roiAnnotation); + }); + + if (label !== undefined) { + roiAnnotation.setLabel(label); + } else { + const onRelabel = item => + managedViewer.updateROIProperties({ + uid: roiGraphic.uid, + properties: { label: item.label, finding: item.finding }, + }); + this.triggerRelabel(roiAnnotation, true, onRelabel); + } + } + + /** + * Observes when a ROI graphic is modified, updating the correspondent + * annotation with the current graphic and view state. + * + * @param {Object} data The published data + * @param {Object} data.roiGraphic The modified ROI graphic object + */ + _onRoiModified(data) { + const { roiGraphic, managedViewer } = data; + const roiAnnotation = this.getAnnotation(roiGraphic.uid); + if (!roiAnnotation) { + return; + } + roiAnnotation.setRoiGraphic(roiGraphic); + roiAnnotation.setViewState(managedViewer.getViewState()); + } + + /** + * Observes when a ROI graphic is removed, reflecting the removal in the + * annotations' state. + * + * @param {Object} data The published data + * @param {Object} data.roiGraphic The removed ROI graphic object + */ + _onRoiRemoved(data) { + const { roiGraphic } = data; + this.roiUids.delete(roiGraphic.uid); + this.annotations[roiGraphic.uid].destroy(); + delete this.annotations[roiGraphic.uid]; + this._broadcastEvent(EVENTS.ANNOTATION_REMOVED, roiGraphic); + } + + /** + * Observes any changes on ROI graphics and synchronize all the managed + * viewers to reflect those changes. + * Also publishes an ANNOTATION_UPDATED event to notify the subscribers. + * + * @param {Object} data The published data + * @param {Object} data.roiGraphic The added ROI graphic object + * @param {ViewerManager} data.managedViewer The origin viewer for the event + */ + _onRoiUpdated(data) { + const { roiGraphic, managedViewer } = data; + this.synchronizeViewers(managedViewer); + this._broadcastEvent(EVENTS.ANNOTATION_UPDATED, this.getAnnotation(roiGraphic.uid)); + } + + /** + * Observes when an ROI is selected. + * Also publishes an ANNOTATION_SELECTED event to notify the subscribers. + * + * @param {Object} data The published data + * @param {Object} data.roiGraphic The added ROI graphic object + * @param {ViewerManager} data.managedViewer The origin viewer for the event + */ + _onRoiSelected(data) { + const { roiGraphic } = data; + const selectedAnnotation = this.getAnnotation(roiGraphic.uid); + if (selectedAnnotation && selectedAnnotation !== this.getSelectedAnnotation()) { + if (this.selectedAnnotation) { + this.clearSelection(); + } + this.selectedAnnotation = selectedAnnotation; + this._broadcastEvent(EVENTS.ANNOTATION_SELECTED, selectedAnnotation); + } + } + + /** + * Creates the subscriptions for the managed viewer being added + * + * @param {ViewerManager} managedViewer The viewer being added + */ + _addManagedViewerSubscriptions(managedViewer) { + managedViewer._roiAddedSubscription = managedViewer.subscribe( + ViewerEvents.ADDED, + this._onRoiAdded + ); + managedViewer._roiModifiedSubscription = managedViewer.subscribe( + ViewerEvents.MODIFIED, + this._onRoiModified + ); + managedViewer._roiRemovedSubscription = managedViewer.subscribe( + ViewerEvents.REMOVED, + this._onRoiRemoved + ); + managedViewer._roiUpdatedSubscription = managedViewer.subscribe( + ViewerEvents.UPDATED, + this._onRoiUpdated + ); + managedViewer._roiSelectedSubscription = managedViewer.subscribe( + ViewerEvents.UPDATED, + this._onRoiSelected + ); + } + + /** + * Removes the subscriptions for the managed viewer being removed + * + * @param {ViewerManager} managedViewer The viewer being removed + */ + _removeManagedViewerSubscriptions(managedViewer) { + managedViewer._roiAddedSubscription && managedViewer._roiAddedSubscription.unsubscribe(); + managedViewer._roiModifiedSubscription && managedViewer._roiModifiedSubscription.unsubscribe(); + managedViewer._roiRemovedSubscription && managedViewer._roiRemovedSubscription.unsubscribe(); + managedViewer._roiUpdatedSubscription && managedViewer._roiUpdatedSubscription.unsubscribe(); + managedViewer._roiSelectedSubscription && managedViewer._roiSelectedSubscription.unsubscribe(); + + managedViewer._roiAddedSubscription = null; + managedViewer._roiModifiedSubscription = null; + managedViewer._roiRemovedSubscription = null; + managedViewer._roiUpdatedSubscription = null; + managedViewer._roiSelectedSubscription = null; + } + + /** + * Returns the managed viewers that are displaying the image with the given + * study and series UIDs + * + * @param {String} studyInstanceUID UID for the study + * @param {String} seriesInstanceUID UID for the series + * + * @returns {Array} The managed viewers for the given series UID + */ + _getManagedViewersForSeries(studyInstanceUID, seriesInstanceUID) { + const filter = managedViewer => + managedViewer.studyInstanceUID === studyInstanceUID && + managedViewer.seriesInstanceUID === seriesInstanceUID; + return Array.from(this.managedViewers).filter(filter); + } + + /** + * Returns the managed viewers that are displaying the image with the given + * study UID + * + * @param {String} studyInstanceUID UID for the study + * + * @returns {Array} The managed viewers for the given series UID + */ + getManagedViewersForStudy(studyInstanceUID) { + const filter = managedViewer => managedViewer.studyInstanceUID === studyInstanceUID; + return Array.from(this.managedViewers).filter(filter); + } + + /** + * Restores the created annotations for the viewer being added + * + * @param {ViewerManager} managedViewer The viewer being added + */ + _restoreAnnotations(managedViewer) { + const { studyInstanceUID, seriesInstanceUID } = managedViewer; + const annotations = this.getAnnotationsForSeries(studyInstanceUID, seriesInstanceUID); + annotations.forEach(roiAnnotation => { + managedViewer.addRoiGraphic(roiAnnotation.roiGraphic); + }); + } + + /** + * Creates a managed viewer instance for the given third-party API's viewer. + * Restores existing annotations for the given study/series. + * Adds event subscriptions for the viewer being added. + * Focuses the selected annotation when the viewer is being loaded into the + * active viewport. + * + * @param viewer - Third-party viewer API's object to be managed + * @param viewportId - The viewport Id where the viewer will be loaded + * @param container - The DOM element where it will be rendered + * @param studyInstanceUID - The study UID of the loaded image + * @param seriesInstanceUID - The series UID of the loaded image + * @param displaySets - All displaySets related to the same StudyInstanceUID + * + * @returns {ViewerManager} managed viewer + */ + addViewer(viewer, viewportId, container, studyInstanceUID, seriesInstanceUID) { + const managedViewer = new ViewerManager( + viewer, + viewportId, + container, + studyInstanceUID, + seriesInstanceUID + ); + + this._restoreAnnotations(managedViewer); + viewer._manager = managedViewer; + this.managedViewers.add(managedViewer); + + // this._potentiallyLoadSR(studyInstanceUID, displaySets); + this._addManagedViewerSubscriptions(managedViewer); + + if (this.pendingFocus) { + this.pendingFocus = false; + this.focusAnnotation(this.selectedAnnotation, viewportId); + } + + return managedViewer; + } + + _potentiallyLoadSR(StudyInstanceUID, displaySets) { + const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID); + const smDisplaySet = displaySets.find(ds => ds.Modality === 'SM'); + + const { FrameOfReferenceUID, othersFrameOfReferenceUID } = smDisplaySet; + + if (!studyMetadata) { + return; + } + + let derivedDisplaySets = FrameOfReferenceUID + ? displaySets.filter( + ds => + ds.ReferencedFrameOfReferenceUID === FrameOfReferenceUID || + // sometimes each depth instance has the different FrameOfReferenceID + othersFrameOfReferenceUID.includes(ds.ReferencedFrameOfReferenceUID) + ) + : []; + + if (!derivedDisplaySets.length) { + return; + } + + derivedDisplaySets = derivedDisplaySets.filter(ds => ds.Modality === 'SR'); + + if (derivedDisplaySets.some(ds => ds.isLoaded === true)) { + // Don't auto load + return; + } + + // find most recent and load it. + let recentDateTime = 0; + let recentDisplaySet = derivedDisplaySets[0]; + + derivedDisplaySets.forEach(ds => { + const dateTime = Number(`${ds.SeriesDate}${ds.SeriesTime}`); + if (dateTime > recentDateTime) { + recentDateTime = dateTime; + recentDisplaySet = ds; + } + }); + + recentDisplaySet.isLoading = true; + + recentDisplaySet.load(smDisplaySet); + } + + /** + * Removes the given third-party viewer API's object from the managed viewers + * and clears all its event subscriptions + * + * @param {Object} viewer Third-party viewer API's object to be removed + */ + removeViewer(viewer) { + const managedViewer = viewer._manager; + + this._removeManagedViewerSubscriptions(managedViewer); + managedViewer.destroy(); + this.managedViewers.delete(managedViewer); + } + + /** + * Toggle ROIs visibility + */ + toggleROIsVisibility() { + this.isROIsVisible ? this.hideROIs() : this.showROIs; + this.isROIsVisible = !this.isROIsVisible; + } + + /** + * Hide all ROIs + */ + hideROIs() { + this.managedViewers.forEach(mv => mv.hideROIs()); + } + + /** Show all ROIs */ + showROIs() { + this.managedViewers.forEach(mv => mv.showROIs()); + } + + /** + * Returns a RoiAnnotation instance for the given ROI UID + * + * @param {String} uid UID of the annotation + * + * @returns {RoiAnnotation} The RoiAnnotation instance found for the given UID + */ + getAnnotation(uid) { + return this.annotations[uid]; + } + + /** + * Returns all the RoiAnnotation instances being managed + * + * @returns {Array} All RoiAnnotation instances + */ + getAnnotations() { + const annotations = []; + Object.keys(this.annotations).forEach(uid => { + annotations.push(this.getAnnotation(uid)); + }); + return annotations; + } + + /** + * Returns the RoiAnnotation instances registered with the given study UID + * + * @param {String} studyInstanceUID UID for the study + */ + getAnnotationsForStudy(studyInstanceUID) { + const filter = a => a.studyInstanceUID === studyInstanceUID; + return this.getAnnotations().filter(filter); + } + + /** + * Returns the RoiAnnotation instances registered with the given study and + * series UIDs + * + * @param {String} studyInstanceUID UID for the study + * @param {String} seriesInstanceUID UID for the series + */ + getAnnotationsForSeries(studyInstanceUID, seriesInstanceUID) { + const filter = annotation => + annotation.studyInstanceUID === studyInstanceUID && + annotation.seriesInstanceUID === seriesInstanceUID; + return this.getAnnotations().filter(filter); + } + + /** + * Returns the selected RoiAnnotation instance or null if none is selected + * + * @returns {RoiAnnotation} The selected RoiAnnotation instance + */ + getSelectedAnnotation() { + return this.selectedAnnotation; + } + + /** + * Clear current RoiAnnotation selection + */ + clearSelection() { + if (this.selectedAnnotation) { + this.setROIStyle(this.selectedAnnotation.uid, { + stroke: { + color: '#00ff00', + }, + }); + } + this.selectedAnnotation = null; + } + + /** + * Selects the given RoiAnnotation instance, publishing an ANNOTATION_SELECTED + * event to notify all the subscribers + * + * @param {RoiAnnotation} roiAnnotation The instance to be selected + */ + selectAnnotation(roiAnnotation) { + if (this.selectedAnnotation) { + this.clearSelection(); + } + + this.selectedAnnotation = roiAnnotation; + this._broadcastEvent(EVENTS.ANNOTATION_SELECTED, roiAnnotation); + this.setROIStyle(roiAnnotation.uid, styles.active); + } + + /** + * Toggles overview map + * + * @param viewportId The active viewport index + * @returns {void} + */ + toggleOverviewMap(viewportId) { + const managedViewers = Array.from(this.managedViewers); + const managedViewer = managedViewers.find(mv => mv.viewportId === viewportId); + if (managedViewer) { + managedViewer.toggleOverviewMap(); + } + } + + /** + * Removes a RoiAnnotation instance from the managed annotations and reflects + * its removal on all third-party viewers being managed + * + * @param {RoiAnnotation} roiAnnotation The instance to be removed + */ + removeAnnotation(roiAnnotation) { + const { uid, studyInstanceUID, seriesInstanceUID } = roiAnnotation; + const filter = managedViewer => + managedViewer.studyInstanceUID === studyInstanceUID && + managedViewer.seriesInstanceUID === seriesInstanceUID; + + const managedViewers = Array.from(this.managedViewers).filter(filter); + + managedViewers.forEach(managedViewer => managedViewer.removeRoiGraphic(uid)); + + if (this.annotations[uid]) { + this.roiUids.delete(uid); + this.annotations[uid].destroy(); + delete this.annotations[uid]; + + this._broadcastEvent(EVENTS.ANNOTATION_REMOVED, roiAnnotation); + } + } + + /** + * Focus the given RoiAnnotation instance by changing the OpenLayers' Map view + * state of the managed viewer with the given viewport index. + * If the image for the given annotation is not yet loaded into the viewport, + * it will set a pendingFocus flag to true in order to perform the focus when + * the managed viewer instance is created. + * + * @param {RoiAnnotation} roiAnnotation RoiAnnotation instance to be focused + * @param {string} viewportId Index of the viewport to focus + */ + focusAnnotation(roiAnnotation, viewportId) { + const filter = mv => mv.viewportId === viewportId; + const managedViewer = Array.from(this.managedViewers).find(filter); + if (managedViewer) { + managedViewer.setViewStateByExtent(roiAnnotation); + } else { + this.pendingFocus = true; + } + } + + /** + * Synchronize the ROI graphics for all the managed viewers that has the same + * series UID of the given managed viewer + * + * @param {ViewerManager} baseManagedViewer Reference managed viewer + */ + synchronizeViewers(baseManagedViewer) { + const { studyInstanceUID, seriesInstanceUID } = baseManagedViewer; + const managedViewers = this._getManagedViewersForSeries(studyInstanceUID, seriesInstanceUID); + + // Prevent infinite loops arrising from updates. + managedViewers.forEach(managedViewer => this._removeManagedViewerSubscriptions(managedViewer)); + + managedViewers.forEach(managedViewer => { + if (managedViewer === baseManagedViewer) { + return; + } + + const annotations = this.getAnnotationsForSeries(studyInstanceUID, seriesInstanceUID); + managedViewer.clearRoiGraphics(); + annotations.forEach(roiAnnotation => { + managedViewer.addRoiGraphic(roiAnnotation.roiGraphic); + }); + }); + + managedViewers.forEach(managedViewer => this._addManagedViewerSubscriptions(managedViewer)); + } + + /** + * Activates interactions across all the viewers being managed + * + * @param {Array} interactions interactions + */ + activateInteractions(interactions) { + this.managedViewers.forEach(mv => mv.activateInteractions(interactions)); + this.activeInteractions = interactions; + } + + getActiveInteractions() { + return this.activeInteractions; + } + + /** + * Triggers the relabelling process for the given RoiAnnotation instance, by + * publishing the RELABEL event to notify the subscribers + * + * @param {RoiAnnotation} roiAnnotation The instance to be relabelled + * @param {boolean} newAnnotation Whether the annotation is newly drawn (so it deletes on cancel). + */ + triggerRelabel(roiAnnotation, newAnnotation = false, onRelabel) { + if (!onRelabel) { + onRelabel = ({ label }) => + this.managedViewers.forEach(mv => + mv.updateROIProperties({ + uid: roiAnnotation.uid, + properties: { label }, + }) + ); + } + + this._broadcastEvent(EVENTS.RELABEL, { + roiAnnotation, + deleteCallback: () => this.removeAnnotation(roiAnnotation), + successCallback: onRelabel, + newAnnotation, + }); + } + + /** + * Triggers the deletion process for the given RoiAnnotation instance, by + * publishing the DELETE event to notify the subscribers + * + * @param {RoiAnnotation} roiAnnotation The instance to be deleted + */ + triggerDelete(roiAnnotation) { + this._broadcastEvent(EVENTS.DELETE, roiAnnotation); + } + + /** + * Set ROI style for all managed viewers + * + * @param {string} uid The ROI uid that will be styled + * @param {object} styleOptions - Style options + * @param {object*} styleOptions.stroke - Style options for the outline of the geometry + * @param {number[]} styleOptions.stroke.color - RGBA color of the outline + * @param {number} styleOptions.stroke.width - Width of the outline + * @param {object*} styleOptions.fill - Style options for body the geometry + * @param {number[]} styleOptions.fill.color - RGBA color of the body + * @param {object*} styleOptions.image - Style options for image + */ + setROIStyle(uid, styleOptions) { + this.managedViewers.forEach(mv => mv.setROIStyle(uid, styleOptions)); + } + + /** + * Get all managed viewers + * + * @returns {Array} managedViewers + */ + getAllManagedViewers() { + return Array.from(this.managedViewers); + } +} + +export { EVENTS }; diff --git a/extensions/dicom-microscopy/src/tools/viewerManager.js b/extensions/dicom-microscopy/src/tools/viewerManager.js new file mode 100644 index 0000000..b7f3dac --- /dev/null +++ b/extensions/dicom-microscopy/src/tools/viewerManager.js @@ -0,0 +1,464 @@ +import coordinateFormatScoord3d2Geometry from '../utils/coordinateFormatScoord3d2Geometry'; +import styles from '../utils/styles'; + +import { PubSubService } from '@ohif/core'; + +// Events from the third-party viewer +const ApiEvents = { + /** Triggered when a ROI was added. */ + ROI_ADDED: 'dicommicroscopyviewer_roi_added', + /** Triggered when a ROI was modified. */ + ROI_MODIFIED: 'dicommicroscopyviewer_roi_modified', + /** Triggered when a ROI was removed. */ + ROI_REMOVED: 'dicommicroscopyviewer_roi_removed', + /** Triggered when a ROI was drawn. */ + ROI_DRAWN: `dicommicroscopyviewer_roi_drawn`, + /** Triggered when a ROI was selected. */ + ROI_SELECTED: `dicommicroscopyviewer_roi_selected`, + /** Triggered when a viewport move has started. */ + MOVE_STARTED: `dicommicroscopyviewer_move_started`, + /** Triggered when a viewport move has ended. */ + MOVE_ENDED: `dicommicroscopyviewer_move_ended`, + /** Triggered when a loading of data has started. */ + LOADING_STARTED: `dicommicroscopyviewer_loading_started`, + /** Triggered when a loading of data has ended. */ + LOADING_ENDED: `dicommicroscopyviewer_loading_ended`, + /** Triggered when an error occurs during loading of data. */ + LOADING_ERROR: `dicommicroscopyviewer_loading_error`, + /* Triggered when the loading of an image tile has started. */ + FRAME_LOADING_STARTED: `dicommicroscopyviewer_frame_loading_started`, + /* Triggered when the loading of an image tile has ended. */ + FRAME_LOADING_ENDED: `dicommicroscopyviewer_frame_loading_ended`, + /* Triggered when the error occurs during loading of an image tile. */ + FRAME_LOADING_ERROR: `dicommicroscopyviewer_frame_loading_ended`, +}; + +const EVENTS = { + ADDED: 'added', + MODIFIED: 'modified', + REMOVED: 'removed', + UPDATED: 'updated', + SELECTED: 'selected', +}; + +/** + * ViewerManager encapsulates the complexity of the third-party viewer and + * expose only the features/behaviors that are relevant to the application + */ +class ViewerManager extends PubSubService { + constructor(viewer, viewportId, container, studyInstanceUID, seriesInstanceUID) { + super(EVENTS); + this.viewer = viewer; + this.viewportId = viewportId; + this.container = container; + this.studyInstanceUID = studyInstanceUID; + this.seriesInstanceUID = seriesInstanceUID; + + this.onRoiAdded = this.roiAddedHandler.bind(this); + this.onRoiModified = this.roiModifiedHandler.bind(this); + this.onRoiRemoved = this.roiRemovedHandler.bind(this); + this.onRoiSelected = this.roiSelectedHandler.bind(this); + this.contextMenuCallback = () => {}; + + // init symbols + const symbols = Object.getOwnPropertySymbols(this.viewer); + this._drawingSource = symbols.find(p => p.description === 'drawingSource'); + this._pyramid = symbols.find(p => p.description === 'pyramid'); + this._map = symbols.find(p => p.description === 'map'); + this._affine = symbols.find(p => p.description === 'affine'); + + this.registerEvents(); + this.activateDefaultInteractions(); + } + + addContextMenuCallback(callback) { + this.contextMenuCallback = callback; + } + + /** + * Destroys this managed viewer instance, clearing all the event handlers + */ + destroy() { + this.unregisterEvents(); + } + + /** + * This is to overrides the _broadcastEvent method of PubSubService and always + * send the ROI graphic object and this managed viewer instance. + * Due to the way that PubSubService is written, the same name override of the + * function doesn't work. + * + * @param {String} key key Subscription key + * @param {Object} roiGraphic ROI graphic object created by the third-party API + */ + publish(key, roiGraphic) { + this._broadcastEvent(key, { + roiGraphic, + managedViewer: this, + }); + } + + /** + * Registers all the relevant event handlers for the third-party API + */ + registerEvents() { + this.container.addEventListener(ApiEvents.ROI_ADDED, this.onRoiAdded); + this.container.addEventListener(ApiEvents.ROI_MODIFIED, this.onRoiModified); + this.container.addEventListener(ApiEvents.ROI_REMOVED, this.onRoiRemoved); + this.container.addEventListener(ApiEvents.ROI_SELECTED, this.onRoiSelected); + } + + /** + * Clears all the relevant event handlers for the third-party API + */ + unregisterEvents() { + this.container.removeEventListener(ApiEvents.ROI_ADDED, this.onRoiAdded); + this.container.removeEventListener(ApiEvents.ROI_MODIFIED, this.onRoiModified); + this.container.removeEventListener(ApiEvents.ROI_REMOVED, this.onRoiRemoved); + this.container.removeEventListener(ApiEvents.ROI_SELECTED, this.onRoiSelected); + } + + /** + * Handles the ROI_ADDED event triggered by the third-party API + * + * @param {Event} event Event triggered by the third-party API + */ + roiAddedHandler(event) { + const roiGraphic = event.detail.payload; + this.publish(EVENTS.ADDED, roiGraphic); + this.publish(EVENTS.UPDATED, roiGraphic); + } + + /** + * Handles the ROI_MODIFIED event triggered by the third-party API + * + * @param {Event} event Event triggered by the third-party API + */ + roiModifiedHandler(event) { + const roiGraphic = event.detail.payload; + this.publish(EVENTS.MODIFIED, roiGraphic); + this.publish(EVENTS.UPDATED, roiGraphic); + } + + /** + * Handles the ROI_REMOVED event triggered by the third-party API + * + * @param {Event} event Event triggered by the third-party API + */ + roiRemovedHandler(event) { + const roiGraphic = event.detail.payload; + this.publish(EVENTS.REMOVED, roiGraphic); + this.publish(EVENTS.UPDATED, roiGraphic); + } + + /** + * Handles the ROI_SELECTED event triggered by the third-party API + * + * @param {Event} event Event triggered by the third-party API + */ + roiSelectedHandler(event) { + const roiGraphic = event.detail.payload; + this.publish(EVENTS.SELECTED, roiGraphic); + } + + /** + * Run the given callback operation without triggering any events for this + * instance, so subscribers will not be affected + * + * @param {Function} callback Callback that will run sinlently + */ + runSilently(callback) { + this.unregisterEvents(); + callback(); + this.registerEvents(); + } + + /** + * Removes all the ROI graphics from the third-party API + */ + clearRoiGraphics() { + this.runSilently(() => this.viewer.removeAllROIs()); + } + + showROIs() { + this.viewer.showROIs(); + } + + hideROIs() { + this.viewer.hideROIs(); + } + + /** + * Adds the given ROI graphic into the third-party API + * + * @param {Object} roiGraphic ROI graphic object to be added + */ + addRoiGraphic(roiGraphic) { + this.runSilently(() => this.viewer.addROI(roiGraphic, styles.default)); + } + + /** + * Adds the given ROI graphic into the third-party API, and also add a label. + * Used for importing from SR. + * + * @param {Object} roiGraphic ROI graphic object to be added. + * @param {String} label The label of the annotation. + */ + addRoiGraphicWithLabel(roiGraphic, label) { + // NOTE: Dicom Microscopy Viewer will override styles for "Text" evaluations + // to hide all other geometries, we are not going to use its label. + // if (label) { + // if (!roiGraphic.properties) roiGraphic.properties = {}; + // roiGraphic.properties.label = label; + // } + this.runSilently(() => this.viewer.addROI(roiGraphic, styles.default)); + + this._broadcastEvent(EVENTS.ADDED, { + roiGraphic, + managedViewer: this, + label, + }); + } + + /** + * Sets ROI style + * + * @param {String} uid ROI graphic UID to be styled + * @param {object} styleOptions - Style options + * @param {object} styleOptions.stroke - Style options for the outline of the geometry + * @param {number[]} styleOptions.stroke.color - RGBA color of the outline + * @param {number} styleOptions.stroke.width - Width of the outline + * @param {object} styleOptions.fill - Style options for body the geometry + * @param {number[]} styleOptions.fill.color - RGBA color of the body + * @param {object} styleOptions.image - Style options for image + */ + setROIStyle(uid, styleOptions) { + this.viewer.setROIStyle(uid, styleOptions); + } + + /** + * Removes the ROI graphic with the given UID from the third-party API + * + * @param {String} uid ROI graphic UID to be removed + */ + removeRoiGraphic(uid) { + this.viewer.removeROI(uid); + } + + /** + * Update properties of regions of interest. + * + * @param {object} roi - ROI to be updated + * @param {string} roi.uid - Unique identifier of the region of interest + * @param {object} roi.properties - ROI properties + * @returns {void} + */ + updateROIProperties({ uid, properties }) { + this.viewer.updateROI({ uid, properties }); + } + + /** + * Toggles overview map + * + * @returns {void} + */ + toggleOverviewMap() { + this.viewer.toggleOverviewMap(); + } + + /** + * Activates the viewer default interactions + * @returns {void} + */ + activateDefaultInteractions() { + /** Disable browser's native context menu inside the canvas */ + document.querySelector('.DicomMicroscopyViewer').addEventListener( + 'contextmenu', + event => { + event.preventDefault(); + // comment out when context menu for microscopy is enabled + // if (typeof this.contextMenuCallback === 'function') { + // this.contextMenuCallback(event); + // } + }, + false + ); + const defaultInteractions = [ + [ + 'dragPan', + { + bindings: { + mouseButtons: ['middle'], + }, + }, + ], + [ + 'dragZoom', + { + bindings: { + mouseButtons: ['right'], + }, + }, + ], + ['modify', {}], + ]; + this.activateInteractions(defaultInteractions); + } + + /** + * Activates interactions + * @param {Array} interactions Interactions to be activated + * @returns {void} + */ + activateInteractions(interactions) { + const interactionsMap = { + draw: activate => (activate ? 'activateDrawInteraction' : 'deactivateDrawInteraction'), + modify: activate => (activate ? 'activateModifyInteraction' : 'deactivateModifyInteraction'), + translate: activate => + activate ? 'activateTranslateInteraction' : 'deactivateTranslateInteraction', + snap: activate => (activate ? 'activateSnapInteraction' : 'deactivateSnapInteraction'), + dragPan: activate => + activate ? 'activateDragPanInteraction' : 'deactivateDragPanInteraction', + dragZoom: activate => + activate ? 'activateDragZoomInteraction' : 'deactivateDragZoomInteraction', + select: activate => (activate ? 'activateSelectInteraction' : 'deactivateSelectInteraction'), + }; + + const availableInteractionsName = Object.keys(interactionsMap); + availableInteractionsName.forEach(availableInteractionName => { + const interaction = interactions.find( + interaction => interaction[0] === availableInteractionName + ); + if (!interaction) { + const deactivateInteractionMethod = interactionsMap[availableInteractionName](false); + this.viewer[deactivateInteractionMethod](); + } else { + const [name, config] = interaction; + const activateInteractionMethod = interactionsMap[name](true); + this.viewer[activateInteractionMethod](config); + } + }); + } + + /** + * Accesses the internals of third-party API and returns the OpenLayers Map + * + * @returns {Object} OpenLayers Map component instance + */ + _getMapView() { + const map = this._getMap(); + return map.getView(); + } + + _getMap() { + const symbols = Object.getOwnPropertySymbols(this.viewer); + const _map = symbols.find(s => String(s) === 'Symbol(map)'); + window['map'] = this.viewer[_map]; + return this.viewer[_map]; + } + + /** + * Returns the current state for the OpenLayers View + * + * @returns {Object} Current view state + */ + getViewState() { + const view = this._getMapView(); + return { + center: view.getCenter(), + resolution: view.getResolution(), + zoom: view.getZoom(), + }; + } + + /** + * Sets the current state for the OpenLayers View + * + * @param {Object} viewState View state to be applied + */ + setViewState(viewState) { + const view = this._getMapView(); + + view.setZoom(viewState.zoom); + view.setResolution(viewState.resolution); + view.setCenter(viewState.center); + } + + setViewStateByExtent(roiAnnotation) { + const coordinates = roiAnnotation.getCoordinates(); + + if (Array.isArray(coordinates[0]) && !coordinates[2]) { + this._jumpToPolyline(coordinates); + } else if (Array.isArray(coordinates[0])) { + this._jumpToPolygonOrEllipse(coordinates); + } else { + this._jumpToPoint(coordinates); + } + } + + _jumpToPoint(coord) { + const pyramid = this.viewer[this._pyramid].metadata; + + const mappedCoord = coordinateFormatScoord3d2Geometry(coord, pyramid); + const view = this._getMapView(); + + view.setCenter(mappedCoord); + } + + _jumpToPolyline(coord) { + const pyramid = this.viewer[this._pyramid].metadata; + + const mappedCoord = coordinateFormatScoord3d2Geometry(coord, pyramid); + const view = this._getMapView(); + + const x = mappedCoord[0]; + const y = mappedCoord[1]; + + const xab = (x[0] + y[0]) / 2; + const yab = (x[1] + y[1]) / 2; + const midpoint = [xab, yab]; + + view.setCenter(midpoint); + } + + _jumpToPolygonOrEllipse(coordinates) { + const pyramid = this.viewer[this._pyramid].metadata; + + let minX = Infinity; + let maxX = -Infinity; + let minY = Infinity; + let maxY = -Infinity; + + coordinates.forEach(coord => { + let mappedCoord = coordinateFormatScoord3d2Geometry(coord, pyramid); + + const [x, y] = mappedCoord; + if (x < minX) { + minX = x; + } else if (x > maxX) { + maxX = x; + } + + if (y < minY) { + minY = y; + } else if (y > maxY) { + maxY = y; + } + }); + + const width = maxX - minX; + const height = maxY - minY; + + minX -= 0.5 * width; + maxX += 0.5 * width; + minY -= 0.5 * height; + maxY += 0.5 * height; + + const map = this._getMap(); + map.getView().fit([minX, minY, maxX, maxY], map.getSize()); + } +} + +export { EVENTS }; + +export default ViewerManager; diff --git a/extensions/dicom-microscopy/src/types/AppTypes.ts b/extensions/dicom-microscopy/src/types/AppTypes.ts new file mode 100644 index 0000000..87fe897 --- /dev/null +++ b/extensions/dicom-microscopy/src/types/AppTypes.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +import MicroscopyServiceType from '../services/MicroscopyService'; + +declare global { + namespace AppTypes { + export type MicroscopyService = MicroscopyServiceType; + export interface Services { + microscopyService?: MicroscopyServiceType; + } + } +} diff --git a/extensions/dicom-microscopy/src/utils/DEVICE_OBSERVER_UID.js b/extensions/dicom-microscopy/src/utils/DEVICE_OBSERVER_UID.js new file mode 100644 index 0000000..cb35858 --- /dev/null +++ b/extensions/dicom-microscopy/src/utils/DEVICE_OBSERVER_UID.js @@ -0,0 +1,5 @@ +// We need to define a UID for this extension as a device, and it should be the same for all saves: + +const uid = '2.25.285241207697168520771311899641885187923'; + +export default uid; diff --git a/extensions/dicom-microscopy/src/utils/RoiAnnotation.js b/extensions/dicom-microscopy/src/utils/RoiAnnotation.js new file mode 100644 index 0000000..550a791 --- /dev/null +++ b/extensions/dicom-microscopy/src/utils/RoiAnnotation.js @@ -0,0 +1,186 @@ +import areaOfPolygon from './areaOfPolygon'; + +import { PubSubService } from '@ohif/core'; + +const EVENTS = { + LABEL_UPDATED: 'labelUpdated', + GRAPHIC_UPDATED: 'graphicUpdated', + VIEW_UPDATED: 'viewUpdated', + REMOVED: 'removed', +}; + +/** + * Represents a single annotation for the Microscopy Viewer + */ +class RoiAnnotation extends PubSubService { + constructor(roiGraphic, studyInstanceUID, seriesInstanceUID, label = '', viewState = null) { + super(EVENTS); + this.uid = roiGraphic.uid; + this.roiGraphic = roiGraphic; + this.studyInstanceUID = studyInstanceUID; + this.seriesInstanceUID = seriesInstanceUID; + this.label = label; + this.viewState = viewState; + this.setMeasurements(roiGraphic); + } + + getScoord3d() { + const roiGraphic = this.roiGraphic; + + const roiGraphicSymbols = Object.getOwnPropertySymbols(roiGraphic); + const _scoord3d = roiGraphicSymbols.find(s => String(s) === 'Symbol(scoord3d)'); + + return roiGraphic[_scoord3d]; + } + + getCoordinates() { + const scoord3d = this.getScoord3d(); + const scoord3dSymbols = Object.getOwnPropertySymbols(scoord3d); + + const _coordinates = scoord3dSymbols.find(s => String(s) === 'Symbol(coordinates)'); + + const coordinates = scoord3d[_coordinates]; + return coordinates; + } + + /** + * When called will trigger the REMOVED event + */ + destroy() { + this._broadcastEvent(EVENTS.REMOVED, this); + } + + /** + * Updates the ROI graphic for the annotation and triggers the GRAPHIC_UPDATED + * event + * + * @param {Object} roiGraphic + */ + setRoiGraphic(roiGraphic) { + this.roiGraphic = roiGraphic; + this.setMeasurements(); + this._broadcastEvent(EVENTS.GRAPHIC_UPDATED, this); + } + + /** + * Update ROI measurement values based on its scoord3d coordinates. + * + * @returns {void} + */ + setMeasurements() { + const type = this.roiGraphic.scoord3d.graphicType; + const coordinates = this.roiGraphic.scoord3d.graphicData; + + switch (type) { + case 'ELLIPSE': + // This is a circle so only need one side + const point1 = coordinates[0]; + const point2 = coordinates[1]; + + let xLength2 = point2[0] - point1[0]; + let yLength2 = point2[1] - point1[1]; + + xLength2 *= xLength2; + yLength2 *= yLength2; + + const length = Math.sqrt(xLength2 + yLength2); + const radius = length / 2; + + const areaEllipse = Math.PI * radius * radius; + this._area = areaEllipse; + this._length = undefined; + break; + + case 'POLYGON': + const areaPolygon = areaOfPolygon(coordinates); + this._area = areaPolygon; + this._length = undefined; + break; + + case 'POINT': + this._area = undefined; + this._length = undefined; + break; + + case 'POLYLINE': + let len = 0; + for (let i = 1; i < coordinates.length; i++) { + const p1 = coordinates[i - 1]; + const p2 = coordinates[i]; + + let xLen = p2[0] - p1[0]; + let yLen = p2[1] - p1[1]; + + xLen *= xLen; + yLen *= yLen; + len += Math.sqrt(xLen + yLen); + } + + this._area = undefined; + this._length = len; + break; + } + } + + /** + * Update the OpenLayer Map's view state for the annotation and triggers the + * VIEW_UPDATED event + * + * @param {Object} viewState The new view state for the annotation + */ + setViewState(viewState) { + this.viewState = viewState; + this._broadcastEvent(EVENTS.VIEW_UPDATED, this); + } + + /** + * Update the label for the annotation and triggers the LABEL_UPDATED event + * + * @param {String} label New label for the annotation + */ + setLabel(label, finding) { + this.label = label || (finding && finding.CodeMeaning); + this.finding = finding || { + CodingSchemeDesignator: '@ohif/extension-dicom-microscopy', + CodeValue: label, + CodeMeaning: label, + }; + this._broadcastEvent(EVENTS.LABEL_UPDATED, this); + } + + /** + * Returns the geometry type of the annotation concatenated with the label + * defined for the annotation. + * Difference with getDetailedLabel() is that this will return empty string for empty + * label. + * + * @returns {String} Text with geometry type and label + */ + getLabel() { + const label = this.label ? `${this.label}` : ''; + return label; + } + + /** + * Returns the geometry type of the annotation concatenated with the label + * defined for the annotation + * + * @returns {String} Text with geometry type and label + */ + getDetailedLabel() { + const label = this.label ? `${this.label}` : '(empty)'; + return label; + } + + getLength() { + return this._length; + } + + getArea() { + return this._area; + } +} + +export { EVENTS }; + +export default RoiAnnotation; diff --git a/extensions/dicom-microscopy/src/utils/areaOfPolygon.js b/extensions/dicom-microscopy/src/utils/areaOfPolygon.js new file mode 100644 index 0000000..ea07684 --- /dev/null +++ b/extensions/dicom-microscopy/src/utils/areaOfPolygon.js @@ -0,0 +1,15 @@ +export default function areaOfPolygon(coordinates) { + // Shoelace algorithm. + const n = coordinates.length; + let area = 0.0; + let j = n - 1; + + for (let i = 0; i < n; i++) { + area += (coordinates[j][0] + coordinates[i][0]) * (coordinates[j][1] - coordinates[i][1]); + j = i; // j is previous vertex to i + } + + // Return absolute value of half the sum + // (The value is halved as we are summing up triangles, not rectangles). + return Math.abs(area / 2.0); +} diff --git a/extensions/dicom-microscopy/src/utils/constructSR.ts b/extensions/dicom-microscopy/src/utils/constructSR.ts new file mode 100644 index 0000000..07e5094 --- /dev/null +++ b/extensions/dicom-microscopy/src/utils/constructSR.ts @@ -0,0 +1,192 @@ +import dcmjs from 'dcmjs'; +import DEVICE_OBSERVER_UID from './DEVICE_OBSERVER_UID'; + +/** + * + * @param {*} metadata - Microscopy Image instance metadata + * @param {*} SeriesDescription - SR description + * @param {*} annotations - Annotations + * + * @return Comprehensive3DSR dataset + */ +export default function constructSR(metadata, { SeriesDescription, SeriesNumber }, annotations) { + // Handle malformed data + if (!metadata.SpecimenDescriptionSequence) { + metadata.SpecimenDescriptionSequence = { + SpecimenUID: metadata.SeriesInstanceUID, + SpecimenIdentifier: metadata.SeriesDescription, + }; + } + const { SpecimenDescriptionSequence } = metadata; + + // construct Comprehensive3DSR dataset + const observationContext = new dcmjs.sr.templates.ObservationContext({ + observerPersonContext: new dcmjs.sr.templates.ObserverContext({ + observerType: new dcmjs.sr.coding.CodedConcept({ + value: '121006', + schemeDesignator: 'DCM', + meaning: 'Person', + }), + observerIdentifyingAttributes: new dcmjs.sr.templates.PersonObserverIdentifyingAttributes({ + name: '@ohif/extension-dicom-microscopy', + }), + }), + observerDeviceContext: new dcmjs.sr.templates.ObserverContext({ + observerType: new dcmjs.sr.coding.CodedConcept({ + value: '121007', + schemeDesignator: 'DCM', + meaning: 'Device', + }), + observerIdentifyingAttributes: new dcmjs.sr.templates.DeviceObserverIdentifyingAttributes({ + uid: DEVICE_OBSERVER_UID, + }), + }), + subjectContext: new dcmjs.sr.templates.SubjectContext({ + subjectClass: new dcmjs.sr.coding.CodedConcept({ + value: '121027', + schemeDesignator: 'DCM', + meaning: 'Specimen', + }), + subjectClassSpecificContext: new dcmjs.sr.templates.SubjectContextSpecimen({ + uid: SpecimenDescriptionSequence.SpecimenUID, + identifier: SpecimenDescriptionSequence.SpecimenIdentifier || metadata.SeriesInstanceUID, + containerIdentifier: metadata.ContainerIdentifier || metadata.SeriesInstanceUID, + }), + }), + }); + + const imagingMeasurements = []; + for (let i = 0; i < annotations.length; i++) { + const { roiGraphic: roi, label } = annotations[i]; + let { measurements, evaluations, marker, presentationState } = roi.properties; + + console.log('[SR] storing marker...', marker); + console.log('[SR] storing measurements...', measurements); + console.log('[SR] storing evaluations...', evaluations); + console.log('[SR] storing presentation state...', presentationState); + + if (presentationState) { + presentationState.marker = marker; + } + + /** Avoid incompatibility with dcmjs */ + measurements = measurements.map((measurement: any) => { + const ConceptName = Array.isArray(measurement.ConceptNameCodeSequence) + ? measurement.ConceptNameCodeSequence[0] + : measurement.ConceptNameCodeSequence; + + const MeasuredValue = Array.isArray(measurement.MeasuredValueSequence) + ? measurement.MeasuredValueSequence[0] + : measurement.MeasuredValueSequence; + + const MeasuredValueUnits = Array.isArray(MeasuredValue.MeasurementUnitsCodeSequence) + ? MeasuredValue.MeasurementUnitsCodeSequence[0] + : MeasuredValue.MeasurementUnitsCodeSequence; + + return new dcmjs.sr.valueTypes.NumContentItem({ + name: new dcmjs.sr.coding.CodedConcept({ + meaning: ConceptName.CodeMeaning, + value: ConceptName.CodeValue, + schemeDesignator: ConceptName.CodingSchemeDesignator, + }), + value: MeasuredValue.NumericValue, + unit: new dcmjs.sr.coding.CodedConcept({ + value: MeasuredValueUnits.CodeValue, + meaning: MeasuredValueUnits.CodeMeaning, + schemeDesignator: MeasuredValueUnits.CodingSchemeDesignator, + }), + }); + }); + + /** Avoid incompatibility with dcmjs */ + evaluations = evaluations.map((evaluation: any) => { + const ConceptName = Array.isArray(evaluation.ConceptNameCodeSequence) + ? evaluation.ConceptNameCodeSequence[0] + : evaluation.ConceptNameCodeSequence; + + return new dcmjs.sr.valueTypes.TextContentItem({ + name: new dcmjs.sr.coding.CodedConcept({ + value: ConceptName.CodeValue, + meaning: ConceptName.CodeMeaning, + schemeDesignator: ConceptName.CodingSchemeDesignator, + }), + value: evaluation.TextValue, + relationshipType: evaluation.RelationshipType, + }); + }); + + const identifier = `ROI #${i + 1}`; + const group = new dcmjs.sr.templates.PlanarROIMeasurementsAndQualitativeEvaluations({ + trackingIdentifier: new dcmjs.sr.templates.TrackingIdentifier({ + uid: roi.uid, + identifier: presentationState + ? identifier.concat(`(${JSON.stringify(presentationState)})`) + : identifier, + }), + referencedRegion: new dcmjs.sr.contentItems.ImageRegion3D({ + graphicType: roi.scoord3d.graphicType, + graphicData: roi.scoord3d.graphicData, + frameOfReferenceUID: roi.scoord3d.frameOfReferenceUID, + }), + findingType: new dcmjs.sr.coding.CodedConcept({ + value: label, + schemeDesignator: '@ohif/extension-dicom-microscopy', + meaning: 'FREETEXT', + }), + /** Evaluations will conflict with current tracking identifier */ + /** qualitativeEvaluations: evaluations, */ + measurements, + }); + imagingMeasurements.push(...group); + } + + const measurementReport = new dcmjs.sr.templates.MeasurementReport({ + languageOfContentItemAndDescendants: new dcmjs.sr.templates.LanguageOfContentItemAndDescendants( + {} + ), + observationContext, + procedureReported: new dcmjs.sr.coding.CodedConcept({ + value: '112703', + schemeDesignator: 'DCM', + meaning: 'Whole Slide Imaging', + }), + imagingMeasurements, + }); + + const dataset = new dcmjs.sr.documents.Comprehensive3DSR({ + content: measurementReport[0], + evidence: [metadata], + seriesInstanceUID: dcmjs.data.DicomMetaDictionary.uid(), + seriesNumber: SeriesNumber, + seriesDescription: SeriesDescription || 'Whole slide imaging structured report', + sopInstanceUID: dcmjs.data.DicomMetaDictionary.uid(), + instanceNumber: 1, + manufacturer: 'dcmjs-org', + }); + dataset.SpecificCharacterSet = 'ISO_IR 192'; + const fileMetaInformationVersionArray = new Uint8Array(2); + fileMetaInformationVersionArray[1] = 1; + + dataset._meta = { + FileMetaInformationVersion: { + Value: [fileMetaInformationVersionArray.buffer], // TODO + vr: 'OB', + }, + MediaStorageSOPClassUID: dataset.sopClassUID, + MediaStorageSOPInstanceUID: dataset.sopInstanceUID, + TransferSyntaxUID: { + Value: ['1.2.840.10008.1.2.1'], + vr: 'UI', + }, + ImplementationClassUID: { + Value: [dcmjs.data.DicomMetaDictionary.uid()], + vr: 'UI', + }, + ImplementationVersionName: { + Value: ['@ohif/extension-dicom-microscopy'], + vr: 'SH', + }, + }; + + return dataset; +} diff --git a/extensions/dicom-microscopy/src/utils/coordinateFormatScoord3d2Geometry.js b/extensions/dicom-microscopy/src/utils/coordinateFormatScoord3d2Geometry.js new file mode 100644 index 0000000..e4530b9 --- /dev/null +++ b/extensions/dicom-microscopy/src/utils/coordinateFormatScoord3d2Geometry.js @@ -0,0 +1,109 @@ +import { inv, multiply } from 'mathjs'; + +// TODO -> This is pulled out of some internal logic from Dicom Microscopy Viewer, +// We should likely just expose this there. + +export default function coordinateFormatScoord3d2Geometry(coordinates, pyramid) { + let transform = false; + if (!Array.isArray(coordinates[0])) { + coordinates = [coordinates]; + transform = true; + } + const metadata = pyramid[pyramid.length - 1]; + const orientation = metadata.ImageOrientationSlide; + const spacing = _getPixelSpacing(metadata); + const origin = metadata.TotalPixelMatrixOriginSequence[0]; + const offset = [ + Number(origin.XOffsetInSlideCoordinateSystem), + Number(origin.YOffsetInSlideCoordinateSystem), + ]; + + coordinates = coordinates.map(c => { + const slideCoord = [c[0], c[1]]; + const pixelCoord = mapSlideCoord2PixelCoord({ + offset, + orientation, + spacing, + point: slideCoord, + }); + return [pixelCoord[0], -(pixelCoord[1] + 1), 0]; + }); + if (transform) { + return coordinates[0]; + } + return coordinates; +} + +function _getPixelSpacing(metadata) { + if (metadata.PixelSpacing) { + return metadata.PixelSpacing; + } + const functionalGroup = metadata.SharedFunctionalGroupsSequence[0]; + const pixelMeasures = functionalGroup.PixelMeasuresSequence[0]; + return pixelMeasures.PixelSpacing; +} + +function mapSlideCoord2PixelCoord(options) { + // X and Y Offset in Slide Coordinate System + if (!('offset' in options)) { + throw new Error('Option "offset" is required.'); + } + if (!Array.isArray(options.offset)) { + throw new Error('Option "offset" must be an array.'); + } + if (options.offset.length !== 2) { + throw new Error('Option "offset" must be an array with 2 elements.'); + } + const offset = options.offset; + + // Image Orientation Slide with direction cosines for Row and Column direction + if (!('orientation' in options)) { + throw new Error('Option "orientation" is required.'); + } + if (!Array.isArray(options.orientation)) { + throw new Error('Option "orientation" must be an array.'); + } + if (options.orientation.length !== 6) { + throw new Error('Option "orientation" must be an array with 6 elements.'); + } + const orientation = options.orientation; + + // Pixel Spacing along the Row and Column direction + if (!('spacing' in options)) { + throw new Error('Option "spacing" is required.'); + } + if (!Array.isArray(options.spacing)) { + throw new Error('Option "spacing" must be an array.'); + } + if (options.spacing.length !== 2) { + throw new Error('Option "spacing" must be an array with 2 elements.'); + } + const spacing = options.spacing; + + // X and Y coordinate in the Slide Coordinate System + if (!('point' in options)) { + throw new Error('Option "point" is required.'); + } + if (!Array.isArray(options.point)) { + throw new Error('Option "point" must be an array.'); + } + if (options.point.length !== 2) { + throw new Error('Option "point" must be an array with 2 elements.'); + } + const point = options.point; + + const m = [ + [orientation[0] * spacing[1], orientation[3] * spacing[0], offset[0]], + [orientation[1] * spacing[1], orientation[4] * spacing[0], offset[1]], + [0, 0, 1], + ]; + const mInverted = inv(m); + + const vSlide = [[point[0]], [point[1]], [1]]; + + const vImage = multiply(mInverted, vSlide); + + const row = Number(vImage[1][0].toFixed(4)); + const col = Number(vImage[0][0].toFixed(4)); + return [col, row]; +} diff --git a/extensions/dicom-microscopy/src/utils/dcmCodeValues.js b/extensions/dicom-microscopy/src/utils/dcmCodeValues.js new file mode 100644 index 0000000..82d4899 --- /dev/null +++ b/extensions/dicom-microscopy/src/utils/dcmCodeValues.js @@ -0,0 +1,14 @@ +const DCM_CODE_VALUES = { + IMAGING_MEASUREMENTS: '126010', + MEASUREMENT_GROUP: '125007', + IMAGE_REGION: '111030', + FINDING: '121071', + TRACKING_UNIQUE_IDENTIFIER: '112039', + LENGTH: '410668003', + AREA: '42798000', + SHORT_AXIS: 'G-A186', + LONG_AXIS: 'G-A185', + ELLIPSE_AREA: 'G-D7FE', // TODO: Remove this +}; + +export default DCM_CODE_VALUES; diff --git a/extensions/dicom-microscopy/src/utils/dicomWebClient.ts b/extensions/dicom-microscopy/src/utils/dicomWebClient.ts new file mode 100644 index 0000000..42455fe --- /dev/null +++ b/extensions/dicom-microscopy/src/utils/dicomWebClient.ts @@ -0,0 +1,81 @@ +import { errorHandler, DicomMetadataStore } from '@ohif/core'; +import { StaticWadoClient } from '@ohif/extension-default'; + +/** + * create a DICOMwebClient object to be used by Dicom Microscopy Viewer + * + * Referenced the code from `/extensions/default/src/DicomWebDataSource/index.js` + * + * @param param0 + * @returns + */ +export default function getDicomWebClient({ extensionManager, servicesManager }: withAppTypes) { + const dataSourceConfig = window.config.dataSources.find( + ds => ds.sourceName === extensionManager.activeDataSource + ); + const { userAuthenticationService } = servicesManager.services; + + const { wadoRoot, staticWado, singlepart } = dataSourceConfig.configuration; + + const wadoConfig = { + url: wadoRoot || '/dicomlocal', + staticWado, + singlepart, + headers: userAuthenticationService.getAuthorizationHeader(), + errorInterceptor: errorHandler.getHTTPErrorHandler(), + }; + + const client = new StaticWadoClient(wadoConfig); + client.wadoURL = wadoConfig.url; + + if (extensionManager.activeDataSource === 'dicomlocal') { + /** + * For local data source, override the retrieveInstanceFrames() method of the + * dicomweb-client to retrieve image data from memory cached metadata. + * Other methods of the client doesn't matter, as we are feeding the DMV + * with the series metadata already. + * + * @param {Object} options + * @param {String} options.studyInstanceUID - Study Instance UID + * @param {String} options.seriesInstanceUID - Series Instance UID + * @param {String} options.sopInstanceUID - SOP Instance UID + * @param {String} options.frameNumbers - One-based indices of Frame Items + * @param {Object} [options.queryParams] - HTTP query parameters + * @returns {ArrayBuffer[]} Rendered Frame Items as byte arrays + */ + // + client.retrieveInstanceFrames = async options => { + if (!('studyInstanceUID' in options)) { + throw new Error('Study Instance UID is required for retrieval of instance frames'); + } + if (!('seriesInstanceUID' in options)) { + throw new Error('Series Instance UID is required for retrieval of instance frames'); + } + if (!('sopInstanceUID' in options)) { + throw new Error('SOP Instance UID is required for retrieval of instance frames'); + } + if (!('frameNumbers' in options)) { + throw new Error('frame numbers are required for retrieval of instance frames'); + } + console.log( + `retrieve frames ${options.frameNumbers.toString()} of instance ${options.sopInstanceUID}` + ); + + const instance = DicomMetadataStore.getInstance( + options.studyInstanceUID, + options.seriesInstanceUID, + options.sopInstanceUID + ); + + const frameNumbers = Array.isArray(options.frameNumbers) + ? options.frameNumbers + : options.frameNumbers.split(','); + + return frameNumbers.map(fr => + Array.isArray(instance.PixelData) ? instance.PixelData[+fr - 1] : instance.PixelData + ); + }; + } + + return client; +} diff --git a/extensions/dicom-microscopy/src/utils/getSourceDisplaySet.js b/extensions/dicom-microscopy/src/utils/getSourceDisplaySet.js new file mode 100644 index 0000000..a2c7439 --- /dev/null +++ b/extensions/dicom-microscopy/src/utils/getSourceDisplaySet.js @@ -0,0 +1,32 @@ +/** + * Get referenced SM displaySet from SR displaySet + * + * @param {*} allDisplaySets + * @param {*} microscopySRDisplaySet + * @returns + */ +export default function getSourceDisplaySet(allDisplaySets, microscopySRDisplaySet) { + const { ReferencedFrameOfReferenceUID } = microscopySRDisplaySet; + + const otherDisplaySets = allDisplaySets.filter( + ds => ds.displaySetInstanceUID !== microscopySRDisplaySet.displaySetInstanceUID + ); + const referencedDisplaySet = otherDisplaySets.find( + displaySet => + displaySet.Modality === 'SM' && + (displaySet.FrameOfReferenceUID === ReferencedFrameOfReferenceUID || + // sometimes each depth instance has the different FrameOfReferenceID + displaySet.othersFrameOfReferenceUID.includes(ReferencedFrameOfReferenceUID)) + ); + + if (!referencedDisplaySet && otherDisplaySets.length >= 1) { + console.warn( + 'No display set with FrameOfReferenceUID', + ReferencedFrameOfReferenceUID, + 'single series, assuming data error, defaulting to only series.' + ); + return otherDisplaySets.find(displaySet => displaySet.Modality === 'SM'); + } + + return referencedDisplaySet; +} diff --git a/extensions/dicom-microscopy/src/utils/loadSR.ts b/extensions/dicom-microscopy/src/utils/loadSR.ts new file mode 100644 index 0000000..c6d2802 --- /dev/null +++ b/extensions/dicom-microscopy/src/utils/loadSR.ts @@ -0,0 +1,184 @@ +import dcmjs from 'dcmjs'; + +import DCM_CODE_VALUES from './dcmCodeValues'; +import toArray from './toArray'; + +const MeasurementReport = dcmjs.adapters.DICOMMicroscopyViewer.MeasurementReport; + +// Define as async so that it returns a promise, expected by the ViewportGrid +export default async function loadSR( + microscopyService, + microscopySRDisplaySet, + referencedDisplaySet +) { + const naturalizedDataset = microscopySRDisplaySet.metadata; + + const { StudyInstanceUID, FrameOfReferenceUID } = referencedDisplaySet; + + const managedViewers = microscopyService.getManagedViewersForStudy(StudyInstanceUID); + + if (!managedViewers || !managedViewers.length) { + return; + } + + microscopySRDisplaySet.isLoaded = true; + + const { rois, labels } = await _getROIsFromToolState(microscopyService, naturalizedDataset, FrameOfReferenceUID); + + const managedViewer = managedViewers[0]; + + for (let i = 0; i < rois.length; i++) { + // NOTE: When saving Microscopy SR, we are attaching identifier property + // to each ROI, and when read for display, it is coming in as "TEXT" + // evaluation. + // As the Dicom Microscopy Viewer will override styles for "Text" evaluations + // to hide all other geometries, we are going to manually remove that + // evaluation item. + const roi = rois[i]; + const roiSymbols = Object.getOwnPropertySymbols(roi); + const _properties = roiSymbols.find(s => s.description === 'properties'); + const properties = roi[_properties]; + properties['evaluations'] = []; + + managedViewer.addRoiGraphicWithLabel(roi, labels[i]); + } +} + +async function _getROIsFromToolState(microscopyService, naturalizedDataset, FrameOfReferenceUID) { + const toolState = MeasurementReport.generateToolState(naturalizedDataset); + const tools = Object.getOwnPropertyNames(toolState); + // Does a dynamic import to prevent webpack from rebuilding the library + const DICOMMicroscopyViewer = await microscopyService.importDicomMicroscopyViewer(); + + const measurementGroupContentItems = _getMeasurementGroups(naturalizedDataset); + + const rois = []; + const labels = []; + + tools.forEach(t => { + const toolSpecificToolState = toolState[t]; + let scoord3d; + + const capsToolType = t.toUpperCase(); + + const measurementGroupContentItemsForTool = measurementGroupContentItems.filter(mg => { + const imageRegionContentItem = toArray(mg.ContentSequence).find( + ci => ci.ConceptNameCodeSequence.CodeValue === DCM_CODE_VALUES.IMAGE_REGION + ); + + return imageRegionContentItem.GraphicType === capsToolType; + }); + + toolSpecificToolState.forEach((coordinates, index) => { + const properties = {}; + + const options = { + coordinates, + frameOfReferenceUID: FrameOfReferenceUID, + }; + + if (t === 'Polygon') { + scoord3d = new DICOMMicroscopyViewer.scoord3d.Polygon(options); + } else if (t === 'Polyline') { + scoord3d = new DICOMMicroscopyViewer.scoord3d.Polyline(options); + } else if (t === 'Point') { + scoord3d = new DICOMMicroscopyViewer.scoord3d.Point(options); + } else if (t === 'Ellipse') { + scoord3d = new DICOMMicroscopyViewer.scoord3d.Ellipse(options); + } else { + throw new Error('Unsupported tool type'); + } + + const measurementGroup = measurementGroupContentItemsForTool[index]; + const findingGroup = toArray(measurementGroup.ContentSequence).find( + ci => ci.ConceptNameCodeSequence.CodeValue === DCM_CODE_VALUES.FINDING + ); + + const trackingGroup = toArray(measurementGroup.ContentSequence).find( + ci => ci.ConceptNameCodeSequence.CodeValue === DCM_CODE_VALUES.TRACKING_UNIQUE_IDENTIFIER + ); + + /** + * Extract presentation state from tracking identifier. + * Currently is stored in SR but should be stored in its tags. + */ + if (trackingGroup) { + const regExp = /\(([^)]+)\)/; + const matches = regExp.exec(trackingGroup.TextValue); + if (matches && matches[1]) { + properties.presentationState = JSON.parse(matches[1]); + properties.marker = properties.presentationState.marker; + } + } + + let measurements = toArray(measurementGroup.ContentSequence).filter(ci => + [ + DCM_CODE_VALUES.LENGTH, + DCM_CODE_VALUES.AREA, + DCM_CODE_VALUES.SHORT_AXIS, + DCM_CODE_VALUES.LONG_AXIS, + DCM_CODE_VALUES.ELLIPSE_AREA, + ].includes(ci.ConceptNameCodeSequence.CodeValue) + ); + + let evaluations = toArray(measurementGroup.ContentSequence).filter(ci => + [DCM_CODE_VALUES.TRACKING_UNIQUE_IDENTIFIER].includes(ci.ConceptNameCodeSequence.CodeValue) + ); + + /** + * TODO: Resolve bug in DCMJS. + * ConceptNameCodeSequence should be a sequence with only one item. + */ + evaluations = evaluations.map(evaluation => { + const e = { ...evaluation }; + e.ConceptNameCodeSequence = toArray(e.ConceptNameCodeSequence); + return e; + }); + + /** + * TODO: Resolve bug in DCMJS. + * ConceptNameCodeSequence should be a sequence with only one item. + */ + measurements = measurements.map(measurement => { + const m = { ...measurement }; + m.ConceptNameCodeSequence = toArray(m.ConceptNameCodeSequence); + return m; + }); + + if (measurements && measurements.length) { + properties.measurements = measurements; + console.log('[SR] retrieving measurements...', measurements); + } + + if (evaluations && evaluations.length) { + properties.evaluations = evaluations; + console.log('[SR] retrieving evaluations...', evaluations); + } + + const roi = new DICOMMicroscopyViewer.roi.ROI({ scoord3d, properties }); + rois.push(roi); + + if (findingGroup) { + labels.push(findingGroup.ConceptCodeSequence.CodeValue); + } else { + labels.push(''); + } + }); + }); + + return { rois, labels }; +} + +function _getMeasurementGroups(naturalizedDataset) { + const { ContentSequence } = naturalizedDataset; + + const imagingMeasurementsContentItem = ContentSequence.find( + ci => ci.ConceptNameCodeSequence.CodeValue === DCM_CODE_VALUES.IMAGING_MEASUREMENTS + ); + + const measurementGroupContentItems = toArray( + imagingMeasurementsContentItem.ContentSequence + ).filter(ci => ci.ConceptNameCodeSequence.CodeValue === DCM_CODE_VALUES.MEASUREMENT_GROUP); + + return measurementGroupContentItems; +} diff --git a/extensions/dicom-microscopy/src/utils/saveByteArray.ts b/extensions/dicom-microscopy/src/utils/saveByteArray.ts new file mode 100644 index 0000000..123f1c1 --- /dev/null +++ b/extensions/dicom-microscopy/src/utils/saveByteArray.ts @@ -0,0 +1,12 @@ +/** + * Trigger file download from an array buffer + * @param buffer + * @param filename + */ +export function saveByteArray(buffer: ArrayBuffer, filename: string) { + const blob = new Blob([buffer], { type: 'application/dicom' }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = filename; + link.click(); +} diff --git a/extensions/dicom-microscopy/src/utils/styles.js b/extensions/dicom-microscopy/src/utils/styles.js new file mode 100644 index 0000000..030b910 --- /dev/null +++ b/extensions/dicom-microscopy/src/utils/styles.js @@ -0,0 +1,48 @@ +const defaultFill = { + color: 'rgba(255,255,255,0.4)', +}; + +const emptyFill = { + color: 'rgba(255,255,255,0.0)', +}; + +const defaultStroke = { + color: 'rgb(0,255,0)', + width: 1.5, +}; + +const activeStroke = { + color: 'rgb(255,255,0)', + width: 1.5, +}; + +const defaultStyle = { + image: { + circle: { + fill: defaultFill, + stroke: activeStroke, + radius: 5, + }, + }, + fill: defaultFill, + stroke: activeStroke, +}; + +const emptyStyle = { + image: { + circle: { + fill: emptyFill, + stroke: defaultStroke, + radius: 5, + }, + }, + fill: emptyFill, + stroke: defaultStroke, +}; + +const styles = { + active: defaultStyle, + default: emptyStyle, +}; + +export default styles; diff --git a/extensions/dicom-microscopy/src/utils/toArray.js b/extensions/dicom-microscopy/src/utils/toArray.js new file mode 100644 index 0000000..2800085 --- /dev/null +++ b/extensions/dicom-microscopy/src/utils/toArray.js @@ -0,0 +1,3 @@ +export default function toArray(item) { + return Array.isArray(item) ? item : [item]; +} diff --git a/extensions/dicom-pdf/.webpack/webpack.dev.js b/extensions/dicom-pdf/.webpack/webpack.dev.js new file mode 100644 index 0000000..a973e32 --- /dev/null +++ b/extensions/dicom-pdf/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, ENTRY, DIST_DIR }); +}; diff --git a/extensions/dicom-pdf/.webpack/webpack.prod.js b/extensions/dicom-pdf/.webpack/webpack.prod.js new file mode 100644 index 0000000..7957a51 --- /dev/null +++ b/extensions/dicom-pdf/.webpack/webpack.prod.js @@ -0,0 +1,56 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const pkg = require('./../package.json'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; + +const ROOT_DIR = path.join(__dirname, './..'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +const outputName = `ohif-${pkg.name.split('/').pop()}`; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, ENTRY, DIST_DIR }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: false, + }, + output: { + path: ROOT_DIR, + library: 'ohif-extension-dicom-pdf', + libraryTarget: 'umd', + filename: `${pkg.main}`, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + new MiniCssExtractPlugin({ + filename: `./dist/${outputName}.css`, + chunkFilename: `./dist/${outputName}.css`, + }), + // new BundleAnalyzerPlugin(), + ], + }); +}; diff --git a/extensions/dicom-pdf/CHANGELOG.md b/extensions/dicom-pdf/CHANGELOG.md new file mode 100644 index 0000000..e955890 --- /dev/null +++ b/extensions/dicom-pdf/CHANGELOG.md @@ -0,0 +1,3003 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + + +### Bug Fixes + +* **cli:** version txt had a new line which it should not ([#4233](https://github.com/OHIF/Viewers/issues/4233)) ([097ef76](https://github.com/OHIF/Viewers/commit/097ef7665559a672d73e1babfc42afccc3cdd41d)) +* **pdf-viewport:** Allow Drag and Drop on PDF Viewport ([#4225](https://github.com/OHIF/Viewers/issues/4225)) ([729efb6](https://github.com/OHIF/Viewers/commit/729efb6d766e0f72f1fd8adefbca6fb46b355b2b)) + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + + +### Bug Fixes + +* **sr:** dcm4chee requires the patient name for an SR to match what is in the original study ([#3739](https://github.com/OHIF/Viewers/issues/3739)) ([d98439f](https://github.com/OHIF/Viewers/commit/d98439fe7f3825076dbc87b664a1d1480ff414d3)) + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + +**Note:** Version bump only for package @ohif/extension-dicom-pdf diff --git a/extensions/dicom-pdf/LICENSE b/extensions/dicom-pdf/LICENSE new file mode 100644 index 0000000..19e20dd --- /dev/null +++ b/extensions/dicom-pdf/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/dicom-pdf/README.md b/extensions/dicom-pdf/README.md new file mode 100644 index 0000000..1e2c608 --- /dev/null +++ b/extensions/dicom-pdf/README.md @@ -0,0 +1,5 @@ +# DICOM Encapsulated PDF +This extension adds support for displaying DICOM encapsulated PDF documents. + +The extension is a "standard" extension in that it is installed and available +by default. diff --git a/extensions/dicom-pdf/package.json b/extensions/dicom-pdf/package.json new file mode 100644 index 0000000..bdec56f --- /dev/null +++ b/extensions/dicom-pdf/package.json @@ -0,0 +1,45 @@ +{ + "name": "@ohif/extension-dicom-pdf", + "version": "3.10.0-beta.111", + "description": "OHIF extension for PDF display", + "author": "OHIF", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/ohif-extension-dicom-pdf.umd.js", + "module": "src/index.tsx", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "files": [ + "dist", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package-1": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.111", + "@ohif/ui": "3.10.0-beta.111", + "dcmjs": "*", + "dicom-parser": "^1.8.9", + "hammerjs": "^2.0.8", + "prop-types": "^15.6.2", + "react": "^18.3.1" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "classnames": "^2.3.2" + } +} diff --git a/extensions/dicom-pdf/src/getSopClassHandlerModule.js b/extensions/dicom-pdf/src/getSopClassHandlerModule.js new file mode 100644 index 0000000..e0ef0e2 --- /dev/null +++ b/extensions/dicom-pdf/src/getSopClassHandlerModule.js @@ -0,0 +1,70 @@ +import { SOPClassHandlerId } from './id'; +import { utils, classes } from '@ohif/core'; + +const { ImageSet } = classes; + +const SOP_CLASS_UIDS = { + ENCAPSULATED_PDF: '1.2.840.10008.5.1.4.1.1.104.1', +}; + +const sopClassUids = Object.values(SOP_CLASS_UIDS); + +const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager) => { + const dataSource = extensionManager.getActiveDataSource()[0]; + return instances.map(instance => { + const { Modality, SOPInstanceUID } = instance; + const { SeriesDescription = 'PDF', MIMETypeOfEncapsulatedDocument } = instance; + const { SeriesNumber, SeriesDate, SeriesInstanceUID, StudyInstanceUID, SOPClassUID } = instance; + const pdfUrl = dataSource.retrieve.directURL({ + instance, + tag: 'EncapsulatedDocument', + defaultType: MIMETypeOfEncapsulatedDocument || 'application/pdf', + singlepart: 'pdf', + }); + + const displaySet = { + //plugin: id, + Modality, + displaySetInstanceUID: utils.guid(), + SeriesDescription, + SeriesNumber, + SeriesDate, + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + SOPClassHandlerId, + SOPClassUID, + referencedImages: null, + measurements: null, + pdfUrl, + instances: [instance], + thumbnailSrc: dataSource.retrieve.directURL({ + instance, + defaultPath: '/thumbnail', + defaultType: 'image/jpeg', + tag: 'Absent', + }), + isDerivedDisplaySet: true, + isLoaded: false, + sopClassUids, + numImageFrames: 0, + numInstances: 1, + instance, + }; + return displaySet; + }); +}; + +export default function getSopClassHandlerModule({ servicesManager, extensionManager }) { + const getDisplaySetsFromSeries = instances => { + return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager); + }; + + return [ + { + name: 'dicom-pdf', + sopClassUids, + getDisplaySetsFromSeries, + }, + ]; +} diff --git a/extensions/dicom-pdf/src/id.js b/extensions/dicom-pdf/src/id.js new file mode 100644 index 0000000..22e153e --- /dev/null +++ b/extensions/dicom-pdf/src/id.js @@ -0,0 +1,6 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; +const SOPClassHandlerId = `${id}.sopClassHandlerModule.dicom-pdf`; + +export { id, SOPClassHandlerId }; diff --git a/extensions/dicom-pdf/src/index.tsx b/extensions/dicom-pdf/src/index.tsx new file mode 100644 index 0000000..a655cbc --- /dev/null +++ b/extensions/dicom-pdf/src/index.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import getSopClassHandlerModule from './getSopClassHandlerModule'; +import { id } from './id.js'; + +const Component = React.lazy(() => { + return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstonePdfViewport'); +}); + +const OHIFCornerstonePdfViewport = props => { + return ( + Loading...}> + + + ); +}; + +/** + * + */ +const dicomPDFExtension = { + /** + * Only required property. Should be a unique value across all extensions. + */ + id, + /** + * + * + * @param {object} [configuration={}] + * @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools` + */ + getViewportModule({ servicesManager, extensionManager }) { + const ExtendedOHIFCornerstonePdfViewport = props => { + return ( + + ); + }; + + return [{ name: 'dicom-pdf', component: ExtendedOHIFCornerstonePdfViewport }]; + }, + getSopClassHandlerModule, +}; + +export default dicomPDFExtension; diff --git a/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.css b/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.css new file mode 100644 index 0000000..1837c21 --- /dev/null +++ b/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.css @@ -0,0 +1,11 @@ +.pdf-no-click { + pointer-events: none; + height: 100%; + width: 100%; +} + +.pdf-yes-click { + pointer-events: auto; + height: 100%; + width: 100%; +} diff --git a/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.tsx b/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.tsx new file mode 100644 index 0000000..8c60fd9 --- /dev/null +++ b/extensions/dicom-pdf/src/viewports/OHIFCornerstonePdfViewport.tsx @@ -0,0 +1,61 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import './OHIFCornerstonePdfViewport.css'; + +function OHIFCornerstonePdfViewport({ displaySets }) { + const [url, setUrl] = useState(null); + + useEffect(() => { + document.body.addEventListener('drag', makePdfDropTarget); + return function cleanup() { + document.body.removeEventListener('drag', makePdfDropTarget); + }; + }, []); + + const [style, setStyle] = useState('pdf-yes-click'); + + const makePdfScrollable = () => { + setStyle('pdf-yes-click'); + }; + + const makePdfDropTarget = () => { + setStyle('pdf-no-click'); + }; + + if (displaySets && displaySets.length > 1) { + throw new Error( + 'OHIFCornerstonePdfViewport: only one display set is supported for dicom pdf right now' + ); + } + + const { pdfUrl } = displaySets[0]; + + useEffect(() => { + const load = async () => { + setUrl(await pdfUrl); + }; + + load(); + }, [pdfUrl]); + + return ( +
+ +
No online PDF viewer installed
+
+
+ ); +} + +OHIFCornerstonePdfViewport.propTypes = { + displaySets: PropTypes.arrayOf(PropTypes.object).isRequired, +}; + +export default OHIFCornerstonePdfViewport; diff --git a/extensions/dicom-video/.webpack/webpack.dev.js b/extensions/dicom-video/.webpack/webpack.dev.js new file mode 100644 index 0000000..a973e32 --- /dev/null +++ b/extensions/dicom-video/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, ENTRY, DIST_DIR }); +}; diff --git a/extensions/dicom-video/.webpack/webpack.prod.js b/extensions/dicom-video/.webpack/webpack.prod.js new file mode 100644 index 0000000..79c5691 --- /dev/null +++ b/extensions/dicom-video/.webpack/webpack.prod.js @@ -0,0 +1,48 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const pkg = require('./../package.json'); +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; + +const ROOT_DIR = path.join(__dirname, './..'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, ENTRY, DIST_DIR }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: false, + }, + output: { + path: ROOT_DIR, + library: 'ohif-extension-dicom-video', + libraryTarget: 'umd', + filename: pkg.main, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + // new BundleAnalyzerPlugin(), + ], + }); +}; diff --git a/extensions/dicom-video/CHANGELOG.md b/extensions/dicom-video/CHANGELOG.md new file mode 100644 index 0000000..7c59499 --- /dev/null +++ b/extensions/dicom-video/CHANGELOG.md @@ -0,0 +1,3008 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + + +### Features + +* Add CS3D WSI and Video Viewports and add annotation navigation for MPR ([#4182](https://github.com/OHIF/Viewers/issues/4182)) ([7599ec9](https://github.com/OHIF/Viewers/commit/7599ec9421129dcade94e6fa6ec7908424ab3134)) + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + + +### Bug Fixes + +* Use correct external URL for rendered responses with relative URI ([#4236](https://github.com/OHIF/Viewers/issues/4236)) ([d8f6991](https://github.com/OHIF/Viewers/commit/d8f6991dbe72465080cfc5de39c7ea225702f2e0)) + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + + +### Bug Fixes + +* **dicom-video:** Update get direct func for dicom json to use url if present and fix config argument ([#4017](https://github.com/OHIF/Viewers/issues/4017)) ([4f99244](https://github.com/OHIF/Viewers/commit/4f99244d864427d69be6f863cb7a6a78411adb12)) + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + + +### Bug Fixes + +* **sr:** dcm4chee requires the patient name for an SR to match what is in the original study ([#3739](https://github.com/OHIF/Viewers/issues/3739)) ([d98439f](https://github.com/OHIF/Viewers/commit/d98439fe7f3825076dbc87b664a1d1480ff414d3)) + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + +**Note:** Version bump only for package @ohif/extension-dicom-video + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + +**Note:** Version bump only for package @ohif/extension-dicom-video diff --git a/extensions/dicom-video/LICENSE b/extensions/dicom-video/LICENSE new file mode 100644 index 0000000..19e20dd --- /dev/null +++ b/extensions/dicom-video/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/dicom-video/README.md b/extensions/dicom-video/README.md new file mode 100644 index 0000000..bc9d1a9 --- /dev/null +++ b/extensions/dicom-video/README.md @@ -0,0 +1,15 @@ +# DICOM Video +This extension adds support for displaying DICOM video objects in a script tag. +The video data must currently be available as video/mp4 on the BulkDataURI that +is provided in the DICOMweb metadata response, and the video must have one of the +specified SOP Class UID's in order to be recognized by the SOP class handler. + +Those are: +* Video Microscop Image Storage +* Video Photographic Image Storage +* Video Endoscopic Image Storage +* Secondary Capture Image Storage +* Multiframe True Color Secondary Capture Image Storage + +The extension is a "standard" extension in that it is installed and available +by default. diff --git a/extensions/dicom-video/babel.config.js b/extensions/dicom-video/babel.config.js new file mode 100644 index 0000000..325ca2a --- /dev/null +++ b/extensions/dicom-video/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/extensions/dicom-video/package.json b/extensions/dicom-video/package.json new file mode 100644 index 0000000..6136cc8 --- /dev/null +++ b/extensions/dicom-video/package.json @@ -0,0 +1,45 @@ +{ + "name": "@ohif/extension-dicom-video", + "version": "3.10.0-beta.111", + "description": "OHIF extension for video display", + "author": "OHIF", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/ohif-extension-dicom-video.umd.js", + "module": "src/index.tsx", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "files": [ + "dist", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package-1": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.111", + "@ohif/ui": "3.10.0-beta.111", + "dcmjs": "*", + "dicom-parser": "^1.8.9", + "hammerjs": "^2.0.8", + "prop-types": "^15.6.2", + "react": "^18.3.1" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "classnames": "^2.3.2" + } +} diff --git a/extensions/dicom-video/src/getSopClassHandlerModule.js b/extensions/dicom-video/src/getSopClassHandlerModule.js new file mode 100644 index 0000000..a495127 --- /dev/null +++ b/extensions/dicom-video/src/getSopClassHandlerModule.js @@ -0,0 +1,114 @@ +import { SOPClassHandlerId } from './id'; +import { utils } from '@ohif/core'; +import { utilities as csUtils, Enums as csEnums } from '@cornerstonejs/core'; + +const SOP_CLASS_UIDS = { + VIDEO_MICROSCOPIC_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.2.1', + VIDEO_PHOTOGRAPHIC_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.4.1', + VIDEO_ENDOSCOPIC_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.1.1', + /** Need to use fallback, could be video or image */ + SECONDARY_CAPTURE_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.7', + MULTIFRAME_TRUE_COLOR_SECONDARY_CAPTURE_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.7.4', +}; + +const sopClassUids = Object.values(SOP_CLASS_UIDS); +const secondaryCaptureSopClassUids = [ + SOP_CLASS_UIDS.SECONDARY_CAPTURE_IMAGE_STORAGE, + SOP_CLASS_UIDS.MULTIFRAME_TRUE_COLOR_SECONDARY_CAPTURE_IMAGE_STORAGE, +]; + +const SupportedTransferSyntaxes = { + MPEG4_AVC_264_HIGH_PROFILE: '1.2.840.10008.1.2.4.102', + MPEG4_AVC_264_BD_COMPATIBLE_HIGH_PROFILE: '1.2.840.10008.1.2.4.103', + MPEG4_AVC_264_HIGH_PROFILE_FOR_2D_VIDEO: '1.2.840.10008.1.2.4.104', + MPEG4_AVC_264_HIGH_PROFILE_FOR_3D_VIDEO: '1.2.840.10008.1.2.4.105', + MPEG4_AVC_264_STEREO_HIGH_PROFILE: '1.2.840.10008.1.2.4.106', + HEVC_265_MAIN_PROFILE: '1.2.840.10008.1.2.4.107', + HEVC_265_MAIN_10_PROFILE: '1.2.840.10008.1.2.4.108', +}; + +const supportedTransferSyntaxUIDs = Object.values(SupportedTransferSyntaxes); + +const _getDisplaySetsFromSeries = (instances, servicesManager, extensionManager) => { + const dataSource = extensionManager.getActiveDataSource()[0]; + return instances + .filter(metadata => { + const tsuid = + metadata.AvailableTransferSyntaxUID || metadata.TransferSyntaxUID || metadata['00083002']; + + if (supportedTransferSyntaxUIDs.includes(tsuid)) { + return true; + } + + if (metadata.SOPClassUID === SOP_CLASS_UIDS.VIDEO_PHOTOGRAPHIC_IMAGE_STORAGE) { + return true; + } + + // Assume that an instance with one of the secondary capture SOPClassUIDs and + // with at least 90 frames (i.e. typically 3 seconds of video) is indeed a video. + return ( + secondaryCaptureSopClassUids.includes(metadata.SOPClassUID) && metadata.NumberOfFrames >= 90 + ); + }) + .map(instance => { + const { Modality, SOPInstanceUID, SeriesDescription = 'VIDEO', imageId } = instance; + const { SeriesNumber, SeriesDate, SeriesInstanceUID, StudyInstanceUID, NumberOfFrames, url } = + instance; + const videoUrl = dataSource.retrieve.directURL({ + instance, + singlepart: 'video', + tag: 'PixelData', + url, + }); + const displaySet = { + //plugin: id, + Modality, + displaySetInstanceUID: utils.guid(), + SeriesDescription, + SeriesNumber, + SeriesDate, + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + SOPClassHandlerId, + referencedImages: null, + measurements: null, + viewportType: csEnums.ViewportType.VIDEO, + // The videoUrl is deprecated, the preferred URL is renderedUrl + videoUrl, + renderedUrl: videoUrl, + instances: [instance], + thumbnailSrc: dataSource.retrieve.directURL({ + instance, + defaultPath: '/thumbnail', + defaultType: 'image/jpeg', + tag: 'Absent', + }), + imageIds: [imageId], + isDerivedDisplaySet: true, + isLoaded: false, + sopClassUids, + numImageFrames: NumberOfFrames, + instance, + }; + csUtils.genericMetadataProvider.add(imageId, { + type: 'imageUrlModule', + metadata: { rendered: videoUrl }, + }); + return displaySet; + }); +}; + +export default function getSopClassHandlerModule({ servicesManager, extensionManager }) { + const getDisplaySetsFromSeries = instances => { + return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager); + }; + + return [ + { + name: 'dicom-video', + sopClassUids, + getDisplaySetsFromSeries, + }, + ]; +} diff --git a/extensions/dicom-video/src/id.js b/extensions/dicom-video/src/id.js new file mode 100644 index 0000000..ef30eaf --- /dev/null +++ b/extensions/dicom-video/src/id.js @@ -0,0 +1,6 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; +const SOPClassHandlerId = `${id}.sopClassHandlerModule.dicom-video`; + +export { SOPClassHandlerId, id }; diff --git a/extensions/dicom-video/src/index.tsx b/extensions/dicom-video/src/index.tsx new file mode 100644 index 0000000..0fde8ca --- /dev/null +++ b/extensions/dicom-video/src/index.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import getSopClassHandlerModule from './getSopClassHandlerModule'; +import { id } from './id'; + +const Component = React.lazy(() => { + return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneVideoViewport'); +}); + +const OHIFCornerstoneVideoViewport = props => { + return ( + Loading...}> + + + ); +}; + +/** + * + */ +const dicomVideoExtension = { + /** + * Only required property. Should be a unique value across all extensions. + */ + id, + + /** + * + * + * @param {object} [configuration={}] + * @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools` + */ + getViewportModule({ servicesManager, extensionManager }) { + const ExtendedOHIFCornerstoneVideoViewport = props => { + return ( + + ); + }; + + return [{ name: 'dicom-video', component: ExtendedOHIFCornerstoneVideoViewport }]; + }, + getSopClassHandlerModule, +}; + +function _getToolAlias(toolName) { + let toolAlias = toolName; + + switch (toolName) { + case 'EllipticalRoi': + toolAlias = 'SREllipticalRoi'; + break; + } + + return toolAlias; +} + +export default dicomVideoExtension; diff --git a/extensions/dicom-video/src/viewports/OHIFCornerstoneVideoViewport.tsx b/extensions/dicom-video/src/viewports/OHIFCornerstoneVideoViewport.tsx new file mode 100644 index 0000000..8f7e965 --- /dev/null +++ b/extensions/dicom-video/src/viewports/OHIFCornerstoneVideoViewport.tsx @@ -0,0 +1,55 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; + +function OHIFCornerstoneVideoViewport({ displaySets }) { + if (displaySets && displaySets.length > 1) { + throw new Error( + 'OHIFCornerstoneVideoViewport: only one display set is supported for dicom video right now' + ); + } + + const { videoUrl } = displaySets[0]; + const mimeType = 'video/mp4'; + const [url, setUrl] = useState(null); + + useEffect(() => { + const load = async () => { + setUrl(await videoUrl); + }; + + load(); + }, [videoUrl]); + + // Need to copies of the source to fix a firefox bug + return ( +
+ +
+ ); +} + +OHIFCornerstoneVideoViewport.propTypes = { + displaySets: PropTypes.arrayOf(PropTypes.object).isRequired, +}; + +export default OHIFCornerstoneVideoViewport; diff --git a/extensions/dicom-video/video-screenshot.jpg b/extensions/dicom-video/video-screenshot.jpg new file mode 100644 index 0000000..2a05ca7 Binary files /dev/null and b/extensions/dicom-video/video-screenshot.jpg differ diff --git a/extensions/measurement-tracking/.webpack/webpack.dev.js b/extensions/measurement-tracking/.webpack/webpack.dev.js new file mode 100644 index 0000000..6aea859 --- /dev/null +++ b/extensions/measurement-tracking/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/extensions/measurement-tracking/.webpack/webpack.prod.js b/extensions/measurement-tracking/.webpack/webpack.prod.js new file mode 100644 index 0000000..027461f --- /dev/null +++ b/extensions/measurement-tracking/.webpack/webpack.prod.js @@ -0,0 +1,51 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; + +const pkg = require('./../package.json'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); + +const ROOT_DIR = path.join(__dirname, './../'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: false, + }, + output: { + path: ROOT_DIR, + library: 'ohif-extension-measurement-tracking', + libraryTarget: 'umd', + filename: pkg.main, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + // new BundleAnalyzerPlugin(), + ], + }); +}; diff --git a/extensions/measurement-tracking/CHANGELOG.md b/extensions/measurement-tracking/CHANGELOG.md new file mode 100644 index 0000000..00a144c --- /dev/null +++ b/extensions/measurement-tracking/CHANGELOG.md @@ -0,0 +1,3252 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + + +### Bug Fixes + +* **sr:** sr hydration and load was not working, Screenshot Comparison, and Testing ([#4814](https://github.com/OHIF/Viewers/issues/4814)) ([9233143](https://github.com/OHIF/Viewers/commit/9233143b9da5850080365e1526e24b44e9910075)) + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + + +### Bug Fixes + +* Delay for all series thumbnails on fetching thumbnail ([#4802](https://github.com/OHIF/Viewers/issues/4802)) ([bda98b0](https://github.com/OHIF/Viewers/commit/bda98b0beebde6294a522b5c7e0ca76724020a2f)) + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + + +### Bug Fixes + +* cache thumbnail in display set ([#4782](https://github.com/OHIF/Viewers/issues/4782)) ([2410c6a](https://github.com/OHIF/Viewers/commit/2410c6a50904c1235993900e837876cc26af019b)) + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + + +### Bug Fixes + +* **core:** Address 3D reconstruction and Android compatibility issues and clean up 4D data mode ([#4762](https://github.com/OHIF/Viewers/issues/4762)) ([149d6d0](https://github.com/OHIF/Viewers/commit/149d6d049cd333b9e5846576b403ff387558a66f)) + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + + +### Bug Fixes + +* **measurement label auto-completion:** Customization of measurement label auto-completion fails for measurements following arrow annotations. ([#4739](https://github.com/OHIF/Viewers/issues/4739)) ([e035ef1](https://github.com/OHIF/Viewers/commit/e035ef1dcc72ecbe2a757e3b814551d768d7e610)) + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + + +### Bug Fixes + +* for initialImageIndex mismatch issue for loading SR after disabling prompts ([#4732](https://github.com/OHIF/Viewers/issues/4732)) ([8e3e208](https://github.com/OHIF/Viewers/commit/8e3e2085d45eba230d0210849b65a6e609c9d81a)) + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + + +### Features + +* **customization:** new customization service api ([#4688](https://github.com/OHIF/Viewers/issues/4688)) ([55ad8ef](https://github.com/OHIF/Viewers/commit/55ad8efbabc3fabd8031fc08927b2f92ae5aec69)) + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + + +### Bug Fixes + +* **seg:** sphere scissor on stack and cpu rendering reset properties was broken ([#4721](https://github.com/OHIF/Viewers/issues/4721)) ([f00d182](https://github.com/OHIF/Viewers/commit/f00d18292f02e8910215d913edfc994850a68d88)) + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + + +### Bug Fixes + +* Having sop instance in a per-frame or shared attribute breaks load ([#4560](https://github.com/OHIF/Viewers/issues/4560)) ([cded082](https://github.com/OHIF/Viewers/commit/cded08261788143e0d5be57a55c927fd96aafb22)) + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + + +### Features + +* **multimonitor:** Add simple multi-monitor support to open another study([#4178](https://github.com/OHIF/Viewers/issues/4178)) ([07c628e](https://github.com/OHIF/Viewers/commit/07c628e689b28f831317a7c28d712509b69c6b13)) + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + + +### Features + +* **dev:** move to rsbuild for dev - faster ([#4674](https://github.com/OHIF/Viewers/issues/4674)) ([d4a4267](https://github.com/OHIF/Viewers/commit/d4a4267429c02916dd51f6aefb290d96dd1c3b04)) + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + + +### Features + +* Start using group filtering to define measurements table layout ([#4501](https://github.com/OHIF/Viewers/issues/4501)) ([82440e8](https://github.com/OHIF/Viewers/commit/82440e88d5debe808f0b14281b77e430c2489779)) + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + + +### Bug Fixes + +* **docker:** publish manifest for multiarch and update cs3d ([#4650](https://github.com/OHIF/Viewers/issues/4650)) ([836e67a](https://github.com/OHIF/Viewers/commit/836e67a6ab8de66d8908c75856774318729544f4)) + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + + +### Bug Fixes + +* **tools:** enable additional tools in volume viewport ([#4620](https://github.com/OHIF/Viewers/issues/4620)) ([1992002](https://github.com/OHIF/Viewers/commit/1992002d2dced171c17b9a0163baf707fc551e3d)) + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + + +### Features + +* **measurements:** Provide for the Load (SR) measurements button to optionally clear existing measurements prior to loading the SR. ([#4586](https://github.com/OHIF/Viewers/issues/4586)) ([4d3d5e7](https://github.com/OHIF/Viewers/commit/4d3d5e794cb99212eba06bf91dbb30a258725efe)) + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + + +### Features + +* migrate icons to ui-next ([#4606](https://github.com/OHIF/Viewers/issues/4606)) ([4e2ae32](https://github.com/OHIF/Viewers/commit/4e2ae328744ed95589c2cdf7a531454a25bf88b5)) + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + + +### Features + +* **node:** move to node 20 ([#4594](https://github.com/OHIF/Viewers/issues/4594)) ([1f04d6c](https://github.com/OHIF/Viewers/commit/1f04d6c1be729a26fe7bcda923770a1cd461053c)) + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + + +### Bug Fixes + +* **CinePlayer:** always show cine player for dynamic data ([#4575](https://github.com/OHIF/Viewers/issues/4575)) ([b8e8bbe](https://github.com/OHIF/Viewers/commit/b8e8bbe482b66e8cbe9167d03e9d8dedd2d3b6c5)) + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + + +### Bug Fixes + +* **multiframe:** metadata handling of NM studies and loading order ([#4554](https://github.com/OHIF/Viewers/issues/4554)) ([7624ccb](https://github.com/OHIF/Viewers/commit/7624ccb5e495c0a151227a458d8d5bfb8babb22c)) + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + + +### Bug Fixes + +* **colorlut:** use the correct colorlut index and update vtk ([#4544](https://github.com/OHIF/Viewers/issues/4544)) ([b9c26e7](https://github.com/OHIF/Viewers/commit/b9c26e775a49044673473418dd5bdee2e5562ab9)) + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + + +### Bug Fixes + +* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed)) + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + + +### Bug Fixes + +* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30)) + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + + +### Bug Fixes + +* **tmtv:** fix toggle one up weird behaviours ([#4473](https://github.com/OHIF/Viewers/issues/4473)) ([aa2b649](https://github.com/OHIF/Viewers/commit/aa2b649444eb4fe5422e72ea7830a709c4d24a90)) + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + + +### Bug Fixes + +* build ([#4471](https://github.com/OHIF/Viewers/issues/4471)) ([3d11ef2](https://github.com/OHIF/Viewers/commit/3d11ef28f213361ec7586809317bd219fa70e742)) + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + + +### Features + +* **new-study-panel:** default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser ([#4417](https://github.com/OHIF/Viewers/issues/4417)) ([a7fd9fa](https://github.com/OHIF/Viewers/commit/a7fd9fa5bfff7a1b533d99cb96f7147a35fd528f)) + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + + +### Features + +* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3)) + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + + +### Features + +* **studies-panel:** New OHIF study panel - under experimental flag ([#4254](https://github.com/OHIF/Viewers/issues/4254)) ([7a96406](https://github.com/OHIF/Viewers/commit/7a96406a116e46e62c396855fa64f434e2984b58)) + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + + +### Features + +* Add CS3D WSI and Video Viewports and add annotation navigation for MPR ([#4182](https://github.com/OHIF/Viewers/issues/4182)) ([7599ec9](https://github.com/OHIF/Viewers/commit/7599ec9421129dcade94e6fa6ec7908424ab3134)) + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + + +### Features + +* **pmap:** added support for parametric map ([#4284](https://github.com/OHIF/Viewers/issues/4284)) ([fc0064f](https://github.com/OHIF/Viewers/commit/fc0064fd9d8cdc8fde81b81f0e71fd5d077ca22b)) + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + + +### Bug Fixes + +* Cobb angle not working in basic-test mode and open contour ([#4280](https://github.com/OHIF/Viewers/issues/4280)) ([6fd3c7e](https://github.com/OHIF/Viewers/commit/6fd3c7e293fec851dd30e650c1347cc0bc7a99ee)) +* webpack import bugs showing warnings on import ([#4265](https://github.com/OHIF/Viewers/issues/4265)) ([24c511f](https://github.com/OHIF/Viewers/commit/24c511f4bc04c4143bbd3d0d48029f41f7f36014)) + + +### Features + +* Add interleaved HTJ2K and volume progressive loading ([#4276](https://github.com/OHIF/Viewers/issues/4276)) ([a2084f3](https://github.com/OHIF/Viewers/commit/a2084f319b731d98b59485799fb80357094f8c38)) + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + + +### Features + +* **studyPrefetcher:** Study Prefetcher ([#4206](https://github.com/OHIF/Viewers/issues/4206)) ([2048b19](https://github.com/OHIF/Viewers/commit/2048b19484c0b1fae73f993cfaa814f861bbd230)) + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) +* **studybrowser:** Differentiate recent and all in study panel based on a provided time period ([#4242](https://github.com/OHIF/Viewers/issues/4242)) ([6f93449](https://github.com/OHIF/Viewers/commit/6f9344914951c204feaff48aaeb43cd7d727623d)) + + +### Features + +* **sort:** custom series sort in study panel ([#4214](https://github.com/OHIF/Viewers/issues/4214)) ([a433d40](https://github.com/OHIF/Viewers/commit/a433d406e2cac13f644203996c682260b54e8865)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + + +### Features + +* Add customization merge, append or replace functionality ([#3871](https://github.com/OHIF/Viewers/issues/3871)) ([55dcfa1](https://github.com/OHIF/Viewers/commit/55dcfa1f6994a7036e7e594efb23673382a41915)) + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + + +### Bug Fixes + +* **seg:** maintain algorithm name and algorithm type when DICOM seg is exported or downloaded ([#4203](https://github.com/OHIF/Viewers/issues/4203)) ([a29e94d](https://github.com/OHIF/Viewers/commit/a29e94de803f79bbb3372d00ad8eb14b4224edc2)) + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + + +### Bug Fixes + +* **hydration:** Maintain the same slice that the user was on pre hydration in post hydration for SR and SEG. ([#4200](https://github.com/OHIF/Viewers/issues/4200)) ([430330f](https://github.com/OHIF/Viewers/commit/430330f7e384d503cb6fc695a7a9642ddfaac313)) + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + + +### Bug Fixes + +* **tmtv:** crosshairs should not have viewport indicators ([#4197](https://github.com/OHIF/Viewers/issues/4197)) ([f85da32](https://github.com/OHIF/Viewers/commit/f85da32f34389ef7cecae03c07e0af26468b52a6)) + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + + +### Bug Fixes + +* **ultrasound:** Upgrade cornerstone3D version to resolve coloring issues ([#4181](https://github.com/OHIF/Viewers/issues/4181)) ([75a71db](https://github.com/OHIF/Viewers/commit/75a71db7f89840250ad1c2b35df5a35aceb8be7d)) + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + + +### Features + +* **measurements:** show untracked measurements in measurement panel under additional findings ([#4160](https://github.com/OHIF/Viewers/issues/4160)) ([18686c2](https://github.com/OHIF/Viewers/commit/18686c2caf13ede3e881303100bd4cc34b8b135f)) + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + + +### Bug Fixes + +* **crosshairs:** reset angle, position, and slabthickness for crosshairs when reset viewport tool is used ([#4113](https://github.com/OHIF/Viewers/issues/4113)) ([73d9e99](https://github.com/OHIF/Viewers/commit/73d9e99d5d6f38ab6c36f4471d54f18798feacb4)) + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + + +### Bug Fixes + +* dicom json for orthanc by Update package versions for [@cornerstonejs](https://github.com/cornerstonejs) dependencies ([#4165](https://github.com/OHIF/Viewers/issues/4165)) ([34c7d72](https://github.com/OHIF/Viewers/commit/34c7d72142847486b98c9c52469940083eeaf87e)) + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + + +### Bug Fixes + +* **segmentation:** Address issue where segmentation creation failed on layout change ([#4153](https://github.com/OHIF/Viewers/issues/4153)) ([29944c8](https://github.com/OHIF/Viewers/commit/29944c8512c35718af03c03ef82bc43675ee1872)) + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + + +### Bug Fixes + +* **bugs:** enhancements and bugs in several areas ([#4086](https://github.com/OHIF/Viewers/issues/4086)) ([730f434](https://github.com/OHIF/Viewers/commit/730f4349100f21b4489a21707dbb2dca9dbfbba2)) + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + + +### Bug Fixes + +* **rt:** enhanced RT support, utilize SVGs for rendering. ([#4074](https://github.com/OHIF/Viewers/issues/4074)) ([0156bc4](https://github.com/OHIF/Viewers/commit/0156bc426f1840ae0d090223e94a643726e856cb)) + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + + +### Bug Fixes + +* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f)) + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + + +### Bug Fixes + +* **bugs:** fix patient header for doc, track ball rotate resize observer and add segmentation button not being enabled on viewport data change ([#4068](https://github.com/OHIF/Viewers/issues/4068)) ([c09311d](https://github.com/OHIF/Viewers/commit/c09311d3b7df05fcd00a9f36a7233e9d7e5589d0)) + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + + +### Bug Fixes + +* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951)) + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + + +### Features + +* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb)) + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + + +### Bug Fixes + +* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af)) + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes - final ([#4048](https://github.com/OHIF/Viewers/issues/4048)) ([170bb96](https://github.com/OHIF/Viewers/commit/170bb96983082c39b22b7352e0c54aacf3e73b02)) + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + + +### Bug Fixes + +* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025)) + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637)) + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + + +### Features + +* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13)) + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + + +### Bug Fixes + +* **general:** enhancements and bug fixes ([#4018](https://github.com/OHIF/Viewers/issues/4018)) ([2b83393](https://github.com/OHIF/Viewers/commit/2b83393f91cb16ea06821d79d14ff60f80c29c90)) + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + + +### Features + +* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2)) + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + + +### Bug Fixes + +* **cornerstone-dicom-sr:** Freehand SR hydration support ([#3996](https://github.com/OHIF/Viewers/issues/3996)) ([5645ac1](https://github.com/OHIF/Viewers/commit/5645ac1b271e1ed8c57f5d71100809362447267e)) + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + + +### Features + +* **measurement:** Add support measurement label autocompletion ([#3855](https://github.com/OHIF/Viewers/issues/3855)) ([56b1eae](https://github.com/OHIF/Viewers/commit/56b1eae6356a6534960df1196bdd1e95b0a9a470)) + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + + +### Features + +* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1)) + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + + +### Bug Fixes + +* **new layout:** address black screen bugs ([#4008](https://github.com/OHIF/Viewers/issues/4008)) ([158a181](https://github.com/OHIF/Viewers/commit/158a1816703e0ad66cae08cb9bd1ffb93bbd8d43)) + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + + +### Bug Fixes + +* **SR display:** and the token based navigation ([#3995](https://github.com/OHIF/Viewers/issues/3995)) ([feed230](https://github.com/OHIF/Viewers/commit/feed2304c124dc2facc7a7371ed9851548c223c5)) + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + + +### Bug Fixes + +* **demo:** Deploy issue ([#3951](https://github.com/OHIF/Viewers/issues/3951)) ([21e8a2b](https://github.com/OHIF/Viewers/commit/21e8a2bd0b7cc72f90a31e472d285d761be15d30)) + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + + +### Bug Fixes + +* Update CS3D to fix second render ([#3892](https://github.com/OHIF/Viewers/issues/3892)) ([d00a86b](https://github.com/OHIF/Viewers/commit/d00a86b022742ea089d246d06cfd691f43b64412)) + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + + +### Features + +* **hp:** enable OHIF to run with partial metadata for large studies at the cost of less effective hanging protocol ([#3804](https://github.com/OHIF/Viewers/issues/3804)) ([0049f4c](https://github.com/OHIF/Viewers/commit/0049f4c0303f0b6ea995972326fc8784259f5a47)) + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + + +### Bug Fixes + +* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6)) + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + + +### Features + +* improve disableEditing flag ([#3875](https://github.com/OHIF/Viewers/issues/3875)) ([2049c09](https://github.com/OHIF/Viewers/commit/2049c0936c86f819604c243d3dc7b3fe971b5b2c)) + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + + +### Features + +* **i18n:** enhanced i18n support ([#3761](https://github.com/OHIF/Viewers/issues/3761)) ([d14a8f0](https://github.com/OHIF/Viewers/commit/d14a8f0199db95cd9e85866a011b64d6bf830d57)) + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + + +### Bug Fixes + +* **auth:** fix the issue with oauth at a non root path ([#3840](https://github.com/OHIF/Viewers/issues/3840)) ([6651008](https://github.com/OHIF/Viewers/commit/6651008fbb35dabd5991c7f61128e6ef324012df)) + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + + +### Bug Fixes + +* Update the CS3D packages to add the most recent HTJ2K TSUIDS ([#3806](https://github.com/OHIF/Viewers/issues/3806)) ([9d1884d](https://github.com/OHIF/Viewers/commit/9d1884d7d8b6b2a1cdc26965a96995838aa72682)) + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + + +### Bug Fixes + +* **thumbnail:** Avoid multiple promise creations for thumbnails ([#3756](https://github.com/OHIF/Viewers/issues/3756)) ([b23eeff](https://github.com/OHIF/Viewers/commit/b23eeff93745769e67e60c33d75293d6242c5ec9)) + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + + +### Bug Fixes + +* **sr:** dcm4chee requires the patient name for an SR to match what is in the original study ([#3739](https://github.com/OHIF/Viewers/issues/3739)) ([d98439f](https://github.com/OHIF/Viewers/commit/d98439fe7f3825076dbc87b664a1d1480ff414d3)) + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + + +### Bug Fixes + +* **voi:** should publish voi change event on reset ([#3707](https://github.com/OHIF/Viewers/issues/3707)) ([52f34c6](https://github.com/OHIF/Viewers/commit/52f34c64d014f433ec1661a39b47e7fb27f15332)) + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + + +### Bug Fixes + +* **modality unit:** fix the modality unit per target via upgrade of cs3d ([#3706](https://github.com/OHIF/Viewers/issues/3706)) ([0a42d57](https://github.com/OHIF/Viewers/commit/0a42d573bbca7f2551a831a46d3aa6b56674a580)) + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + + +### Features + +* **Segmentation:** download RTSS from Labelmap([#3692](https://github.com/OHIF/Viewers/issues/3692)) ([40673f6](https://github.com/OHIF/Viewers/commit/40673f64b36b1150149c55632aa1825178a39e65)) + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + + +### Bug Fixes + +* **measurement and microscopy:** various small fixes for measurement and microscopy side panel ([#3696](https://github.com/OHIF/Viewers/issues/3696)) ([c1d5ee7](https://github.com/OHIF/Viewers/commit/c1d5ee7e3f7f4c0c6bed9ae81eba5519741c5155)) + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + + +### Bug Fixes + +* **config:** support more values for the useSharedArrayBuffer ([#3688](https://github.com/OHIF/Viewers/issues/3688)) ([1129c15](https://github.com/OHIF/Viewers/commit/1129c155d2c7d46c98a5df7c09879aa3d459fa7e)) + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) +* **SidePanel:** new side panel tab look-and-feel ([#3657](https://github.com/OHIF/Viewers/issues/3657)) ([85c899b](https://github.com/OHIF/Viewers/commit/85c899b399e2521480724be145538993721b9378)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + + +### Performance Improvements + +* **memory:** add 16 bit texture via configuration - reduces memory by half ([#3662](https://github.com/OHIF/Viewers/issues/3662)) ([2bd3b26](https://github.com/OHIF/Viewers/commit/2bd3b26a6aa54b211ef988f3ad64ef1fe5648bab)) + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + + +### Features + +* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f)) + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + + +### Features + +* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd)) + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + + +### Features + +* **data source UI config:** Popup the configuration dialogue whenever a data source is not fully configured ([#3620](https://github.com/OHIF/Viewers/issues/3620)) ([adedc8c](https://github.com/OHIF/Viewers/commit/adedc8c382e18a2e86a569e3d023cc55a157363f)) + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-measurement-tracking + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + + +### Features + +* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1)) + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + + +### Bug Fixes + +* **memory leak:** array buffer was sticking around in volume viewports ([#3611](https://github.com/OHIF/Viewers/issues/3611)) ([65b49ae](https://github.com/OHIF/Viewers/commit/65b49aeb1b5f38224e4892bdf32453500ee351f8)) diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json new file mode 100644 index 0000000..88c16f9 --- /dev/null +++ b/extensions/measurement-tracking/package.json @@ -0,0 +1,56 @@ +{ + "name": "@ohif/extension-measurement-tracking", + "version": "3.10.0-beta.111", + "description": "Tracking features and functionality for basic image viewing", + "author": "OHIF Core Team", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/ohif-extension-measurement-tracking.umd.js", + "module": "src/index.tsx", + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.18.0" + }, + "files": [ + "dist", + "README.md" + ], + "keywords": [ + "ohif-extension" + ], + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:dicom-pdf": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package-1": "yarn run build", + "start": "yarn run dev" + }, + "peerDependencies": { + "@cornerstonejs/core": "^2.19.14", + "@cornerstonejs/tools": "^2.19.14", + "@ohif/core": "3.10.0-beta.111", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.111", + "@ohif/extension-default": "3.10.0-beta.111", + "@ohif/ui": "3.10.0-beta.111", + "classnames": "^2.3.2", + "dcmjs": "*", + "lodash.debounce": "^4.0.8", + "prop-types": "^15.6.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "webpack": "5.89.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "@ohif/ui": "3.10.0-beta.111", + "@xstate/react": "^3.2.2", + "xstate": "^4.10.0" + } +} diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx new file mode 100644 index 0000000..e587c12 --- /dev/null +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.tsx @@ -0,0 +1,302 @@ +import React, { useContext, useEffect, useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { Machine } from 'xstate'; +import { useMachine } from '@xstate/react'; +import { useViewportGrid } from '@ohif/ui-next'; +import { promptLabelAnnotation, promptSaveReport } from '@ohif/extension-default'; +import { machineConfiguration, defaultOptions, RESPONSE } from './measurementTrackingMachine'; +import promptBeginTracking from './promptBeginTracking'; +import promptTrackNewSeries from './promptTrackNewSeries'; +import promptTrackNewStudy from './promptTrackNewStudy'; +import promptHydrateStructuredReport from './promptHydrateStructuredReport'; +import hydrateStructuredReport from './hydrateStructuredReport'; +import { useAppConfig } from '@state'; + +const TrackedMeasurementsContext = React.createContext(); +TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext'; +const useTrackedMeasurements = () => useContext(TrackedMeasurementsContext); + +const SR_SOPCLASSHANDLERID = '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr'; + +/** + * + * @param {*} param0 + */ +function TrackedMeasurementsContextProvider( + { servicesManager, commandsManager, extensionManager }: withAppTypes, // Bound by consumer + { children } // Component props +) { + const [appConfig] = useAppConfig(); + + const [viewportGrid, viewportGridService] = useViewportGrid(); + const { activeViewportId, viewports } = viewportGrid; + const { measurementService, displaySetService, customizationService } = servicesManager.services; + + const machineOptions = Object.assign({}, defaultOptions); + machineOptions.actions = Object.assign({}, machineOptions.actions, { + jumpToFirstMeasurementInActiveViewport: (ctx, evt) => { + const { trackedStudy, trackedSeries, activeViewportId } = ctx; + const measurements = measurementService.getMeasurements(); + const trackedMeasurements = measurements.filter( + m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID) + ); + + console.log( + 'jumping to measurement reset viewport', + activeViewportId, + trackedMeasurements[0] + ); + + const referencedDisplaySetUID = trackedMeasurements[0].displaySetInstanceUID; + const referencedDisplaySet = displaySetService.getDisplaySetByUID(referencedDisplaySetUID); + + const referencedImages = referencedDisplaySet.images; + const isVolumeIdReferenced = referencedImages[0].imageId.startsWith('volumeId'); + + const measurementData = trackedMeasurements[0].data; + + let imageIndex = 0; + if (!isVolumeIdReferenced && measurementData) { + // if it is imageId referenced find the index of the imageId, we don't have + // support for volumeId referenced images yet + imageIndex = referencedImages.findIndex(image => { + const imageIdToUse = Object.keys(measurementData)[0].substring(8); + return image.imageId === imageIdToUse; + }); + + if (imageIndex === -1) { + console.warn('Could not find image index for tracked measurement, using 0'); + imageIndex = 0; + } + } + + viewportGridService.setDisplaySetsForViewport({ + viewportId: activeViewportId, + displaySetInstanceUIDs: [referencedDisplaySetUID], + viewportOptions: { + initialImageOptions: { + index: imageIndex, + }, + }, + }); + }, + + jumpToSameImageInActiveViewport: (ctx, evt) => { + const { trackedStudy, trackedSeries, activeViewportId } = ctx; + const measurements = measurementService.getMeasurements(); + const trackedMeasurements = measurements.filter( + m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID) + ); + + // Jump to the last tracked measurement - most recent + const trackedMeasurement = trackedMeasurements[trackedMeasurements.length - 1]; + const referencedDisplaySetUID = trackedMeasurement.displaySetInstanceUID; + + // update the previously stored positionPresentation with the new viewportId + // presentation so that when we put the referencedDisplaySet back in the viewport + // it will be in the correct position zoom and pan + commandsManager.runCommand('updateStoredPositionPresentation', { + viewportId: activeViewportId, + displaySetInstanceUID: referencedDisplaySetUID, + referencedImageId: trackedMeasurement.referencedImageId, + }); + + viewportGridService.setDisplaySetsForViewport({ + viewportId: activeViewportId, + displaySetInstanceUIDs: [referencedDisplaySetUID], + }); + }, + showStructuredReportDisplaySetInActiveViewport: (ctx, evt) => { + if (evt.data.createdDisplaySetInstanceUIDs.length > 0) { + const StructuredReportDisplaySetInstanceUID = evt.data.createdDisplaySetInstanceUIDs[0]; + + viewportGridService.setDisplaySetsForViewport({ + viewportId: evt.data.viewportId, + displaySetInstanceUIDs: [StructuredReportDisplaySetInstanceUID], + }); + } + }, + discardPreviouslyTrackedMeasurements: (ctx, evt) => { + const measurements = measurementService.getMeasurements(); + const filteredMeasurements = measurements.filter(ms => + ctx.prevTrackedSeries.includes(ms.referenceSeriesUID) + ); + const measurementIds = filteredMeasurements.map(fm => fm.id); + + for (let i = 0; i < measurementIds.length; i++) { + measurementService.remove(measurementIds[i]); + } + }, + clearAllMeasurements: (ctx, evt) => { + const measurements = measurementService.getMeasurements(); + const measurementIds = measurements.map(fm => fm.uid); + + for (let i = 0; i < measurementIds.length; i++) { + measurementService.remove(measurementIds[i]); + } + }, + }); + machineOptions.services = Object.assign({}, machineOptions.services, { + promptBeginTracking: promptBeginTracking.bind(null, { + servicesManager, + extensionManager, + appConfig, + }), + promptTrackNewSeries: promptTrackNewSeries.bind(null, { + servicesManager, + extensionManager, + appConfig, + }), + promptTrackNewStudy: promptTrackNewStudy.bind(null, { + servicesManager, + extensionManager, + appConfig, + }), + promptSaveReport: promptSaveReport.bind(null, { + servicesManager, + commandsManager, + extensionManager, + appConfig, + }), + promptHydrateStructuredReport: promptHydrateStructuredReport.bind(null, { + servicesManager, + extensionManager, + commandsManager, + appConfig, + }), + hydrateStructuredReport: hydrateStructuredReport.bind(null, { + servicesManager, + extensionManager, + commandsManager, + appConfig, + }), + promptLabelAnnotation: promptLabelAnnotation.bind(null, { + servicesManager, + extensionManager, + commandsManager, + }), + }); + machineOptions.guards = Object.assign({}, machineOptions.guards, { + isLabelOnMeasure: (ctx, evt, condMeta) => { + const labelConfig = customizationService.getCustomization('measurementLabels'); + return labelConfig?.labelOnMeasure; + }, + isLabelOnMeasureAndShouldKillMachine: (ctx, evt, condMeta) => { + const labelConfig = customizationService.getCustomization('measurementLabels'); + return evt.data && evt.data.userResponse === RESPONSE.NO_NEVER && labelConfig?.labelOnMeasure; + }, + }); + + // TODO: IMPROVE + // - Add measurement_updated to cornerstone; debounced? (ext side, or consumption?) + // - Friendlier transition/api in front of measurementTracking machine? + // - Blocked: viewport overlay shouldn't clip when resized + // TODO: PRIORITY + // - Fix "ellipses" series description dynamic truncate length + // - Fix viewport border resize + // - created/destroyed hooks for extensions (cornerstone measurement subscriptions in it's `init`) + + const measurementTrackingMachine = useMemo(() => { + return Machine(machineConfiguration, machineOptions); + }, []); // Empty dependency array ensures this is only created once + + const [trackedMeasurements, sendTrackedMeasurementsEvent] = useMachine( + measurementTrackingMachine + ); + + useEffect(() => { + // Update the state machine with the active viewport ID + sendTrackedMeasurementsEvent('UPDATE_ACTIVE_VIEWPORT_ID', { + activeViewportId, + }); + }, [activeViewportId, sendTrackedMeasurementsEvent]); + + // ~~ Listen for changes to ViewportGrid for potential SRs hung in panes when idle + useEffect(() => { + const triggerPromptHydrateFlow = async () => { + if (viewports.size > 0) { + const activeViewport = viewports.get(activeViewportId); + + if (!activeViewport || !activeViewport?.displaySetInstanceUIDs?.length) { + return; + } + + // Todo: Getting the first displaySetInstanceUID is wrong, but we don't have + // tracking fusion viewports yet. This should change when we do. + const { displaySetService } = servicesManager.services; + const displaySet = displaySetService.getDisplaySetByUID( + activeViewport.displaySetInstanceUIDs[0] + ); + + if (!displaySet) { + return; + } + + // If this is an SR produced by our SR SOPClassHandler, + // and it hasn't been loaded yet, do that now so we + // can check if it can be rehydrated or not. + // + // Note: This happens: + // - If the viewport is not currently an OHIFCornerstoneSRViewport + // - If the displaySet has never been hung + // + // Otherwise, the displaySet will be loaded by the useEffect handler + // listening to displaySet changes inside OHIFCornerstoneSRViewport. + // The issue here is that this handler in TrackedMeasurementsContext + // ends up occurring before the Viewport is created, so the displaySet + // is not loaded yet, and isRehydratable is undefined unless we call load(). + if ( + displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID && + !displaySet.isLoaded && + displaySet.load + ) { + await displaySet.load(); + } + + // Magic string + // load function added by our sopClassHandler module + if ( + displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID && + displaySet.isRehydratable === true + ) { + console.log('sending event...', trackedMeasurements); + sendTrackedMeasurementsEvent('PROMPT_HYDRATE_SR', { + displaySetInstanceUID: displaySet.displaySetInstanceUID, + SeriesInstanceUID: displaySet.SeriesInstanceUID, + viewportId: activeViewportId, + }); + } + } + }; + triggerPromptHydrateFlow(); + }, [ + trackedMeasurements, + activeViewportId, + sendTrackedMeasurementsEvent, + servicesManager.services, + viewports, + ]); + + useEffect(() => { + // The command needs to be bound to the context's sendTrackedMeasurementsEvent + // so the command has to be registered in a React component. + commandsManager.registerCommand('DEFAULT', 'loadTrackedSRMeasurements', { + commandFn: props => sendTrackedMeasurementsEvent('HYDRATE_SR', props), + }); + }, [commandsManager, sendTrackedMeasurementsEvent]); + + return ( + + {children} + + ); +} + +TrackedMeasurementsContextProvider.propTypes = { + children: PropTypes.oneOf([PropTypes.func, PropTypes.node]), + appConfig: PropTypes.object, +}; + +export { TrackedMeasurementsContext, TrackedMeasurementsContextProvider, useTrackedMeasurements }; diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/hydrateStructuredReport.tsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/hydrateStructuredReport.tsx new file mode 100644 index 0000000..2d9d238 --- /dev/null +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/hydrateStructuredReport.tsx @@ -0,0 +1,31 @@ +import { hydrateStructuredReport as baseHydrateStructuredReport } from '@ohif/extension-cornerstone-dicom-sr'; + +function hydrateStructuredReport( + { servicesManager, extensionManager, commandsManager, appConfig }: withAppTypes, + ctx, + evt +) { + const { displaySetService } = servicesManager.services; + const { viewportId, displaySetInstanceUID } = evt; + const srDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + + return new Promise((resolve, reject) => { + const hydrationResult = baseHydrateStructuredReport( + { servicesManager, extensionManager, commandsManager, appConfig }, + displaySetInstanceUID + ); + + const StudyInstanceUID = hydrationResult.StudyInstanceUID; + const SeriesInstanceUIDs = hydrationResult.SeriesInstanceUIDs; + + resolve({ + displaySetInstanceUID: evt.displaySetInstanceUID, + srSeriesInstanceUID: srDisplaySet.SeriesInstanceUID, + viewportId, + StudyInstanceUID, + SeriesInstanceUIDs, + }); + }); +} + +export default hydrateStructuredReport; diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/index.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/index.js new file mode 100644 index 0000000..8e33344 --- /dev/null +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/index.js @@ -0,0 +1,5 @@ +export { + TrackedMeasurementsContext, + TrackedMeasurementsContextProvider, + useTrackedMeasurements, +} from './TrackedMeasurementsContext.tsx'; diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js new file mode 100644 index 0000000..e23de0c --- /dev/null +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js @@ -0,0 +1,490 @@ +import { assign } from 'xstate'; + +const RESPONSE = { + NO_NEVER: -1, + CANCEL: 0, + CREATE_REPORT: 1, + ADD_SERIES: 2, + SET_STUDY_AND_SERIES: 3, + NO_NOT_FOR_SERIES: 4, + HYDRATE_REPORT: 5, +}; + +const machineConfiguration = { + id: 'measurementTracking', + initial: 'idle', + context: { + activeViewportId: null, + trackedStudy: '', + trackedSeries: [], + ignoredSeries: [], + // + prevTrackedStudy: '', + prevTrackedSeries: [], + prevIgnoredSeries: [], + // + ignoredSRSeriesForHydration: [], + isDirty: false, + }, + states: { + off: { + type: 'final', + }, + labellingOnly: { + on: { + TRACK_SERIES: [ + { + target: 'promptLabelAnnotation', + actions: ['setPreviousState'], + }, + { + target: 'off', + }, + ], + }, + }, + idle: { + entry: 'clearContext', + on: { + TRACK_SERIES: [ + { + target: 'promptLabelAnnotation', + cond: 'isLabelOnMeasure', + actions: ['setPreviousState'], + }, + { + target: 'promptBeginTracking', + actions: ['setPreviousState'], + }, + ], + // Unused? We may only do PROMPT_HYDRATE_SR now? + SET_TRACKED_SERIES: [ + { + target: 'tracking', + actions: ['setTrackedStudyAndMultipleSeries', 'setIsDirtyToClean'], + }, + ], + PROMPT_HYDRATE_SR: { + target: 'promptHydrateStructuredReport', + cond: 'hasNotIgnoredSRSeriesForHydration', + }, + RESTORE_PROMPT_HYDRATE_SR: 'promptHydrateStructuredReport', + HYDRATE_SR: 'hydrateStructuredReport', + UPDATE_ACTIVE_VIEWPORT_ID: { + actions: assign({ + activeViewportId: (_, event) => event.activeViewportId, + }), + }, + }, + }, + promptBeginTracking: { + invoke: { + src: 'promptBeginTracking', + onDone: [ + { + target: 'tracking', + actions: ['setTrackedStudyAndSeries', 'setIsDirty'], + cond: 'shouldSetStudyAndSeries', + }, + { + target: 'labellingOnly', + cond: 'isLabelOnMeasureAndShouldKillMachine', + }, + { + target: 'off', + cond: 'shouldKillMachine', + }, + { + target: 'idle', + }, + ], + onError: { + target: 'idle', + }, + }, + }, + tracking: { + on: { + TRACK_SERIES: [ + { + target: 'promptLabelAnnotation', + cond: 'isLabelOnMeasure', + actions: ['setPreviousState'], + }, + { + target: 'promptTrackNewStudy', + cond: 'isNewStudy', + }, + { + target: 'promptTrackNewSeries', + cond: 'isNewSeries', + }, + ], + UNTRACK_SERIES: [ + { + target: 'tracking', + actions: ['removeTrackedSeries', 'setIsDirty'], + cond: 'hasRemainingTrackedSeries', + }, + { + target: 'idle', + }, + ], + SET_TRACKED_SERIES: [ + { + target: 'tracking', + actions: ['setTrackedStudyAndMultipleSeries'], + }, + ], + SAVE_REPORT: 'promptSaveReport', + SET_DIRTY: [ + { + target: 'tracking', + actions: ['setIsDirty'], + cond: 'shouldSetDirty', + }, + { + target: 'tracking', + }, + ], + }, + }, + promptTrackNewSeries: { + invoke: { + src: 'promptTrackNewSeries', + onDone: [ + { + target: 'tracking', + actions: ['addTrackedSeries', 'setIsDirty'], + cond: 'shouldAddSeries', + }, + { + target: 'tracking', + actions: [ + 'discardPreviouslyTrackedMeasurements', + 'setTrackedStudyAndSeries', + 'setIsDirty', + ], + cond: 'shouldSetStudyAndSeries', + }, + { + target: 'promptSaveReport', + cond: 'shouldPromptSaveReport', + }, + { + target: 'tracking', + }, + ], + onError: { + target: 'idle', + }, + }, + }, + promptTrackNewStudy: { + invoke: { + src: 'promptTrackNewStudy', + onDone: [ + { + target: 'tracking', + actions: [ + 'discardPreviouslyTrackedMeasurements', + 'setTrackedStudyAndSeries', + 'setIsDirty', + ], + cond: 'shouldSetStudyAndSeries', + }, + { + target: 'tracking', + actions: ['ignoreSeries'], + cond: 'shouldAddIgnoredSeries', + }, + { + target: 'promptSaveReport', + cond: 'shouldPromptSaveReport', + }, + { + target: 'tracking', + }, + ], + onError: { + target: 'idle', + }, + }, + }, + promptSaveReport: { + invoke: { + src: 'promptSaveReport', + onDone: [ + // "clicked the save button" + // - should clear all measurements + // - show DICOM SR + { + target: 'idle', + actions: ['clearAllMeasurements', 'showStructuredReportDisplaySetInActiveViewport'], + cond: 'shouldSaveAndContinueWithSameReport', + }, + // "starting a new report" + // - remove "just saved" measurements + // - start tracking a new study + report + { + target: 'tracking', + actions: ['discardPreviouslyTrackedMeasurements', 'setTrackedStudyAndSeries'], + cond: 'shouldSaveAndStartNewReport', + }, + // Cancel, back to tracking + { + target: 'tracking', + }, + ], + onError: { + target: 'idle', + }, + }, + }, + promptHydrateStructuredReport: { + invoke: { + src: 'promptHydrateStructuredReport', + onDone: [ + { + target: 'tracking', + actions: [ + 'setTrackedStudyAndMultipleSeries', + 'jumpToSameImageInActiveViewport', + 'setIsDirtyToClean', + ], + cond: 'shouldHydrateStructuredReport', + }, + { + target: 'idle', + actions: ['ignoreHydrationForSRSeries'], + cond: 'shouldIgnoreHydrationForSR', + }, + ], + onError: { + target: 'idle', + }, + }, + }, + hydrateStructuredReport: { + invoke: { + src: 'hydrateStructuredReport', + onDone: [ + { + target: 'tracking', + actions: [ + 'setTrackedStudyAndMultipleSeries', + 'jumpToSameImageInActiveViewport', + 'setIsDirtyToClean', + ], + }, + ], + onError: { + target: 'idle', + }, + }, + }, + promptLabelAnnotation: { + invoke: { + src: 'promptLabelAnnotation', + onDone: [ + { + target: 'labellingOnly', + cond: 'wasLabellingOnly', + }, + { + target: 'promptBeginTracking', + cond: 'wasIdle', + }, + { + target: 'promptTrackNewStudy', + cond: 'wasTrackingAndIsNewStudy', + }, + { + target: 'promptTrackNewSeries', + cond: 'wasTrackingAndIsNewSeries', + }, + { + target: 'tracking', + cond: 'wasTracking', + }, + { + target: 'off', + }, + ], + }, + }, + }, + strict: true, +}; + +const defaultOptions = { + services: { + promptBeginTracking: (ctx, evt) => { + // return { userResponse, StudyInstanceUID, SeriesInstanceUID } + }, + promptTrackNewStudy: (ctx, evt) => { + // return { userResponse, StudyInstanceUID, SeriesInstanceUID } + }, + promptTrackNewSeries: (ctx, evt) => { + // return { userResponse, StudyInstanceUID, SeriesInstanceUID } + }, + }, + actions: { + discardPreviouslyTrackedMeasurements: (ctx, evt) => { + console.log('discardPreviouslyTrackedMeasurements: not implemented'); + }, + clearAllMeasurements: (ctx, evt) => { + console.log('clearAllMeasurements: not implemented'); + }, + jumpToFirstMeasurementInActiveViewport: (ctx, evt) => { + console.warn('jumpToFirstMeasurementInActiveViewport: not implemented'); + }, + showStructuredReportDisplaySetInActiveViewport: (ctx, evt) => { + console.warn('showStructuredReportDisplaySetInActiveViewport: not implemented'); + }, + clearContext: assign({ + trackedStudy: '', + trackedSeries: [], + ignoredSeries: [], + prevTrackedStudy: '', + prevTrackedSeries: [], + prevIgnoredSeries: [], + }), + // Promise resolves w/ `evt.data.*` + setTrackedStudyAndSeries: assign((ctx, evt) => ({ + prevTrackedStudy: ctx.trackedStudy, + prevTrackedSeries: ctx.trackedSeries.slice(), + prevIgnoredSeries: ctx.ignoredSeries.slice(), + // + trackedStudy: evt.data.StudyInstanceUID, + trackedSeries: [evt.data.SeriesInstanceUID], + ignoredSeries: [], + })), + setTrackedStudyAndMultipleSeries: assign((ctx, evt) => { + const studyInstanceUID = evt.StudyInstanceUID || evt.data.StudyInstanceUID; + const seriesInstanceUIDs = evt.SeriesInstanceUIDs || evt.data.SeriesInstanceUIDs; + + return { + prevTrackedStudy: ctx.trackedStudy, + prevTrackedSeries: ctx.trackedSeries.slice(), + prevIgnoredSeries: ctx.ignoredSeries.slice(), + // + trackedStudy: studyInstanceUID, + trackedSeries: [...ctx.trackedSeries, ...seriesInstanceUIDs], + ignoredSeries: [], + }; + }), + setIsDirtyToClean: assign((ctx, evt) => ({ + isDirty: false, + })), + setIsDirty: assign((ctx, evt) => ({ + isDirty: true, + })), + ignoreSeries: assign((ctx, evt) => ({ + prevIgnoredSeries: [...ctx.ignoredSeries], + ignoredSeries: [...ctx.ignoredSeries, evt.data.SeriesInstanceUID], + })), + ignoreHydrationForSRSeries: assign((ctx, evt) => ({ + ignoredSRSeriesForHydration: [ + ...ctx.ignoredSRSeriesForHydration, + evt.data.srSeriesInstanceUID, + ], + })), + addTrackedSeries: assign((ctx, evt) => ({ + prevTrackedSeries: [...ctx.trackedSeries], + trackedSeries: [...ctx.trackedSeries, evt.data.SeriesInstanceUID], + })), + removeTrackedSeries: assign((ctx, evt) => ({ + prevTrackedSeries: ctx.trackedSeries.slice().filter(ser => ser !== evt.SeriesInstanceUID), + trackedSeries: ctx.trackedSeries.slice().filter(ser => ser !== evt.SeriesInstanceUID), + })), + setPreviousState: assign((ctx, evt, meta) => { + return { + prevState: meta.state.value, + }; + }), + }, + guards: { + // We set dirty any time we performan an action that: + // - Tracks a new study + // - Tracks a new series + // - Adds a measurement to an already tracked study/series + // + // We set clean any time we restore from an SR + // + // This guard/condition is specific to "new measurements" + // to make sure we only track dirty when the new measurement is specific + // to a series we're already tracking + // + // tl;dr + // Any report change, that is not a hydration of an existing report, should + // result in a "dirty" report + // + // Where dirty means there would be "loss of data" if we blew away measurements + // without creating a new SR. + shouldSetDirty: (ctx, evt) => { + return ( + // When would this happen? + evt.SeriesInstanceUID === undefined || ctx.trackedSeries.includes(evt.SeriesInstanceUID) + ); + }, + wasLabellingOnly: (ctx, evt, condMeta) => { + return ctx.prevState === 'labellingOnly'; + }, + wasIdle: (ctx, evt, condMeta) => { + return ctx.prevState === 'idle'; + }, + wasTracking: (ctx, evt, condMeta) => { + return ctx.prevState === 'tracking'; + }, + wasTrackingAndIsNewStudy: (ctx, evt, condMeta) => { + return ( + ctx.prevState === 'tracking' && + !ctx.ignoredSeries.includes(evt.data.SeriesInstanceUID) && + ctx.trackedStudy !== evt.data.StudyInstanceUID + ); + }, + wasTrackingAndIsNewSeries: (ctx, evt, condMeta) => { + return ( + ctx.prevState === 'tracking' && + !ctx.ignoredSeries.includes(evt.data.SeriesInstanceUID) && + !ctx.trackedSeries.includes(evt.data.SeriesInstanceUID) + ); + }, + + shouldKillMachine: (ctx, evt) => evt.data && evt.data.userResponse === RESPONSE.NO_NEVER, + shouldAddSeries: (ctx, evt) => evt.data && evt.data.userResponse === RESPONSE.ADD_SERIES, + shouldSetStudyAndSeries: (ctx, evt) => + evt.data && evt.data.userResponse === RESPONSE.SET_STUDY_AND_SERIES, + shouldAddIgnoredSeries: (ctx, evt) => + evt.data && evt.data.userResponse === RESPONSE.NO_NOT_FOR_SERIES, + shouldPromptSaveReport: (ctx, evt) => + evt.data && evt.data.userResponse === RESPONSE.CREATE_REPORT, + shouldIgnoreHydrationForSR: (ctx, evt) => evt.data && evt.data.userResponse === RESPONSE.CANCEL, + shouldSaveAndContinueWithSameReport: (ctx, evt) => + evt.data && + evt.data.userResponse === RESPONSE.CREATE_REPORT && + evt.data.isBackupSave === true, + shouldSaveAndStartNewReport: (ctx, evt) => + evt.data && + evt.data.userResponse === RESPONSE.CREATE_REPORT && + evt.data.isBackupSave === false, + shouldHydrateStructuredReport: (ctx, evt) => + evt.data && evt.data.userResponse === RESPONSE.HYDRATE_REPORT, + // Has more than 1, or SeriesInstanceUID is not in list + // --> Post removal would have non-empty trackedSeries array + hasRemainingTrackedSeries: (ctx, evt) => + ctx.trackedSeries.length > 1 || !ctx.trackedSeries.includes(evt.SeriesInstanceUID), + hasNotIgnoredSRSeriesForHydration: (ctx, evt) => { + return !ctx.ignoredSRSeriesForHydration.includes(evt.SeriesInstanceUID); + }, + isNewStudy: (ctx, evt) => + !ctx.ignoredSeries.includes(evt.SeriesInstanceUID) && + ctx.trackedStudy !== evt.StudyInstanceUID, + isNewSeries: (ctx, evt) => + !ctx.ignoredSeries.includes(evt.SeriesInstanceUID) && + !ctx.trackedSeries.includes(evt.SeriesInstanceUID), + }, +}; + +export { defaultOptions, machineConfiguration, RESPONSE }; diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js new file mode 100644 index 0000000..2638206 --- /dev/null +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js @@ -0,0 +1,82 @@ +import { ButtonEnums } from '@ohif/ui'; +import i18n from 'i18next'; + +const RESPONSE = { + NO_NEVER: -1, + CANCEL: 0, + CREATE_REPORT: 1, + ADD_SERIES: 2, + SET_STUDY_AND_SERIES: 3, +}; + +function promptBeginTracking({ servicesManager, extensionManager }, ctx, evt) { + const { uiViewportDialogService } = servicesManager.services; + const appConfig = extensionManager._appConfig; + // When the state change happens after a promise, the state machine sends the retult in evt.data; + // In case of direct transition to the state, the state machine sends the data in evt; + const { viewportId, StudyInstanceUID, SeriesInstanceUID } = evt.data || evt; + + return new Promise(async function (resolve, reject) { + let promptResult = appConfig?.disableConfirmationPrompts + ? RESPONSE.SET_STUDY_AND_SERIES + : await _askTrackMeasurements(uiViewportDialogService, viewportId); + + resolve({ + userResponse: promptResult, + StudyInstanceUID, + SeriesInstanceUID, + viewportId, + }); + }); +} + +function _askTrackMeasurements(uiViewportDialogService, viewportId) { + return new Promise(function (resolve, reject) { + const message = i18n.t('MeasurementTable:Track measurements for this series?'); + const actions = [ + { + id: 'prompt-begin-tracking-cancel', + type: ButtonEnums.type.secondary, + text: i18n.t('Common:No'), + value: RESPONSE.CANCEL, + }, + { + id: 'prompt-begin-tracking-no-do-not-ask-again', + type: ButtonEnums.type.secondary, + text: i18n.t('MeasurementTable:No, do not ask again'), + value: RESPONSE.NO_NEVER, + }, + { + id: 'prompt-begin-tracking-yes', + type: ButtonEnums.type.primary, + text: i18n.t('Common:Yes'), + value: RESPONSE.SET_STUDY_AND_SERIES, + }, + ]; + const onSubmit = result => { + uiViewportDialogService.hide(); + resolve(result); + }; + + uiViewportDialogService.show({ + viewportId, + id: 'measurement-tracking-prompt-begin-tracking', + type: 'info', + message, + actions, + onSubmit, + onOutsideClick: () => { + uiViewportDialogService.hide(); + resolve(RESPONSE.CANCEL); + }, + onKeyPress: event => { + if (event.key === 'Enter') { + const action = actions.find(action => action.id === 'prompt-begin-tracking-yes'); + onSubmit(action.value); + } + }, + }); + }); +} + +export default promptBeginTracking; diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.js new file mode 100644 index 0000000..4fd6640 --- /dev/null +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.js @@ -0,0 +1,94 @@ +import { hydrateStructuredReport } from '@ohif/extension-cornerstone-dicom-sr'; +import { ButtonEnums } from '@ohif/ui'; + +const RESPONSE = { + NO_NEVER: -1, + CANCEL: 0, + CREATE_REPORT: 1, + ADD_SERIES: 2, + SET_STUDY_AND_SERIES: 3, + NO_NOT_FOR_SERIES: 4, + HYDRATE_REPORT: 5, +}; + +function promptHydrateStructuredReport( + { servicesManager, extensionManager, commandsManager, appConfig }, + ctx, + evt +) { + const { uiViewportDialogService, displaySetService } = servicesManager.services; + const { viewportId, displaySetInstanceUID } = evt; + const srDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + return new Promise(async function (resolve, reject) { + const promptResult = appConfig?.disableConfirmationPrompts + ? RESPONSE.HYDRATE_REPORT + : await _askTrackMeasurements(uiViewportDialogService, viewportId); + + // Need to do action here... So we can set state... + let StudyInstanceUID, SeriesInstanceUIDs; + + if (promptResult === RESPONSE.HYDRATE_REPORT) { + console.warn('!! HYDRATING STRUCTURED REPORT'); + const hydrationResult = hydrateStructuredReport( + { servicesManager, extensionManager, commandsManager, appConfig }, + displaySetInstanceUID + ); + + StudyInstanceUID = hydrationResult.StudyInstanceUID; + SeriesInstanceUIDs = hydrationResult.SeriesInstanceUIDs; + } + + resolve({ + userResponse: promptResult, + displaySetInstanceUID: evt.displaySetInstanceUID, + srSeriesInstanceUID: srDisplaySet.SeriesInstanceUID, + viewportId, + StudyInstanceUID, + SeriesInstanceUIDs, + }); + }); +} + +function _askTrackMeasurements(uiViewportDialogService, viewportId) { + return new Promise(function (resolve, reject) { + const message = 'Do you want to continue tracking measurements for this study?'; + const actions = [ + { + id: 'no-hydrate', + type: ButtonEnums.type.secondary, + text: 'No', + value: RESPONSE.CANCEL, + }, + { + id: 'yes-hydrate', + type: ButtonEnums.type.primary, + text: 'Yes', + value: RESPONSE.HYDRATE_REPORT, + }, + ]; + const onSubmit = result => { + uiViewportDialogService.hide(); + resolve(result); + }; + + uiViewportDialogService.show({ + viewportId, + type: 'info', + message, + actions, + onSubmit, + onOutsideClick: () => { + uiViewportDialogService.hide(); + resolve(RESPONSE.CANCEL); + }, + onKeyPress: event => { + if (event.key === 'Enter') { + const action = actions.find(action => action.value === RESPONSE.HYDRATE_REPORT); + onSubmit(action.value); + } + }, + }); + }); +} + +export default promptHydrateStructuredReport; diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js new file mode 100644 index 0000000..d5e5575 --- /dev/null +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js @@ -0,0 +1,112 @@ +import { ButtonEnums } from '@ohif/ui'; + +const RESPONSE = { + NO_NEVER: -1, + CANCEL: 0, + CREATE_REPORT: 1, + ADD_SERIES: 2, + SET_STUDY_AND_SERIES: 3, + NO_NOT_FOR_SERIES: 4, +}; + +function promptTrackNewSeries({ servicesManager, extensionManager }, ctx, evt) { + const { UIViewportDialogService } = servicesManager.services; + // When the state change happens after a promise, the state machine sends the retult in evt.data; + // In case of direct transition to the state, the state machine sends the data in evt; + const { viewportId, StudyInstanceUID, SeriesInstanceUID } = evt.data || evt; + + return new Promise(async function (resolve, reject) { + let promptResult = await _askShouldAddMeasurements(UIViewportDialogService, viewportId); + + if (promptResult === RESPONSE.CREATE_REPORT) { + promptResult = ctx.isDirty + ? await _askSaveDiscardOrCancel(UIViewportDialogService, viewportId) + : RESPONSE.SET_STUDY_AND_SERIES; + } + + resolve({ + userResponse: promptResult, + StudyInstanceUID, + SeriesInstanceUID, + viewportId, + isBackupSave: false, + }); + }); +} + +function _askShouldAddMeasurements(uiViewportDialogService, viewportId) { + return new Promise(function (resolve, reject) { + const message = 'Do you want to add this measurement to the existing report?'; + const actions = [ + { + type: ButtonEnums.type.secondary, + text: 'Cancel', + value: RESPONSE.CANCEL, + }, + { + type: ButtonEnums.type.primary, + text: 'Create new report', + value: RESPONSE.CREATE_REPORT, + }, + { + type: ButtonEnums.type.primary, + text: 'Add to existing report', + value: RESPONSE.ADD_SERIES, + }, + ]; + const onSubmit = result => { + uiViewportDialogService.hide(); + resolve(result); + }; + + uiViewportDialogService.show({ + viewportId, + type: 'info', + message, + actions, + onSubmit, + onOutsideClick: () => { + uiViewportDialogService.hide(); + resolve(RESPONSE.CANCEL); + }, + }); + }); +} + +function _askSaveDiscardOrCancel(UIViewportDialogService, viewportId) { + return new Promise(function (resolve, reject) { + const message = + 'You have existing tracked measurements. What would you like to do with your existing tracked measurements?'; + const actions = [ + { type: 'cancel', text: 'Cancel', value: RESPONSE.CANCEL }, + { + type: 'secondary', + text: 'Save', + value: RESPONSE.CREATE_REPORT, + }, + { + type: 'primary', + text: 'Discard', + value: RESPONSE.SET_STUDY_AND_SERIES, + }, + ]; + const onSubmit = result => { + UIViewportDialogService.hide(); + resolve(result); + }; + + UIViewportDialogService.show({ + viewportId, + type: 'warning', + message, + actions, + onSubmit, + onOutsideClick: () => { + UIViewportDialogService.hide(); + resolve(RESPONSE.CANCEL); + }, + }); + }); +} + +export default promptTrackNewSeries; diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.ts b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.ts new file mode 100644 index 0000000..1ce5869 --- /dev/null +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.ts @@ -0,0 +1,120 @@ +import i18n from 'i18next'; + +const RESPONSE = { + NO_NEVER: -1, + CANCEL: 0, + CREATE_REPORT: 1, + ADD_SERIES: 2, + SET_STUDY_AND_SERIES: 3, + NO_NOT_FOR_SERIES: 4, +}; + +function promptTrackNewStudy({ servicesManager, extensionManager }: withAppTypes, ctx, evt) { + const { uiViewportDialogService } = servicesManager.services; + // When the state change happens after a promise, the state machine sends the retult in evt.data; + // In case of direct transition to the state, the state machine sends the data in evt; + const { viewportId, StudyInstanceUID, SeriesInstanceUID } = evt.data || evt; + + return new Promise(async function (resolve, reject) { + let promptResult = await _askTrackMeasurements(uiViewportDialogService, viewportId); + + if (promptResult === RESPONSE.SET_STUDY_AND_SERIES) { + promptResult = ctx.isDirty + ? await _askSaveDiscardOrCancel(uiViewportDialogService, viewportId) + : RESPONSE.SET_STUDY_AND_SERIES; + } + + resolve({ + userResponse: promptResult, + StudyInstanceUID, + SeriesInstanceUID, + viewportId, + isBackupSave: false, + }); + }); +} + +function _askTrackMeasurements( + UIViewportDialogService: AppTypes.UIViewportDialogService, + viewportId +) { + return new Promise(function (resolve, reject) { + const message = i18n.t('MeasurementTable:Track measurements for this series?'); + const actions = [ + { type: 'cancel', text: i18n.t('MeasurementTable:No'), value: RESPONSE.CANCEL }, + { + type: 'secondary', + text: i18n.t('MeasurementTable:No, do not ask again'), + value: RESPONSE.NO_NOT_FOR_SERIES, + }, + { + type: 'primary', + text: i18n.t('MeasurementTable:Yes'), + value: RESPONSE.SET_STUDY_AND_SERIES, + }, + ]; + const onSubmit = result => { + UIViewportDialogService.hide(); + resolve(result); + }; + + UIViewportDialogService.show({ + viewportId, + type: 'info', + message, + actions, + onSubmit, + onOutsideClick: () => { + UIViewportDialogService.hide(); + resolve(RESPONSE.CANCEL); + }, + onKeyPress: event => { + if (event.key === 'Enter') { + const action = actions.find(action => action.value === RESPONSE.SET_STUDY_AND_SERIES); + onSubmit(action.value); + } + }, + }); + }); +} + +function _askSaveDiscardOrCancel( + UIViewportDialogService: AppTypes.UIViewportDialogService, + viewportId +) { + return new Promise(function (resolve, reject) { + const message = + 'Measurements cannot span across multiple studies. Do you want to save your tracked measurements?'; + const actions = [ + { type: 'cancel', text: 'Cancel', value: RESPONSE.CANCEL }, + { + type: 'secondary', + text: 'No, discard previously tracked series & measurements', + value: RESPONSE.SET_STUDY_AND_SERIES, + }, + { + type: 'primary', + text: 'Yes', + value: RESPONSE.CREATE_REPORT, + }, + ]; + const onSubmit = result => { + UIViewportDialogService.hide(); + resolve(result); + }; + + UIViewportDialogService.show({ + viewportId, + type: 'warning', + message, + actions, + onSubmit, + onOutsideClick: () => { + UIViewportDialogService.hide(); + resolve(RESPONSE.CANCEL); + }, + }); + }); +} + +export default promptTrackNewStudy; diff --git a/extensions/measurement-tracking/src/contexts/index.js b/extensions/measurement-tracking/src/contexts/index.js new file mode 100644 index 0000000..8b6723a --- /dev/null +++ b/extensions/measurement-tracking/src/contexts/index.js @@ -0,0 +1,5 @@ +export { + TrackedMeasurementsContext, + TrackedMeasurementsContextProvider, + useTrackedMeasurements, +} from './TrackedMeasurementsContext'; diff --git a/extensions/measurement-tracking/src/getContextModule.tsx b/extensions/measurement-tracking/src/getContextModule.tsx new file mode 100644 index 0000000..b19097b --- /dev/null +++ b/extensions/measurement-tracking/src/getContextModule.tsx @@ -0,0 +1,24 @@ +import { + TrackedMeasurementsContext, + TrackedMeasurementsContextProvider, + useTrackedMeasurements, +} from './contexts'; + +function getContextModule({ servicesManager, extensionManager, commandsManager }) { + const BoundTrackedMeasurementsContextProvider = TrackedMeasurementsContextProvider.bind(null, { + servicesManager, + extensionManager, + commandsManager, + }); + + return [ + { + name: 'TrackedMeasurementsContext', + context: TrackedMeasurementsContext, + provider: BoundTrackedMeasurementsContextProvider, + }, + ]; +} + +export { useTrackedMeasurements }; +export default getContextModule; diff --git a/extensions/measurement-tracking/src/getPanelModule.tsx b/extensions/measurement-tracking/src/getPanelModule.tsx new file mode 100644 index 0000000..9dfab40 --- /dev/null +++ b/extensions/measurement-tracking/src/getPanelModule.tsx @@ -0,0 +1,37 @@ +import { Types } from '@ohif/core'; +import { PanelMeasurementTableTracking, PanelStudyBrowserTracking } from './panels'; +import i18n from 'i18next'; +import React from 'react'; + +// TODO: +// - No loading UI exists yet +// - cancel promises when component is destroyed +// - show errors in UI for thumbnails if promise fails + +function getPanelModule({ commandsManager, extensionManager, servicesManager }): Types.Panel[] { + return [ + { + name: 'seriesList', + iconName: 'tab-studies', + iconLabel: 'Studies', + label: i18n.t('SidePanel:Studies'), + component: props => , + }, + { + name: 'trackedMeasurements', + iconName: 'tab-linear', + iconLabel: 'Measure', + label: i18n.t('SidePanel:Measurements'), + component: props => ( + + ), + }, + ]; +} + +export default getPanelModule; diff --git a/extensions/measurement-tracking/src/getViewportModule.tsx b/extensions/measurement-tracking/src/getViewportModule.tsx new file mode 100644 index 0000000..ee5f4a0 --- /dev/null +++ b/extensions/measurement-tracking/src/getViewportModule.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +const Component = React.lazy(() => { + return import(/* webpackPrefetch: true */ './viewports/TrackedCornerstoneViewport'); +}); + +const OHIFCornerstoneViewport = props => { + return ( + Loading...}> + + + ); +}; + +function getViewportModule({ servicesManager, commandsManager, extensionManager }) { + const ExtendedOHIFCornerstoneTrackingViewport = props => { + return ( + + ); + }; + + return [ + { + name: 'cornerstone-tracked', + component: ExtendedOHIFCornerstoneTrackingViewport, + }, + ]; +} + +export default getViewportModule; diff --git a/extensions/measurement-tracking/src/id.js b/extensions/measurement-tracking/src/id.js new file mode 100644 index 0000000..ebe5acd --- /dev/null +++ b/extensions/measurement-tracking/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/extensions/measurement-tracking/src/index.tsx b/extensions/measurement-tracking/src/index.tsx new file mode 100644 index 0000000..3cced77 --- /dev/null +++ b/extensions/measurement-tracking/src/index.tsx @@ -0,0 +1,43 @@ +import React from 'react'; + +import getContextModule from './getContextModule'; +import getPanelModule from './getPanelModule'; +import getViewportModule from './getViewportModule'; +import { id } from './id.js'; +import { ViewportActionButton } from '@ohif/ui'; +import i18n from '@ohif/i18n'; + +const measurementTrackingExtension = { + /** + * Only required property. Should be a unique value across all extensions. + */ + id, + + getContextModule, + getPanelModule, + getViewportModule, + + onModeEnter({ servicesManager }) { + const { toolbarService } = servicesManager.services; + + toolbarService.addButtons( + [ + { + // A button for loading tracked, SR measurements. + // Note that the command run is registered in TrackedMeasurementsContext + // because it must be bound to a React context's data. + id: 'loadSRMeasurements', + component: props => ( + {i18n.t('Common:LOAD')} + ), + props: { + commands: ['loadTrackedSRMeasurements'], + }, + }, + ], + true // replace the button if it is already defined + ); + }, +}; + +export default measurementTrackingExtension; diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx new file mode 100644 index 0000000..81dd403 --- /dev/null +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx @@ -0,0 +1,91 @@ +import React, { useEffect, useState } from 'react'; +import { DicomMetadataStore, utils } from '@ohif/core'; +import { useViewportGrid } from '@ohif/ui-next'; +import { Button, Icons } from '@ohif/ui-next'; +import { PanelMeasurement, StudySummaryFromMetadata } from '@ohif/extension-cornerstone'; +import { useTrackedMeasurements } from '../getContextModule'; + +const { filterAnd, filterPlanarMeasurement, filterMeasurementsBySeriesUID } = + utils.MeasurementFilters; + +function PanelMeasurementTableTracking({ + servicesManager, + extensionManager, + commandsManager, +}: withAppTypes) { + const [viewportGrid] = useViewportGrid(); + const { customizationService } = servicesManager.services; + const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements(); + const { trackedStudy, trackedSeries } = trackedMeasurements.context; + const measurementFilter = trackedStudy + ? filterAnd(filterPlanarMeasurement, filterMeasurementsBySeriesUID(trackedSeries)) + : filterPlanarMeasurement; + + const disableEditing = customizationService.getCustomization('panelMeasurement.disableEditing'); + + return ( + <> + + { + const disabled = additionalFindings.length === 0 && measurements.length === 0; + + if (disableEditing || disabled) { + return null; + } + + return ( +
+
+ + + +
+
+ ); + }} + >
+ + ); +} + +export default PanelMeasurementTableTracking; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx new file mode 100644 index 0000000..d60e1cc --- /dev/null +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx @@ -0,0 +1,737 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import PropTypes from 'prop-types'; +import { useSystem, utils } from '@ohif/core'; +import { useImageViewer, Dialog, ButtonEnums } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; +import { StudyBrowser } from '@ohif/ui-next'; + +import { useTrackedMeasurements } from '../../getContextModule'; +import { Separator } from '@ohif/ui-next'; +import { MoreDropdownMenu, PanelStudyBrowserHeader } from '@ohif/extension-default'; +import { defaultActionIcons } from './constants'; +const { formatDate, createStudyBrowserTabs } = utils; + +const thumbnailNoImageModalities = [ + 'SR', + 'SEG', + 'SM', + 'RTSTRUCT', + 'RTPLAN', + 'RTDOSE', + 'DOC', + 'OT', + 'PMAP', +]; + +/** + * + * @param {*} param0 + */ +export default function PanelStudyBrowserTracking({ + getImageSrc, + getStudiesForPatientByMRN, + requestDisplaySetCreationForStudy, + dataSource, +}) { + const { servicesManager, commandsManager } = useSystem(); + const { + displaySetService, + uiDialogService, + hangingProtocolService, + uiNotificationService, + measurementService, + studyPrefetcherService, + customizationService, + } = servicesManager.services; + const navigate = useNavigate(); + const studyMode = customizationService.getCustomization('studyBrowser.studyMode'); + + const { t } = useTranslation('Common'); + + // Normally you nest the components so the tree isn't so deep, and the data + // doesn't have to have such an intense shape. This works well enough for now. + // Tabs --> Studies --> DisplaySets --> Thumbnails + const { StudyInstanceUIDs } = useImageViewer(); + const [{ activeViewportId, viewports, isHangingProtocolLayout }, viewportGridService] = + useViewportGrid(); + const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements(); + + const [activeTabName, setActiveTabName] = useState(studyMode); + const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([ + ...StudyInstanceUIDs, + ]); + const [studyDisplayList, setStudyDisplayList] = useState([]); + const [hasLoadedViewports, setHasLoadedViewports] = useState(false); + const [displaySets, setDisplaySets] = useState([]); + const [displaySetsLoadingState, setDisplaySetsLoadingState] = useState({}); + const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({}); + const [jumpToDisplaySet, setJumpToDisplaySet] = useState(null); + + const [viewPresets, setViewPresets] = useState( + customizationService.getCustomization('studyBrowser.viewPresets') + ); + + const [actionIcons, setActionIcons] = useState(defaultActionIcons); + + const updateActionIconValue = actionIcon => { + actionIcon.value = !actionIcon.value; + const newActionIcons = [...actionIcons]; + setActionIcons(newActionIcons); + }; + + const updateViewPresetValue = viewPreset => { + if (!viewPreset) { + return; + } + const newViewPresets = viewPresets.map(preset => { + preset.selected = preset.id === viewPreset.id; + return preset; + }); + setViewPresets(newViewPresets); + }; + + const onDoubleClickThumbnailHandler = displaySetInstanceUID => { + let updatedViewports = []; + const viewportId = activeViewportId; + try { + updatedViewports = hangingProtocolService.getViewportsRequireUpdate( + viewportId, + displaySetInstanceUID, + isHangingProtocolLayout + ); + } catch (error) { + console.warn(error); + uiNotificationService.show({ + title: 'Thumbnail Double Click', + message: + 'The selected display sets could not be added to the viewport due to a mismatch in the Hanging Protocol rules.', + type: 'error', + duration: 3000, + }); + } + + viewportGridService.setDisplaySetsForViewports(updatedViewports); + }; + + const activeViewportDisplaySetInstanceUIDs = + viewports.get(activeViewportId)?.displaySetInstanceUIDs; + + const { trackedSeries } = trackedMeasurements.context; + + useEffect(() => { + setActiveTabName(studyMode); + }, [studyMode]); + + // ~~ studyDisplayList + useEffect(() => { + // Fetch all studies for the patient in each primary study + async function fetchStudiesForPatient(StudyInstanceUID) { + // current study qido + const qidoForStudyUID = await dataSource.query.studies.search({ + studyInstanceUid: StudyInstanceUID, + }); + + if (!qidoForStudyUID?.length) { + navigate('/notfoundstudy', '_self'); + throw new Error('Invalid study URL'); + } + + let qidoStudiesForPatient = qidoForStudyUID; + + // try to fetch the prior studies based on the patientID if the + // server can respond. + try { + qidoStudiesForPatient = await getStudiesForPatientByMRN(qidoForStudyUID); + } catch (error) { + console.warn(error); + } + + const mappedStudies = _mapDataSourceStudies(qidoStudiesForPatient); + const actuallyMappedStudies = mappedStudies.map(qidoStudy => { + return { + studyInstanceUid: qidoStudy.StudyInstanceUID, + date: formatDate(qidoStudy.StudyDate) || t('NoStudyDate'), + description: qidoStudy.StudyDescription, + modalities: qidoStudy.ModalitiesInStudy, + numInstances: qidoStudy.NumInstances, + }; + }); + + setStudyDisplayList(prevArray => { + const ret = [...prevArray]; + for (const study of actuallyMappedStudies) { + if (!prevArray.find(it => it.studyInstanceUid === study.studyInstanceUid)) { + ret.push(study); + } + } + return ret; + }); + } + + StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [StudyInstanceUIDs, getStudiesForPatientByMRN]); + + // ~~ Initial Thumbnails + useEffect(() => { + if (!hasLoadedViewports) { + if (activeViewportId) { + // Once there is an active viewport id, it means the layout is ready + // so wait a bit of time to allow the viewports preferential loading + // which improves user experience of responsiveness significantly on slower + // systems. + const delayMs = 250 + displaySetService.getActiveDisplaySets().length * 10; + window.setTimeout(() => setHasLoadedViewports(true), delayMs); + } + + return; + } + + let currentDisplaySets = displaySetService.activeDisplaySets; + // filter non based on the list of modalities that are supported by cornerstone + currentDisplaySets = currentDisplaySets.filter( + ds => !thumbnailNoImageModalities.includes(ds.Modality) + ); + + if (!currentDisplaySets.length) { + return; + } + + currentDisplaySets.forEach(async dSet => { + const newImageSrcEntry = {}; + const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID); + const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); + + const imageId = getImageIdForThumbnail(displaySet, imageIds); + + // TODO: Is it okay that imageIds are not returned here for SR displaySets? + if (!imageId || displaySet?.unsupported) { + return; + } + // When the image arrives, render it and store the result in the thumbnailImgSrcMap + let { thumbnailSrc } = displaySet; + if (!thumbnailSrc && displaySet.getThumbnailSrc) { + thumbnailSrc = await displaySet.getThumbnailSrc(); + } + if (!thumbnailSrc) { + const thumbnailSrc = await getImageSrc(imageId); + displaySet.thumbnailSrc = thumbnailSrc; + } + newImageSrcEntry[dSet.displaySetInstanceUID] = thumbnailSrc; + + setThumbnailImageSrcMap(prevState => { + return { ...prevState, ...newImageSrcEntry }; + }); + }); + }, [displaySetService, dataSource, getImageSrc, activeViewportId, hasLoadedViewports]); + + // ~~ displaySets + useEffect(() => { + const currentDisplaySets = displaySetService.activeDisplaySets; + + if (!currentDisplaySets.length) { + return; + } + + const mappedDisplaySets = _mapDisplaySets( + currentDisplaySets, + displaySetsLoadingState, + thumbnailImageSrcMap, + trackedSeries, + viewports, + viewportGridService, + dataSource, + displaySetService, + uiDialogService, + uiNotificationService + ); + + setDisplaySets(mappedDisplaySets); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + displaySetService.activeDisplaySets, + displaySetsLoadingState, + trackedSeries, + viewports, + dataSource, + thumbnailImageSrcMap, + ]); + + // -- displaySetsLoadingState + useEffect(() => { + const { unsubscribe } = studyPrefetcherService.subscribe( + studyPrefetcherService.EVENTS.DISPLAYSET_LOAD_PROGRESS, + updatedDisplaySetLoadingState => { + const { displaySetInstanceUID, loadingProgress } = updatedDisplaySetLoadingState; + + setDisplaySetsLoadingState(prevState => ({ + ...prevState, + [displaySetInstanceUID]: loadingProgress, + })); + } + ); + + return () => unsubscribe(); + }, [studyPrefetcherService]); + + // ~~ subscriptions --> displaySets + useEffect(() => { + // DISPLAY_SETS_ADDED returns an array of DisplaySets that were added + const SubscriptionDisplaySetsAdded = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SETS_ADDED, + data => { + if (!hasLoadedViewports) { + return; + } + const { displaySetsAdded, options } = data; + displaySetsAdded.forEach(async dSet => { + const displaySetInstanceUID = dSet.displaySetInstanceUID; + + const newImageSrcEntry = {}; + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + if (displaySet?.unsupported) { + return; + } + + if (options.madeInClient) { + setJumpToDisplaySet(displaySetInstanceUID); + } + + const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); + const imageId = getImageIdForThumbnail(displaySet, imageIds); + + // TODO: Is it okay that imageIds are not returned here for SR displaysets? + if (!imageId) { + return; + } + + // When the image arrives, render it and store the result in the thumbnailImgSrcMap + newImageSrcEntry[displaySetInstanceUID] = await getImageSrc(imageId); + setThumbnailImageSrcMap(prevState => { + return { ...prevState, ...newImageSrcEntry }; + }); + }); + } + ); + + return () => { + SubscriptionDisplaySetsAdded.unsubscribe(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [displaySetService, dataSource, getImageSrc, thumbnailImageSrcMap, trackedSeries, viewports]); + + useEffect(() => { + // TODO: Will this always hold _all_ the displaySets we care about? + // DISPLAY_SETS_CHANGED returns `DisplaySerService.activeDisplaySets` + const SubscriptionDisplaySetsChanged = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SETS_CHANGED, + changedDisplaySets => { + const mappedDisplaySets = _mapDisplaySets( + changedDisplaySets, + displaySetsLoadingState, + thumbnailImageSrcMap, + trackedSeries, + viewports, + viewportGridService, + dataSource, + displaySetService, + uiDialogService, + uiNotificationService + ); + + setDisplaySets(mappedDisplaySets); + } + ); + + const SubscriptionDisplaySetMetaDataInvalidated = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, + () => { + const mappedDisplaySets = _mapDisplaySets( + displaySetService.getActiveDisplaySets(), + displaySetsLoadingState, + thumbnailImageSrcMap, + trackedSeries, + viewports, + viewportGridService, + dataSource, + displaySetService, + uiDialogService, + uiNotificationService + ); + + setDisplaySets(mappedDisplaySets); + } + ); + + return () => { + SubscriptionDisplaySetsChanged.unsubscribe(); + SubscriptionDisplaySetMetaDataInvalidated.unsubscribe(); + }; + }, [ + displaySetsLoadingState, + thumbnailImageSrcMap, + trackedSeries, + viewports, + dataSource, + displaySetService, + ]); + + const tabs = createStudyBrowserTabs(StudyInstanceUIDs, studyDisplayList, displaySets); + + // TODO: Should not fire this on "close" + function _handleStudyClick(StudyInstanceUID) { + const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(StudyInstanceUID); + const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy + ? [...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)] + : [...expandedStudyInstanceUIDs, StudyInstanceUID]; + + setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); + + if (!shouldCollapseStudy) { + const madeInClient = true; + requestDisplaySetCreationForStudy(displaySetService, StudyInstanceUID, madeInClient); + } + } + + useEffect(() => { + if (jumpToDisplaySet) { + // Get element by displaySetInstanceUID + const displaySetInstanceUID = jumpToDisplaySet; + const element = document.getElementById(`thumbnail-${displaySetInstanceUID}`); + + if (element && typeof element.scrollIntoView === 'function') { + // TODO: Any way to support IE here? + element.scrollIntoView({ behavior: 'smooth' }); + + setJumpToDisplaySet(null); + } + } + }, [jumpToDisplaySet, expandedStudyInstanceUIDs, activeTabName]); + + useEffect(() => { + if (!jumpToDisplaySet) { + return; + } + + const displaySetInstanceUID = jumpToDisplaySet; + // Set the activeTabName and expand the study + const thumbnailLocation = _findTabAndStudyOfDisplaySet(displaySetInstanceUID, tabs); + if (!thumbnailLocation) { + console.warn('jumpToThumbnail: displaySet thumbnail not found.'); + + return; + } + const { tabName, StudyInstanceUID } = thumbnailLocation; + setActiveTabName(tabName); + const studyExpanded = expandedStudyInstanceUIDs.includes(StudyInstanceUID); + if (!studyExpanded) { + const updatedExpandedStudyInstanceUIDs = [...expandedStudyInstanceUIDs, StudyInstanceUID]; + setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); + } + }, [expandedStudyInstanceUIDs, jumpToDisplaySet, tabs]); + + const onClickUntrack = displaySetInstanceUID => { + const onConfirm = () => { + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + sendTrackedMeasurementsEvent('UNTRACK_SERIES', { + SeriesInstanceUID: displaySet.SeriesInstanceUID, + }); + const measurements = measurementService.getMeasurements(); + measurements.forEach(m => { + if (m.referenceSeriesUID === displaySet.SeriesInstanceUID) { + measurementService.remove(m.uid); + } + }); + }; + + uiDialogService.create({ + id: 'untrack-series', + centralize: true, + isDraggable: false, + showOverlay: true, + content: Dialog, + contentProps: { + title: 'Untrack Series', + body: () => ( +
+

Are you sure you want to untrack this series?

+

+ This action cannot be undone and will delete all your existing measurements. +

+
+ ), + actions: [ + { + id: 'cancel', + text: 'Cancel', + type: ButtonEnums.type.secondary, + }, + { + id: 'yes', + text: 'Yes', + type: ButtonEnums.type.primary, + classes: ['untrack-yes-button'], + }, + ], + onClose: () => uiDialogService.dismiss({ id: 'untrack-series' }), + onSubmit: async ({ action }) => { + switch (action.id) { + case 'yes': + onConfirm(); + uiDialogService.dismiss({ id: 'untrack-series' }); + break; + case 'cancel': + uiDialogService.dismiss({ id: 'untrack-series' }); + break; + } + }, + }, + }); + }; + + return ( + <> + <> + + + + + { + setActiveTabName(clickedTabName); + }} + onClickUntrack={displaySetInstanceUID => { + onClickUntrack(displaySetInstanceUID); + }} + onClickThumbnail={() => {}} + onDoubleClickThumbnail={onDoubleClickThumbnailHandler} + activeDisplaySetInstanceUIDs={activeViewportDisplaySetInstanceUIDs} + showSettings={actionIcons.find(icon => icon.id === 'settings').value} + viewPresets={viewPresets} + ThumbnailMenuItems={MoreDropdownMenu({ + commandsManager, + servicesManager, + menuItemsKey: 'studyBrowser.thumbnailMenuItems', + })} + StudyMenuItems={MoreDropdownMenu({ + commandsManager, + servicesManager, + menuItemsKey: 'studyBrowser.studyMenuItems', + })} + /> + + ); +} + +PanelStudyBrowserTracking.propTypes = { + dataSource: PropTypes.shape({ + getImageIdsForDisplaySet: PropTypes.func.isRequired, + }).isRequired, + getImageSrc: PropTypes.func.isRequired, + getStudiesForPatientByMRN: PropTypes.func.isRequired, + requestDisplaySetCreationForStudy: PropTypes.func.isRequired, +}; + +function getImageIdForThumbnail(displaySet: any, imageIds: any) { + let imageId; + if (displaySet.isDynamicVolume) { + const timePoints = displaySet.dynamicVolumeInfo.timePoints; + const middleIndex = Math.floor(timePoints.length / 2); + const middleTimePointImageIds = timePoints[middleIndex]; + imageId = middleTimePointImageIds[Math.floor(middleTimePointImageIds.length / 2)]; + } else { + imageId = imageIds[Math.floor(imageIds.length / 2)]; + } + return imageId; +} + +/** + * Maps from the DataSource's format to a naturalized object + * + * @param {*} studies + */ +function _mapDataSourceStudies(studies) { + return studies.map(study => { + // TODO: Why does the data source return in this format? + return { + AccessionNumber: study.accession, + StudyDate: study.date, + StudyDescription: study.description, + NumInstances: study.instances, + ModalitiesInStudy: study.modalities, + PatientID: study.mrn, + PatientName: study.patientName, + StudyInstanceUID: study.studyInstanceUid, + StudyTime: study.time, + }; + }); +} + +function _mapDisplaySets( + displaySets, + displaySetLoadingState, + thumbnailImageSrcMap, + trackedSeriesInstanceUIDs, + viewports, // TODO: make array of `displaySetInstanceUIDs`? + viewportGridService, + dataSource, + displaySetService, + uiDialogService, + uiNotificationService +) { + const thumbnailDisplaySets = []; + const thumbnailNoImageDisplaySets = []; + displaySets + .filter(ds => !ds.excludeFromThumbnailBrowser) + .forEach(ds => { + const { thumbnailSrc, displaySetInstanceUID } = ds; // thumbnailImageSrcMap[ds.displaySetInstanceUID]; + const componentType = _getComponentType(ds); + + const array = + componentType === 'thumbnailTracked' ? thumbnailDisplaySets : thumbnailNoImageDisplaySets; + + const loadingProgress = displaySetLoadingState?.[displaySetInstanceUID]; + + const thumbnailProps = { + displaySetInstanceUID, + description: ds.SeriesDescription, + seriesNumber: ds.SeriesNumber, + modality: ds.Modality, + seriesDate: formatDate(ds.SeriesDate), + numInstances: ds.numImageFrames, + loadingProgress, + countIcon: ds.countIcon, + messages: ds.messages, + StudyInstanceUID: ds.StudyInstanceUID, + componentType, + imageSrc: thumbnailSrc || thumbnailImageSrcMap[displaySetInstanceUID], + dragData: { + type: 'displayset', + displaySetInstanceUID, + // .. Any other data to pass + }, + isTracked: trackedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID), + isHydratedForDerivedDisplaySet: ds.isHydrated, + }; + + if (componentType === 'thumbnailNoImage') { + if (dataSource.reject && dataSource.reject.series) { + thumbnailProps.canReject = !ds?.unsupported; + thumbnailProps.onReject = () => { + uiDialogService.create({ + id: 'ds-reject-sr', + centralize: true, + isDraggable: false, + showOverlay: true, + content: Dialog, + contentProps: { + title: 'Delete Report', + body: () => ( +
+

Are you sure you want to delete this report?

+

This action cannot be undone.

+
+ ), + actions: [ + { + id: 'cancel', + text: 'Cancel', + type: ButtonEnums.type.secondary, + }, + { + id: 'yes', + text: 'Yes', + type: ButtonEnums.type.primary, + classes: ['reject-yes-button'], + }, + ], + onClose: () => uiDialogService.dismiss({ id: 'ds-reject-sr' }), + onShow: () => { + const yesButton = document.querySelector('.reject-yes-button'); + + yesButton.focus(); + }, + onSubmit: async ({ action }) => { + switch (action.id) { + case 'yes': + try { + await dataSource.reject.series(ds.StudyInstanceUID, ds.SeriesInstanceUID); + displaySetService.deleteDisplaySet(displaySetInstanceUID); + uiDialogService.dismiss({ id: 'ds-reject-sr' }); + uiNotificationService.show({ + title: 'Delete Report', + message: 'Report deleted successfully', + type: 'success', + }); + } catch (error) { + uiDialogService.dismiss({ id: 'ds-reject-sr' }); + uiNotificationService.show({ + title: 'Delete Report', + message: 'Failed to delete report', + type: 'error', + }); + } + break; + case 'cancel': + uiDialogService.dismiss({ id: 'ds-reject-sr' }); + break; + } + }, + }, + }); + }; + } else { + thumbnailProps.canReject = false; + } + } + + array.push(thumbnailProps); + }); + + return [...thumbnailDisplaySets, ...thumbnailNoImageDisplaySets]; +} + +function _getComponentType(ds) { + if (thumbnailNoImageModalities.includes(ds.Modality) || ds?.unsupported) { + return 'thumbnailNoImage'; + } + + return 'thumbnailTracked'; +} + +function _findTabAndStudyOfDisplaySet(displaySetInstanceUID, tabs) { + for (let t = 0; t < tabs.length; t++) { + const { studies } = tabs[t]; + + for (let s = 0; s < studies.length; s++) { + const { displaySets } = studies[s]; + + for (let d = 0; d < displaySets.length; d++) { + const displaySet = displaySets[d]; + + if (displaySet.displaySetInstanceUID === displaySetInstanceUID) { + return { + tabName: tabs[t].name, + StudyInstanceUID: studies[s].studyInstanceUid, + }; + } + } + } + } +} diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/constants/actionIcons.ts b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/constants/actionIcons.ts new file mode 100644 index 0000000..26b1b62 --- /dev/null +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/constants/actionIcons.ts @@ -0,0 +1,11 @@ +import type { actionIcon } from '../PanelStudyBrowserTracking/types/actionsIcon'; + +const defaultActionIcons = [ + { + id: 'settings', + iconName: 'Settings', + value: false, + }, +] as actionIcon[]; + +export { defaultActionIcons }; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/constants/index.ts b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/constants/index.ts new file mode 100644 index 0000000..fb47a5e --- /dev/null +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/constants/index.ts @@ -0,0 +1,4 @@ +import { defaultActionIcons } from './actionIcons'; +import { defaultViewPresets } from './viewPresets'; + +export { defaultActionIcons, defaultViewPresets }; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/constants/viewPresets.ts b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/constants/viewPresets.ts new file mode 100644 index 0000000..4ecba95 --- /dev/null +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/constants/viewPresets.ts @@ -0,0 +1,16 @@ +import type { viewPreset } from '../PanelStudyBrowserTracking/types/viewPreset'; + +const defaultViewPresets = [ + { + id: 'list', + iconName: 'ListView', + selected: false, + }, + { + id: 'thumbnails', + iconName: 'ThumbnailView', + selected: true, + }, +] as viewPreset[]; + +export { defaultViewPresets }; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/getImageSrcFromImageId.js b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/getImageSrcFromImageId.js new file mode 100644 index 0000000..8b5f74d --- /dev/null +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/getImageSrcFromImageId.js @@ -0,0 +1,17 @@ +/** + * @param {*} cornerstone + * @param {*} imageId + */ +function getImageSrcFromImageId(cornerstone, imageId) { + return new Promise((resolve, reject) => { + const canvas = document.createElement('canvas'); + cornerstone.utilities + .loadImageToCanvas({ canvas, imageId, thumbnail: true }) + .then(imageId => { + resolve(canvas.toDataURL()); + }) + .catch(reject); + }); +} + +export default getImageSrcFromImageId; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.tsx new file mode 100644 index 0000000..962384a --- /dev/null +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.tsx @@ -0,0 +1,72 @@ +import React, { useCallback } from 'react'; +import PropTypes from 'prop-types'; +// +import PanelStudyBrowserTracking from './PanelStudyBrowserTracking'; +import getImageSrcFromImageId from './getImageSrcFromImageId'; +import { requestDisplaySetCreationForStudy } from '@ohif/extension-default'; +import { useSystem } from '@ohif/core'; + +function _getStudyForPatientUtility(extensionManager) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-default.utilityModule.common' + ); + + const { getStudiesForPatientByMRN } = utilityModule.exports; + return getStudiesForPatientByMRN; +} + +/** + * Wraps the PanelStudyBrowser and provides features afforded by managers/services + * + * @param {object} params + * @param {object} commandsManager + * @param {object} extensionManager + */ +function WrappedPanelStudyBrowserTracking() { + const { extensionManager } = useSystem(); + const dataSource = extensionManager.getActiveDataSource()[0]; + + const getStudiesForPatientByMRN = _getStudyForPatientUtility(extensionManager); + const _getStudiesForPatientByMRN = getStudiesForPatientByMRN.bind(null, dataSource); + const _getImageSrcFromImageId = useCallback( + _createGetImageSrcFromImageIdFn(extensionManager), + [] + ); + const _requestDisplaySetCreationForStudy = requestDisplaySetCreationForStudy.bind( + null, + dataSource + ); + + return ( + + ); +} + +/** + * Grabs cornerstone library reference using a dependent command from + * the @ohif/extension-cornerstone extension. Then creates a helper function + * that can take an imageId and return an image src. + * + * @param {func} getCommand - CommandManager's getCommand method + * @returns {func} getImageSrcFromImageId - A utility function powered by + * cornerstone + */ +function _createGetImageSrcFromImageIdFn(extensionManager) { + const utilities = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.common' + ); + + try { + const { cornerstone } = utilities.exports.getCornerstoneLibraries(); + return getImageSrcFromImageId.bind(null, cornerstone); + } catch (ex) { + throw new Error('Required command not found'); + } +} + +export default WrappedPanelStudyBrowserTracking; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/types/actionIcon.ts b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/types/actionIcon.ts new file mode 100644 index 0000000..0b522d5 --- /dev/null +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/types/actionIcon.ts @@ -0,0 +1,7 @@ +type actionIcon = { + id: string; + iconName: string; + value: boolean; +}; + +export type { actionIcon }; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/types/index.ts b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/types/index.ts new file mode 100644 index 0000000..d2b31d4 --- /dev/null +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/types/index.ts @@ -0,0 +1,4 @@ +import type { actionIcon } from './actionIcon'; +import type { viewPreset } from './viewPreset'; + +export type { actionIcon, viewPreset }; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/types/viewPreset.ts b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/types/viewPreset.ts new file mode 100644 index 0000000..ff7f5b4 --- /dev/null +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/types/viewPreset.ts @@ -0,0 +1,7 @@ +type viewPreset = { + id: string; + iconName: string; + selected: boolean; +}; + +export type { viewPreset }; diff --git a/extensions/measurement-tracking/src/panels/index.js b/extensions/measurement-tracking/src/panels/index.js new file mode 100644 index 0000000..4db70ae --- /dev/null +++ b/extensions/measurement-tracking/src/panels/index.js @@ -0,0 +1,4 @@ +import PanelStudyBrowserTracking from './PanelStudyBrowserTracking'; +import PanelMeasurementTableTracking from './PanelMeasurementTableTracking'; + +export { PanelMeasurementTableTracking, PanelStudyBrowserTracking }; diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx new file mode 100644 index 0000000..50d630c --- /dev/null +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx @@ -0,0 +1,345 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import PropTypes from 'prop-types'; + +import { ViewportActionArrows } from '@ohif/ui'; +import { useViewportGrid, Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next'; + +import { annotation } from '@cornerstonejs/tools'; +import { useTrackedMeasurements } from './../getContextModule'; +import { BaseVolumeViewport, Enums } from '@cornerstonejs/core'; +import { useTranslation } from 'react-i18next'; + +function TrackedCornerstoneViewport( + props: withAppTypes<{ viewportId: string; displaySets: AppTypes.DisplaySet[] }> +) { + const { displaySets, viewportId, servicesManager, extensionManager } = props; + + const { + measurementService, + cornerstoneViewportService, + viewportGridService, + viewportActionCornersService, + } = servicesManager.services; + + // Todo: handling more than one displaySet on the same viewport + const displaySet = displaySets[0]; + const { t } = useTranslation('Common'); + + const [viewportGrid] = useViewportGrid(); + const { activeViewportId } = viewportGrid; + + const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements(); + + const [isTracked, setIsTracked] = useState(false); + const [trackedMeasurementUID, setTrackedMeasurementUID] = useState(null); + const [viewportElem, setViewportElem] = useState(null); + + const { trackedSeries } = trackedMeasurements.context; + + const { SeriesInstanceUID } = displaySet; + + const updateIsTracked = useCallback(() => { + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + + if (viewport instanceof BaseVolumeViewport) { + // A current image id will only exist for volume viewports that can have measurements tracked. + // Typically these are those volume viewports for the series of acquisition. + const currentImageId = viewport?.getCurrentImageId(); + + if (!currentImageId) { + if (isTracked) { + setIsTracked(false); + } + return; + } + } + + if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) { + setIsTracked(!isTracked); + } + }, [isTracked, trackedMeasurements, viewportId, SeriesInstanceUID]); + + const onElementEnabled = useCallback( + evt => { + if (evt.detail.element !== viewportElem) { + // The VOLUME_VIEWPORT_NEW_VOLUME event allows updateIsTracked to reliably fetch the image id for a volume viewport. + evt.detail.element?.addEventListener( + Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, + updateIsTracked + ); + setViewportElem(evt.detail.element); + } + }, + [updateIsTracked, viewportElem] + ); + + const onElementDisabled = useCallback(() => { + viewportElem?.removeEventListener(Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME, updateIsTracked); + }, [updateIsTracked, viewportElem]); + + useEffect(updateIsTracked, [updateIsTracked]); + + useEffect(() => { + const { unsubscribe } = cornerstoneViewportService.subscribe( + cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED, + props => { + if (props.viewportId !== viewportId) { + return; + } + + updateIsTracked(); + } + ); + + return () => { + unsubscribe(); + }; + }, [updateIsTracked, viewportId]); + + useEffect(() => { + if (isTracked) { + annotation.config.style.setViewportToolStyles(viewportId, { + ReferenceLines: { + lineDash: '4,4', + }, + global: { + lineDash: '', + }, + }); + + cornerstoneViewportService.getRenderingEngine().renderViewport(viewportId); + + return; + } + + annotation.config.style.setViewportToolStyles(viewportId, { + global: { + lineDash: '4,4', + }, + }); + + cornerstoneViewportService.getRenderingEngine().renderViewport(viewportId); + + return () => { + annotation.config.style.setViewportToolStyles(viewportId, {}); + }; + }, [isTracked]); + + /** + * The effect for listening to measurement service measurement added events + * and in turn firing an event to update the measurement tracking state machine. + * The TrackedCornerstoneViewport is the best place for this because when + * a measurement is added, at least one TrackedCornerstoneViewport will be in + * the DOM and thus can react to the events fired. + */ + useEffect(() => { + const added = measurementService.EVENTS.MEASUREMENT_ADDED; + const addedRaw = measurementService.EVENTS.RAW_MEASUREMENT_ADDED; + const subscriptions = []; + + [added, addedRaw].forEach(evt => { + subscriptions.push( + measurementService.subscribe(evt, ({ source, measurement }) => { + const { activeViewportId } = viewportGridService.getState(); + + // Each TrackedCornerstoneViewport receives the MeasurementService's events. + // Only send the tracked measurements event for the active viewport to avoid + // sending it more than once. + if (viewportId === activeViewportId) { + const { + referenceStudyUID: StudyInstanceUID, + referenceSeriesUID: SeriesInstanceUID, + uid: measurementId, + toolName, + } = measurement; + + sendTrackedMeasurementsEvent('SET_DIRTY', { SeriesInstanceUID }); + sendTrackedMeasurementsEvent('TRACK_SERIES', { + viewportId, + StudyInstanceUID, + SeriesInstanceUID, + measurementId, + toolName, + }); + } + }).unsubscribe + ); + }); + + return () => { + subscriptions.forEach(unsub => { + unsub(); + }); + }; + }, [measurementService, sendTrackedMeasurementsEvent, viewportId, viewportGridService]); + + const switchMeasurement = useCallback( + direction => { + const newTrackedMeasurementUID = _getNextMeasurementUID( + direction, + servicesManager, + trackedMeasurementUID, + trackedMeasurements + ); + + if (!newTrackedMeasurementUID) { + return; + } + + setTrackedMeasurementUID(newTrackedMeasurementUID); + + measurementService.jumpToMeasurement(viewportId, newTrackedMeasurementUID); + }, + [measurementService, servicesManager, trackedMeasurementUID, trackedMeasurements, viewportId] + ); + + useEffect(() => { + const statusComponent = _getStatusComponent(isTracked, t); + const arrowsComponent = _getArrowsComponent( + isTracked, + switchMeasurement, + viewportId === activeViewportId + ); + + viewportActionCornersService.addComponents([ + { + viewportId, + id: 'viewportStatusComponent', + component: statusComponent, + indexPriority: -100, + location: viewportActionCornersService.LOCATIONS.topLeft, + }, + { + viewportId, + id: 'viewportActionArrowsComponent', + component: arrowsComponent, + indexPriority: 0, + location: viewportActionCornersService.LOCATIONS.topRight, + }, + ]); + }, [activeViewportId, isTracked, switchMeasurement, viewportActionCornersService, viewportId]); + + const getCornerstoneViewport = () => { + const { component: Component } = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.viewportModule.cornerstone' + ); + + return ( + { + props.onElementEnabled?.(evt); + onElementEnabled(evt); + }} + onElementDisabled={onElementDisabled} + /> + ); + }; + + return ( +
+ {getCornerstoneViewport()} +
+ ); +} + +TrackedCornerstoneViewport.propTypes = { + displaySets: PropTypes.arrayOf(PropTypes.object.isRequired).isRequired, + viewportId: PropTypes.string.isRequired, + dataSource: PropTypes.object, + children: PropTypes.node, +}; + +function _getNextMeasurementUID( + direction, + servicesManager: AppTypes.ServicesManager, + trackedMeasurementId, + trackedMeasurements +) { + const { measurementService, viewportGridService } = servicesManager.services; + const measurements = measurementService.getMeasurements(); + + const { activeViewportId, viewports } = viewportGridService.getState(); + const { displaySetInstanceUIDs: activeViewportDisplaySetInstanceUIDs } = + viewports.get(activeViewportId); + + const { trackedSeries } = trackedMeasurements.context; + + // Get the potentially trackable measurements for the series of the + // active viewport. + // The measurements to jump between are the same + // regardless if this series is tracked or not. + + const filteredMeasurements = measurements.filter( + m => + trackedSeries.includes(m.referenceSeriesUID) && + activeViewportDisplaySetInstanceUIDs.includes(m.displaySetInstanceUID) + ); + + if (!filteredMeasurements.length) { + // No measurements on this series. + return; + } + + const measurementCount = filteredMeasurements.length; + + const uids = filteredMeasurements.map(fm => fm.uid); + let measurementIndex = uids.findIndex(uid => uid === trackedMeasurementId); + + if (measurementIndex === -1) { + // Not tracking a measurement, or previous measurement now deleted, revert to 0. + measurementIndex = 0; + } else { + measurementIndex += direction; + if (measurementIndex < 0) { + measurementIndex = measurementCount - 1; + } else if (measurementIndex === measurementCount) { + measurementIndex = 0; + } + } + + const newTrackedMeasurementId = uids[measurementIndex]; + + return newTrackedMeasurementId; +} + +const _getArrowsComponent = (isTracked, switchMeasurement, isActiveViewport) => { + if (!isTracked) { + return null; + } + + return ( + switchMeasurement(direction)} + className={isActiveViewport ? 'visible' : 'invisible group-hover/pane:visible'} + /> + ); +}; + +function _getStatusComponent(isTracked, t) { + if (!isTracked) { + return null; + } + + return ( + + + + + + + + {isTracked ? ( + <>{t('Series is tracked and can be viewed in the measurement panel')} + ) : ( + <>{t('Measurements for untracked series will not be shown in the measurements panel')} + )} + + + ); +} + +export default TrackedCornerstoneViewport; diff --git a/extensions/test-extension/.webpack/webpack.dev.js b/extensions/test-extension/.webpack/webpack.dev.js new file mode 100644 index 0000000..6aea859 --- /dev/null +++ b/extensions/test-extension/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/extensions/test-extension/.webpack/webpack.prod.js b/extensions/test-extension/.webpack/webpack.prod.js new file mode 100644 index 0000000..1210b05 --- /dev/null +++ b/extensions/test-extension/.webpack/webpack.prod.js @@ -0,0 +1,48 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const pkg = require('./../package.json'); + +const ROOT_DIR = path.join(__dirname, './..'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: false, + }, + output: { + path: ROOT_DIR, + library: 'ohif-extension-test', + libraryTarget: 'umd', + libraryExport: 'default', + filename: pkg.main, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + ], + }); +}; diff --git a/extensions/test-extension/CHANGELOG.md b/extensions/test-extension/CHANGELOG.md new file mode 100644 index 0000000..cacffb8 --- /dev/null +++ b/extensions/test-extension/CHANGELOG.md @@ -0,0 +1,3014 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + + +### Bug Fixes + +* **dependencies:** Update dcmjs library and improve documentation links ([#4741](https://github.com/OHIF/Viewers/issues/4741)) ([d554f02](https://github.com/OHIF/Viewers/commit/d554f02f7cdb876e4132fb94e3b3df8d11b7bb5c)) + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + + +### Features + +* **customization:** new customization service api ([#4688](https://github.com/OHIF/Viewers/issues/4688)) ([55ad8ef](https://github.com/OHIF/Viewers/commit/55ad8efbabc3fabd8031fc08927b2f92ae5aec69)) + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + + +### Bug Fixes + +* Inconsistencies and update the style setting on load for embedded styles from codingValues ([#4599](https://github.com/OHIF/Viewers/issues/4599)) ([e0088ec](https://github.com/OHIF/Viewers/commit/e0088ec91807fa6a8e11e1e6942f51cedd080cc9)) + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + + +### Bug Fixes + +* **seg:** jump to the first slice in SEG and RT that has data ([#4605](https://github.com/OHIF/Viewers/issues/4605)) ([9bf24d6](https://github.com/OHIF/Viewers/commit/9bf24d6dc58ed8f65c90899a17c11044b792cf40)) + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + + +### Features + +* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2)) + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + + +### Bug Fixes + +* **sr:** dcm4chee requires the patient name for an SR to match what is in the original study ([#3739](https://github.com/OHIF/Viewers/issues/3739)) ([d98439f](https://github.com/OHIF/Viewers/commit/d98439fe7f3825076dbc87b664a1d1480ff414d3)) + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + + +### Features + +* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd)) + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + +**Note:** Version bump only for package @ohif/extension-test + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + +**Note:** Version bump only for package @ohif/extension-test diff --git a/extensions/test-extension/LICENSE b/extensions/test-extension/LICENSE new file mode 100644 index 0000000..19e20dd --- /dev/null +++ b/extensions/test-extension/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/test-extension/README.md b/extensions/test-extension/README.md new file mode 100644 index 0000000..985985f --- /dev/null +++ b/extensions/test-extension/README.md @@ -0,0 +1,4 @@ +# Test Extension +This extension will provide uitlities and module to help with testing. It is not intended to be used in production. +For instance, we can use the hanging protocol module to inject a hanging protocol into the core and later +use it to test the hanging protocol detection mechanism. diff --git a/extensions/test-extension/babel.config.js b/extensions/test-extension/babel.config.js new file mode 100644 index 0000000..325ca2a --- /dev/null +++ b/extensions/test-extension/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/extensions/test-extension/package.json b/extensions/test-extension/package.json new file mode 100644 index 0000000..960a8b4 --- /dev/null +++ b/extensions/test-extension/package.json @@ -0,0 +1,45 @@ +{ + "name": "@ohif/extension-test", + "version": "3.10.0-beta.111", + "description": "OHIF extension used inside e2e testing", + "author": "OHIF", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/ohif-extension-test.umd.js", + "module": "src/index.tsx", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "files": [ + "dist", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package-1": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.111", + "@ohif/ui": "3.10.0-beta.111", + "dcmjs": "0.38.0", + "dicom-parser": "^1.8.9", + "hammerjs": "^2.0.8", + "prop-types": "^15.6.2", + "react": "^18.3.1" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "classnames": "^2.3.2" + } +} diff --git a/extensions/test-extension/src/custom-attribute/maxNumImageFrames.ts b/extensions/test-extension/src/custom-attribute/maxNumImageFrames.ts new file mode 100644 index 0000000..f59aaf2 --- /dev/null +++ b/extensions/test-extension/src/custom-attribute/maxNumImageFrames.ts @@ -0,0 +1,2 @@ +export default (study, extraData) => + Math.max(...(extraData?.displaySets?.map?.(ds => ds.numImageFrames ?? 0) || [0])); diff --git a/extensions/test-extension/src/custom-attribute/numberOfDisplaySets.ts b/extensions/test-extension/src/custom-attribute/numberOfDisplaySets.ts new file mode 100644 index 0000000..d4a8ae4 --- /dev/null +++ b/extensions/test-extension/src/custom-attribute/numberOfDisplaySets.ts @@ -0,0 +1 @@ +export default (study, extraData) => extraData?.displaySets?.length; diff --git a/extensions/test-extension/src/custom-attribute/sameAs.ts b/extensions/test-extension/src/custom-attribute/sameAs.ts new file mode 100644 index 0000000..8e0cb98 --- /dev/null +++ b/extensions/test-extension/src/custom-attribute/sameAs.ts @@ -0,0 +1,33 @@ +/** + * This function extracts an attribute from the already matched display sets, and + * compares it to the attribute in the current display set, and indicates if they match. + * From 'this', it uses: + * `sameAttribute` as the attribute name to look for + * `sameDisplaySetId` as the display set id to look for + * From `options`, it looks for + */ +export default function (displaySet, options) { + const { sameAttribute, sameDisplaySetId } = this; + if (!sameAttribute) { + console.log('sameAttribute not defined in', this); + return `sameAttribute not defined in ${this.id}`; + } + if (!sameDisplaySetId) { + console.log('sameDisplaySetId not defined in', this); + return `sameDisplaySetId not defined in ${this.id}`; + } + const { displaySetMatchDetails, displaySets } = options; + const match = displaySetMatchDetails.get(sameDisplaySetId); + if (!match) { + console.log('No match for display set', sameDisplaySetId); + return false; + } + const { displaySetInstanceUID } = match; + const altDisplaySet = displaySets.find(it => it.displaySetInstanceUID == displaySetInstanceUID); + if (!altDisplaySet) { + console.log('No display set found with', displaySetInstanceUID, 'in', displaySets); + return false; + } + const testValue = altDisplaySet[sameAttribute]; + return testValue === displaySet[sameAttribute]; +} diff --git a/extensions/test-extension/src/custom-context-menu/codingValues.ts b/extensions/test-extension/src/custom-context-menu/codingValues.ts new file mode 100644 index 0000000..e3e90dd --- /dev/null +++ b/extensions/test-extension/src/custom-context-menu/codingValues.ts @@ -0,0 +1,94 @@ +/** + * Coding values is a map of simple string coding values to a set of + * attributes associated with the coding value. + * + * The simple string is in the format `:` + * That allows extracting the DICOM attributes from the designator/value, and + * allows for passing around the simple string. + * The additional attributes contained in the object include: + * * text - this is the coding scheme text display value, and may be language specific + * * type - this defines a named type, typically 'site'. Different names can be used + * to allow setting different findingSites values in order to define a hierarchy. + * * color - used to apply annotation color + * It is also possible to define additional attributes here, used by custom + * extensions. + * + * See https://dicom.nema.org/medical/dicom/current/output/html/part16.html + * for definitions of SCT and other code values. + */ +export default { + codingValues: { + // Sites + 'SCT:69536005': { + text: 'Head', + type: 'site', + style: { + color: 'red', + }, + }, + 'SCT:45048000': { + text: 'Neck', + type: 'site', + style: { + color: 'blue', + }, + }, + 'SCT:818981001': { + text: 'Abdomen', + type: 'site', + style: { + color: 'orange', + }, + }, + 'SCT:816092008': { + text: 'Pelvis', + type: 'site', + style: { + color: 'cyan', + }, + }, + + // Findings + 'SCT:371861004': { + text: 'Mild intimal coronary irregularities', + style: { + color: 'green', + }, + }, + 'SCT:194983005': { + text: 'Aortic insufficiency', + style: { + color: 'darkred', + }, + }, + 'SCT:399232001': { + text: '2-chamber', + }, + 'SCT:103340004': { + text: 'SAX', + }, + 'SCT:91134007': { + text: 'MV', + }, + 'SCT:122972007': { + text: 'PV', + }, + + // Orientations + 'SCT:24422004': { + text: 'Axial', + color: '#000000', + type: 'orientation', + }, + 'SCT:81654009': { + text: 'Coronal', + color: '#000000', + type: 'orientation', + }, + 'SCT:30730003': { + text: 'Sagittal', + color: '#000000', + type: 'orientation', + }, + }, +}; diff --git a/extensions/test-extension/src/custom-context-menu/contextMenuCodeItem.ts b/extensions/test-extension/src/custom-context-menu/contextMenuCodeItem.ts new file mode 100644 index 0000000..63bb149 --- /dev/null +++ b/extensions/test-extension/src/custom-context-menu/contextMenuCodeItem.ts @@ -0,0 +1,24 @@ +export default { + '@ohif/contextMenuAnnotationCode': { + /** Applies the code value setup for this item */ + $transform: function (customizationService) { + const { code: codeRef } = this; + if (!codeRef) { + throw new Error(`item ${this} has no code ref`); + } + const codingValues = customizationService.getCustomization('codingValues'); + const code = codingValues[codeRef]; + return { + ...this, + codeRef, + code: { ref: codeRef, ...code }, + label: this.label || code.text || codeRef, + commands: [ + { + commandName: 'updateMeasurement', + }, + ], + }; + }, + }, +}; diff --git a/extensions/test-extension/src/custom-context-menu/findingsContextMenu.ts b/extensions/test-extension/src/custom-context-menu/findingsContextMenu.ts new file mode 100644 index 0000000..028244b --- /dev/null +++ b/extensions/test-extension/src/custom-context-menu/findingsContextMenu.ts @@ -0,0 +1,98 @@ +export default { + measurementsContextMenu: { + $set: { + inheritsFrom: 'ohif.contextMenu', + menus: [ + { + // selector restricts context menu to when there is nearbyToolData + selector: ({ nearbyToolData }) => !!nearbyToolData, + items: [ + { + label: 'Site', + actionType: 'ShowSubMenu', + subMenu: 'siteSelectionSubMenu', + }, + { + label: 'Finding', + actionType: 'ShowSubMenu', + subMenu: 'findingSelectionSubMenu', + }, + { + // inheritsFrom is implicit here in the configuration setup + label: 'Delete Measurement', + commands: [ + { + commandName: 'deleteMeasurement', + }, + ], + }, + { + label: 'Add Label', + commands: [ + { + commandName: 'setMeasurementLabel', + }, + ], + }, + + // The example below shows how to include a delegating sub-menu, + // Only available on the @ohif/mnGrid hanging protocol + // To demonstrate, select the 3x1 layout from the protocol menu + // and right click on a measurement. + { + label: 'IncludeSubMenu', + selector: ({ protocol }) => protocol?.id === '@ohif/mnGrid', + delegating: true, + subMenu: 'orientationSelectionSubMenu', + }, + ], + }, + + { + id: 'orientationSelectionSubMenu', + selector: ({ nearbyToolData }) => !!nearbyToolData, + items: [ + { + inheritsFrom: '@ohif/contextMenuAnnotationCode', + code: 'SCT:24422004', + }, + { + inheritsFrom: '@ohif/contextMenuAnnotationCode', + code: 'SCT:81654009', + }, + ], + }, + + { + id: 'findingSelectionSubMenu', + selector: ({ nearbyToolData }) => !!nearbyToolData, + items: [ + { + inheritsFrom: '@ohif/contextMenuAnnotationCode', + code: 'SCT:371861004', + }, + { + inheritsFrom: '@ohif/contextMenuAnnotationCode', + code: 'SCT:194983005', + }, + ], + }, + + { + id: 'siteSelectionSubMenu', + selector: ({ nearbyToolData }) => !!nearbyToolData, + items: [ + { + inheritsFrom: '@ohif/contextMenuAnnotationCode', + code: 'SCT:69536005', + }, + { + inheritsFrom: '@ohif/contextMenuAnnotationCode', + code: 'SCT:45048000', + }, + ], + }, + ], + }, + }, +}; diff --git a/extensions/test-extension/src/custom-context-menu/index.ts b/extensions/test-extension/src/custom-context-menu/index.ts new file mode 100644 index 0000000..800e31f --- /dev/null +++ b/extensions/test-extension/src/custom-context-menu/index.ts @@ -0,0 +1,5 @@ +import codingValues from './codingValues'; +import contextMenuCodeItem from './contextMenuCodeItem'; +import findingsContextMenu from './findingsContextMenu'; + +export { codingValues, contextMenuCodeItem, findingsContextMenu }; diff --git a/extensions/test-extension/src/getCustomizationModule.ts b/extensions/test-extension/src/getCustomizationModule.ts new file mode 100644 index 0000000..f58360d --- /dev/null +++ b/extensions/test-extension/src/getCustomizationModule.ts @@ -0,0 +1,20 @@ +import { codingValues, contextMenuCodeItem, findingsContextMenu } from './custom-context-menu'; + +export default function getCustomizationModule() { + return [ + { + name: 'custom-context-menu', + value: { + ...codingValues, + ...contextMenuCodeItem, + ...findingsContextMenu, + }, + }, + { + name: 'contextMenuCodeItem', + value: { + ...contextMenuCodeItem, + }, + }, + ]; +} diff --git a/extensions/test-extension/src/hp/index.ts b/extensions/test-extension/src/hp/index.ts new file mode 100644 index 0000000..a1b0c22 --- /dev/null +++ b/extensions/test-extension/src/hp/index.ts @@ -0,0 +1,17 @@ +import hpMN from './hpMN'; + +const hangingProtocols = [ + { + name: '@ohif/hp-extension.mn', + protocol: hpMN, + }, +]; + +/** + * Registers a single study hanging protocol which can be referenced as + * `@ohif/hp-exgtension.mn`, that has initial layouts which show images + * only display sets, up to a 2x2 view. + */ +export default function getHangingProtocolModule() { + return hangingProtocols; +} diff --git a/extensions/test-extension/src/hpTestSwitch.ts b/extensions/test-extension/src/hpTestSwitch.ts new file mode 100644 index 0000000..4e497a6 --- /dev/null +++ b/extensions/test-extension/src/hpTestSwitch.ts @@ -0,0 +1,239 @@ +import { Types } from '@ohif/core'; + +const viewport0a = { + viewportOptions: { + viewportId: 'viewportA', + toolGroupId: 'default', + allowUnmatchedView: true, + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + }, + ], +}; + +const viewport1b = { + viewportOptions: { + viewportId: 'viewportB', + toolGroupId: 'default', + allowUnmatchedView: true, + }, + displaySets: [ + { + matchedDisplaySetsIndex: 1, + id: 'defaultDisplaySetId', + }, + ], +}; + +const viewport2c = { + viewportOptions: { + viewportId: 'viewportC', + toolGroupId: 'default', + allowUnmatchedView: true, + }, + displaySets: [ + { + matchedDisplaySetsIndex: 2, + id: 'defaultDisplaySetId', + }, + ], +}; + +const viewport3d = { + viewportOptions: { + viewportId: 'viewportD', + toolGroupId: 'default', + allowUnmatchedView: true, + }, + displaySets: [ + { + matchedDisplaySetsIndex: 3, + id: 'defaultDisplaySetId', + }, + ], +}; + +const viewport4e = { + viewportOptions: { + viewportId: 'viewportE', + toolGroupId: 'default', + allowUnmatchedView: true, + }, + displaySets: [ + { + matchedDisplaySetsIndex: 4, + id: 'defaultDisplaySetId', + }, + ], +}; + +const viewport5f = { + viewportOptions: { + viewportId: 'viewportF', + toolGroupId: 'default', + allowUnmatchedView: true, + }, + displaySets: [ + { + matchedDisplaySetsIndex: 5, + id: 'defaultDisplaySetId', + }, + ], +}; + +const viewport3a = { + viewportOptions: { + viewportId: 'viewportA', + toolGroupId: 'default', + allowUnmatchedView: true, + }, + displaySets: [ + { + matchedDisplaySetsIndex: 3, + id: 'defaultDisplaySetId', + }, + ], +}; + +const viewport2b = { + viewportOptions: { + viewportId: 'viewportB', + toolGroupId: 'default', + allowUnmatchedView: true, + }, + displaySets: [ + { + matchedDisplaySetsIndex: 2, + id: 'defaultDisplaySetId', + }, + ], +}; + +const viewport1c = { + viewportOptions: { + viewportId: 'viewportC', + toolGroupId: 'default', + allowUnmatchedView: true, + }, + displaySets: [ + { + matchedDisplaySetsIndex: 1, + id: 'defaultDisplaySetId', + }, + ], +}; +const viewport0d = { + viewportOptions: { + viewportId: 'viewportD', + toolGroupId: 'default', + allowUnmatchedView: true, + }, + displaySets: [ + { + matchedDisplaySetsIndex: 0, + id: 'defaultDisplaySetId', + }, + ], +}; + +const viewportStructure = { + layoutType: 'grid', + properties: { + rows: 2, + columns: 2, + }, +}; + +const viewportStructure32 = { + layoutType: 'grid', + properties: { + rows: 2, + columns: 3, + }, +}; + +/** + * This hanging protocol is a test hanging protocol used to apply various + * layouts in different positions for display, re-using earlier names in + * various orders. + */ +const hpTestSwitch: Types.HangingProtocol.Protocol = { + hasUpdatedPriorsInformation: false, + id: '@ohif/mnTestSwitch', + description: 'Has various hanging protocol grid layouts', + name: 'Test Switch', + protocolMatchingRules: [ + { + id: 'OneOrMoreSeries', + weight: 25, + attribute: 'numberOfDisplaySetsWithImages', + constraint: { + greaterThan: 0, + }, + }, + ], + toolGroupIds: ['default'], + displaySetSelectors: { + defaultDisplaySetId: { + seriesMatchingRules: [ + { + attribute: 'numImageFrames', + constraint: { + greaterThan: { value: 0 }, + }, + }, + // This display set will select the specified items by preference + // It has no affect if nothing is specified in the URL. + { + attribute: 'isDisplaySetFromUrl', + weight: 20, + constraint: { + equals: true, + }, + }, + ], + }, + }, + defaultViewport: { + viewportOptions: { + viewportType: 'stack', + toolGroupId: 'default', + allowUnmatchedView: true, + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + matchedDisplaySetsIndex: -1, + }, + ], + }, + stages: [ + { + name: '2x2 0a1b2c3d', + viewportStructure, + viewports: [viewport0a, viewport1b, viewport2c, viewport3d], + }, + { + name: '3x2 0a1b4e2c3d5f', + viewportStructure: viewportStructure32, + // Note the following structure simply preserves the viewportId for + // a given screen position + viewports: [viewport0a, viewport1b, viewport4e, viewport2c, viewport3d, viewport5f], + }, + { + name: '2x2 1c0d3a2b', + viewportStructure, + viewports: [viewport1c, viewport0d, viewport3a, viewport2b], + }, + { + name: '2x2 3a2b1c0d', + viewportStructure, + viewports: [viewport3a, viewport2b, viewport1c, viewport0d], + }, + ], + numberOfPriorsReferenced: -1, +}; + +export default hpTestSwitch; diff --git a/extensions/test-extension/src/id.js b/extensions/test-extension/src/id.js new file mode 100644 index 0000000..ebe5acd --- /dev/null +++ b/extensions/test-extension/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/extensions/test-extension/src/index.tsx b/extensions/test-extension/src/index.tsx new file mode 100644 index 0000000..2751312 --- /dev/null +++ b/extensions/test-extension/src/index.tsx @@ -0,0 +1,64 @@ +import { Types } from '@ohif/core'; + +import { id } from './id'; + +import hpTestSwitch from './hpTestSwitch'; + +import getCustomizationModule from './getCustomizationModule'; +import sameAs from './custom-attribute/sameAs'; +import numberOfDisplaySets from './custom-attribute/numberOfDisplaySets'; +import maxNumImageFrames from './custom-attribute/maxNumImageFrames'; + +/** + * The test extension provides additional behavior for testing various + * customizations and settings for OHIF. + */ +const testExtension: Types.Extensions.Extension = { + /** + * Only required property. Should be a unique value across all extensions. + */ + id, + + /** + * Register additional behavior: + * * HP custom attribute seriesDescriptions to retrieve an array of all series descriptions + * * HP custom attribute numberOfDisplaySets to retrieve the number of display sets + * * HP custom attribute numberOfDisplaySetsWithImages to retrieve the number of display sets containing images + * * HP custom attribute to return a boolean true, when the attribute sameAttribute has the same + * value as another series description in an already matched display set selector named with the value + * in `sameDisplaySetId` + */ + preRegistration: ({ servicesManager }: Types.Extensions.ExtensionParams) => { + const { hangingProtocolService } = servicesManager.services; + hangingProtocolService.addCustomAttribute( + 'numberOfDisplaySets', + 'Number of displays sets', + numberOfDisplaySets + ); + hangingProtocolService.addCustomAttribute( + 'maxNumImageFrames', + 'Maximum of number of image frames', + maxNumImageFrames + ); + hangingProtocolService.addCustomAttribute( + 'sameAs', + 'Match an attribute in an existing display set', + sameAs + ); + }, + + /** Registers some customizations */ + getCustomizationModule, + + getHangingProtocolModule: () => { + return [ + // Create a MxN hanging protocol available by default + { + name: hpTestSwitch.id, + protocol: hpTestSwitch, + }, + ]; + }, +}; + +export default testExtension; diff --git a/extensions/tmtv/.webpack/webpack.dev.js b/extensions/tmtv/.webpack/webpack.dev.js new file mode 100644 index 0000000..18f5f72 --- /dev/null +++ b/extensions/tmtv/.webpack/webpack.dev.js @@ -0,0 +1,8 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/extensions/tmtv/.webpack/webpack.prod.js b/extensions/tmtv/.webpack/webpack.prod.js new file mode 100644 index 0000000..bc65614 --- /dev/null +++ b/extensions/tmtv/.webpack/webpack.prod.js @@ -0,0 +1,54 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const pkg = require('./../package.json'); + +const ROOT_DIR = path.join(__dirname, './..'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +const outputName = `ohif-${pkg.name.split('/').pop()}`; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: false, + }, + output: { + path: ROOT_DIR, + library: 'ohif-extension-tmtv', + libraryTarget: 'umd', + filename: pkg.main, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + new MiniCssExtractPlugin({ + filename: `./dist/${outputName}.css`, + chunkFilename: `./dist/${outputName}.css`, + }), + ], + }); +}; diff --git a/extensions/tmtv/CHANGELOG.md b/extensions/tmtv/CHANGELOG.md new file mode 100644 index 0000000..31f0916 --- /dev/null +++ b/extensions/tmtv/CHANGELOG.md @@ -0,0 +1,3069 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + + +### Features + +* **customization:** new customization service api ([#4688](https://github.com/OHIF/Viewers/issues/4688)) ([55ad8ef](https://github.com/OHIF/Viewers/commit/55ad8efbabc3fabd8031fc08927b2f92ae5aec69)) + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + + +### Bug Fixes + +* Inconsistent Handling of Patient Name Tag ([#4703](https://github.com/OHIF/Viewers/issues/4703)) ([8aedb2e](https://github.com/OHIF/Viewers/commit/8aedb2ec54a0ccf2550f745fed6f0b8aa184a860)) + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + + +### Features + +* **new-study-panel:** default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser ([#4417](https://github.com/OHIF/Viewers/issues/4417)) ([a7fd9fa](https://github.com/OHIF/Viewers/commit/a7fd9fa5bfff7a1b533d99cb96f7147a35fd528f)) + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + + +### Bug Fixes + +* **tmtv:** prevent fusion row in tmtv from getting inverted unexpectedly ([#4420](https://github.com/OHIF/Viewers/issues/4420)) ([33af9bb](https://github.com/OHIF/Viewers/commit/33af9bb021ff3a6c3b683d4df2c730413400ff8a)) + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + + +### Features + +* **hangingProtocols:** added selection of the HangingProtocol stage from the url ([#4310](https://github.com/OHIF/Viewers/issues/4310)) ([fa2435d](https://github.com/OHIF/Viewers/commit/fa2435d5e94e5f903404ca94687b086f90f8d1f8)) +* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3)) + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + + +### Features + +* **studies-panel:** New OHIF study panel - under experimental flag ([#4254](https://github.com/OHIF/Viewers/issues/4254)) ([7a96406](https://github.com/OHIF/Viewers/commit/7a96406a116e46e62c396855fa64f434e2984b58)) + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + + +### Bug Fixes + +* the start/end command in TMTV for the ROIStartEndThreshold tools ([#4281](https://github.com/OHIF/Viewers/issues/4281)) ([38c19fa](https://github.com/OHIF/Viewers/commit/38c19fab77cdb21d14bdae35813d73f43012cbd7)) + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + + +### Bug Fixes + +* stdValue in TMTV mode ([#4278](https://github.com/OHIF/Viewers/issues/4278)) ([b2c6291](https://github.com/OHIF/Viewers/commit/b2c629123c5cf05afbeb19bd1424c327c1f5a606)) + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + + +### Bug Fixes + +* **tmtv:** crosshairs should not have viewport indicators ([#4197](https://github.com/OHIF/Viewers/issues/4197)) ([f85da32](https://github.com/OHIF/Viewers/commit/f85da32f34389ef7cecae03c07e0af26468b52a6)) + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + + +### Bug Fixes + +* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951)) + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + + +### Features + +* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb)) + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes - more ([#4043](https://github.com/OHIF/Viewers/issues/4043)) ([3754c22](https://github.com/OHIF/Viewers/commit/3754c224b4dab28182adb0a41e37d890942144d8)) + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + + +### Bug Fixes + +* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025)) + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637)) + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + + +### Features + +* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13)) + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + + +### Features + +* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2)) + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + + +### Features + +* **segmentation:** Enhanced segmentation panel design for TMTV ([#3988](https://github.com/OHIF/Viewers/issues/3988)) ([9f3235f](https://github.com/OHIF/Viewers/commit/9f3235ff096636aafa88d8a42859e8dc85d9036d)) + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + + +### Features + +* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1)) + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + + +### Bug Fixes + +* **sr:** dcm4chee requires the patient name for an SR to match what is in the original study ([#3739](https://github.com/OHIF/Viewers/issues/3739)) ([d98439f](https://github.com/OHIF/Viewers/commit/d98439fe7f3825076dbc87b664a1d1480ff414d3)) + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + + +### Bug Fixes + +* **export:** wrong export for the tmtv RT function ([#3715](https://github.com/OHIF/Viewers/issues/3715)) ([a3f2a1a](https://github.com/OHIF/Viewers/commit/a3f2a1a7b0d16bfcc0ecddc2ab731e54c5e377c8)) + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + + +### Features + +* **Segmentation:** download RTSS from Labelmap([#3692](https://github.com/OHIF/Viewers/issues/3692)) ([40673f6](https://github.com/OHIF/Viewers/commit/40673f64b36b1150149c55632aa1825178a39e65)) + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + + +### Features + +* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd)) + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + +**Note:** Version bump only for package @ohif/extension-tmtv + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + +**Note:** Version bump only for package @ohif/extension-tmtv diff --git a/extensions/tmtv/LICENSE b/extensions/tmtv/LICENSE new file mode 100644 index 0000000..19e20dd --- /dev/null +++ b/extensions/tmtv/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/tmtv/README.md b/extensions/tmtv/README.md new file mode 100644 index 0000000..f175352 --- /dev/null +++ b/extensions/tmtv/README.md @@ -0,0 +1 @@ +# TMTV Extension diff --git a/extensions/tmtv/babel.config.js b/extensions/tmtv/babel.config.js new file mode 100644 index 0000000..325ca2a --- /dev/null +++ b/extensions/tmtv/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/extensions/tmtv/package.json b/extensions/tmtv/package.json new file mode 100644 index 0000000..8ca3077 --- /dev/null +++ b/extensions/tmtv/package.json @@ -0,0 +1,45 @@ +{ + "name": "@ohif/extension-tmtv", + "version": "3.10.0-beta.111", + "description": "OHIF extension for Total Metabolic Tumor Volume", + "author": "OHIF", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/ohif-extension-tmtv.umd.js", + "module": "src/index.tsx", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "files": [ + "dist", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.111", + "@ohif/ui": "3.10.0-beta.111", + "dcmjs": "*", + "dicom-parser": "^1.8.9", + "hammerjs": "^2.0.8", + "prop-types": "^15.6.2", + "react": "^18.3.1" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "classnames": "^2.3.2" + } +} diff --git a/extensions/tmtv/src/Panels/PanelPetSUV.tsx b/extensions/tmtv/src/Panels/PanelPetSUV.tsx new file mode 100644 index 0000000..259a4c7 --- /dev/null +++ b/extensions/tmtv/src/Panels/PanelPetSUV.tsx @@ -0,0 +1,246 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { PanelSection, Input, Button } from '@ohif/ui'; +import { DicomMetadataStore } from '@ohif/core'; +import { useTranslation } from 'react-i18next'; +import { Separator } from '@ohif/ui-next'; + +const DEFAULT_MEATADATA = { + PatientWeight: null, + PatientSex: null, + SeriesTime: null, + RadiopharmaceuticalInformationSequence: { + RadionuclideTotalDose: null, + RadionuclideHalfLife: null, + RadiopharmaceuticalStartTime: null, + }, +}; + +/* + * PETSUV panel enables the user to modify the patient related information, such as + * patient sex, patientWeight. This is allowed since + * sometimes these metadata are missing or wrong. By changing them + * @param param0 + * @returns + */ +export default function PanelPetSUV({ servicesManager, commandsManager }: withAppTypes) { + const { t } = useTranslation('PanelSUV'); + const { displaySetService, toolGroupService, toolbarService, hangingProtocolService } = + servicesManager.services; + const [metadata, setMetadata] = useState(DEFAULT_MEATADATA); + const [ptDisplaySet, setPtDisplaySet] = useState(null); + + const handleMetadataChange = metadata => { + setMetadata(prevState => { + const newState = { ...prevState }; + Object.keys(metadata).forEach(key => { + if (typeof metadata[key] === 'object') { + newState[key] = { + ...prevState[key], + ...metadata[key], + }; + } else { + newState[key] = metadata[key]; + } + }); + return newState; + }); + }; + + const getMatchingPTDisplaySet = viewportMatchDetails => { + const ptDisplaySet = commandsManager.runCommand('getMatchingPTDisplaySet', { + viewportMatchDetails, + }); + + if (!ptDisplaySet) { + return; + } + + const metadata = commandsManager.runCommand('getPTMetadata', { + ptDisplaySet, + }); + + return { + ptDisplaySet, + metadata, + }; + }; + + useEffect(() => { + const displaySets = displaySetService.getActiveDisplaySets(); + const { viewportMatchDetails } = hangingProtocolService.getMatchDetails(); + if (!displaySets.length) { + return; + } + + const displaySetInfo = getMatchingPTDisplaySet(viewportMatchDetails); + + if (!displaySetInfo) { + return; + } + + const { ptDisplaySet, metadata } = displaySetInfo; + setPtDisplaySet(ptDisplaySet); + setMetadata(metadata); + }, []); + + // get the patientMetadata from the StudyInstanceUIDs and update the state + useEffect(() => { + const { unsubscribe } = hangingProtocolService.subscribe( + hangingProtocolService.EVENTS.PROTOCOL_CHANGED, + ({ viewportMatchDetails }) => { + const displaySetInfo = getMatchingPTDisplaySet(viewportMatchDetails); + + if (!displaySetInfo) { + return; + } + const { ptDisplaySet, metadata } = displaySetInfo; + setPtDisplaySet(ptDisplaySet); + setMetadata(metadata); + } + ); + return () => { + unsubscribe(); + }; + }, []); + + function updateMetadata() { + if (!ptDisplaySet) { + throw new Error('No ptDisplaySet found'); + } + + // metadata should be dcmjs naturalized + DicomMetadataStore.updateMetadataForSeries( + ptDisplaySet.StudyInstanceUID, + ptDisplaySet.SeriesInstanceUID, + metadata + ); + + // update the displaySets + displaySetService.setDisplaySetMetadataInvalidated(ptDisplaySet.displaySetInstanceUID); + + // Crosshair position depends on the metadata values such as the positioning interaction + // between series, so when the metadata is updated, the crosshairs need to be reset. + setTimeout(() => { + commandsManager.runCommand('resetCrosshairs'); + }, 0); + } + return ( + <> +
+
+ +
+
+ { + handleMetadataChange({ + PatientSex: e.target.value, + }); + }} + /> + kg} + labelClassName="text-[13px] font-inter text-white" + className="!m-0 !h-[26px] !w-[117px]" + value={metadata.PatientWeight || ''} + onChange={e => { + handleMetadataChange({ + PatientWeight: e.target.value, + }); + }} + id="weight-input" + /> + bq} + labelClassName="text-[13px] font-inter text-white" + className="!m-0 !h-[26px] !w-[117px]" + value={ + metadata.RadiopharmaceuticalInformationSequence.RadionuclideTotalDose || '' + } + onChange={e => { + handleMetadataChange({ + RadiopharmaceuticalInformationSequence: { + RadionuclideTotalDose: e.target.value, + }, + }); + }} + /> + s} + labelClassName="text-[13px] font-inter text-white" + className="!m-0 !h-[26px] !w-[117px]" + value={metadata.RadiopharmaceuticalInformationSequence.RadionuclideHalfLife || ''} + onChange={e => { + handleMetadataChange({ + RadiopharmaceuticalInformationSequence: { + RadionuclideHalfLife: e.target.value, + }, + }); + }} + /> + s} + labelClassName="text-[13px] font-inter text-white" + className="!m-0 !h-[26px] !w-[117px]" + value={ + metadata.RadiopharmaceuticalInformationSequence.RadiopharmaceuticalStartTime || + '' + } + onChange={e => { + handleMetadataChange({ + RadiopharmaceuticalInformationSequence: { + RadiopharmaceuticalStartTime: e.target.value, + }, + }); + }} + /> + s} + labelClassName="text-[13px] font-inter text-white" + className="!m-0 !h-[26px] !w-[117px]" + value={metadata.SeriesTime || ''} + onChange={() => {}} + /> + +
+
+
+
+
+ + ); +} + +PanelPetSUV.propTypes = { + servicesManager: PropTypes.shape({ + services: PropTypes.shape({ + measurementService: PropTypes.shape({ + getMeasurements: PropTypes.func.isRequired, + subscribe: PropTypes.func.isRequired, + EVENTS: PropTypes.object.isRequired, + VALUE_TYPES: PropTypes.object.isRequired, + }).isRequired, + }).isRequired, + }).isRequired, +}; diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx new file mode 100644 index 0000000..315cbef --- /dev/null +++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx @@ -0,0 +1,76 @@ +import React, { useEffect } from 'react'; +import { useActiveViewportSegmentationRepresentations } from '@ohif/extension-cornerstone'; +import { handleROIThresholding } from '../../utils/handleROIThresholding'; +import { debounce } from '@ohif/core/src/utils'; + +export default function PanelRoiThresholdSegmentation({ + servicesManager, + commandsManager, +}: withAppTypes) { + const { segmentationService } = servicesManager.services; + const { segmentationsWithRepresentations: segmentationsInfo } = + useActiveViewportSegmentationRepresentations({ servicesManager }); + + useEffect(() => { + const segmentationIds = segmentationsInfo.map( + segmentationInfo => segmentationInfo.segmentation.segmentationId + ); + + const initialRun = async () => { + for (const segmentationId of segmentationIds) { + await handleROIThresholding({ + segmentationId, + commandsManager, + segmentationService, + }); + } + }; + + initialRun(); + }, []); + + useEffect(() => { + const debouncedHandleROIThresholding = debounce(async eventDetail => { + const { segmentationId } = eventDetail; + await handleROIThresholding({ + segmentationId, + commandsManager, + segmentationService, + }); + }, 100); + + const dataModifiedCallback = eventDetail => { + debouncedHandleROIThresholding(eventDetail); + }; + + const dataModifiedSubscription = segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED, + dataModifiedCallback + ); + + return () => { + dataModifiedSubscription.unsubscribe(); + }; + }, [commandsManager, segmentationService]); + + // Find the first segmentation with a TMTV value since all of them have the same value + const tmtvSegmentation = segmentationsInfo.find( + info => info.segmentation.cachedStats?.tmtv !== undefined + ); + const tmtvValue = tmtvSegmentation?.segmentation.cachedStats?.tmtv; + + return ( +
+
+ {tmtvValue !== null && tmtvValue !== undefined ? ( +
+ + {'TMTV:'} + +
{`${tmtvValue?.toFixed(3)} mL`}
+
+ ) : null} +
+
+ ); +} diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ROIThresholdConfiguration.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ROIThresholdConfiguration.tsx new file mode 100644 index 0000000..1f650b1 --- /dev/null +++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ROIThresholdConfiguration.tsx @@ -0,0 +1,191 @@ +import React from 'react'; +import { Input, Label, Select, LegacyButton, LegacyButtonGroup } from '@ohif/ui'; +import { useTranslation } from 'react-i18next'; + +export const ROI_STAT = 'roi_stat'; +const RANGE = 'range'; + +const options = [ + { value: ROI_STAT, label: 'Max', placeHolder: 'Max' }, + { value: RANGE, label: 'Range', placeHolder: 'Range' }, +]; + +function ROIThresholdConfiguration({ config, dispatch, runCommand }) { + const { t } = useTranslation('ROIThresholdConfiguration'); + + return ( +
+
+
+ { + dispatch({ + type: 'setWeight', + payload: { + weight: e.target.value, + }, + }); + }} + /> + )} + {config.strategy !== ROI_STAT && ( +
+ + + + + + + + + + + + + + +
+ +
+ + +
+ { + dispatch({ + type: 'setThreshold', + payload: { + ctLower: e.target.value, + }, + }); + }} + /> + { + dispatch({ + type: 'setThreshold', + payload: { + ctUpper: e.target.value, + }, + }); + }} + /> +
+
+ + +
+ { + dispatch({ + type: 'setThreshold', + payload: { + ptLower: e.target.value, + }, + }); + }} + /> + { + dispatch({ + type: 'setThreshold', + payload: { + ptUpper: e.target.value, + }, + }); + }} + /> +
+
+
+ )} +
+ ); +} + +export default ROIThresholdConfiguration; diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/index.ts b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/index.ts new file mode 100644 index 0000000..01ce5d6 --- /dev/null +++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/index.ts @@ -0,0 +1,3 @@ +import PanelROIThresholdExport from './PanelROIThresholdExport'; + +export default PanelROIThresholdExport; diff --git a/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/segmentationEditHandler.tsx b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/segmentationEditHandler.tsx new file mode 100644 index 0000000..605d17b --- /dev/null +++ b/extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/segmentationEditHandler.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Input, Dialog, ButtonEnums } from '@ohif/ui'; + +function segmentationItemEditHandler({ id, servicesManager }: withAppTypes) { + const { segmentationService, uiDialogService } = servicesManager.services; + + const segmentation = segmentationService.getSegmentation(id); + + const onSubmitHandler = ({ action, value }) => { + switch (action.id) { + case 'save': { + segmentationService.addOrUpdateSegmentation({ + ...segmentation, + ...value, + }); + } + } + uiDialogService.dismiss({ id: 'enter-annotation' }); + }; + + uiDialogService.create({ + id: 'enter-annotation', + centralize: true, + isDraggable: false, + showOverlay: true, + content: Dialog, + contentProps: { + title: 'Enter your Segmentation', + noCloseButton: true, + value: { label: segmentation.label || '' }, + body: ({ value, setValue }) => { + const onChangeHandler = event => { + event.persist(); + setValue(value => ({ ...value, label: event.target.value })); + }; + + const onKeyPressHandler = event => { + if (event.key === 'Enter') { + onSubmitHandler({ value, action: { id: 'save' } }); + } + }; + return ( + + ); + }, + actions: [ + { id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary }, + { id: 'save', text: 'Save', type: ButtonEnums.type.primary }, + ], + onSubmit: onSubmitHandler, + }, + }); +} + +export default segmentationItemEditHandler; diff --git a/extensions/tmtv/src/Panels/PanelTMTV.tsx b/extensions/tmtv/src/Panels/PanelTMTV.tsx new file mode 100644 index 0000000..753a8cc --- /dev/null +++ b/extensions/tmtv/src/Panels/PanelTMTV.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { + PanelSegmentation, + useActiveViewportSegmentationRepresentations, +} from '@ohif/extension-cornerstone'; +import { Button, Icons } from '@ohif/ui-next'; + +export default function PanelTMTV({ + servicesManager, + commandsManager, + extensionManager, + configuration, +}: withAppTypes) { + return ( + <> + + + + + ); +} + +const ExportCSV = ({ servicesManager, commandsManager }: withAppTypes) => { + const { segmentationsWithRepresentations: representations } = + useActiveViewportSegmentationRepresentations({ servicesManager }); + + const tmtv = representations[0]?.segmentation.cachedStats?.tmtv; + + const segmentations = representations.map(representation => representation.segmentation); + + if (!segmentations.length) { + return null; + } + + return ( +
+ +
+ ); +}; diff --git a/extensions/tmtv/src/Panels/RectangleROIOptions.tsx b/extensions/tmtv/src/Panels/RectangleROIOptions.tsx new file mode 100644 index 0000000..19101ba --- /dev/null +++ b/extensions/tmtv/src/Panels/RectangleROIOptions.tsx @@ -0,0 +1,136 @@ +import React, { useState, useCallback, useReducer, useEffect } from 'react'; +import { Button } from '@ohif/ui'; +import ROIThresholdConfiguration, { + ROI_STAT, +} from './PanelROIThresholdSegmentation/ROIThresholdConfiguration'; +import * as cs3dTools from '@cornerstonejs/tools'; + +const LOWER_CT_THRESHOLD_DEFAULT = -1024; +const UPPER_CT_THRESHOLD_DEFAULT = 1024; +const LOWER_PT_THRESHOLD_DEFAULT = 2.5; +const UPPER_PT_THRESHOLD_DEFAULT = 100; +const WEIGHT_DEFAULT = 0.41; // a default weight for suv max often used in the literature +const DEFAULT_STRATEGY = ROI_STAT; + +function reducer(state, action) { + const { payload } = action; + const { strategy, ctLower, ctUpper, ptLower, ptUpper, weight } = payload; + + switch (action.type) { + case 'setStrategy': + return { + ...state, + strategy, + }; + case 'setThreshold': + return { + ...state, + ctLower: ctLower ? ctLower : state.ctLower, + ctUpper: ctUpper ? ctUpper : state.ctUpper, + ptLower: ptLower ? ptLower : state.ptLower, + ptUpper: ptUpper ? ptUpper : state.ptUpper, + }; + case 'setWeight': + return { + ...state, + weight, + }; + default: + return state; + } +} + +function RectangleROIOptions({ servicesManager, commandsManager }: withAppTypes) { + const { segmentationService } = servicesManager.services; + const [selectedSegmentationId, setSelectedSegmentationId] = useState(null); + + const runCommand = useCallback( + (commandName, commandOptions = {}) => { + return commandsManager.runCommand(commandName, commandOptions); + }, + [commandsManager] + ); + + const [config, dispatch] = useReducer(reducer, { + strategy: DEFAULT_STRATEGY, + ctLower: LOWER_CT_THRESHOLD_DEFAULT, + ctUpper: UPPER_CT_THRESHOLD_DEFAULT, + ptLower: LOWER_PT_THRESHOLD_DEFAULT, + ptUpper: UPPER_PT_THRESHOLD_DEFAULT, + weight: WEIGHT_DEFAULT, + }); + + const handleROIThresholding = useCallback(() => { + const segmentationId = selectedSegmentationId; + const activeSegmentIndex = + cs3dTools.segmentation.segmentIndex.getActiveSegmentIndex(segmentationId); + + // run the threshold based on the active segment index + // Todo: later find a way to associate each rectangle with a segment (e.g., maybe with color?) + runCommand('thresholdSegmentationByRectangleROITool', { + segmentationId, + config, + segmentIndex: activeSegmentIndex, + }); + }, [selectedSegmentationId, config]); + + useEffect(() => { + const segmentations = segmentationService.getSegmentationRepresentations(); + + if (!segmentations.length) { + return; + } + + const isActive = segmentations.find(seg => seg.isActive); + setSelectedSegmentationId(isActive.id); + }, []); + + /** + * Update UI based on segmentation changes (added, removed, updated) + */ + useEffect(() => { + // ~~ Subscription + const updated = segmentationService.EVENTS.SEGMENTATION_MODIFIED; + const subscriptions = []; + + [updated].forEach(evt => { + const { unsubscribe } = segmentationService.subscribe(evt, () => { + const segmentations = segmentationService.getSegmentationRepresentations(); + + if (!segmentations.length) { + return; + } + + const isActive = segmentations.find(seg => seg.isActive); + setSelectedSegmentationId(isActive.id); + }); + subscriptions.push(unsubscribe); + }); + + return () => { + subscriptions.forEach(unsub => { + unsub(); + }); + }; + }, []); + + return ( +
+ + {selectedSegmentationId !== null && ( + + )} +
+ ); +} + +export default RectangleROIOptions; diff --git a/extensions/tmtv/src/Panels/index.tsx b/extensions/tmtv/src/Panels/index.tsx new file mode 100644 index 0000000..255c923 --- /dev/null +++ b/extensions/tmtv/src/Panels/index.tsx @@ -0,0 +1,4 @@ +import PanelPetSUV from './PanelPetSUV'; +import PanelROIThresholdExport from './PanelROIThresholdSegmentation'; + +export { PanelPetSUV, PanelROIThresholdExport }; diff --git a/extensions/tmtv/src/commandsModule.ts b/extensions/tmtv/src/commandsModule.ts new file mode 100644 index 0000000..75de0c5 --- /dev/null +++ b/extensions/tmtv/src/commandsModule.ts @@ -0,0 +1,712 @@ +import OHIF from '@ohif/core'; +import * as cs from '@cornerstonejs/core'; +import * as csTools from '@cornerstonejs/tools'; +import { classes } from '@ohif/core'; +import i18n from '@ohif/i18n'; +import getThresholdValues from './utils/getThresholdValue'; +import createAndDownloadTMTVReport from './utils/createAndDownloadTMTVReport'; + +import dicomRTAnnotationExport from './utils/dicomRTAnnotationExport/RTStructureSet'; + +import { getWebWorkerManager } from '@cornerstonejs/core'; +import { Enums } from '@cornerstonejs/tools'; +import { utils } from '@ohif/core'; + +const { SegmentationRepresentations } = Enums; +const { formatPN } = utils; + +const metadataProvider = classes.MetadataProvider; +const ROI_THRESHOLD_MANUAL_TOOL_IDS = [ + 'RectangleROIStartEndThreshold', + 'RectangleROIThreshold', + 'CircleROIStartEndThreshold', +]; + +const workerManager = getWebWorkerManager(); + +const options = { + maxWorkerInstances: 1, + autoTerminateOnIdle: { + enabled: true, + idleTimeThreshold: 3000, + }, +}; + +// Register the task +const workerFn = () => { + return new Worker(new URL('./utils/calculateSUVPeakWorker.js', import.meta.url), { + name: 'suv-peak-worker', // name used by the browser to name the worker + }); +}; + +function getVolumesFromSegmentation(segmentationId) { + const csSegmentation = csTools.segmentation.state.getSegmentation(segmentationId); + const labelmapData = csSegmentation.representationData[ + SegmentationRepresentations.Labelmap + ] as csTools.Types.LabelmapToolOperationDataVolume; + + const { volumeId, referencedVolumeId } = labelmapData; + const labelmapVolume = cs.cache.getVolume(volumeId); + const referencedVolume = cs.cache.getVolume(referencedVolumeId); + + return { labelmapVolume, referencedVolume }; +} + +function getLabelmapVolumeFromSegmentation(segmentation) { + const { representationData } = segmentation; + const { volumeId } = representationData[ + SegmentationRepresentations.Labelmap + ] as csTools.Types.LabelmapToolOperationDataVolume; + + return cs.cache.getVolume(volumeId); +} + +const commandsModule = ({ servicesManager, commandsManager, extensionManager }: withAppTypes) => { + const { + viewportGridService, + uiNotificationService, + displaySetService, + hangingProtocolService, + toolGroupService, + cornerstoneViewportService, + segmentationService, + } = servicesManager.services; + + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.common' + ); + + const { getEnabledElement } = utilityModule.exports; + + function _getActiveViewportsEnabledElement() { + const { activeViewportId } = viewportGridService.getState(); + const { element } = getEnabledElement(activeViewportId) || {}; + const enabledElement = cs.getEnabledElement(element); + return enabledElement; + } + + function _getAnnotationsSelectedByToolNames(toolNames) { + return toolNames.reduce((allAnnotationUIDs, toolName) => { + const annotationUIDs = + csTools.annotation.selection.getAnnotationsSelectedByToolName(toolName); + + return allAnnotationUIDs.concat(annotationUIDs); + }, []); + } + + const actions = { + getMatchingPTDisplaySet: ({ viewportMatchDetails }) => { + // Todo: this is assuming that the hanging protocol has successfully matched + // the correct PT. For future, we should have a way to filter out the PTs + // that are in the viewer layout (but then we have the problem of the attenuation + // corrected PT vs the non-attenuation correct PT) + + let ptDisplaySet = null; + for (const [viewportId, viewportDetails] of viewportMatchDetails) { + const { displaySetsInfo } = viewportDetails; + const displaySets = displaySetsInfo.map(({ displaySetInstanceUID }) => + displaySetService.getDisplaySetByUID(displaySetInstanceUID) + ); + + if (!displaySets || displaySets.length === 0) { + continue; + } + + ptDisplaySet = displaySets.find(displaySet => displaySet.Modality === 'PT'); + if (ptDisplaySet) { + break; + } + } + + return ptDisplaySet; + }, + getPTMetadata: ({ ptDisplaySet }) => { + const dataSource = extensionManager.getDataSources()[0]; + const imageIds = dataSource.getImageIdsForDisplaySet(ptDisplaySet); + + const firstImageId = imageIds[0]; + const instance = metadataProvider.get('instance', firstImageId); + if (instance.Modality !== 'PT') { + return; + } + + const metadata = { + SeriesTime: instance.SeriesTime, + Modality: instance.Modality, + PatientSex: instance.PatientSex, + PatientWeight: instance.PatientWeight, + RadiopharmaceuticalInformationSequence: { + RadionuclideTotalDose: + instance.RadiopharmaceuticalInformationSequence[0].RadionuclideTotalDose, + RadionuclideHalfLife: + instance.RadiopharmaceuticalInformationSequence[0].RadionuclideHalfLife, + RadiopharmaceuticalStartTime: + instance.RadiopharmaceuticalInformationSequence[0].RadiopharmaceuticalStartTime, + RadiopharmaceuticalStartDateTime: + instance.RadiopharmaceuticalInformationSequence[0].RadiopharmaceuticalStartDateTime, + }, + }; + + return metadata; + }, + createNewLabelmapFromPT: async ({ label }) => { + // Create a segmentation of the same resolution as the source data + // using volumeLoader.createAndCacheDerivedVolume. + + const { viewportMatchDetails } = hangingProtocolService.getMatchDetails(); + + const ptDisplaySet = actions.getMatchingPTDisplaySet({ + viewportMatchDetails, + }); + + let withPTViewportId = null; + + for (const [viewportId, { displaySetsInfo }] of viewportMatchDetails.entries()) { + const isPT = displaySetsInfo.some( + ({ displaySetInstanceUID }) => + displaySetInstanceUID === ptDisplaySet.displaySetInstanceUID + ); + + if (isPT) { + withPTViewportId = viewportId; + break; + } + } + + if (!ptDisplaySet) { + uiNotificationService.error('No matching PT display set found'); + return; + } + + const currentSegmentations = + segmentationService.getSegmentationRepresentations(withPTViewportId); + + const displaySet = displaySetService.getDisplaySetByUID(ptDisplaySet.displaySetInstanceUID); + + const segmentationId = await segmentationService.createLabelmapForDisplaySet(displaySet, { + label: `Segmentation ${currentSegmentations.length + 1}`, + segments: { 1: { label: `${i18n.t('Segment')} 1`, active: true } }, + }); + + segmentationService.addSegmentationRepresentation(withPTViewportId, { + segmentationId, + }); + + return segmentationId; + }, + thresholdSegmentationByRectangleROITool: ({ segmentationId, config, segmentIndex }) => { + const segmentation = csTools.segmentation.state.getSegmentation(segmentationId); + + const { representationData } = segmentation; + const { displaySetMatchDetails: matchDetails } = hangingProtocolService.getMatchDetails(); + const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use + + const ctDisplaySet = matchDetails.get('ctDisplaySet'); + const ctVolumeId = `${volumeLoaderScheme}:${ctDisplaySet.displaySetInstanceUID}`; // VolumeId with loader id + volume id + + const { volumeId: segVolumeId } = representationData[ + SegmentationRepresentations.Labelmap + ] as csTools.Types.LabelmapToolOperationDataVolume; + const { referencedVolumeId } = cs.cache.getVolume(segVolumeId); + + const annotationUIDs = _getAnnotationsSelectedByToolNames(ROI_THRESHOLD_MANUAL_TOOL_IDS); + + if (annotationUIDs.length === 0) { + uiNotificationService.show({ + title: 'Commands Module', + message: 'No ROIThreshold Tool is Selected', + type: 'error', + }); + return; + } + + const labelmapVolume = cs.cache.getVolume(segmentationId); + let referencedVolume = cs.cache.getVolume(referencedVolumeId); + const ctReferencedVolume = cs.cache.getVolume(ctVolumeId); + + // check if viewport is + + if (!referencedVolume) { + throw new Error('No Reference volume found'); + } + + if (!labelmapVolume) { + throw new Error('No Reference labelmap found'); + } + + const annotation = csTools.annotation.state.getAnnotation(annotationUIDs[0]); + + const { + metadata: { + enabledElement: { viewport }, + }, + } = annotation; + + const showingReferenceVolume = viewport.hasVolumeId(referencedVolumeId); + + if (!showingReferenceVolume) { + // if the reference volume is not being displayed, we can't + // rely on it for thresholding, we have couple of options here + // 1. We choose whatever volume is being displayed + // 2. We check if it is a fusion viewport, we pick the volume + // that matches the size and dimensions of the labelmap. This might + // happen if the 4D PT is converted to a computed volume and displayed + // and wants to threshold the labelmap + // 3. We throw an error + const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport( + viewport.id + ); + + displaySetInstanceUIDs.forEach(displaySetInstanceUID => { + const volume = cs.cache + .getVolumes() + .find(volume => volume.volumeId.includes(displaySetInstanceUID)); + + if ( + cs.utilities.isEqual(volume.dimensions, labelmapVolume.dimensions) && + cs.utilities.isEqual(volume.spacing, labelmapVolume.spacing) + ) { + referencedVolume = volume; + } + }); + } + + const { ptLower, ptUpper, ctLower, ctUpper } = getThresholdValues( + annotationUIDs, + [referencedVolume, ctReferencedVolume], + config + ); + + return csTools.utilities.segmentation.rectangleROIThresholdVolumeByRange( + annotationUIDs, + labelmapVolume, + [ + { volume: referencedVolume, lower: ptLower, upper: ptUpper }, + { volume: ctReferencedVolume, lower: ctLower, upper: ctUpper }, + ], + { overwrite: true, segmentIndex } + ); + }, + calculateSuvPeak: async ({ segmentationId, segmentIndex }) => { + const segmentation = segmentationService.getSegmentation(segmentationId); + + const { representationData } = segmentation; + const { volumeId, referencedVolumeId } = representationData[ + SegmentationRepresentations.Labelmap + ] as csTools.Types.LabelmapToolOperationDataVolume; + + const labelmap = cs.cache.getVolume(volumeId); + const referencedVolume = cs.cache.getVolume(referencedVolumeId); + + // if we put it in the top, it will appear in other modes + workerManager.registerWorker('suv-peak-worker', workerFn, options); + + const annotationUIDs = _getAnnotationsSelectedByToolNames(ROI_THRESHOLD_MANUAL_TOOL_IDS); + + const annotations = annotationUIDs.map(annotationUID => + csTools.annotation.state.getAnnotation(annotationUID) + ); + + const labelmapProps = { + dimensions: labelmap.dimensions, + origin: labelmap.origin, + direction: labelmap.direction, + spacing: labelmap.spacing, + metadata: labelmap.metadata, + scalarData: labelmap.voxelManager.getCompleteScalarDataArray(), + }; + + const referenceVolumeProps = { + dimensions: referencedVolume.dimensions, + origin: referencedVolume.origin, + direction: referencedVolume.direction, + spacing: referencedVolume.spacing, + metadata: referencedVolume.metadata, + scalarData: referencedVolume.voxelManager.getCompleteScalarDataArray(), + }; + + // metadata in annotations has enabledElement which is not serializable + // we need to remove it + // Todo: we should probably have a sanitization function for this + const annotationsToSend = annotations.map(annotation => { + return { + ...annotation, + metadata: { + ...annotation.metadata, + enabledElement: { + ...annotation.metadata.enabledElement, + viewport: null, + renderingEngine: null, + element: null, + }, + }, + }; + }); + + const suvPeak = + (await workerManager.executeTask('suv-peak-worker', 'calculateSuvPeak', { + labelmapProps, + referenceVolumeProps, + annotations: annotationsToSend, + segmentIndex, + })) || {}; + + return { + suvPeak: suvPeak.mean, + suvMax: suvPeak.max, + suvMaxIJK: suvPeak.maxIJK, + suvMaxLPS: suvPeak.maxLPS, + }; + }, + getLesionStats: ({ segmentationId, segmentIndex = 1 }) => { + const { labelmapVolume, referencedVolume } = getVolumesFromSegmentation(segmentationId); + const { voxelManager: segVoxelManager, imageData, spacing } = labelmapVolume; + const { voxelManager: refVoxelManager } = referencedVolume; + + let segmentationMax = -Infinity; + let segmentationMin = Infinity; + const segmentationValues = []; + let voxelCount = 0; + + const callback = ({ value, index }) => { + if (value === segmentIndex) { + const refValue = refVoxelManager.getAtIndex(index) as number; + segmentationValues.push(refValue); + if (refValue > segmentationMax) { + segmentationMax = refValue; + } + if (refValue < segmentationMin) { + segmentationMin = refValue; + } + voxelCount++; + } + }; + + segVoxelManager.forEach(callback, { imageData }); + const mean = segmentationValues.reduce((a, b) => a + b, 0) / voxelCount; + const stats = { + minValue: segmentationMin, + maxValue: segmentationMax, + meanValue: mean, + stdValue: Math.sqrt( + segmentationValues.map(k => (k - mean) ** 2).reduce((acc, curr) => acc + curr, 0) / + voxelCount + ), + volume: voxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3, + }; + + return stats; + }, + calculateLesionGlycolysis: ({ lesionStats }) => { + const { meanValue, volume } = lesionStats; + + return { + lesionGlyoclysisStats: volume * meanValue, + }; + }, + calculateTMTV: async ({ segmentations }) => { + const labelmapProps = segmentations.map(segmentation => { + const labelmap = getLabelmapVolumeFromSegmentation(segmentation); + return { + dimensions: labelmap.dimensions, + spacing: labelmap.spacing, + scalarData: labelmap.voxelManager.getCompleteScalarDataArray(), + origin: labelmap.origin, + direction: labelmap.direction, + }; + }); + + if (!labelmapProps.length) { + return; + } + + const tmtv = await workerManager.executeTask( + 'suv-peak-worker', + 'calculateTMTV', + labelmapProps + ); + + return tmtv; + }, + exportTMTVReportCSV: async ({ segmentations, tmtv, config, options }) => { + const segReport = commandsManager.runCommand('getSegmentationCSVReport', { + segmentations, + }); + + const tlg = await actions.getTotalLesionGlycolysis({ segmentations }); + const additionalReportRows = [ + { key: 'Total Lesion Glycolysis', value: { tlg: tlg.toFixed(4) } }, + { key: 'Threshold Configuration', value: { ...config } }, + ]; + + if (tmtv !== undefined) { + additionalReportRows.unshift({ + key: 'Total Metabolic Tumor Volume', + value: { tmtv }, + }); + } + + createAndDownloadTMTVReport(segReport, additionalReportRows, options); + }, + getTotalLesionGlycolysis: async ({ segmentations }) => { + const labelmapProps = segmentations.map(segmentation => { + const labelmap = getLabelmapVolumeFromSegmentation(segmentation); + return { + dimensions: labelmap.dimensions, + spacing: labelmap.spacing, + scalarData: labelmap.voxelManager.getCompleteScalarDataArray(), + origin: labelmap.origin, + direction: labelmap.direction, + }; + }); + + const { referencedVolume: ptVolume } = getVolumesFromSegmentation( + segmentations[0].segmentationId + ); + + const ptVolumeProps = { + dimensions: ptVolume.dimensions, + spacing: ptVolume.spacing, + scalarData: ptVolume.voxelManager.getCompleteScalarDataArray(), + origin: ptVolume.origin, + direction: ptVolume.direction, + }; + + return await workerManager.executeTask('suv-peak-worker', 'getTotalLesionGlycolysis', { + labelmapProps, + referenceVolumeProps: ptVolumeProps, + }); + }, + setStartSliceForROIThresholdTool: () => { + const { viewport } = _getActiveViewportsEnabledElement(); + const { focalPoint } = viewport.getCamera(); + + const selectedAnnotationUIDs = _getAnnotationsSelectedByToolNames( + ROI_THRESHOLD_MANUAL_TOOL_IDS + ); + + const annotationUID = selectedAnnotationUIDs[0]; + + const annotation = csTools.annotation.state.getAnnotation(annotationUID); + + // set the current focal point + annotation.data.startCoordinate = focalPoint; + // IMPORTANT: invalidate the toolData for the cached stat to get updated + // and re-calculate the projection points + annotation.invalidated = true; + viewport.render(); + }, + setEndSliceForROIThresholdTool: () => { + const { viewport } = _getActiveViewportsEnabledElement(); + + const selectedAnnotationUIDs = _getAnnotationsSelectedByToolNames( + ROI_THRESHOLD_MANUAL_TOOL_IDS + ); + + const annotationUID = selectedAnnotationUIDs[0]; + + const annotation = csTools.annotation.state.getAnnotation(annotationUID); + + // get the current focal point + const focalPointToEnd = viewport.getCamera().focalPoint; + annotation.data.endCoordinate = focalPointToEnd; + + // IMPORTANT: invalidate the toolData for the cached stat to get updated + // and re-calculate the projection points + annotation.invalidated = true; + + viewport.render(); + }, + createTMTVRTReport: () => { + // get all Rectangle ROI annotation + const stateManager = csTools.annotation.state.getAnnotationManager(); + + const annotations = []; + + Object.keys(stateManager.annotations).forEach(frameOfReferenceUID => { + const forAnnotations = stateManager.annotations[frameOfReferenceUID]; + const ROIAnnotations = ROI_THRESHOLD_MANUAL_TOOL_IDS.reduce( + (annotations, toolName) => [...annotations, ...(forAnnotations[toolName] ?? [])], + [] + ); + + annotations.push(...ROIAnnotations); + }); + + commandsManager.runCommand('exportRTReportForAnnotations', { + annotations, + }); + }, + getSegmentationCSVReport: ({ segmentations }) => { + if (!segmentations || !segmentations.length) { + segmentations = segmentationService.getSegmentations(); + } + + const report = {}; + + for (const segmentation of segmentations) { + const { label, segmentationId, representationData } = + segmentation as csTools.Types.Segmentation; + const id = segmentationId; + + const segReport = { id, label }; + + if (!representationData) { + report[id] = segReport; + continue; + } + + const { cachedStats } = segmentation.segments[1] || {}; // Assuming we want stats from the first segment + + if (cachedStats) { + Object.entries(cachedStats).forEach(([key, value]) => { + if (typeof value !== 'object') { + segReport[key] = value; + } else { + Object.entries(value).forEach(([subKey, subValue]) => { + const newKey = `${key}_${subKey}`; + segReport[newKey] = subValue; + }); + } + }); + } + + const labelmapVolume = + segmentation.representationData[SegmentationRepresentations.Labelmap]; + + if (!labelmapVolume) { + report[id] = segReport; + continue; + } + + const referencedVolumeId = labelmapVolume.referencedVolumeId; + + const referencedVolume = cs.cache.getVolume(referencedVolumeId); + + if (!referencedVolume) { + report[id] = segReport; + continue; + } + + if (!referencedVolume.imageIds || !referencedVolume.imageIds.length) { + report[id] = segReport; + continue; + } + + const firstImageId = referencedVolume.imageIds[0]; + const instance = OHIF.classes.MetadataProvider.get('instance', firstImageId); + + if (!instance) { + report[id] = segReport; + continue; + } + + report[id] = { + ...segReport, + PatientID: instance.PatientID ?? '000000', + PatientName: formatPN(instance.PatientName), + StudyInstanceUID: instance.StudyInstanceUID, + SeriesInstanceUID: instance.SeriesInstanceUID, + StudyDate: instance.StudyDate, + }; + } + + return report; + }, + exportRTReportForAnnotations: ({ annotations }) => { + dicomRTAnnotationExport(annotations); + }, + setFusionPTColormap: ({ toolGroupId, colormap }) => { + const toolGroup = toolGroupService.getToolGroup(toolGroupId); + + if (!toolGroup) { + return; + } + + const { viewportMatchDetails } = hangingProtocolService.getMatchDetails(); + + const ptDisplaySet = actions.getMatchingPTDisplaySet({ + viewportMatchDetails, + }); + + if (!ptDisplaySet) { + return; + } + + const fusionViewportIds = toolGroup.getViewportIds(); + + const viewports = []; + fusionViewportIds.forEach(viewportId => { + commandsManager.runCommand('setViewportColormap', { + viewportId, + displaySetInstanceUID: ptDisplaySet.displaySetInstanceUID, + colormap: { + name: colormap, + }, + }); + + viewports.push(cornerstoneViewportService.getCornerstoneViewport(viewportId)); + }); + + viewports.forEach(viewport => { + viewport.render(); + }); + }, + }; + + const definitions = { + setEndSliceForROIThresholdTool: { + commandFn: actions.setEndSliceForROIThresholdTool, + }, + setStartSliceForROIThresholdTool: { + commandFn: actions.setStartSliceForROIThresholdTool, + }, + getMatchingPTDisplaySet: { + commandFn: actions.getMatchingPTDisplaySet, + }, + getPTMetadata: { + commandFn: actions.getPTMetadata, + }, + createNewLabelmapFromPT: { + commandFn: actions.createNewLabelmapFromPT, + }, + thresholdSegmentationByRectangleROITool: { + commandFn: actions.thresholdSegmentationByRectangleROITool, + }, + getTotalLesionGlycolysis: { + commandFn: actions.getTotalLesionGlycolysis, + }, + calculateSuvPeak: { + commandFn: actions.calculateSuvPeak, + }, + getLesionStats: { + commandFn: actions.getLesionStats, + }, + calculateTMTV: { + commandFn: actions.calculateTMTV, + }, + exportTMTVReportCSV: { + commandFn: actions.exportTMTVReportCSV, + }, + createTMTVRTReport: { + commandFn: actions.createTMTVRTReport, + }, + getSegmentationCSVReport: { + commandFn: actions.getSegmentationCSVReport, + }, + exportRTReportForAnnotations: { + commandFn: actions.exportRTReportForAnnotations, + }, + setFusionPTColormap: { + commandFn: actions.setFusionPTColormap, + }, + }; + + return { + actions, + definitions, + defaultContext: 'TMTV:CORNERSTONE', + }; +}; + +export default commandsModule; diff --git a/extensions/tmtv/src/getHangingProtocolModule.ts b/extensions/tmtv/src/getHangingProtocolModule.ts new file mode 100644 index 0000000..d4f8d9b --- /dev/null +++ b/extensions/tmtv/src/getHangingProtocolModule.ts @@ -0,0 +1,350 @@ +import { + ctAXIAL, + ctCORONAL, + ctSAGITTAL, + fusionAXIAL, + fusionCORONAL, + fusionSAGITTAL, + mipSAGITTAL, + ptAXIAL, + ptCORONAL, + ptSAGITTAL, +} from './utils/hpViewports'; + +/** + * represents a 3x4 viewport layout configuration. The layout displays CT axial, sagittal, and coronal + * images in the first row, PT axial, sagittal, and coronal images in the second row, and fusion axial, + * sagittal, and coronal images in the third row. The fourth column is fully spanned by a MIP sagittal + * image, covering all three rows. It has synchronizers for windowLevel for all CT and PT images, and + * also camera synchronizer for each orientation + */ +const stage1: AppTypes.HangingProtocol.ProtocolStage = { + name: 'default', + id: 'default', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 3, + columns: 4, + layoutOptions: [ + { + x: 0, + y: 0, + width: 1 / 4, + height: 1 / 3, + }, + { + x: 1 / 4, + y: 0, + width: 1 / 4, + height: 1 / 3, + }, + { + x: 2 / 4, + y: 0, + width: 1 / 4, + height: 1 / 3, + }, + { + x: 0, + y: 1 / 3, + width: 1 / 4, + height: 1 / 3, + }, + { + x: 1 / 4, + y: 1 / 3, + width: 1 / 4, + height: 1 / 3, + }, + { + x: 2 / 4, + y: 1 / 3, + width: 1 / 4, + height: 1 / 3, + }, + { + x: 0, + y: 2 / 3, + width: 1 / 4, + height: 1 / 3, + }, + { + x: 1 / 4, + y: 2 / 3, + width: 1 / 4, + height: 1 / 3, + }, + { + x: 2 / 4, + y: 2 / 3, + width: 1 / 4, + height: 1 / 3, + }, + { + x: 3 / 4, + y: 0, + width: 1 / 4, + height: 1, + }, + ], + }, + }, + viewports: [ + ctAXIAL, + ctSAGITTAL, + ctCORONAL, + ptAXIAL, + ptSAGITTAL, + ptCORONAL, + fusionAXIAL, + fusionSAGITTAL, + fusionCORONAL, + mipSAGITTAL, + ], + createdDate: '2021-02-23T18:32:42.850Z', +}; + +/** + * The layout displays CT axial image in the top-left viewport, fusion axial image + * in the top-right viewport, PT axial image in the bottom-left viewport, and MIP + * sagittal image in the bottom-right viewport. The layout follows a simple grid + * pattern with 2 rows and 2 columns. It includes synchronizers as well. + */ +const stage2 = { + name: 'Fusion 2x2', + id: 'Fusion-2x2', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 2, + columns: 2, + }, + }, + viewports: [ctAXIAL, fusionAXIAL, ptAXIAL, mipSAGITTAL], +}; + +/** + * The top row displays CT images in axial, sagittal, and coronal orientations from + * left to right, respectively. The bottom row displays PT images in axial, sagittal, + * and coronal orientations from left to right, respectively. + * The layout follows a simple grid pattern with 2 rows and 3 columns. + * It includes synchronizers as well. + */ +const stage3: AppTypes.HangingProtocol.ProtocolStage = { + name: '2x3-layout', + id: '2x3-layout', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 2, + columns: 3, + }, + }, + viewports: [ctAXIAL, ctSAGITTAL, ctCORONAL, ptAXIAL, ptSAGITTAL, ptCORONAL], +}; + +/** + * In this layout, the top row displays PT images in coronal, sagittal, and axial + * orientations from left to right, respectively, followed by a MIP sagittal image + * that spans both rows on the rightmost side. The bottom row displays fusion images + * in coronal, sagittal, and axial orientations from left to right, respectively. + * There is no viewport in the bottom row's rightmost position, as the MIP sagittal viewport + * from the top row spans the full height of both rows. + * It includes synchronizers as well. + */ +const stage4: AppTypes.HangingProtocol.ProtocolStage = { + name: '2x4-layout', + id: '2x4-layout', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 2, + columns: 4, + layoutOptions: [ + { + x: 0, + y: 0, + width: 1 / 4, + height: 1 / 2, + }, + { + x: 1 / 4, + y: 0, + width: 1 / 4, + height: 1 / 2, + }, + { + x: 2 / 4, + y: 0, + width: 1 / 4, + height: 1 / 2, + }, + { + x: 3 / 4, + y: 0, + width: 1 / 4, + height: 1, + }, + { + x: 0, + y: 1 / 2, + width: 1 / 4, + height: 1 / 2, + }, + { + x: 1 / 4, + y: 1 / 2, + width: 1 / 4, + height: 1 / 2, + }, + { + x: 2 / 4, + y: 1 / 2, + width: 1 / 4, + height: 1 / 2, + }, + ], + }, + }, + viewports: [ + ptCORONAL, + ptSAGITTAL, + ptAXIAL, + mipSAGITTAL, + fusionCORONAL, + fusionSAGITTAL, + fusionAXIAL, + ], +}; + +/** + * This layout displays three fusion viewports: axial, sagittal, and coronal. + * It follows a simple grid pattern with 1 row and 3 columns. + */ +// const stage0: AppTypes.HangingProtocol.ProtocolStage = { +// name: 'Fusion 1x3', +// viewportStructure: { +// layoutType: 'grid', +// properties: { +// rows: 1, +// columns: 3, +// }, +// }, +// viewports: [fusionAXIAL, fusionSAGITTAL, fusionCORONAL], +// }; + +const ptCT: AppTypes.HangingProtocol.Protocol = { + id: '@ohif/extension-tmtv.hangingProtocolModule.ptCT', + locked: true, + name: 'Default', + createdDate: '2021-02-23T19:22:08.894Z', + modifiedDate: '2022-10-04T19:22:08.894Z', + availableTo: {}, + editableBy: {}, + imageLoadStrategy: 'interleaveTopToBottom', // "default" , "interleaveTopToBottom", "interleaveCenter" + protocolMatchingRules: [ + { + attribute: 'ModalitiesInStudy', + constraint: { + contains: ['CT', 'PT'], + }, + }, + { + attribute: 'StudyDescription', + constraint: { + contains: 'PETCT', + }, + }, + { + attribute: 'StudyDescription', + constraint: { + contains: 'PET/CT', + }, + }, + ], + displaySetSelectors: { + ctDisplaySet: { + seriesMatchingRules: [ + { + attribute: 'Modality', + constraint: { + equals: { + value: 'CT', + }, + }, + required: true, + }, + { + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + { + attribute: 'SeriesDescription', + constraint: { + contains: 'CT', + }, + }, + { + attribute: 'SeriesDescription', + constraint: { + contains: 'CT WB', + }, + }, + ], + }, + ptDisplaySet: { + seriesMatchingRules: [ + { + attribute: 'Modality', + constraint: { + equals: 'PT', + }, + required: true, + }, + { + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + { + attribute: 'SeriesDescription', + constraint: { + contains: 'Corrected', + }, + }, + { + weight: 2, + attribute: 'SeriesDescription', + constraint: { + doesNotContain: { + value: 'Uncorrected', + }, + }, + }, + ], + }, + }, + stages: [stage1, stage2, stage3, stage4], + numberOfPriorsReferenced: -1, +}; + +function getHangingProtocolModule() { + return [ + { + name: ptCT.id, + protocol: ptCT, + }, + ]; +} + +export default getHangingProtocolModule; diff --git a/extensions/tmtv/src/getPanelModule.tsx b/extensions/tmtv/src/getPanelModule.tsx new file mode 100644 index 0000000..fb9cade --- /dev/null +++ b/extensions/tmtv/src/getPanelModule.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { PanelPetSUV, PanelROIThresholdExport } from './Panels'; +import { Toolbox } from '@ohif/ui-next'; +import PanelTMTV from './Panels/PanelTMTV'; + +function getPanelModule({ commandsManager, extensionManager, servicesManager }) { + const wrappedPanelPetSuv = () => { + return ( + + ); + }; + + const wrappedROIThresholdToolbox = () => { + return ( + + ); + }; + + const wrappedROIThresholdExport = () => { + return ( + + ); + }; + + const wrappedPanelTMTV = () => { + return ( + <> + + + + + ); + }; + + return [ + { + name: 'petSUV', + iconName: 'tab-patient-info', + iconLabel: 'Patient Info', + label: 'Patient Info', + component: wrappedPanelPetSuv, + }, + { + name: 'tmtv', + iconName: 'tab-segmentation', + iconLabel: 'Segmentation', + component: wrappedPanelTMTV, + }, + { + name: 'tmtvBox', + iconName: 'tab-segmentation', + iconLabel: 'Segmentation', + label: 'Segmentation Toolbox', + component: wrappedROIThresholdToolbox, + }, + { + name: 'tmtvExport', + iconName: 'tab-segmentation', + iconLabel: 'Segmentation', + label: 'Segmentation Export', + component: wrappedROIThresholdExport, + }, + ]; +} + +export default getPanelModule; diff --git a/extensions/tmtv/src/getToolbarModule.tsx b/extensions/tmtv/src/getToolbarModule.tsx new file mode 100644 index 0000000..33c178b --- /dev/null +++ b/extensions/tmtv/src/getToolbarModule.tsx @@ -0,0 +1,10 @@ +import RectangleROIOptions from './Panels/RectangleROIOptions'; + +export default function getToolbarModule({ commandsManager, servicesManager }) { + return [ + { + name: 'tmtv.RectangleROIThresholdOptions', + defaultComponent: () => RectangleROIOptions({ commandsManager, servicesManager }), + }, + ]; +} diff --git a/extensions/tmtv/src/id.js b/extensions/tmtv/src/id.js new file mode 100644 index 0000000..ebe5acd --- /dev/null +++ b/extensions/tmtv/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/extensions/tmtv/src/index.tsx b/extensions/tmtv/src/index.tsx new file mode 100644 index 0000000..9d853c9 --- /dev/null +++ b/extensions/tmtv/src/index.tsx @@ -0,0 +1,31 @@ +import { id } from './id'; +import getHangingProtocolModule from './getHangingProtocolModule'; +import getPanelModule from './getPanelModule'; +import init from './init'; +import commandsModule from './commandsModule'; +import getToolbarModule from './getToolbarModule'; + +/** + * + */ +const tmtvExtension = { + /** + * Only required property. Should be a unique value across all extensions. + */ + id, + preRegistration({ servicesManager, commandsManager, extensionManager, configuration = {} }) { + init({ servicesManager, commandsManager, extensionManager, configuration }); + }, + getToolbarModule, + getPanelModule, + getHangingProtocolModule, + getCommandsModule({ servicesManager, commandsManager, extensionManager }) { + return commandsModule({ + servicesManager, + commandsManager, + extensionManager, + }); + }, +}; + +export default tmtvExtension; diff --git a/extensions/tmtv/src/init.js b/extensions/tmtv/src/init.js new file mode 100644 index 0000000..898d341 --- /dev/null +++ b/extensions/tmtv/src/init.js @@ -0,0 +1,52 @@ +import { + addTool, + RectangleROIStartEndThresholdTool, + CircleROIStartEndThresholdTool, +} from '@cornerstonejs/tools'; +import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone'; + +import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory'; + +const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums; + +/** + * + * @param {Object} servicesManager + * @param {Object} configuration + * @param {Object|Array} configuration.csToolsConfig + */ +export default function init({ servicesManager }) { + const { measurementService, displaySetService, cornerstoneViewportService } = + servicesManager.services; + + addTool(RectangleROIStartEndThresholdTool); + addTool(CircleROIStartEndThresholdTool); + + const { RectangleROIStartEndThreshold, CircleROIStartEndThreshold } = + measurementServiceMappingsFactory( + measurementService, + displaySetService, + cornerstoneViewportService + ); + + const csTools3DVer1MeasurementSource = measurementService.getSource( + CORNERSTONE_3D_TOOLS_SOURCE_NAME, + CORNERSTONE_3D_TOOLS_SOURCE_VERSION + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'RectangleROIStartEndThreshold', + RectangleROIStartEndThreshold.matchingCriteria, + RectangleROIStartEndThreshold.toAnnotation, + RectangleROIStartEndThreshold.toMeasurement + ); + + measurementService.addMapping( + csTools3DVer1MeasurementSource, + 'CircleROIStartEndThreshold', + CircleROIStartEndThreshold.matchingCriteria, + CircleROIStartEndThreshold.toAnnotation, + CircleROIStartEndThreshold.toMeasurement + ); +} diff --git a/extensions/tmtv/src/utils/calculateSUVPeakWorker.js b/extensions/tmtv/src/utils/calculateSUVPeakWorker.js new file mode 100644 index 0000000..dbfdea3 --- /dev/null +++ b/extensions/tmtv/src/utils/calculateSUVPeakWorker.js @@ -0,0 +1,209 @@ +import { utilities } from '@cornerstonejs/core'; +import { utilities as cstUtils } from '@cornerstonejs/tools'; +import { vec3 } from 'gl-matrix'; +import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; +import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; +import { expose } from 'comlink'; + +const createVolume = ({ dimensions, origin, direction, spacing, metadata, scalarData }) => { + const imageData = vtkImageData.newInstance(); + imageData.setDimensions(dimensions); + imageData.setOrigin(origin); + imageData.setDirection(direction); + imageData.setSpacing(spacing); + + const scalarArray = vtkDataArray.newInstance({ + name: 'Pixels', + numberOfComponents: 1, + values: scalarData, + }); + + imageData.getPointData().setScalars(scalarArray); + + imageData.modified(); + + const voxelManager = utilities.VoxelManager.createScalarVolumeVoxelManager({ + scalarData, + dimensions, + numberOfComponents: 1, + }); + return { + imageData, + spacing, + origin, + direction, + metadata, + voxelManager, + }; +}; + +/** + * This method calculates the SUV peak on a segmented ROI from a reference PET + * volume. If a rectangle annotation is provided, the peak is calculated within that + * rectangle. Otherwise, the calculation is performed on the entire volume which + * will be slower but same result. + * @param viewport Viewport to use for the calculation + * @param labelmap Labelmap from which the mask is taken + * @param referenceVolume PET volume to use for SUV calculation + * @param toolData [Optional] list of toolData to use for SUV calculation + * @param segmentIndex The index of the segment to use for masking + * @returns + */ +function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, segmentIndex = 1 }) { + const labelmapInfo = createVolume(labelmapProps); + const referenceInfo = createVolume(referenceVolumeProps); + + if (referenceInfo.metadata.Modality !== 'PT') { + return; + } + + const { dimensions, imageData: labelmapImageData } = labelmapInfo; + const { imageData: referenceVolumeImageData } = referenceInfo; + + let boundsIJK; + // Todo: using the first annotation for now + if (annotations?.length && annotations[0].data?.cachedStats) { + const { projectionPoints } = annotations[0].data.cachedStats; + const pointsToUse = [].concat(...projectionPoints); // cannot use flat() because of typescript compiler right now + + const rectangleCornersIJK = pointsToUse.map(world => { + const ijk = vec3.fromValues(0, 0, 0); + referenceVolumeImageData.worldToIndex(world, ijk); + return ijk; + }); + + boundsIJK = cstUtils.boundingBox.getBoundingBoxAroundShape(rectangleCornersIJK, dimensions); + } + + let max = 0; + let maxIJK = [0, 0, 0]; + let maxLPS = [0, 0, 0]; + + const callback = ({ pointIJK, pointLPS }) => { + const value = labelmapInfo.voxelManager.getAtIJKPoint(pointIJK); + + if (value !== segmentIndex) { + return; + } + + const referenceValue = referenceInfo.voxelManager.getAtIJKPoint(pointIJK); + + if (referenceValue > max) { + max = referenceValue; + maxIJK = pointIJK; + maxLPS = pointLPS; + } + }; + + labelmapInfo.voxelManager.forEach(callback, { + boundsIJK, + imageData: labelmapImageData, + isInObject: () => true, + returnPoints: true, + }); + + const direction = labelmapImageData.getDirection().slice(0, 3); + + /** + * 2. Find the bottom and top of the great circle for the second sphere (1cc sphere) + * V = (4/3)ฯ€r3 + */ + const radius = Math.pow(1 / ((4 / 3) * Math.PI), 1 / 3) * 10; + const diameter = radius * 2; + + const secondaryCircleWorld = vec3.create(); + const bottomWorld = vec3.create(); + const topWorld = vec3.create(); + referenceVolumeImageData.indexToWorld(maxIJK, secondaryCircleWorld); + vec3.scaleAndAdd(bottomWorld, secondaryCircleWorld, direction, -diameter / 2); + vec3.scaleAndAdd(topWorld, secondaryCircleWorld, direction, diameter / 2); + const suvPeakCirclePoints = [bottomWorld, topWorld]; + + /** + * 3. Find the Mean and Max of the 1cc sphere centered on the suv Max of the previous + * sphere + */ + let count = 0; + let acc = 0; + const suvPeakMeanCallback = ({ value }) => { + acc += value; + count += 1; + }; + + cstUtils.pointInSurroundingSphereCallback( + referenceVolumeImageData, + suvPeakCirclePoints, + suvPeakMeanCallback + ); + + const mean = acc / count; + + return { + max, + maxIJK, + maxLPS, + mean, + }; +} + +function calculateTMTV(labelmapProps, segmentIndex = 1) { + const labelmaps = labelmapProps.map(props => createVolume(props)); + + const mergedLabelmap = + labelmaps.length === 1 + ? labelmaps[0] + : cstUtils.segmentation.createMergedLabelmapForIndex(labelmaps); + + const { imageData, spacing } = mergedLabelmap; + const values = imageData.getPointData().getScalars().getData(); + + // count non-zero values inside the outputData, this would + // consider the overlapping regions to be only counted once + const numVoxels = values.reduce((acc, curr) => { + if (curr > 0) { + return acc + 1; + } + return acc; + }, 0); + + return 1e-3 * numVoxels * spacing[0] * spacing[1] * spacing[2]; +} + +function getTotalLesionGlycolysis({ labelmapProps, referenceVolumeProps }) { + const labelmaps = labelmapProps.map(props => createVolume(props)); + + const mergedLabelmap = + labelmaps.length === 1 + ? labelmaps[0] + : cstUtils.segmentation.createMergedLabelmapForIndex(labelmaps); + + // grabbing the first labelmap referenceVolume since it will be the same for all + const { spacing } = labelmaps[0]; + + const ptVolume = createVolume(referenceVolumeProps); + + let suv = 0; + let totalLesionVoxelCount = 0; + const scalarDataLength = mergedLabelmap.voxelManager.getScalarDataLength(); + for (let i = 0; i < scalarDataLength; i++) { + // if not background + if (mergedLabelmap.voxelManager.getAtIndex(i) !== 0) { + suv += ptVolume.voxelManager.getAtIndex(i); + totalLesionVoxelCount += 1; + } + } + + // Average SUV for the merged labelmap + const averageSuv = suv / totalLesionVoxelCount; + + // total Lesion Glycolysis [suv * ml] + return averageSuv * totalLesionVoxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3; +} + +const obj = { + calculateSuvPeak, + calculateTMTV, + getTotalLesionGlycolysis, +}; + +expose(obj); diff --git a/extensions/tmtv/src/utils/calculateTMTV.ts b/extensions/tmtv/src/utils/calculateTMTV.ts new file mode 100644 index 0000000..0fcc680 --- /dev/null +++ b/extensions/tmtv/src/utils/calculateTMTV.ts @@ -0,0 +1,42 @@ +import { Types } from '@cornerstonejs/core'; +import { utilities } from '@cornerstonejs/tools'; + +/** + * Given a list of labelmaps (with the possibility of overlapping regions), + * and a referenceVolume, it calculates the total metabolic tumor volume (TMTV) + * by flattening and rasterizing each segment into a single labelmap and summing + * the total number of volume voxels. It should be noted that for this calculation + * we do not double count voxels that are part of multiple labelmaps. + * @param {} labelmaps + * @param {number} segmentIndex + * @returns {number} TMTV in ml + */ +function calculateTMTV(labelmaps: Array, segmentIndex = 1): number { + const volumeId = 'mergedLabelmap'; + + const mergedLabelmap = utilities.segmentation.createMergedLabelmapForIndex( + labelmaps, + segmentIndex, + volumeId + ); + + const { imageData, spacing, voxelManager } = mergedLabelmap; + + // count non-zero values inside the outputData, this would + // consider the overlapping regions to be only counted once + let numVoxels = 0; + const callback = ({ value }) => { + if (value > 0) { + numVoxels += 1; + } + }; + + voxelManager.forEach(callback, { + imageData, + isInObject: () => true, + }); + + return 1e-3 * numVoxels * spacing[0] * spacing[1] * spacing[2]; +} + +export default calculateTMTV; diff --git a/extensions/tmtv/src/utils/colormaps/index.js b/extensions/tmtv/src/utils/colormaps/index.js new file mode 100644 index 0000000..b5afdd9 --- /dev/null +++ b/extensions/tmtv/src/utils/colormaps/index.js @@ -0,0 +1,1554 @@ +export default [ + { + ColorSpace: 'RGB', + Name: 'hot_iron', + RGBPoints: [ + 0.0, 0.0039215686, 0.0039215686, 0.0156862745, 0.00392156862745098, 0.0039215686, + 0.0039215686, 0.0156862745, 0.00784313725490196, 0.0039215686, 0.0039215686, 0.031372549, + 0.011764705882352941, 0.0039215686, 0.0039215686, 0.0470588235, 0.01568627450980392, + 0.0039215686, 0.0039215686, 0.062745098, 0.0196078431372549, 0.0039215686, 0.0039215686, + 0.0784313725, 0.023529411764705882, 0.0039215686, 0.0039215686, 0.0941176471, + 0.027450980392156862, 0.0039215686, 0.0039215686, 0.1098039216, 0.03137254901960784, + 0.0039215686, 0.0039215686, 0.1254901961, 0.03529411764705882, 0.0039215686, 0.0039215686, + 0.1411764706, 0.0392156862745098, 0.0039215686, 0.0039215686, 0.1568627451, + 0.043137254901960784, 0.0039215686, 0.0039215686, 0.1725490196, 0.047058823529411764, + 0.0039215686, 0.0039215686, 0.1882352941, 0.050980392156862744, 0.0039215686, 0.0039215686, + 0.2039215686, 0.054901960784313725, 0.0039215686, 0.0039215686, 0.2196078431, + 0.05882352941176471, 0.0039215686, 0.0039215686, 0.2352941176, 0.06274509803921569, + 0.0039215686, 0.0039215686, 0.2509803922, 0.06666666666666667, 0.0039215686, 0.0039215686, + 0.262745098, 0.07058823529411765, 0.0039215686, 0.0039215686, 0.2784313725, + 0.07450980392156863, 0.0039215686, 0.0039215686, 0.2941176471, 0.0784313725490196, + 0.0039215686, 0.0039215686, 0.3098039216, 0.08235294117647059, 0.0039215686, 0.0039215686, + 0.3254901961, 0.08627450980392157, 0.0039215686, 0.0039215686, 0.3411764706, + 0.09019607843137255, 0.0039215686, 0.0039215686, 0.3568627451, 0.09411764705882353, + 0.0039215686, 0.0039215686, 0.3725490196, 0.09803921568627451, 0.0039215686, 0.0039215686, + 0.3882352941, 0.10196078431372549, 0.0039215686, 0.0039215686, 0.4039215686, + 0.10588235294117647, 0.0039215686, 0.0039215686, 0.4196078431, 0.10980392156862745, + 0.0039215686, 0.0039215686, 0.4352941176, 0.11372549019607843, 0.0039215686, 0.0039215686, + 0.4509803922, 0.11764705882352942, 0.0039215686, 0.0039215686, 0.4666666667, + 0.12156862745098039, 0.0039215686, 0.0039215686, 0.4823529412, 0.12549019607843137, + 0.0039215686, 0.0039215686, 0.4980392157, 0.12941176470588237, 0.0039215686, 0.0039215686, + 0.5137254902, 0.13333333333333333, 0.0039215686, 0.0039215686, 0.5294117647, + 0.13725490196078433, 0.0039215686, 0.0039215686, 0.5450980392, 0.1411764705882353, + 0.0039215686, 0.0039215686, 0.5607843137, 0.1450980392156863, 0.0039215686, 0.0039215686, + 0.5764705882, 0.14901960784313725, 0.0039215686, 0.0039215686, 0.5921568627, + 0.15294117647058825, 0.0039215686, 0.0039215686, 0.6078431373, 0.1568627450980392, + 0.0039215686, 0.0039215686, 0.6235294118, 0.1607843137254902, 0.0039215686, 0.0039215686, + 0.6392156863, 0.16470588235294117, 0.0039215686, 0.0039215686, 0.6549019608, + 0.16862745098039217, 0.0039215686, 0.0039215686, 0.6705882353, 0.17254901960784313, + 0.0039215686, 0.0039215686, 0.6862745098, 0.17647058823529413, 0.0039215686, 0.0039215686, + 0.7019607843, 0.1803921568627451, 0.0039215686, 0.0039215686, 0.7176470588, + 0.1843137254901961, 0.0039215686, 0.0039215686, 0.7333333333, 0.18823529411764706, + 0.0039215686, 0.0039215686, 0.7490196078, 0.19215686274509805, 0.0039215686, 0.0039215686, + 0.7607843137, 0.19607843137254902, 0.0039215686, 0.0039215686, 0.7764705882, 0.2, + 0.0039215686, 0.0039215686, 0.7921568627, 0.20392156862745098, 0.0039215686, 0.0039215686, + 0.8078431373, 0.20784313725490197, 0.0039215686, 0.0039215686, 0.8235294118, + 0.21176470588235294, 0.0039215686, 0.0039215686, 0.8392156863, 0.21568627450980393, + 0.0039215686, 0.0039215686, 0.8549019608, 0.2196078431372549, 0.0039215686, 0.0039215686, + 0.8705882353, 0.2235294117647059, 0.0039215686, 0.0039215686, 0.8862745098, + 0.22745098039215686, 0.0039215686, 0.0039215686, 0.9019607843, 0.23137254901960785, + 0.0039215686, 0.0039215686, 0.9176470588, 0.23529411764705885, 0.0039215686, 0.0039215686, + 0.9333333333, 0.23921568627450984, 0.0039215686, 0.0039215686, 0.9490196078, + 0.24313725490196078, 0.0039215686, 0.0039215686, 0.9647058824, 0.24705882352941178, + 0.0039215686, 0.0039215686, 0.9803921569, 0.25098039215686274, 0.0039215686, 0.0039215686, + 0.9960784314, 0.2549019607843137, 0.0039215686, 0.0039215686, 0.9960784314, + 0.25882352941176473, 0.0156862745, 0.0039215686, 0.9803921569, 0.2627450980392157, + 0.031372549, 0.0039215686, 0.9647058824, 0.26666666666666666, 0.0470588235, 0.0039215686, + 0.9490196078, 0.27058823529411763, 0.062745098, 0.0039215686, 0.9333333333, + 0.27450980392156865, 0.0784313725, 0.0039215686, 0.9176470588, 0.2784313725490196, + 0.0941176471, 0.0039215686, 0.9019607843, 0.2823529411764706, 0.1098039216, 0.0039215686, + 0.8862745098, 0.28627450980392155, 0.1254901961, 0.0039215686, 0.8705882353, + 0.2901960784313726, 0.1411764706, 0.0039215686, 0.8549019608, 0.29411764705882354, + 0.1568627451, 0.0039215686, 0.8392156863, 0.2980392156862745, 0.1725490196, 0.0039215686, + 0.8235294118, 0.30196078431372547, 0.1882352941, 0.0039215686, 0.8078431373, + 0.3058823529411765, 0.2039215686, 0.0039215686, 0.7921568627, 0.30980392156862746, + 0.2196078431, 0.0039215686, 0.7764705882, 0.3137254901960784, 0.2352941176, 0.0039215686, + 0.7607843137, 0.3176470588235294, 0.2509803922, 0.0039215686, 0.7490196078, + 0.3215686274509804, 0.262745098, 0.0039215686, 0.7333333333, 0.3254901960784314, 0.2784313725, + 0.0039215686, 0.7176470588, 0.32941176470588235, 0.2941176471, 0.0039215686, 0.7019607843, + 0.3333333333333333, 0.3098039216, 0.0039215686, 0.6862745098, 0.33725490196078434, + 0.3254901961, 0.0039215686, 0.6705882353, 0.3411764705882353, 0.3411764706, 0.0039215686, + 0.6549019608, 0.34509803921568627, 0.3568627451, 0.0039215686, 0.6392156863, + 0.34901960784313724, 0.3725490196, 0.0039215686, 0.6235294118, 0.35294117647058826, + 0.3882352941, 0.0039215686, 0.6078431373, 0.3568627450980392, 0.4039215686, 0.0039215686, + 0.5921568627, 0.3607843137254902, 0.4196078431, 0.0039215686, 0.5764705882, + 0.36470588235294116, 0.4352941176, 0.0039215686, 0.5607843137, 0.3686274509803922, + 0.4509803922, 0.0039215686, 0.5450980392, 0.37254901960784315, 0.4666666667, 0.0039215686, + 0.5294117647, 0.3764705882352941, 0.4823529412, 0.0039215686, 0.5137254902, + 0.3803921568627451, 0.4980392157, 0.0039215686, 0.4980392157, 0.3843137254901961, + 0.5137254902, 0.0039215686, 0.4823529412, 0.38823529411764707, 0.5294117647, 0.0039215686, + 0.4666666667, 0.39215686274509803, 0.5450980392, 0.0039215686, 0.4509803922, + 0.396078431372549, 0.5607843137, 0.0039215686, 0.4352941176, 0.4, 0.5764705882, 0.0039215686, + 0.4196078431, 0.403921568627451, 0.5921568627, 0.0039215686, 0.4039215686, + 0.40784313725490196, 0.6078431373, 0.0039215686, 0.3882352941, 0.4117647058823529, + 0.6235294118, 0.0039215686, 0.3725490196, 0.41568627450980394, 0.6392156863, 0.0039215686, + 0.3568627451, 0.4196078431372549, 0.6549019608, 0.0039215686, 0.3411764706, + 0.4235294117647059, 0.6705882353, 0.0039215686, 0.3254901961, 0.42745098039215684, + 0.6862745098, 0.0039215686, 0.3098039216, 0.43137254901960786, 0.7019607843, 0.0039215686, + 0.2941176471, 0.43529411764705883, 0.7176470588, 0.0039215686, 0.2784313725, + 0.4392156862745098, 0.7333333333, 0.0039215686, 0.262745098, 0.44313725490196076, + 0.7490196078, 0.0039215686, 0.2509803922, 0.4470588235294118, 0.7607843137, 0.0039215686, + 0.2352941176, 0.45098039215686275, 0.7764705882, 0.0039215686, 0.2196078431, + 0.4549019607843137, 0.7921568627, 0.0039215686, 0.2039215686, 0.4588235294117647, + 0.8078431373, 0.0039215686, 0.1882352941, 0.4627450980392157, 0.8235294118, 0.0039215686, + 0.1725490196, 0.4666666666666667, 0.8392156863, 0.0039215686, 0.1568627451, + 0.4705882352941177, 0.8549019608, 0.0039215686, 0.1411764706, 0.4745098039215686, + 0.8705882353, 0.0039215686, 0.1254901961, 0.4784313725490197, 0.8862745098, 0.0039215686, + 0.1098039216, 0.48235294117647065, 0.9019607843, 0.0039215686, 0.0941176471, + 0.48627450980392156, 0.9176470588, 0.0039215686, 0.0784313725, 0.49019607843137253, + 0.9333333333, 0.0039215686, 0.062745098, 0.49411764705882355, 0.9490196078, 0.0039215686, + 0.0470588235, 0.4980392156862745, 0.9647058824, 0.0039215686, 0.031372549, 0.5019607843137255, + 0.9803921569, 0.0039215686, 0.0156862745, 0.5058823529411764, 0.9960784314, 0.0039215686, + 0.0039215686, 0.5098039215686274, 0.9960784314, 0.0156862745, 0.0039215686, + 0.5137254901960784, 0.9960784314, 0.031372549, 0.0039215686, 0.5176470588235295, 0.9960784314, + 0.0470588235, 0.0039215686, 0.5215686274509804, 0.9960784314, 0.062745098, 0.0039215686, + 0.5254901960784314, 0.9960784314, 0.0784313725, 0.0039215686, 0.5294117647058824, + 0.9960784314, 0.0941176471, 0.0039215686, 0.5333333333333333, 0.9960784314, 0.1098039216, + 0.0039215686, 0.5372549019607843, 0.9960784314, 0.1254901961, 0.0039215686, + 0.5411764705882353, 0.9960784314, 0.1411764706, 0.0039215686, 0.5450980392156862, + 0.9960784314, 0.1568627451, 0.0039215686, 0.5490196078431373, 0.9960784314, 0.1725490196, + 0.0039215686, 0.5529411764705883, 0.9960784314, 0.1882352941, 0.0039215686, + 0.5568627450980392, 0.9960784314, 0.2039215686, 0.0039215686, 0.5607843137254902, + 0.9960784314, 0.2196078431, 0.0039215686, 0.5647058823529412, 0.9960784314, 0.2352941176, + 0.0039215686, 0.5686274509803921, 0.9960784314, 0.2509803922, 0.0039215686, + 0.5725490196078431, 0.9960784314, 0.262745098, 0.0039215686, 0.5764705882352941, 0.9960784314, + 0.2784313725, 0.0039215686, 0.5803921568627451, 0.9960784314, 0.2941176471, 0.0039215686, + 0.5843137254901961, 0.9960784314, 0.3098039216, 0.0039215686, 0.5882352941176471, + 0.9960784314, 0.3254901961, 0.0039215686, 0.592156862745098, 0.9960784314, 0.3411764706, + 0.0039215686, 0.596078431372549, 0.9960784314, 0.3568627451, 0.0039215686, 0.6, 0.9960784314, + 0.3725490196, 0.0039215686, 0.6039215686274509, 0.9960784314, 0.3882352941, 0.0039215686, + 0.6078431372549019, 0.9960784314, 0.4039215686, 0.0039215686, 0.611764705882353, 0.9960784314, + 0.4196078431, 0.0039215686, 0.615686274509804, 0.9960784314, 0.4352941176, 0.0039215686, + 0.6196078431372549, 0.9960784314, 0.4509803922, 0.0039215686, 0.6235294117647059, + 0.9960784314, 0.4666666667, 0.0039215686, 0.6274509803921569, 0.9960784314, 0.4823529412, + 0.0039215686, 0.6313725490196078, 0.9960784314, 0.4980392157, 0.0039215686, + 0.6352941176470588, 0.9960784314, 0.5137254902, 0.0039215686, 0.6392156862745098, + 0.9960784314, 0.5294117647, 0.0039215686, 0.6431372549019608, 0.9960784314, 0.5450980392, + 0.0039215686, 0.6470588235294118, 0.9960784314, 0.5607843137, 0.0039215686, + 0.6509803921568628, 0.9960784314, 0.5764705882, 0.0039215686, 0.6549019607843137, + 0.9960784314, 0.5921568627, 0.0039215686, 0.6588235294117647, 0.9960784314, 0.6078431373, + 0.0039215686, 0.6627450980392157, 0.9960784314, 0.6235294118, 0.0039215686, + 0.6666666666666666, 0.9960784314, 0.6392156863, 0.0039215686, 0.6705882352941176, + 0.9960784314, 0.6549019608, 0.0039215686, 0.6745098039215687, 0.9960784314, 0.6705882353, + 0.0039215686, 0.6784313725490196, 0.9960784314, 0.6862745098, 0.0039215686, + 0.6823529411764706, 0.9960784314, 0.7019607843, 0.0039215686, 0.6862745098039216, + 0.9960784314, 0.7176470588, 0.0039215686, 0.6901960784313725, 0.9960784314, 0.7333333333, + 0.0039215686, 0.6941176470588235, 0.9960784314, 0.7490196078, 0.0039215686, + 0.6980392156862745, 0.9960784314, 0.7607843137, 0.0039215686, 0.7019607843137254, + 0.9960784314, 0.7764705882, 0.0039215686, 0.7058823529411765, 0.9960784314, 0.7921568627, + 0.0039215686, 0.7098039215686275, 0.9960784314, 0.8078431373, 0.0039215686, + 0.7137254901960784, 0.9960784314, 0.8235294118, 0.0039215686, 0.7176470588235294, + 0.9960784314, 0.8392156863, 0.0039215686, 0.7215686274509804, 0.9960784314, 0.8549019608, + 0.0039215686, 0.7254901960784313, 0.9960784314, 0.8705882353, 0.0039215686, + 0.7294117647058823, 0.9960784314, 0.8862745098, 0.0039215686, 0.7333333333333333, + 0.9960784314, 0.9019607843, 0.0039215686, 0.7372549019607844, 0.9960784314, 0.9176470588, + 0.0039215686, 0.7411764705882353, 0.9960784314, 0.9333333333, 0.0039215686, + 0.7450980392156863, 0.9960784314, 0.9490196078, 0.0039215686, 0.7490196078431373, + 0.9960784314, 0.9647058824, 0.0039215686, 0.7529411764705882, 0.9960784314, 0.9803921569, + 0.0039215686, 0.7568627450980392, 0.9960784314, 0.9960784314, 0.0039215686, + 0.7607843137254902, 0.9960784314, 0.9960784314, 0.0196078431, 0.7647058823529411, + 0.9960784314, 0.9960784314, 0.0352941176, 0.7686274509803922, 0.9960784314, 0.9960784314, + 0.0509803922, 0.7725490196078432, 0.9960784314, 0.9960784314, 0.0666666667, + 0.7764705882352941, 0.9960784314, 0.9960784314, 0.0823529412, 0.7803921568627451, + 0.9960784314, 0.9960784314, 0.0980392157, 0.7843137254901961, 0.9960784314, 0.9960784314, + 0.1137254902, 0.788235294117647, 0.9960784314, 0.9960784314, 0.1294117647, 0.792156862745098, + 0.9960784314, 0.9960784314, 0.1450980392, 0.796078431372549, 0.9960784314, 0.9960784314, + 0.1607843137, 0.8, 0.9960784314, 0.9960784314, 0.1764705882, 0.803921568627451, 0.9960784314, + 0.9960784314, 0.1921568627, 0.807843137254902, 0.9960784314, 0.9960784314, 0.2078431373, + 0.8117647058823529, 0.9960784314, 0.9960784314, 0.2235294118, 0.8156862745098039, + 0.9960784314, 0.9960784314, 0.2392156863, 0.8196078431372549, 0.9960784314, 0.9960784314, + 0.2509803922, 0.8235294117647058, 0.9960784314, 0.9960784314, 0.2666666667, + 0.8274509803921568, 0.9960784314, 0.9960784314, 0.2823529412, 0.8313725490196079, + 0.9960784314, 0.9960784314, 0.2980392157, 0.8352941176470589, 0.9960784314, 0.9960784314, + 0.3137254902, 0.8392156862745098, 0.9960784314, 0.9960784314, 0.3333333333, + 0.8431372549019608, 0.9960784314, 0.9960784314, 0.3490196078, 0.8470588235294118, + 0.9960784314, 0.9960784314, 0.3647058824, 0.8509803921568627, 0.9960784314, 0.9960784314, + 0.3803921569, 0.8549019607843137, 0.9960784314, 0.9960784314, 0.3960784314, + 0.8588235294117647, 0.9960784314, 0.9960784314, 0.4117647059, 0.8627450980392157, + 0.9960784314, 0.9960784314, 0.4274509804, 0.8666666666666667, 0.9960784314, 0.9960784314, + 0.4431372549, 0.8705882352941177, 0.9960784314, 0.9960784314, 0.4588235294, + 0.8745098039215686, 0.9960784314, 0.9960784314, 0.4745098039, 0.8784313725490196, + 0.9960784314, 0.9960784314, 0.4901960784, 0.8823529411764706, 0.9960784314, 0.9960784314, + 0.5058823529, 0.8862745098039215, 0.9960784314, 0.9960784314, 0.5215686275, + 0.8901960784313725, 0.9960784314, 0.9960784314, 0.537254902, 0.8941176470588236, 0.9960784314, + 0.9960784314, 0.5529411765, 0.8980392156862745, 0.9960784314, 0.9960784314, 0.568627451, + 0.9019607843137255, 0.9960784314, 0.9960784314, 0.5843137255, 0.9058823529411765, + 0.9960784314, 0.9960784314, 0.6, 0.9098039215686274, 0.9960784314, 0.9960784314, 0.6156862745, + 0.9137254901960784, 0.9960784314, 0.9960784314, 0.631372549, 0.9176470588235294, 0.9960784314, + 0.9960784314, 0.6470588235, 0.9215686274509803, 0.9960784314, 0.9960784314, 0.6666666667, + 0.9254901960784314, 0.9960784314, 0.9960784314, 0.6823529412, 0.9294117647058824, + 0.9960784314, 0.9960784314, 0.6980392157, 0.9333333333333333, 0.9960784314, 0.9960784314, + 0.7137254902, 0.9372549019607843, 0.9960784314, 0.9960784314, 0.7294117647, + 0.9411764705882354, 0.9960784314, 0.9960784314, 0.7450980392, 0.9450980392156864, + 0.9960784314, 0.9960784314, 0.7568627451, 0.9490196078431372, 0.9960784314, 0.9960784314, + 0.7725490196, 0.9529411764705882, 0.9960784314, 0.9960784314, 0.7882352941, + 0.9568627450980394, 0.9960784314, 0.9960784314, 0.8039215686, 0.9607843137254903, + 0.9960784314, 0.9960784314, 0.8196078431, 0.9647058823529413, 0.9960784314, 0.9960784314, + 0.8352941176, 0.9686274509803922, 0.9960784314, 0.9960784314, 0.8509803922, + 0.9725490196078431, 0.9960784314, 0.9960784314, 0.8666666667, 0.9764705882352941, + 0.9960784314, 0.9960784314, 0.8823529412, 0.9803921568627451, 0.9960784314, 0.9960784314, + 0.8980392157, 0.984313725490196, 0.9960784314, 0.9960784314, 0.9137254902, 0.9882352941176471, + 0.9960784314, 0.9960784314, 0.9294117647, 0.9921568627450981, 0.9960784314, 0.9960784314, + 0.9450980392, 0.996078431372549, 0.9960784314, 0.9960784314, 0.9607843137, 1.0, 0.9960784314, + 0.9960784314, 0.9607843137, + ], + }, + { + ColorSpace: 'RGB', + Name: 'red_hot', + RGBPoints: [ + 0.0, 0.0, 0.0, 0.0, 0.00392156862745098, 0.0, 0.0, 0.0, 0.00784313725490196, 0.0, 0.0, 0.0, + 0.011764705882352941, 0.0, 0.0, 0.0, 0.01568627450980392, 0.0039215686, 0.0039215686, + 0.0039215686, 0.0196078431372549, 0.0039215686, 0.0039215686, 0.0039215686, + 0.023529411764705882, 0.0039215686, 0.0039215686, 0.0039215686, 0.027450980392156862, + 0.0039215686, 0.0039215686, 0.0039215686, 0.03137254901960784, 0.0039215686, 0.0039215686, + 0.0039215686, 0.03529411764705882, 0.0156862745, 0.0, 0.0, 0.0392156862745098, 0.0274509804, + 0.0, 0.0, 0.043137254901960784, 0.0392156863, 0.0, 0.0, 0.047058823529411764, 0.0509803922, + 0.0, 0.0, 0.050980392156862744, 0.062745098, 0.0, 0.0, 0.054901960784313725, 0.0784313725, + 0.0, 0.0, 0.05882352941176471, 0.0901960784, 0.0, 0.0, 0.06274509803921569, 0.1058823529, 0.0, + 0.0, 0.06666666666666667, 0.1176470588, 0.0, 0.0, 0.07058823529411765, 0.1294117647, 0.0, 0.0, + 0.07450980392156863, 0.1411764706, 0.0, 0.0, 0.0784313725490196, 0.1529411765, 0.0, 0.0, + 0.08235294117647059, 0.1647058824, 0.0, 0.0, 0.08627450980392157, 0.1764705882, 0.0, 0.0, + 0.09019607843137255, 0.1882352941, 0.0, 0.0, 0.09411764705882353, 0.2039215686, 0.0, 0.0, + 0.09803921568627451, 0.2156862745, 0.0, 0.0, 0.10196078431372549, 0.2274509804, 0.0, 0.0, + 0.10588235294117647, 0.2392156863, 0.0, 0.0, 0.10980392156862745, 0.2549019608, 0.0, 0.0, + 0.11372549019607843, 0.2666666667, 0.0, 0.0, 0.11764705882352942, 0.2784313725, 0.0, 0.0, + 0.12156862745098039, 0.2901960784, 0.0, 0.0, 0.12549019607843137, 0.3058823529, 0.0, 0.0, + 0.12941176470588237, 0.3176470588, 0.0, 0.0, 0.13333333333333333, 0.3294117647, 0.0, 0.0, + 0.13725490196078433, 0.3411764706, 0.0, 0.0, 0.1411764705882353, 0.3529411765, 0.0, 0.0, + 0.1450980392156863, 0.3647058824, 0.0, 0.0, 0.14901960784313725, 0.3764705882, 0.0, 0.0, + 0.15294117647058825, 0.3882352941, 0.0, 0.0, 0.1568627450980392, 0.4039215686, 0.0, 0.0, + 0.1607843137254902, 0.4156862745, 0.0, 0.0, 0.16470588235294117, 0.431372549, 0.0, 0.0, + 0.16862745098039217, 0.4431372549, 0.0, 0.0, 0.17254901960784313, 0.4588235294, 0.0, 0.0, + 0.17647058823529413, 0.4705882353, 0.0, 0.0, 0.1803921568627451, 0.4823529412, 0.0, 0.0, + 0.1843137254901961, 0.4941176471, 0.0, 0.0, 0.18823529411764706, 0.5098039216, 0.0, 0.0, + 0.19215686274509805, 0.5215686275, 0.0, 0.0, 0.19607843137254902, 0.5333333333, 0.0, 0.0, 0.2, + 0.5450980392, 0.0, 0.0, 0.20392156862745098, 0.5568627451, 0.0, 0.0, 0.20784313725490197, + 0.568627451, 0.0, 0.0, 0.21176470588235294, 0.5803921569, 0.0, 0.0, 0.21568627450980393, + 0.5921568627, 0.0, 0.0, 0.2196078431372549, 0.6078431373, 0.0, 0.0, 0.2235294117647059, + 0.6196078431, 0.0, 0.0, 0.22745098039215686, 0.631372549, 0.0, 0.0, 0.23137254901960785, + 0.6431372549, 0.0, 0.0, 0.23529411764705885, 0.6588235294, 0.0, 0.0, 0.23921568627450984, + 0.6705882353, 0.0, 0.0, 0.24313725490196078, 0.6823529412, 0.0, 0.0, 0.24705882352941178, + 0.6941176471, 0.0, 0.0, 0.25098039215686274, 0.7098039216, 0.0, 0.0, 0.2549019607843137, + 0.7215686275, 0.0, 0.0, 0.25882352941176473, 0.7333333333, 0.0, 0.0, 0.2627450980392157, + 0.7450980392, 0.0, 0.0, 0.26666666666666666, 0.7568627451, 0.0, 0.0, 0.27058823529411763, + 0.768627451, 0.0, 0.0, 0.27450980392156865, 0.7843137255, 0.0, 0.0, 0.2784313725490196, + 0.7960784314, 0.0, 0.0, 0.2823529411764706, 0.8117647059, 0.0, 0.0, 0.28627450980392155, + 0.8235294118, 0.0, 0.0, 0.2901960784313726, 0.8352941176, 0.0, 0.0, 0.29411764705882354, + 0.8470588235, 0.0, 0.0, 0.2980392156862745, 0.862745098, 0.0, 0.0, 0.30196078431372547, + 0.8745098039, 0.0, 0.0, 0.3058823529411765, 0.8862745098, 0.0, 0.0, 0.30980392156862746, + 0.8980392157, 0.0, 0.0, 0.3137254901960784, 0.9137254902, 0.0, 0.0, 0.3176470588235294, + 0.9254901961, 0.0, 0.0, 0.3215686274509804, 0.937254902, 0.0, 0.0, 0.3254901960784314, + 0.9490196078, 0.0, 0.0, 0.32941176470588235, 0.9607843137, 0.0, 0.0, 0.3333333333333333, + 0.968627451, 0.0, 0.0, 0.33725490196078434, 0.9803921569, 0.0039215686, 0.0, + 0.3411764705882353, 0.9882352941, 0.0078431373, 0.0, 0.34509803921568627, 1.0, 0.0117647059, + 0.0, 0.34901960784313724, 1.0, 0.0235294118, 0.0, 0.35294117647058826, 1.0, 0.0352941176, 0.0, + 0.3568627450980392, 1.0, 0.0470588235, 0.0, 0.3607843137254902, 1.0, 0.062745098, 0.0, + 0.36470588235294116, 1.0, 0.0745098039, 0.0, 0.3686274509803922, 1.0, 0.0862745098, 0.0, + 0.37254901960784315, 1.0, 0.0980392157, 0.0, 0.3764705882352941, 1.0, 0.1137254902, 0.0, + 0.3803921568627451, 1.0, 0.1254901961, 0.0, 0.3843137254901961, 1.0, 0.137254902, 0.0, + 0.38823529411764707, 1.0, 0.1490196078, 0.0, 0.39215686274509803, 1.0, 0.1647058824, 0.0, + 0.396078431372549, 1.0, 0.1764705882, 0.0, 0.4, 1.0, 0.1882352941, 0.0, 0.403921568627451, + 1.0, 0.2, 0.0, 0.40784313725490196, 1.0, 0.2156862745, 0.0, 0.4117647058823529, 1.0, + 0.2274509804, 0.0, 0.41568627450980394, 1.0, 0.2392156863, 0.0, 0.4196078431372549, 1.0, + 0.2509803922, 0.0, 0.4235294117647059, 1.0, 0.2666666667, 0.0, 0.42745098039215684, 1.0, + 0.2784313725, 0.0, 0.43137254901960786, 1.0, 0.2901960784, 0.0, 0.43529411764705883, 1.0, + 0.3019607843, 0.0, 0.4392156862745098, 1.0, 0.3176470588, 0.0, 0.44313725490196076, 1.0, + 0.3294117647, 0.0, 0.4470588235294118, 1.0, 0.3411764706, 0.0, 0.45098039215686275, 1.0, + 0.3529411765, 0.0, 0.4549019607843137, 1.0, 0.368627451, 0.0, 0.4588235294117647, 1.0, + 0.3803921569, 0.0, 0.4627450980392157, 1.0, 0.3921568627, 0.0, 0.4666666666666667, 1.0, + 0.4039215686, 0.0, 0.4705882352941177, 1.0, 0.4156862745, 0.0, 0.4745098039215686, 1.0, + 0.4274509804, 0.0, 0.4784313725490197, 1.0, 0.4392156863, 0.0, 0.48235294117647065, 1.0, + 0.4509803922, 0.0, 0.48627450980392156, 1.0, 0.4666666667, 0.0, 0.49019607843137253, 1.0, + 0.4784313725, 0.0, 0.49411764705882355, 1.0, 0.4941176471, 0.0, 0.4980392156862745, 1.0, + 0.5058823529, 0.0, 0.5019607843137255, 1.0, 0.5215686275, 0.0, 0.5058823529411764, 1.0, + 0.5333333333, 0.0, 0.5098039215686274, 1.0, 0.5450980392, 0.0, 0.5137254901960784, 1.0, + 0.5568627451, 0.0, 0.5176470588235295, 1.0, 0.568627451, 0.0, 0.5215686274509804, 1.0, + 0.5803921569, 0.0, 0.5254901960784314, 1.0, 0.5921568627, 0.0, 0.5294117647058824, 1.0, + 0.6039215686, 0.0, 0.5333333333333333, 1.0, 0.6196078431, 0.0, 0.5372549019607843, 1.0, + 0.631372549, 0.0, 0.5411764705882353, 1.0, 0.6431372549, 0.0, 0.5450980392156862, 1.0, + 0.6549019608, 0.0, 0.5490196078431373, 1.0, 0.6705882353, 0.0, 0.5529411764705883, 1.0, + 0.6823529412, 0.0, 0.5568627450980392, 1.0, 0.6941176471, 0.0, 0.5607843137254902, 1.0, + 0.7058823529, 0.0, 0.5647058823529412, 1.0, 0.7215686275, 0.0, 0.5686274509803921, 1.0, + 0.7333333333, 0.0, 0.5725490196078431, 1.0, 0.7450980392, 0.0, 0.5764705882352941, 1.0, + 0.7568627451, 0.0, 0.5803921568627451, 1.0, 0.7725490196, 0.0, 0.5843137254901961, 1.0, + 0.7843137255, 0.0, 0.5882352941176471, 1.0, 0.7960784314, 0.0, 0.592156862745098, 1.0, + 0.8078431373, 0.0, 0.596078431372549, 1.0, 0.8196078431, 0.0, 0.6, 1.0, 0.831372549, 0.0, + 0.6039215686274509, 1.0, 0.8470588235, 0.0, 0.6078431372549019, 1.0, 0.8588235294, 0.0, + 0.611764705882353, 1.0, 0.8745098039, 0.0, 0.615686274509804, 1.0, 0.8862745098, 0.0, + 0.6196078431372549, 1.0, 0.8980392157, 0.0, 0.6235294117647059, 1.0, 0.9098039216, 0.0, + 0.6274509803921569, 1.0, 0.9254901961, 0.0, 0.6313725490196078, 1.0, 0.937254902, 0.0, + 0.6352941176470588, 1.0, 0.9490196078, 0.0, 0.6392156862745098, 1.0, 0.9607843137, 0.0, + 0.6431372549019608, 1.0, 0.9764705882, 0.0, 0.6470588235294118, 1.0, 0.9803921569, + 0.0039215686, 0.6509803921568628, 1.0, 0.9882352941, 0.0117647059, 0.6549019607843137, 1.0, + 0.9921568627, 0.0156862745, 0.6588235294117647, 1.0, 1.0, 0.0235294118, 0.6627450980392157, + 1.0, 1.0, 0.0352941176, 0.6666666666666666, 1.0, 1.0, 0.0470588235, 0.6705882352941176, 1.0, + 1.0, 0.0588235294, 0.6745098039215687, 1.0, 1.0, 0.0745098039, 0.6784313725490196, 1.0, 1.0, + 0.0862745098, 0.6823529411764706, 1.0, 1.0, 0.0980392157, 0.6862745098039216, 1.0, 1.0, + 0.1098039216, 0.6901960784313725, 1.0, 1.0, 0.1254901961, 0.6941176470588235, 1.0, 1.0, + 0.137254902, 0.6980392156862745, 1.0, 1.0, 0.1490196078, 0.7019607843137254, 1.0, 1.0, + 0.1607843137, 0.7058823529411765, 1.0, 1.0, 0.1764705882, 0.7098039215686275, 1.0, 1.0, + 0.1882352941, 0.7137254901960784, 1.0, 1.0, 0.2, 0.7176470588235294, 1.0, 1.0, 0.2117647059, + 0.7215686274509804, 1.0, 1.0, 0.2274509804, 0.7254901960784313, 1.0, 1.0, 0.2392156863, + 0.7294117647058823, 1.0, 1.0, 0.2509803922, 0.7333333333333333, 1.0, 1.0, 0.262745098, + 0.7372549019607844, 1.0, 1.0, 0.2784313725, 0.7411764705882353, 1.0, 1.0, 0.2901960784, + 0.7450980392156863, 1.0, 1.0, 0.3019607843, 0.7490196078431373, 1.0, 1.0, 0.3137254902, + 0.7529411764705882, 1.0, 1.0, 0.3294117647, 0.7568627450980392, 1.0, 1.0, 0.3411764706, + 0.7607843137254902, 1.0, 1.0, 0.3529411765, 0.7647058823529411, 1.0, 1.0, 0.3647058824, + 0.7686274509803922, 1.0, 1.0, 0.3803921569, 0.7725490196078432, 1.0, 1.0, 0.3921568627, + 0.7764705882352941, 1.0, 1.0, 0.4039215686, 0.7803921568627451, 1.0, 1.0, 0.4156862745, + 0.7843137254901961, 1.0, 1.0, 0.431372549, 0.788235294117647, 1.0, 1.0, 0.4431372549, + 0.792156862745098, 1.0, 1.0, 0.4549019608, 0.796078431372549, 1.0, 1.0, 0.4666666667, 0.8, + 1.0, 1.0, 0.4784313725, 0.803921568627451, 1.0, 1.0, 0.4901960784, 0.807843137254902, 1.0, + 1.0, 0.5019607843, 0.8117647058823529, 1.0, 1.0, 0.5137254902, 0.8156862745098039, 1.0, 1.0, + 0.5294117647, 0.8196078431372549, 1.0, 1.0, 0.5411764706, 0.8235294117647058, 1.0, 1.0, + 0.5568627451, 0.8274509803921568, 1.0, 1.0, 0.568627451, 0.8313725490196079, 1.0, 1.0, + 0.5843137255, 0.8352941176470589, 1.0, 1.0, 0.5960784314, 0.8392156862745098, 1.0, 1.0, + 0.6078431373, 0.8431372549019608, 1.0, 1.0, 0.6196078431, 0.8470588235294118, 1.0, 1.0, + 0.631372549, 0.8509803921568627, 1.0, 1.0, 0.6431372549, 0.8549019607843137, 1.0, 1.0, + 0.6549019608, 0.8588235294117647, 1.0, 1.0, 0.6666666667, 0.8627450980392157, 1.0, 1.0, + 0.6823529412, 0.8666666666666667, 1.0, 1.0, 0.6941176471, 0.8705882352941177, 1.0, 1.0, + 0.7058823529, 0.8745098039215686, 1.0, 1.0, 0.7176470588, 0.8784313725490196, 1.0, 1.0, + 0.7333333333, 0.8823529411764706, 1.0, 1.0, 0.7450980392, 0.8862745098039215, 1.0, 1.0, + 0.7568627451, 0.8901960784313725, 1.0, 1.0, 0.768627451, 0.8941176470588236, 1.0, 1.0, + 0.7843137255, 0.8980392156862745, 1.0, 1.0, 0.7960784314, 0.9019607843137255, 1.0, 1.0, + 0.8078431373, 0.9058823529411765, 1.0, 1.0, 0.8196078431, 0.9098039215686274, 1.0, 1.0, + 0.8352941176, 0.9137254901960784, 1.0, 1.0, 0.8470588235, 0.9176470588235294, 1.0, 1.0, + 0.8588235294, 0.9215686274509803, 1.0, 1.0, 0.8705882353, 0.9254901960784314, 1.0, 1.0, + 0.8823529412, 0.9294117647058824, 1.0, 1.0, 0.8941176471, 0.9333333333333333, 1.0, 1.0, + 0.9098039216, 0.9372549019607843, 1.0, 1.0, 0.9215686275, 0.9411764705882354, 1.0, 1.0, + 0.937254902, 0.9450980392156864, 1.0, 1.0, 0.9490196078, 0.9490196078431372, 1.0, 1.0, + 0.9607843137, 0.9529411764705882, 1.0, 1.0, 0.9725490196, 0.9568627450980394, 1.0, 1.0, + 0.9882352941, 0.9607843137254903, 1.0, 1.0, 0.9882352941, 0.9647058823529413, 1.0, 1.0, + 0.9921568627, 0.9686274509803922, 1.0, 1.0, 0.9960784314, 0.9725490196078431, 1.0, 1.0, 1.0, + 0.9764705882352941, 1.0, 1.0, 1.0, 0.9803921568627451, 1.0, 1.0, 1.0, 0.984313725490196, 1.0, + 1.0, 1.0, 0.9882352941176471, 1.0, 1.0, 1.0, 0.9921568627450981, 1.0, 1.0, 1.0, + 0.996078431372549, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + ], + }, + { + ColorSpace: 'RGB', + Name: 's_pet', + RGBPoints: [ + 0.0, 0.0156862745, 0.0039215686, 0.0156862745, 0.00392156862745098, 0.0156862745, + 0.0039215686, 0.0156862745, 0.00784313725490196, 0.0274509804, 0.0039215686, 0.031372549, + 0.011764705882352941, 0.0352941176, 0.0039215686, 0.0509803922, 0.01568627450980392, + 0.0392156863, 0.0039215686, 0.0666666667, 0.0196078431372549, 0.0509803922, 0.0039215686, + 0.0823529412, 0.023529411764705882, 0.062745098, 0.0039215686, 0.0980392157, + 0.027450980392156862, 0.0705882353, 0.0039215686, 0.1176470588, 0.03137254901960784, + 0.0745098039, 0.0039215686, 0.1333333333, 0.03529411764705882, 0.0862745098, 0.0039215686, + 0.1490196078, 0.0392156862745098, 0.0980392157, 0.0039215686, 0.1647058824, + 0.043137254901960784, 0.1058823529, 0.0039215686, 0.1843137255, 0.047058823529411764, + 0.1098039216, 0.0039215686, 0.2, 0.050980392156862744, 0.1215686275, 0.0039215686, + 0.2156862745, 0.054901960784313725, 0.1333333333, 0.0039215686, 0.231372549, + 0.05882352941176471, 0.137254902, 0.0039215686, 0.2509803922, 0.06274509803921569, + 0.1490196078, 0.0039215686, 0.262745098, 0.06666666666666667, 0.1607843137, 0.0039215686, + 0.2784313725, 0.07058823529411765, 0.168627451, 0.0039215686, 0.2941176471, + 0.07450980392156863, 0.1725490196, 0.0039215686, 0.3137254902, 0.0784313725490196, + 0.1843137255, 0.0039215686, 0.3294117647, 0.08235294117647059, 0.1960784314, 0.0039215686, + 0.3450980392, 0.08627450980392157, 0.2039215686, 0.0039215686, 0.3607843137, + 0.09019607843137255, 0.2078431373, 0.0039215686, 0.3803921569, 0.09411764705882353, + 0.2196078431, 0.0039215686, 0.3960784314, 0.09803921568627451, 0.231372549, 0.0039215686, + 0.4117647059, 0.10196078431372549, 0.2392156863, 0.0039215686, 0.4274509804, + 0.10588235294117647, 0.2431372549, 0.0039215686, 0.4470588235, 0.10980392156862745, + 0.2509803922, 0.0039215686, 0.462745098, 0.11372549019607843, 0.262745098, 0.0039215686, + 0.4784313725, 0.11764705882352942, 0.2666666667, 0.0039215686, 0.4980392157, + 0.12156862745098039, 0.2666666667, 0.0039215686, 0.4980392157, 0.12549019607843137, + 0.262745098, 0.0039215686, 0.5137254902, 0.12941176470588237, 0.2509803922, 0.0039215686, + 0.5294117647, 0.13333333333333333, 0.2431372549, 0.0039215686, 0.5450980392, + 0.13725490196078433, 0.2392156863, 0.0039215686, 0.5607843137, 0.1411764705882353, + 0.231372549, 0.0039215686, 0.5764705882, 0.1450980392156863, 0.2196078431, 0.0039215686, + 0.5921568627, 0.14901960784313725, 0.2078431373, 0.0039215686, 0.6078431373, + 0.15294117647058825, 0.2039215686, 0.0039215686, 0.6235294118, 0.1568627450980392, + 0.1960784314, 0.0039215686, 0.6392156863, 0.1607843137254902, 0.1843137255, 0.0039215686, + 0.6549019608, 0.16470588235294117, 0.1725490196, 0.0039215686, 0.6705882353, + 0.16862745098039217, 0.168627451, 0.0039215686, 0.6862745098, 0.17254901960784313, + 0.1607843137, 0.0039215686, 0.7019607843, 0.17647058823529413, 0.1490196078, 0.0039215686, + 0.7176470588, 0.1803921568627451, 0.137254902, 0.0039215686, 0.7333333333, 0.1843137254901961, + 0.1333333333, 0.0039215686, 0.7490196078, 0.18823529411764706, 0.1215686275, 0.0039215686, + 0.7607843137, 0.19215686274509805, 0.1098039216, 0.0039215686, 0.7764705882, + 0.19607843137254902, 0.1058823529, 0.0039215686, 0.7921568627, 0.2, 0.0980392157, + 0.0039215686, 0.8078431373, 0.20392156862745098, 0.0862745098, 0.0039215686, 0.8235294118, + 0.20784313725490197, 0.0745098039, 0.0039215686, 0.8392156863, 0.21176470588235294, + 0.0705882353, 0.0039215686, 0.8549019608, 0.21568627450980393, 0.062745098, 0.0039215686, + 0.8705882353, 0.2196078431372549, 0.0509803922, 0.0039215686, 0.8862745098, + 0.2235294117647059, 0.0392156863, 0.0039215686, 0.9019607843, 0.22745098039215686, + 0.0352941176, 0.0039215686, 0.9176470588, 0.23137254901960785, 0.0274509804, 0.0039215686, + 0.9333333333, 0.23529411764705885, 0.0156862745, 0.0039215686, 0.9490196078, + 0.23921568627450984, 0.0078431373, 0.0039215686, 0.9647058824, 0.24313725490196078, + 0.0039215686, 0.0039215686, 0.9960784314, 0.24705882352941178, 0.0039215686, 0.0039215686, + 0.9960784314, 0.25098039215686274, 0.0039215686, 0.0196078431, 0.9647058824, + 0.2549019607843137, 0.0039215686, 0.0392156863, 0.9490196078, 0.25882352941176473, + 0.0039215686, 0.0549019608, 0.9333333333, 0.2627450980392157, 0.0039215686, 0.0745098039, + 0.9176470588, 0.26666666666666666, 0.0039215686, 0.0901960784, 0.9019607843, + 0.27058823529411763, 0.0039215686, 0.1098039216, 0.8862745098, 0.27450980392156865, + 0.0039215686, 0.1254901961, 0.8705882353, 0.2784313725490196, 0.0039215686, 0.1450980392, + 0.8549019608, 0.2823529411764706, 0.0039215686, 0.1607843137, 0.8392156863, + 0.28627450980392155, 0.0039215686, 0.1803921569, 0.8235294118, 0.2901960784313726, + 0.0039215686, 0.1960784314, 0.8078431373, 0.29411764705882354, 0.0039215686, 0.2156862745, + 0.7921568627, 0.2980392156862745, 0.0039215686, 0.231372549, 0.7764705882, + 0.30196078431372547, 0.0039215686, 0.2509803922, 0.7607843137, 0.3058823529411765, + 0.0039215686, 0.262745098, 0.7490196078, 0.30980392156862746, 0.0039215686, 0.2823529412, + 0.7333333333, 0.3137254901960784, 0.0039215686, 0.2980392157, 0.7176470588, + 0.3176470588235294, 0.0039215686, 0.3176470588, 0.7019607843, 0.3215686274509804, + 0.0039215686, 0.3333333333, 0.6862745098, 0.3254901960784314, 0.0039215686, 0.3529411765, + 0.6705882353, 0.32941176470588235, 0.0039215686, 0.368627451, 0.6549019608, + 0.3333333333333333, 0.0039215686, 0.3882352941, 0.6392156863, 0.33725490196078434, + 0.0039215686, 0.4039215686, 0.6235294118, 0.3411764705882353, 0.0039215686, 0.4235294118, + 0.6078431373, 0.34509803921568627, 0.0039215686, 0.4392156863, 0.5921568627, + 0.34901960784313724, 0.0039215686, 0.4588235294, 0.5764705882, 0.35294117647058826, + 0.0039215686, 0.4745098039, 0.5607843137, 0.3568627450980392, 0.0039215686, 0.4941176471, + 0.5450980392, 0.3607843137254902, 0.0039215686, 0.5098039216, 0.5294117647, + 0.36470588235294116, 0.0039215686, 0.5294117647, 0.5137254902, 0.3686274509803922, + 0.0039215686, 0.5450980392, 0.4980392157, 0.37254901960784315, 0.0039215686, 0.5647058824, + 0.4784313725, 0.3764705882352941, 0.0039215686, 0.5803921569, 0.462745098, 0.3803921568627451, + 0.0039215686, 0.6, 0.4470588235, 0.3843137254901961, 0.0039215686, 0.6156862745, 0.4274509804, + 0.38823529411764707, 0.0039215686, 0.6352941176, 0.4117647059, 0.39215686274509803, + 0.0039215686, 0.6509803922, 0.3960784314, 0.396078431372549, 0.0039215686, 0.6705882353, + 0.3803921569, 0.4, 0.0039215686, 0.6862745098, 0.3607843137, 0.403921568627451, 0.0039215686, + 0.7058823529, 0.3450980392, 0.40784313725490196, 0.0039215686, 0.7215686275, 0.3294117647, + 0.4117647058823529, 0.0039215686, 0.7411764706, 0.3137254902, 0.41568627450980394, + 0.0039215686, 0.7529411765, 0.2941176471, 0.4196078431372549, 0.0039215686, 0.7960784314, + 0.2784313725, 0.4235294117647059, 0.0039215686, 0.7960784314, 0.262745098, + 0.42745098039215684, 0.0392156863, 0.8039215686, 0.2509803922, 0.43137254901960786, + 0.0745098039, 0.8117647059, 0.231372549, 0.43529411764705883, 0.1098039216, 0.8196078431, + 0.2156862745, 0.4392156862745098, 0.1450980392, 0.8274509804, 0.2, 0.44313725490196076, + 0.1803921569, 0.8352941176, 0.1843137255, 0.4470588235294118, 0.2156862745, 0.8431372549, + 0.1647058824, 0.45098039215686275, 0.2509803922, 0.8509803922, 0.1490196078, + 0.4549019607843137, 0.2823529412, 0.8588235294, 0.1333333333, 0.4588235294117647, + 0.3176470588, 0.8666666667, 0.1176470588, 0.4627450980392157, 0.3529411765, 0.8745098039, + 0.0980392157, 0.4666666666666667, 0.3882352941, 0.8823529412, 0.0823529412, + 0.4705882352941177, 0.4235294118, 0.8901960784, 0.0666666667, 0.4745098039215686, + 0.4588235294, 0.8980392157, 0.0509803922, 0.4784313725490197, 0.4941176471, 0.9058823529, + 0.0431372549, 0.48235294117647065, 0.5294117647, 0.9137254902, 0.031372549, + 0.48627450980392156, 0.5647058824, 0.9215686275, 0.0196078431, 0.49019607843137253, 0.6, + 0.9294117647, 0.0078431373, 0.49411764705882355, 0.6352941176, 0.937254902, 0.0039215686, + 0.4980392156862745, 0.6705882353, 0.9450980392, 0.0039215686, 0.5019607843137255, + 0.7058823529, 0.9490196078, 0.0039215686, 0.5058823529411764, 0.7411764706, 0.9568627451, + 0.0039215686, 0.5098039215686274, 0.7725490196, 0.9607843137, 0.0039215686, + 0.5137254901960784, 0.8078431373, 0.968627451, 0.0039215686, 0.5176470588235295, 0.8431372549, + 0.9725490196, 0.0039215686, 0.5215686274509804, 0.8784313725, 0.9803921569, 0.0039215686, + 0.5254901960784314, 0.9137254902, 0.9843137255, 0.0039215686, 0.5294117647058824, + 0.9490196078, 0.9921568627, 0.0039215686, 0.5333333333333333, 0.9960784314, 0.9960784314, + 0.0039215686, 0.5372549019607843, 0.9960784314, 0.9960784314, 0.0039215686, + 0.5411764705882353, 0.9960784314, 0.9921568627, 0.0039215686, 0.5450980392156862, + 0.9960784314, 0.9843137255, 0.0039215686, 0.5490196078431373, 0.9960784314, 0.9764705882, + 0.0039215686, 0.5529411764705883, 0.9960784314, 0.968627451, 0.0039215686, 0.5568627450980392, + 0.9960784314, 0.9607843137, 0.0039215686, 0.5607843137254902, 0.9960784314, 0.9529411765, + 0.0039215686, 0.5647058823529412, 0.9960784314, 0.9450980392, 0.0039215686, + 0.5686274509803921, 0.9960784314, 0.937254902, 0.0039215686, 0.5725490196078431, 0.9960784314, + 0.9294117647, 0.0039215686, 0.5764705882352941, 0.9960784314, 0.9215686275, 0.0039215686, + 0.5803921568627451, 0.9960784314, 0.9137254902, 0.0039215686, 0.5843137254901961, + 0.9960784314, 0.9058823529, 0.0039215686, 0.5882352941176471, 0.9960784314, 0.8980392157, + 0.0039215686, 0.592156862745098, 0.9960784314, 0.8901960784, 0.0039215686, 0.596078431372549, + 0.9960784314, 0.8823529412, 0.0039215686, 0.6, 0.9960784314, 0.8745098039, 0.0039215686, + 0.6039215686274509, 0.9960784314, 0.8666666667, 0.0039215686, 0.6078431372549019, + 0.9960784314, 0.8588235294, 0.0039215686, 0.611764705882353, 0.9960784314, 0.8509803922, + 0.0039215686, 0.615686274509804, 0.9960784314, 0.8431372549, 0.0039215686, 0.6196078431372549, + 0.9960784314, 0.8352941176, 0.0039215686, 0.6235294117647059, 0.9960784314, 0.8274509804, + 0.0039215686, 0.6274509803921569, 0.9960784314, 0.8196078431, 0.0039215686, + 0.6313725490196078, 0.9960784314, 0.8117647059, 0.0039215686, 0.6352941176470588, + 0.9960784314, 0.8039215686, 0.0039215686, 0.6392156862745098, 0.9960784314, 0.7960784314, + 0.0039215686, 0.6431372549019608, 0.9960784314, 0.7882352941, 0.0039215686, + 0.6470588235294118, 0.9960784314, 0.7803921569, 0.0039215686, 0.6509803921568628, + 0.9960784314, 0.7725490196, 0.0039215686, 0.6549019607843137, 0.9960784314, 0.7647058824, + 0.0039215686, 0.6588235294117647, 0.9960784314, 0.7568627451, 0.0039215686, + 0.6627450980392157, 0.9960784314, 0.7490196078, 0.0039215686, 0.6666666666666666, + 0.9960784314, 0.7450980392, 0.0039215686, 0.6705882352941176, 0.9960784314, 0.737254902, + 0.0039215686, 0.6745098039215687, 0.9960784314, 0.7294117647, 0.0039215686, + 0.6784313725490196, 0.9960784314, 0.7215686275, 0.0039215686, 0.6823529411764706, + 0.9960784314, 0.7137254902, 0.0039215686, 0.6862745098039216, 0.9960784314, 0.7058823529, + 0.0039215686, 0.6901960784313725, 0.9960784314, 0.6980392157, 0.0039215686, + 0.6941176470588235, 0.9960784314, 0.6901960784, 0.0039215686, 0.6980392156862745, + 0.9960784314, 0.6823529412, 0.0039215686, 0.7019607843137254, 0.9960784314, 0.6745098039, + 0.0039215686, 0.7058823529411765, 0.9960784314, 0.6666666667, 0.0039215686, + 0.7098039215686275, 0.9960784314, 0.6588235294, 0.0039215686, 0.7137254901960784, + 0.9960784314, 0.6509803922, 0.0039215686, 0.7176470588235294, 0.9960784314, 0.6431372549, + 0.0039215686, 0.7215686274509804, 0.9960784314, 0.6352941176, 0.0039215686, + 0.7254901960784313, 0.9960784314, 0.6274509804, 0.0039215686, 0.7294117647058823, + 0.9960784314, 0.6196078431, 0.0039215686, 0.7333333333333333, 0.9960784314, 0.6117647059, + 0.0039215686, 0.7372549019607844, 0.9960784314, 0.6039215686, 0.0039215686, + 0.7411764705882353, 0.9960784314, 0.5960784314, 0.0039215686, 0.7450980392156863, + 0.9960784314, 0.5882352941, 0.0039215686, 0.7490196078431373, 0.9960784314, 0.5803921569, + 0.0039215686, 0.7529411764705882, 0.9960784314, 0.5725490196, 0.0039215686, + 0.7568627450980392, 0.9960784314, 0.5647058824, 0.0039215686, 0.7607843137254902, + 0.9960784314, 0.5568627451, 0.0039215686, 0.7647058823529411, 0.9960784314, 0.5490196078, + 0.0039215686, 0.7686274509803922, 0.9960784314, 0.5411764706, 0.0039215686, + 0.7725490196078432, 0.9960784314, 0.5333333333, 0.0039215686, 0.7764705882352941, + 0.9960784314, 0.5254901961, 0.0039215686, 0.7803921568627451, 0.9960784314, 0.5176470588, + 0.0039215686, 0.7843137254901961, 0.9960784314, 0.5098039216, 0.0039215686, 0.788235294117647, + 0.9960784314, 0.5019607843, 0.0039215686, 0.792156862745098, 0.9960784314, 0.4941176471, + 0.0039215686, 0.796078431372549, 0.9960784314, 0.4862745098, 0.0039215686, 0.8, 0.9960784314, + 0.4784313725, 0.0039215686, 0.803921568627451, 0.9960784314, 0.4705882353, 0.0039215686, + 0.807843137254902, 0.9960784314, 0.462745098, 0.0039215686, 0.8117647058823529, 0.9960784314, + 0.4549019608, 0.0039215686, 0.8156862745098039, 0.9960784314, 0.4470588235, 0.0039215686, + 0.8196078431372549, 0.9960784314, 0.4392156863, 0.0039215686, 0.8235294117647058, + 0.9960784314, 0.431372549, 0.0039215686, 0.8274509803921568, 0.9960784314, 0.4235294118, + 0.0039215686, 0.8313725490196079, 0.9960784314, 0.4156862745, 0.0039215686, + 0.8352941176470589, 0.9960784314, 0.4078431373, 0.0039215686, 0.8392156862745098, + 0.9960784314, 0.4, 0.0039215686, 0.8431372549019608, 0.9960784314, 0.3921568627, 0.0039215686, + 0.8470588235294118, 0.9960784314, 0.3843137255, 0.0039215686, 0.8509803921568627, + 0.9960784314, 0.3764705882, 0.0039215686, 0.8549019607843137, 0.9960784314, 0.368627451, + 0.0039215686, 0.8588235294117647, 0.9960784314, 0.3607843137, 0.0039215686, + 0.8627450980392157, 0.9960784314, 0.3529411765, 0.0039215686, 0.8666666666666667, + 0.9960784314, 0.3450980392, 0.0039215686, 0.8705882352941177, 0.9960784314, 0.337254902, + 0.0039215686, 0.8745098039215686, 0.9960784314, 0.3294117647, 0.0039215686, + 0.8784313725490196, 0.9960784314, 0.3215686275, 0.0039215686, 0.8823529411764706, + 0.9960784314, 0.3137254902, 0.0039215686, 0.8862745098039215, 0.9960784314, 0.3058823529, + 0.0039215686, 0.8901960784313725, 0.9960784314, 0.2980392157, 0.0039215686, + 0.8941176470588236, 0.9960784314, 0.2901960784, 0.0039215686, 0.8980392156862745, + 0.9960784314, 0.2823529412, 0.0039215686, 0.9019607843137255, 0.9960784314, 0.2705882353, + 0.0039215686, 0.9058823529411765, 0.9960784314, 0.2588235294, 0.0039215686, + 0.9098039215686274, 0.9960784314, 0.2509803922, 0.0039215686, 0.9137254901960784, + 0.9960784314, 0.2431372549, 0.0039215686, 0.9176470588235294, 0.9960784314, 0.231372549, + 0.0039215686, 0.9215686274509803, 0.9960784314, 0.2196078431, 0.0039215686, + 0.9254901960784314, 0.9960784314, 0.2117647059, 0.0039215686, 0.9294117647058824, + 0.9960784314, 0.2, 0.0039215686, 0.9333333333333333, 0.9960784314, 0.1882352941, 0.0039215686, + 0.9372549019607843, 0.9960784314, 0.1764705882, 0.0039215686, 0.9411764705882354, + 0.9960784314, 0.168627451, 0.0039215686, 0.9450980392156864, 0.9960784314, 0.1568627451, + 0.0039215686, 0.9490196078431372, 0.9960784314, 0.1450980392, 0.0039215686, + 0.9529411764705882, 0.9960784314, 0.1333333333, 0.0039215686, 0.9568627450980394, + 0.9960784314, 0.1254901961, 0.0039215686, 0.9607843137254903, 0.9960784314, 0.1137254902, + 0.0039215686, 0.9647058823529413, 0.9960784314, 0.1019607843, 0.0039215686, + 0.9686274509803922, 0.9960784314, 0.0901960784, 0.0039215686, 0.9725490196078431, + 0.9960784314, 0.0823529412, 0.0039215686, 0.9764705882352941, 0.9960784314, 0.0705882353, + 0.0039215686, 0.9803921568627451, 0.9960784314, 0.0588235294, 0.0039215686, 0.984313725490196, + 0.9960784314, 0.0470588235, 0.0039215686, 0.9882352941176471, 0.9960784314, 0.0392156863, + 0.0039215686, 0.9921568627450981, 0.9960784314, 0.0274509804, 0.0039215686, 0.996078431372549, + 0.9960784314, 0.0156862745, 0.0039215686, 1.0, 0.9960784314, 0.0156862745, 0.0039215686, + ], + }, + { + ColorSpace: 'RGB', + Name: 'perfusion', + RGBPoints: [ + 0.0, 0.0, 0.0, 0.0, 0.00392156862745098, 0.0078431373, 0.0235294118, 0.0235294118, + 0.00784313725490196, 0.0078431373, 0.031372549, 0.0470588235, 0.011764705882352941, + 0.0078431373, 0.0392156863, 0.062745098, 0.01568627450980392, 0.0078431373, 0.0470588235, + 0.0862745098, 0.0196078431372549, 0.0078431373, 0.0549019608, 0.1019607843, + 0.023529411764705882, 0.0078431373, 0.0549019608, 0.1254901961, 0.027450980392156862, + 0.0078431373, 0.062745098, 0.1411764706, 0.03137254901960784, 0.0078431373, 0.0705882353, + 0.1647058824, 0.03529411764705882, 0.0078431373, 0.0784313725, 0.1803921569, + 0.0392156862745098, 0.0078431373, 0.0862745098, 0.2039215686, 0.043137254901960784, + 0.0078431373, 0.0862745098, 0.2196078431, 0.047058823529411764, 0.0078431373, 0.0941176471, + 0.2431372549, 0.050980392156862744, 0.0078431373, 0.1019607843, 0.2666666667, + 0.054901960784313725, 0.0078431373, 0.1098039216, 0.2823529412, 0.05882352941176471, + 0.0078431373, 0.1176470588, 0.3058823529, 0.06274509803921569, 0.0078431373, 0.1176470588, + 0.3215686275, 0.06666666666666667, 0.0078431373, 0.1254901961, 0.3450980392, + 0.07058823529411765, 0.0078431373, 0.1333333333, 0.3607843137, 0.07450980392156863, + 0.0078431373, 0.1411764706, 0.3843137255, 0.0784313725490196, 0.0078431373, 0.1490196078, 0.4, + 0.08235294117647059, 0.0078431373, 0.1490196078, 0.4235294118, 0.08627450980392157, + 0.0078431373, 0.1568627451, 0.4392156863, 0.09019607843137255, 0.0078431373, 0.1647058824, + 0.462745098, 0.09411764705882353, 0.0078431373, 0.1725490196, 0.4784313725, + 0.09803921568627451, 0.0078431373, 0.1803921569, 0.5019607843, 0.10196078431372549, + 0.0078431373, 0.1803921569, 0.5254901961, 0.10588235294117647, 0.0078431373, 0.1882352941, + 0.5411764706, 0.10980392156862745, 0.0078431373, 0.1960784314, 0.5647058824, + 0.11372549019607843, 0.0078431373, 0.2039215686, 0.5803921569, 0.11764705882352942, + 0.0078431373, 0.2117647059, 0.6039215686, 0.12156862745098039, 0.0078431373, 0.2117647059, + 0.6196078431, 0.12549019607843137, 0.0078431373, 0.2196078431, 0.6431372549, + 0.12941176470588237, 0.0078431373, 0.2274509804, 0.6588235294, 0.13333333333333333, + 0.0078431373, 0.2352941176, 0.6823529412, 0.13725490196078433, 0.0078431373, 0.2431372549, + 0.6980392157, 0.1411764705882353, 0.0078431373, 0.2431372549, 0.7215686275, + 0.1450980392156863, 0.0078431373, 0.2509803922, 0.737254902, 0.14901960784313725, + 0.0078431373, 0.2588235294, 0.7607843137, 0.15294117647058825, 0.0078431373, 0.2666666667, + 0.7843137255, 0.1568627450980392, 0.0078431373, 0.2745098039, 0.8, 0.1607843137254902, + 0.0078431373, 0.2745098039, 0.8235294118, 0.16470588235294117, 0.0078431373, 0.2823529412, + 0.8392156863, 0.16862745098039217, 0.0078431373, 0.2901960784, 0.862745098, + 0.17254901960784313, 0.0078431373, 0.2980392157, 0.8784313725, 0.17647058823529413, + 0.0078431373, 0.3058823529, 0.9019607843, 0.1803921568627451, 0.0078431373, 0.3058823529, + 0.9176470588, 0.1843137254901961, 0.0078431373, 0.2980392157, 0.9411764706, + 0.18823529411764706, 0.0078431373, 0.3058823529, 0.9568627451, 0.19215686274509805, + 0.0078431373, 0.2980392157, 0.9803921569, 0.19607843137254902, 0.0078431373, 0.2980392157, + 0.9882352941, 0.2, 0.0078431373, 0.2901960784, 0.9803921569, 0.20392156862745098, + 0.0078431373, 0.2901960784, 0.9647058824, 0.20784313725490197, 0.0078431373, 0.2823529412, + 0.9568627451, 0.21176470588235294, 0.0078431373, 0.2823529412, 0.9411764706, + 0.21568627450980393, 0.0078431373, 0.2745098039, 0.9333333333, 0.2196078431372549, + 0.0078431373, 0.2666666667, 0.9176470588, 0.2235294117647059, 0.0078431373, 0.2666666667, + 0.9098039216, 0.22745098039215686, 0.0078431373, 0.2588235294, 0.9019607843, + 0.23137254901960785, 0.0078431373, 0.2588235294, 0.8862745098, 0.23529411764705885, + 0.0078431373, 0.2509803922, 0.8784313725, 0.23921568627450984, 0.0078431373, 0.2509803922, + 0.862745098, 0.24313725490196078, 0.0078431373, 0.2431372549, 0.8549019608, + 0.24705882352941178, 0.0078431373, 0.2352941176, 0.8392156863, 0.25098039215686274, + 0.0078431373, 0.2352941176, 0.831372549, 0.2549019607843137, 0.0078431373, 0.2274509804, + 0.8235294118, 0.25882352941176473, 0.0078431373, 0.2274509804, 0.8078431373, + 0.2627450980392157, 0.0078431373, 0.2196078431, 0.8, 0.26666666666666666, 0.0078431373, + 0.2196078431, 0.7843137255, 0.27058823529411763, 0.0078431373, 0.2117647059, 0.7764705882, + 0.27450980392156865, 0.0078431373, 0.2039215686, 0.7607843137, 0.2784313725490196, + 0.0078431373, 0.2039215686, 0.7529411765, 0.2823529411764706, 0.0078431373, 0.1960784314, + 0.7450980392, 0.28627450980392155, 0.0078431373, 0.1960784314, 0.7294117647, + 0.2901960784313726, 0.0078431373, 0.1882352941, 0.7215686275, 0.29411764705882354, + 0.0078431373, 0.1882352941, 0.7058823529, 0.2980392156862745, 0.0078431373, 0.1803921569, + 0.6980392157, 0.30196078431372547, 0.0078431373, 0.1803921569, 0.6823529412, + 0.3058823529411765, 0.0078431373, 0.1725490196, 0.6745098039, 0.30980392156862746, + 0.0078431373, 0.1647058824, 0.6666666667, 0.3137254901960784, 0.0078431373, 0.1647058824, + 0.6509803922, 0.3176470588235294, 0.0078431373, 0.1568627451, 0.6431372549, + 0.3215686274509804, 0.0078431373, 0.1568627451, 0.6274509804, 0.3254901960784314, + 0.0078431373, 0.1490196078, 0.6196078431, 0.32941176470588235, 0.0078431373, 0.1490196078, + 0.6039215686, 0.3333333333333333, 0.0078431373, 0.1411764706, 0.5960784314, + 0.33725490196078434, 0.0078431373, 0.1333333333, 0.5882352941, 0.3411764705882353, + 0.0078431373, 0.1333333333, 0.5725490196, 0.34509803921568627, 0.0078431373, 0.1254901961, + 0.5647058824, 0.34901960784313724, 0.0078431373, 0.1254901961, 0.5490196078, + 0.35294117647058826, 0.0078431373, 0.1176470588, 0.5411764706, 0.3568627450980392, + 0.0078431373, 0.1176470588, 0.5254901961, 0.3607843137254902, 0.0078431373, 0.1098039216, + 0.5176470588, 0.36470588235294116, 0.0078431373, 0.1019607843, 0.5098039216, + 0.3686274509803922, 0.0078431373, 0.1019607843, 0.4941176471, 0.37254901960784315, + 0.0078431373, 0.0941176471, 0.4862745098, 0.3764705882352941, 0.0078431373, 0.0941176471, + 0.4705882353, 0.3803921568627451, 0.0078431373, 0.0862745098, 0.462745098, 0.3843137254901961, + 0.0078431373, 0.0862745098, 0.4470588235, 0.38823529411764707, 0.0078431373, 0.0784313725, + 0.4392156863, 0.39215686274509803, 0.0078431373, 0.0705882353, 0.431372549, 0.396078431372549, + 0.0078431373, 0.0705882353, 0.4156862745, 0.4, 0.0078431373, 0.062745098, 0.4078431373, + 0.403921568627451, 0.0078431373, 0.062745098, 0.3921568627, 0.40784313725490196, 0.0078431373, + 0.0549019608, 0.3843137255, 0.4117647058823529, 0.0078431373, 0.0549019608, 0.368627451, + 0.41568627450980394, 0.0078431373, 0.0470588235, 0.3607843137, 0.4196078431372549, + 0.0078431373, 0.0470588235, 0.3529411765, 0.4235294117647059, 0.0078431373, 0.0392156863, + 0.337254902, 0.42745098039215684, 0.0078431373, 0.031372549, 0.3294117647, + 0.43137254901960786, 0.0078431373, 0.031372549, 0.3137254902, 0.43529411764705883, + 0.0078431373, 0.0235294118, 0.3058823529, 0.4392156862745098, 0.0078431373, 0.0235294118, + 0.2901960784, 0.44313725490196076, 0.0078431373, 0.0156862745, 0.2823529412, + 0.4470588235294118, 0.0078431373, 0.0156862745, 0.2745098039, 0.45098039215686275, + 0.0078431373, 0.0078431373, 0.2588235294, 0.4549019607843137, 0.0235294118, 0.0078431373, + 0.2509803922, 0.4588235294117647, 0.0078431373, 0.0078431373, 0.2352941176, + 0.4627450980392157, 0.0078431373, 0.0078431373, 0.2274509804, 0.4666666666666667, + 0.0078431373, 0.0078431373, 0.2117647059, 0.4705882352941177, 0.0078431373, 0.0078431373, + 0.2039215686, 0.4745098039215686, 0.0078431373, 0.0078431373, 0.1960784314, + 0.4784313725490197, 0.0078431373, 0.0078431373, 0.1803921569, 0.48235294117647065, + 0.0078431373, 0.0078431373, 0.1725490196, 0.48627450980392156, 0.0078431373, 0.0078431373, + 0.1568627451, 0.49019607843137253, 0.0078431373, 0.0078431373, 0.1490196078, + 0.49411764705882355, 0.0078431373, 0.0078431373, 0.1333333333, 0.4980392156862745, + 0.0078431373, 0.0078431373, 0.1254901961, 0.5019607843137255, 0.0078431373, 0.0078431373, + 0.1176470588, 0.5058823529411764, 0.0078431373, 0.0078431373, 0.1019607843, + 0.5098039215686274, 0.0078431373, 0.0078431373, 0.0941176471, 0.5137254901960784, + 0.0078431373, 0.0078431373, 0.0784313725, 0.5176470588235295, 0.0078431373, 0.0078431373, + 0.0705882353, 0.5215686274509804, 0.0078431373, 0.0078431373, 0.0549019608, + 0.5254901960784314, 0.0078431373, 0.0078431373, 0.0470588235, 0.5294117647058824, + 0.0235294118, 0.0078431373, 0.0392156863, 0.5333333333333333, 0.031372549, 0.0078431373, + 0.0235294118, 0.5372549019607843, 0.0392156863, 0.0078431373, 0.0156862745, + 0.5411764705882353, 0.0549019608, 0.0078431373, 0.0, 0.5450980392156862, 0.062745098, + 0.0078431373, 0.0, 0.5490196078431373, 0.0705882353, 0.0078431373, 0.0, 0.5529411764705883, + 0.0862745098, 0.0078431373, 0.0, 0.5568627450980392, 0.0941176471, 0.0078431373, 0.0, + 0.5607843137254902, 0.1019607843, 0.0078431373, 0.0, 0.5647058823529412, 0.1098039216, + 0.0078431373, 0.0, 0.5686274509803921, 0.1254901961, 0.0078431373, 0.0, 0.5725490196078431, + 0.1333333333, 0.0078431373, 0.0, 0.5764705882352941, 0.1411764706, 0.0078431373, 0.0, + 0.5803921568627451, 0.1568627451, 0.0078431373, 0.0, 0.5843137254901961, 0.1647058824, + 0.0078431373, 0.0, 0.5882352941176471, 0.1725490196, 0.0078431373, 0.0, 0.592156862745098, + 0.1882352941, 0.0078431373, 0.0, 0.596078431372549, 0.1960784314, 0.0078431373, 0.0, 0.6, + 0.2039215686, 0.0078431373, 0.0, 0.6039215686274509, 0.2117647059, 0.0078431373, 0.0, + 0.6078431372549019, 0.2274509804, 0.0078431373, 0.0, 0.611764705882353, 0.2352941176, + 0.0078431373, 0.0, 0.615686274509804, 0.2431372549, 0.0078431373, 0.0, 0.6196078431372549, + 0.2588235294, 0.0078431373, 0.0, 0.6235294117647059, 0.2666666667, 0.0078431373, 0.0, + 0.6274509803921569, 0.2745098039, 0.0, 0.0, 0.6313725490196078, 0.2901960784, 0.0156862745, + 0.0, 0.6352941176470588, 0.2980392157, 0.0235294118, 0.0, 0.6392156862745098, 0.3058823529, + 0.0392156863, 0.0, 0.6431372549019608, 0.3137254902, 0.0470588235, 0.0, 0.6470588235294118, + 0.3294117647, 0.0549019608, 0.0, 0.6509803921568628, 0.337254902, 0.0705882353, 0.0, + 0.6549019607843137, 0.3450980392, 0.0784313725, 0.0, 0.6588235294117647, 0.3607843137, + 0.0862745098, 0.0, 0.6627450980392157, 0.368627451, 0.1019607843, 0.0, 0.6666666666666666, + 0.3764705882, 0.1098039216, 0.0, 0.6705882352941176, 0.3843137255, 0.1176470588, 0.0, + 0.6745098039215687, 0.4, 0.1333333333, 0.0, 0.6784313725490196, 0.4078431373, 0.1411764706, + 0.0, 0.6823529411764706, 0.4156862745, 0.1490196078, 0.0, 0.6862745098039216, 0.431372549, + 0.1647058824, 0.0, 0.6901960784313725, 0.4392156863, 0.1725490196, 0.0, 0.6941176470588235, + 0.4470588235, 0.1803921569, 0.0, 0.6980392156862745, 0.462745098, 0.1960784314, 0.0, + 0.7019607843137254, 0.4705882353, 0.2039215686, 0.0, 0.7058823529411765, 0.4784313725, + 0.2117647059, 0.0, 0.7098039215686275, 0.4862745098, 0.2274509804, 0.0, 0.7137254901960784, + 0.5019607843, 0.2352941176, 0.0, 0.7176470588235294, 0.5098039216, 0.2431372549, 0.0, + 0.7215686274509804, 0.5176470588, 0.2588235294, 0.0, 0.7254901960784313, 0.5333333333, + 0.2666666667, 0.0, 0.7294117647058823, 0.5411764706, 0.2745098039, 0.0, 0.7333333333333333, + 0.5490196078, 0.2901960784, 0.0, 0.7372549019607844, 0.5647058824, 0.2980392157, 0.0, + 0.7411764705882353, 0.5725490196, 0.3058823529, 0.0, 0.7450980392156863, 0.5803921569, + 0.3215686275, 0.0, 0.7490196078431373, 0.5882352941, 0.3294117647, 0.0, 0.7529411764705882, + 0.6039215686, 0.337254902, 0.0, 0.7568627450980392, 0.6117647059, 0.3529411765, 0.0, + 0.7607843137254902, 0.6196078431, 0.3607843137, 0.0, 0.7647058823529411, 0.6352941176, + 0.368627451, 0.0, 0.7686274509803922, 0.6431372549, 0.3843137255, 0.0, 0.7725490196078432, + 0.6509803922, 0.3921568627, 0.0, 0.7764705882352941, 0.6588235294, 0.4, 0.0, + 0.7803921568627451, 0.6745098039, 0.4156862745, 0.0, 0.7843137254901961, 0.6823529412, + 0.4235294118, 0.0, 0.788235294117647, 0.6901960784, 0.431372549, 0.0, 0.792156862745098, + 0.7058823529, 0.4470588235, 0.0, 0.796078431372549, 0.7137254902, 0.4549019608, 0.0, 0.8, + 0.7215686275, 0.462745098, 0.0, 0.803921568627451, 0.737254902, 0.4784313725, 0.0, + 0.807843137254902, 0.7450980392, 0.4862745098, 0.0, 0.8117647058823529, 0.7529411765, + 0.4941176471, 0.0, 0.8156862745098039, 0.7607843137, 0.5098039216, 0.0, 0.8196078431372549, + 0.7764705882, 0.5176470588, 0.0, 0.8235294117647058, 0.7843137255, 0.5254901961, 0.0, + 0.8274509803921568, 0.7921568627, 0.5411764706, 0.0, 0.8313725490196079, 0.8078431373, + 0.5490196078, 0.0, 0.8352941176470589, 0.8156862745, 0.5568627451, 0.0, 0.8392156862745098, + 0.8235294118, 0.5725490196, 0.0, 0.8431372549019608, 0.8392156863, 0.5803921569, 0.0, + 0.8470588235294118, 0.8470588235, 0.5882352941, 0.0, 0.8509803921568627, 0.8549019608, + 0.6039215686, 0.0, 0.8549019607843137, 0.862745098, 0.6117647059, 0.0, 0.8588235294117647, + 0.8784313725, 0.6196078431, 0.0, 0.8627450980392157, 0.8862745098, 0.6352941176, 0.0, + 0.8666666666666667, 0.8941176471, 0.6431372549, 0.0, 0.8705882352941177, 0.9098039216, + 0.6509803922, 0.0, 0.8745098039215686, 0.9176470588, 0.6666666667, 0.0, 0.8784313725490196, + 0.9254901961, 0.6745098039, 0.0, 0.8823529411764706, 0.9411764706, 0.6823529412, 0.0, + 0.8862745098039215, 0.9490196078, 0.6980392157, 0.0, 0.8901960784313725, 0.9568627451, + 0.7058823529, 0.0, 0.8941176470588236, 0.9647058824, 0.7137254902, 0.0, 0.8980392156862745, + 0.9803921569, 0.7294117647, 0.0, 0.9019607843137255, 0.9882352941, 0.737254902, 0.0, + 0.9058823529411765, 0.9960784314, 0.7450980392, 0.0, 0.9098039215686274, 0.9960784314, + 0.7607843137, 0.0, 0.9137254901960784, 0.9960784314, 0.768627451, 0.0, 0.9176470588235294, + 0.9960784314, 0.7764705882, 0.0, 0.9215686274509803, 0.9960784314, 0.7921568627, 0.0, + 0.9254901960784314, 0.9960784314, 0.8, 0.0, 0.9294117647058824, 0.9960784314, 0.8078431373, + 0.0, 0.9333333333333333, 0.9960784314, 0.8235294118, 0.0, 0.9372549019607843, 0.9960784314, + 0.831372549, 0.0, 0.9411764705882354, 0.9960784314, 0.8392156863, 0.0, 0.9450980392156864, + 0.9960784314, 0.8549019608, 0.0, 0.9490196078431372, 0.9960784314, 0.862745098, 0.0549019608, + 0.9529411764705882, 0.9960784314, 0.8705882353, 0.1098039216, 0.9568627450980394, + 0.9960784314, 0.8862745098, 0.1647058824, 0.9607843137254903, 0.9960784314, 0.8941176471, + 0.2196078431, 0.9647058823529413, 0.9960784314, 0.9019607843, 0.2666666667, + 0.9686274509803922, 0.9960784314, 0.9176470588, 0.3215686275, 0.9725490196078431, + 0.9960784314, 0.9254901961, 0.3764705882, 0.9764705882352941, 0.9960784314, 0.9333333333, + 0.431372549, 0.9803921568627451, 0.9960784314, 0.9490196078, 0.4862745098, 0.984313725490196, + 0.9960784314, 0.9568627451, 0.5333333333, 0.9882352941176471, 0.9960784314, 0.9647058824, + 0.5882352941, 0.9921568627450981, 0.9960784314, 0.9803921569, 0.6431372549, 0.996078431372549, + 0.9960784314, 0.9882352941, 0.6980392157, 1.0, 0.9960784314, 0.9960784314, 0.7450980392, + ], + }, + { + ColorSpace: 'RGB', + Name: 'rainbow_2', + RGBPoints: [ + 0.0, 0.0, 0.0, 0.0, 0.00392156862745098, 0.0156862745, 0.0, 0.0117647059, 0.00784313725490196, + 0.0352941176, 0.0, 0.0274509804, 0.011764705882352941, 0.0509803922, 0.0, 0.0392156863, + 0.01568627450980392, 0.0705882353, 0.0, 0.0549019608, 0.0196078431372549, 0.0862745098, 0.0, + 0.0745098039, 0.023529411764705882, 0.1058823529, 0.0, 0.0901960784, 0.027450980392156862, + 0.1215686275, 0.0, 0.1098039216, 0.03137254901960784, 0.1411764706, 0.0, 0.1254901961, + 0.03529411764705882, 0.1568627451, 0.0, 0.1490196078, 0.0392156862745098, 0.1764705882, 0.0, + 0.168627451, 0.043137254901960784, 0.1960784314, 0.0, 0.1882352941, 0.047058823529411764, + 0.2117647059, 0.0, 0.2078431373, 0.050980392156862744, 0.2274509804, 0.0, 0.231372549, + 0.054901960784313725, 0.2392156863, 0.0, 0.2470588235, 0.05882352941176471, 0.2509803922, 0.0, + 0.2666666667, 0.06274509803921569, 0.2666666667, 0.0, 0.2823529412, 0.06666666666666667, + 0.2705882353, 0.0, 0.3019607843, 0.07058823529411765, 0.2823529412, 0.0, 0.3176470588, + 0.07450980392156863, 0.2901960784, 0.0, 0.337254902, 0.0784313725490196, 0.3019607843, 0.0, + 0.3568627451, 0.08235294117647059, 0.3098039216, 0.0, 0.3725490196, 0.08627450980392157, + 0.3137254902, 0.0, 0.3921568627, 0.09019607843137255, 0.3215686275, 0.0, 0.4078431373, + 0.09411764705882353, 0.3254901961, 0.0, 0.4274509804, 0.09803921568627451, 0.3333333333, 0.0, + 0.4431372549, 0.10196078431372549, 0.3294117647, 0.0, 0.462745098, 0.10588235294117647, + 0.337254902, 0.0, 0.4784313725, 0.10980392156862745, 0.3411764706, 0.0, 0.4980392157, + 0.11372549019607843, 0.3450980392, 0.0, 0.5176470588, 0.11764705882352942, 0.337254902, 0.0, + 0.5333333333, 0.12156862745098039, 0.3411764706, 0.0, 0.5529411765, 0.12549019607843137, + 0.3411764706, 0.0, 0.568627451, 0.12941176470588237, 0.3411764706, 0.0, 0.5882352941, + 0.13333333333333333, 0.3333333333, 0.0, 0.6039215686, 0.13725490196078433, 0.3294117647, 0.0, + 0.6235294118, 0.1411764705882353, 0.3294117647, 0.0, 0.6392156863, 0.1450980392156863, + 0.3294117647, 0.0, 0.6588235294, 0.14901960784313725, 0.3254901961, 0.0, 0.6784313725, + 0.15294117647058825, 0.3098039216, 0.0, 0.6941176471, 0.1568627450980392, 0.3058823529, 0.0, + 0.7137254902, 0.1607843137254902, 0.3019607843, 0.0, 0.7294117647, 0.16470588235294117, + 0.2980392157, 0.0, 0.7490196078, 0.16862745098039217, 0.2784313725, 0.0, 0.7647058824, + 0.17254901960784313, 0.2745098039, 0.0, 0.7843137255, 0.17647058823529413, 0.2666666667, 0.0, + 0.8, 0.1803921568627451, 0.2588235294, 0.0, 0.8196078431, 0.1843137254901961, 0.2352941176, + 0.0, 0.8392156863, 0.18823529411764706, 0.2274509804, 0.0, 0.8549019608, 0.19215686274509805, + 0.2156862745, 0.0, 0.8745098039, 0.19607843137254902, 0.2078431373, 0.0, 0.8901960784, 0.2, + 0.1803921569, 0.0, 0.9098039216, 0.20392156862745098, 0.168627451, 0.0, 0.9254901961, + 0.20784313725490197, 0.1568627451, 0.0, 0.9450980392, 0.21176470588235294, 0.1411764706, 0.0, + 0.9607843137, 0.21568627450980393, 0.1294117647, 0.0, 0.9803921569, 0.2196078431372549, + 0.0980392157, 0.0, 1.0, 0.2235294117647059, 0.0823529412, 0.0, 1.0, 0.22745098039215686, + 0.062745098, 0.0, 1.0, 0.23137254901960785, 0.0470588235, 0.0, 1.0, 0.23529411764705885, + 0.0156862745, 0.0, 1.0, 0.23921568627450984, 0.0, 0.0, 1.0, 0.24313725490196078, 0.0, + 0.0156862745, 1.0, 0.24705882352941178, 0.0, 0.031372549, 1.0, 0.25098039215686274, 0.0, + 0.062745098, 1.0, 0.2549019607843137, 0.0, 0.0823529412, 1.0, 0.25882352941176473, 0.0, + 0.0980392157, 1.0, 0.2627450980392157, 0.0, 0.1137254902, 1.0, 0.26666666666666666, 0.0, + 0.1490196078, 1.0, 0.27058823529411763, 0.0, 0.1647058824, 1.0, 0.27450980392156865, 0.0, + 0.1803921569, 1.0, 0.2784313725490196, 0.0, 0.2, 1.0, 0.2823529411764706, 0.0, 0.2156862745, + 1.0, 0.28627450980392155, 0.0, 0.2470588235, 1.0, 0.2901960784313726, 0.0, 0.262745098, 1.0, + 0.29411764705882354, 0.0, 0.2823529412, 1.0, 0.2980392156862745, 0.0, 0.2980392157, 1.0, + 0.30196078431372547, 0.0, 0.3294117647, 1.0, 0.3058823529411765, 0.0, 0.3490196078, 1.0, + 0.30980392156862746, 0.0, 0.3647058824, 1.0, 0.3137254901960784, 0.0, 0.3803921569, 1.0, + 0.3176470588235294, 0.0, 0.4156862745, 1.0, 0.3215686274509804, 0.0, 0.431372549, 1.0, + 0.3254901960784314, 0.0, 0.4470588235, 1.0, 0.32941176470588235, 0.0, 0.4666666667, 1.0, + 0.3333333333333333, 0.0, 0.4980392157, 1.0, 0.33725490196078434, 0.0, 0.5137254902, 1.0, + 0.3411764705882353, 0.0, 0.5294117647, 1.0, 0.34509803921568627, 0.0, 0.5490196078, 1.0, + 0.34901960784313724, 0.0, 0.5647058824, 1.0, 0.35294117647058826, 0.0, 0.5960784314, 1.0, + 0.3568627450980392, 0.0, 0.6156862745, 1.0, 0.3607843137254902, 0.0, 0.631372549, 1.0, + 0.36470588235294116, 0.0, 0.6470588235, 1.0, 0.3686274509803922, 0.0, 0.6823529412, 1.0, + 0.37254901960784315, 0.0, 0.6980392157, 1.0, 0.3764705882352941, 0.0, 0.7137254902, 1.0, + 0.3803921568627451, 0.0, 0.7333333333, 1.0, 0.3843137254901961, 0.0, 0.7647058824, 1.0, + 0.38823529411764707, 0.0, 0.7803921569, 1.0, 0.39215686274509803, 0.0, 0.7960784314, 1.0, + 0.396078431372549, 0.0, 0.8156862745, 1.0, 0.4, 0.0, 0.8470588235, 1.0, 0.403921568627451, + 0.0, 0.862745098, 1.0, 0.40784313725490196, 0.0, 0.8823529412, 1.0, 0.4117647058823529, 0.0, + 0.8980392157, 1.0, 0.41568627450980394, 0.0, 0.9137254902, 1.0, 0.4196078431372549, 0.0, + 0.9490196078, 1.0, 0.4235294117647059, 0.0, 0.9647058824, 1.0, 0.42745098039215684, 0.0, + 0.9803921569, 1.0, 0.43137254901960786, 0.0, 1.0, 1.0, 0.43529411764705883, 0.0, 1.0, + 0.9647058824, 0.4392156862745098, 0.0, 1.0, 0.9490196078, 0.44313725490196076, 0.0, 1.0, + 0.9333333333, 0.4470588235294118, 0.0, 1.0, 0.9137254902, 0.45098039215686275, 0.0, 1.0, + 0.8823529412, 0.4549019607843137, 0.0, 1.0, 0.862745098, 0.4588235294117647, 0.0, 1.0, + 0.8470588235, 0.4627450980392157, 0.0, 1.0, 0.831372549, 0.4666666666666667, 0.0, 1.0, + 0.7960784314, 0.4705882352941177, 0.0, 1.0, 0.7803921569, 0.4745098039215686, 0.0, 1.0, + 0.7647058824, 0.4784313725490197, 0.0, 1.0, 0.7490196078, 0.48235294117647065, 0.0, 1.0, + 0.7333333333, 0.48627450980392156, 0.0, 1.0, 0.6980392157, 0.49019607843137253, 0.0, 1.0, + 0.6823529412, 0.49411764705882355, 0.0, 1.0, 0.6666666667, 0.4980392156862745, 0.0, 1.0, + 0.6470588235, 0.5019607843137255, 0.0, 1.0, 0.6156862745, 0.5058823529411764, 0.0, 1.0, + 0.5960784314, 0.5098039215686274, 0.0, 1.0, 0.5803921569, 0.5137254901960784, 0.0, 1.0, + 0.5647058824, 0.5176470588235295, 0.0, 1.0, 0.5294117647, 0.5215686274509804, 0.0, 1.0, + 0.5137254902, 0.5254901960784314, 0.0, 1.0, 0.4980392157, 0.5294117647058824, 0.0, 1.0, + 0.4823529412, 0.5333333333333333, 0.0, 1.0, 0.4470588235, 0.5372549019607843, 0.0, 1.0, + 0.431372549, 0.5411764705882353, 0.0, 1.0, 0.4156862745, 0.5450980392156862, 0.0, 1.0, 0.4, + 0.5490196078431373, 0.0, 1.0, 0.3803921569, 0.5529411764705883, 0.0, 1.0, 0.3490196078, + 0.5568627450980392, 0.0, 1.0, 0.3294117647, 0.5607843137254902, 0.0, 1.0, 0.3137254902, + 0.5647058823529412, 0.0, 1.0, 0.2980392157, 0.5686274509803921, 0.0, 1.0, 0.262745098, + 0.5725490196078431, 0.0, 1.0, 0.2470588235, 0.5764705882352941, 0.0, 1.0, 0.231372549, + 0.5803921568627451, 0.0, 1.0, 0.2156862745, 0.5843137254901961, 0.0, 1.0, 0.1803921569, + 0.5882352941176471, 0.0, 1.0, 0.1647058824, 0.592156862745098, 0.0, 1.0, 0.1490196078, + 0.596078431372549, 0.0, 1.0, 0.1333333333, 0.6, 0.0, 1.0, 0.0980392157, 0.6039215686274509, + 0.0, 1.0, 0.0823529412, 0.6078431372549019, 0.0, 1.0, 0.062745098, 0.611764705882353, 0.0, + 1.0, 0.0470588235, 0.615686274509804, 0.0, 1.0, 0.031372549, 0.6196078431372549, 0.0, 1.0, + 0.0, 0.6235294117647059, 0.0156862745, 1.0, 0.0, 0.6274509803921569, 0.031372549, 1.0, 0.0, + 0.6313725490196078, 0.0470588235, 1.0, 0.0, 0.6352941176470588, 0.0823529412, 1.0, 0.0, + 0.6392156862745098, 0.0980392157, 1.0, 0.0, 0.6431372549019608, 0.1137254902, 1.0, 0.0, + 0.6470588235294118, 0.1294117647, 1.0, 0.0, 0.6509803921568628, 0.1647058824, 1.0, 0.0, + 0.6549019607843137, 0.1803921569, 1.0, 0.0, 0.6588235294117647, 0.2, 1.0, 0.0, + 0.6627450980392157, 0.2156862745, 1.0, 0.0, 0.6666666666666666, 0.2470588235, 1.0, 0.0, + 0.6705882352941176, 0.262745098, 1.0, 0.0, 0.6745098039215687, 0.2823529412, 1.0, 0.0, + 0.6784313725490196, 0.2980392157, 1.0, 0.0, 0.6823529411764706, 0.3137254902, 1.0, 0.0, + 0.6862745098039216, 0.3490196078, 1.0, 0.0, 0.6901960784313725, 0.3647058824, 1.0, 0.0, + 0.6941176470588235, 0.3803921569, 1.0, 0.0, 0.6980392156862745, 0.3960784314, 1.0, 0.0, + 0.7019607843137254, 0.431372549, 1.0, 0.0, 0.7058823529411765, 0.4470588235, 1.0, 0.0, + 0.7098039215686275, 0.4666666667, 1.0, 0.0, 0.7137254901960784, 0.4823529412, 1.0, 0.0, + 0.7176470588235294, 0.5137254902, 1.0, 0.0, 0.7215686274509804, 0.5294117647, 1.0, 0.0, + 0.7254901960784313, 0.5490196078, 1.0, 0.0, 0.7294117647058823, 0.5647058824, 1.0, 0.0, + 0.7333333333333333, 0.6, 1.0, 0.0, 0.7372549019607844, 0.6156862745, 1.0, 0.0, + 0.7411764705882353, 0.631372549, 1.0, 0.0, 0.7450980392156863, 0.6470588235, 1.0, 0.0, + 0.7490196078431373, 0.662745098, 1.0, 0.0, 0.7529411764705882, 0.6980392157, 1.0, 0.0, + 0.7568627450980392, 0.7137254902, 1.0, 0.0, 0.7607843137254902, 0.7333333333, 1.0, 0.0, + 0.7647058823529411, 0.7490196078, 1.0, 0.0, 0.7686274509803922, 0.7803921569, 1.0, 0.0, + 0.7725490196078432, 0.7960784314, 1.0, 0.0, 0.7764705882352941, 0.8156862745, 1.0, 0.0, + 0.7803921568627451, 0.831372549, 1.0, 0.0, 0.7843137254901961, 0.8666666667, 1.0, 0.0, + 0.788235294117647, 0.8823529412, 1.0, 0.0, 0.792156862745098, 0.8980392157, 1.0, 0.0, + 0.796078431372549, 0.9137254902, 1.0, 0.0, 0.8, 0.9490196078, 1.0, 0.0, 0.803921568627451, + 0.9647058824, 1.0, 0.0, 0.807843137254902, 0.9803921569, 1.0, 0.0, 0.8117647058823529, 1.0, + 1.0, 0.0, 0.8156862745098039, 1.0, 0.9803921569, 0.0, 0.8196078431372549, 1.0, 0.9490196078, + 0.0, 0.8235294117647058, 1.0, 0.9333333333, 0.0, 0.8274509803921568, 1.0, 0.9137254902, 0.0, + 0.8313725490196079, 1.0, 0.8980392157, 0.0, 0.8352941176470589, 1.0, 0.8666666667, 0.0, + 0.8392156862745098, 1.0, 0.8470588235, 0.0, 0.8431372549019608, 1.0, 0.831372549, 0.0, + 0.8470588235294118, 1.0, 0.8156862745, 0.0, 0.8509803921568627, 1.0, 0.7803921569, 0.0, + 0.8549019607843137, 1.0, 0.7647058824, 0.0, 0.8588235294117647, 1.0, 0.7490196078, 0.0, + 0.8627450980392157, 1.0, 0.7333333333, 0.0, 0.8666666666666667, 1.0, 0.6980392157, 0.0, + 0.8705882352941177, 1.0, 0.6823529412, 0.0, 0.8745098039215686, 1.0, 0.6666666667, 0.0, + 0.8784313725490196, 1.0, 0.6470588235, 0.0, 0.8823529411764706, 1.0, 0.631372549, 0.0, + 0.8862745098039215, 1.0, 0.6, 0.0, 0.8901960784313725, 1.0, 0.5803921569, 0.0, + 0.8941176470588236, 1.0, 0.5647058824, 0.0, 0.8980392156862745, 1.0, 0.5490196078, 0.0, + 0.9019607843137255, 1.0, 0.5137254902, 0.0, 0.9058823529411765, 1.0, 0.4980392157, 0.0, + 0.9098039215686274, 1.0, 0.4823529412, 0.0, 0.9137254901960784, 1.0, 0.4666666667, 0.0, + 0.9176470588235294, 1.0, 0.431372549, 0.0, 0.9215686274509803, 1.0, 0.4156862745, 0.0, + 0.9254901960784314, 1.0, 0.4, 0.0, 0.9294117647058824, 1.0, 0.3803921569, 0.0, + 0.9333333333333333, 1.0, 0.3490196078, 0.0, 0.9372549019607843, 1.0, 0.3333333333, 0.0, + 0.9411764705882354, 1.0, 0.3137254902, 0.0, 0.9450980392156864, 1.0, 0.2980392157, 0.0, + 0.9490196078431372, 1.0, 0.2823529412, 0.0, 0.9529411764705882, 1.0, 0.2470588235, 0.0, + 0.9568627450980394, 1.0, 0.231372549, 0.0, 0.9607843137254903, 1.0, 0.2156862745, 0.0, + 0.9647058823529413, 1.0, 0.2, 0.0, 0.9686274509803922, 1.0, 0.1647058824, 0.0, + 0.9725490196078431, 1.0, 0.1490196078, 0.0, 0.9764705882352941, 1.0, 0.1333333333, 0.0, + 0.9803921568627451, 1.0, 0.1137254902, 0.0, 0.984313725490196, 1.0, 0.0823529412, 0.0, + 0.9882352941176471, 1.0, 0.0666666667, 0.0, 0.9921568627450981, 1.0, 0.0470588235, 0.0, + 0.996078431372549, 1.0, 0.031372549, 0.0, 1.0, 1.0, 0.0, 0.0, + ], + }, + { + ColorSpace: 'RGB', + Name: 'suv', + RGBPoints: [ + 0.0, 1.0, 1.0, 1.0, 0.00392156862745098, 1.0, 1.0, 1.0, 0.00784313725490196, 1.0, 1.0, 1.0, + 0.011764705882352941, 1.0, 1.0, 1.0, 0.01568627450980392, 1.0, 1.0, 1.0, 0.0196078431372549, + 1.0, 1.0, 1.0, 0.023529411764705882, 1.0, 1.0, 1.0, 0.027450980392156862, 1.0, 1.0, 1.0, + 0.03137254901960784, 1.0, 1.0, 1.0, 0.03529411764705882, 1.0, 1.0, 1.0, 0.0392156862745098, + 1.0, 1.0, 1.0, 0.043137254901960784, 1.0, 1.0, 1.0, 0.047058823529411764, 1.0, 1.0, 1.0, + 0.050980392156862744, 1.0, 1.0, 1.0, 0.054901960784313725, 1.0, 1.0, 1.0, 0.05882352941176471, + 1.0, 1.0, 1.0, 0.06274509803921569, 1.0, 1.0, 1.0, 0.06666666666666667, 1.0, 1.0, 1.0, + 0.07058823529411765, 1.0, 1.0, 1.0, 0.07450980392156863, 1.0, 1.0, 1.0, 0.0784313725490196, + 1.0, 1.0, 1.0, 0.08235294117647059, 1.0, 1.0, 1.0, 0.08627450980392157, 1.0, 1.0, 1.0, + 0.09019607843137255, 1.0, 1.0, 1.0, 0.09411764705882353, 1.0, 1.0, 1.0, 0.09803921568627451, + 1.0, 1.0, 1.0, 0.10196078431372549, 0.737254902, 0.737254902, 0.737254902, + 0.10588235294117647, 0.737254902, 0.737254902, 0.737254902, 0.10980392156862745, 0.737254902, + 0.737254902, 0.737254902, 0.11372549019607843, 0.737254902, 0.737254902, 0.737254902, + 0.11764705882352942, 0.737254902, 0.737254902, 0.737254902, 0.12156862745098039, 0.737254902, + 0.737254902, 0.737254902, 0.12549019607843137, 0.737254902, 0.737254902, 0.737254902, + 0.12941176470588237, 0.737254902, 0.737254902, 0.737254902, 0.13333333333333333, 0.737254902, + 0.737254902, 0.737254902, 0.13725490196078433, 0.737254902, 0.737254902, 0.737254902, + 0.1411764705882353, 0.737254902, 0.737254902, 0.737254902, 0.1450980392156863, 0.737254902, + 0.737254902, 0.737254902, 0.14901960784313725, 0.737254902, 0.737254902, 0.737254902, + 0.15294117647058825, 0.737254902, 0.737254902, 0.737254902, 0.1568627450980392, 0.737254902, + 0.737254902, 0.737254902, 0.1607843137254902, 0.737254902, 0.737254902, 0.737254902, + 0.16470588235294117, 0.737254902, 0.737254902, 0.737254902, 0.16862745098039217, 0.737254902, + 0.737254902, 0.737254902, 0.17254901960784313, 0.737254902, 0.737254902, 0.737254902, + 0.17647058823529413, 0.737254902, 0.737254902, 0.737254902, 0.1803921568627451, 0.737254902, + 0.737254902, 0.737254902, 0.1843137254901961, 0.737254902, 0.737254902, 0.737254902, + 0.18823529411764706, 0.737254902, 0.737254902, 0.737254902, 0.19215686274509805, 0.737254902, + 0.737254902, 0.737254902, 0.19607843137254902, 0.737254902, 0.737254902, 0.737254902, 0.2, + 0.737254902, 0.737254902, 0.737254902, 0.20392156862745098, 0.431372549, 0.0, 0.568627451, + 0.20784313725490197, 0.431372549, 0.0, 0.568627451, 0.21176470588235294, 0.431372549, 0.0, + 0.568627451, 0.21568627450980393, 0.431372549, 0.0, 0.568627451, 0.2196078431372549, + 0.431372549, 0.0, 0.568627451, 0.2235294117647059, 0.431372549, 0.0, 0.568627451, + 0.22745098039215686, 0.431372549, 0.0, 0.568627451, 0.23137254901960785, 0.431372549, 0.0, + 0.568627451, 0.23529411764705885, 0.431372549, 0.0, 0.568627451, 0.23921568627450984, + 0.431372549, 0.0, 0.568627451, 0.24313725490196078, 0.431372549, 0.0, 0.568627451, + 0.24705882352941178, 0.431372549, 0.0, 0.568627451, 0.25098039215686274, 0.431372549, 0.0, + 0.568627451, 0.2549019607843137, 0.431372549, 0.0, 0.568627451, 0.25882352941176473, + 0.431372549, 0.0, 0.568627451, 0.2627450980392157, 0.431372549, 0.0, 0.568627451, + 0.26666666666666666, 0.431372549, 0.0, 0.568627451, 0.27058823529411763, 0.431372549, 0.0, + 0.568627451, 0.27450980392156865, 0.431372549, 0.0, 0.568627451, 0.2784313725490196, + 0.431372549, 0.0, 0.568627451, 0.2823529411764706, 0.431372549, 0.0, 0.568627451, + 0.28627450980392155, 0.431372549, 0.0, 0.568627451, 0.2901960784313726, 0.431372549, 0.0, + 0.568627451, 0.29411764705882354, 0.431372549, 0.0, 0.568627451, 0.2980392156862745, + 0.431372549, 0.0, 0.568627451, 0.30196078431372547, 0.431372549, 0.0, 0.568627451, + 0.3058823529411765, 0.2509803922, 0.3333333333, 0.6509803922, 0.30980392156862746, + 0.2509803922, 0.3333333333, 0.6509803922, 0.3137254901960784, 0.2509803922, 0.3333333333, + 0.6509803922, 0.3176470588235294, 0.2509803922, 0.3333333333, 0.6509803922, + 0.3215686274509804, 0.2509803922, 0.3333333333, 0.6509803922, 0.3254901960784314, + 0.2509803922, 0.3333333333, 0.6509803922, 0.32941176470588235, 0.2509803922, 0.3333333333, + 0.6509803922, 0.3333333333333333, 0.2509803922, 0.3333333333, 0.6509803922, + 0.33725490196078434, 0.2509803922, 0.3333333333, 0.6509803922, 0.3411764705882353, + 0.2509803922, 0.3333333333, 0.6509803922, 0.34509803921568627, 0.2509803922, 0.3333333333, + 0.6509803922, 0.34901960784313724, 0.2509803922, 0.3333333333, 0.6509803922, + 0.35294117647058826, 0.2509803922, 0.3333333333, 0.6509803922, 0.3568627450980392, + 0.2509803922, 0.3333333333, 0.6509803922, 0.3607843137254902, 0.2509803922, 0.3333333333, + 0.6509803922, 0.36470588235294116, 0.2509803922, 0.3333333333, 0.6509803922, + 0.3686274509803922, 0.2509803922, 0.3333333333, 0.6509803922, 0.37254901960784315, + 0.2509803922, 0.3333333333, 0.6509803922, 0.3764705882352941, 0.2509803922, 0.3333333333, + 0.6509803922, 0.3803921568627451, 0.2509803922, 0.3333333333, 0.6509803922, + 0.3843137254901961, 0.2509803922, 0.3333333333, 0.6509803922, 0.38823529411764707, + 0.2509803922, 0.3333333333, 0.6509803922, 0.39215686274509803, 0.2509803922, 0.3333333333, + 0.6509803922, 0.396078431372549, 0.2509803922, 0.3333333333, 0.6509803922, 0.4, 0.2509803922, + 0.3333333333, 0.6509803922, 0.403921568627451, 0.2509803922, 0.3333333333, 0.6509803922, + 0.40784313725490196, 0.0, 0.8, 1.0, 0.4117647058823529, 0.0, 0.8, 1.0, 0.41568627450980394, + 0.0, 0.8, 1.0, 0.4196078431372549, 0.0, 0.8, 1.0, 0.4235294117647059, 0.0, 0.8, 1.0, + 0.42745098039215684, 0.0, 0.8, 1.0, 0.43137254901960786, 0.0, 0.8, 1.0, 0.43529411764705883, + 0.0, 0.8, 1.0, 0.4392156862745098, 0.0, 0.8, 1.0, 0.44313725490196076, 0.0, 0.8, 1.0, + 0.4470588235294118, 0.0, 0.8, 1.0, 0.45098039215686275, 0.0, 0.8, 1.0, 0.4549019607843137, + 0.0, 0.8, 1.0, 0.4588235294117647, 0.0, 0.8, 1.0, 0.4627450980392157, 0.0, 0.8, 1.0, + 0.4666666666666667, 0.0, 0.8, 1.0, 0.4705882352941177, 0.0, 0.8, 1.0, 0.4745098039215686, 0.0, + 0.8, 1.0, 0.4784313725490197, 0.0, 0.8, 1.0, 0.48235294117647065, 0.0, 0.8, 1.0, + 0.48627450980392156, 0.0, 0.8, 1.0, 0.49019607843137253, 0.0, 0.8, 1.0, 0.49411764705882355, + 0.0, 0.8, 1.0, 0.4980392156862745, 0.0, 0.8, 1.0, 0.5019607843137255, 0.0, 0.8, 1.0, + 0.5058823529411764, 0.0, 0.6666666667, 0.5333333333, 0.5098039215686274, 0.0, 0.6666666667, + 0.5333333333, 0.5137254901960784, 0.0, 0.6666666667, 0.5333333333, 0.5176470588235295, 0.0, + 0.6666666667, 0.5333333333, 0.5215686274509804, 0.0, 0.6666666667, 0.5333333333, + 0.5254901960784314, 0.0, 0.6666666667, 0.5333333333, 0.5294117647058824, 0.0, 0.6666666667, + 0.5333333333, 0.5333333333333333, 0.0, 0.6666666667, 0.5333333333, 0.5372549019607843, 0.0, + 0.6666666667, 0.5333333333, 0.5411764705882353, 0.0, 0.6666666667, 0.5333333333, + 0.5450980392156862, 0.0, 0.6666666667, 0.5333333333, 0.5490196078431373, 0.0, 0.6666666667, + 0.5333333333, 0.5529411764705883, 0.0, 0.6666666667, 0.5333333333, 0.5568627450980392, 0.0, + 0.6666666667, 0.5333333333, 0.5607843137254902, 0.0, 0.6666666667, 0.5333333333, + 0.5647058823529412, 0.0, 0.6666666667, 0.5333333333, 0.5686274509803921, 0.0, 0.6666666667, + 0.5333333333, 0.5725490196078431, 0.0, 0.6666666667, 0.5333333333, 0.5764705882352941, 0.0, + 0.6666666667, 0.5333333333, 0.5803921568627451, 0.0, 0.6666666667, 0.5333333333, + 0.5843137254901961, 0.0, 0.6666666667, 0.5333333333, 0.5882352941176471, 0.0, 0.6666666667, + 0.5333333333, 0.592156862745098, 0.0, 0.6666666667, 0.5333333333, 0.596078431372549, 0.0, + 0.6666666667, 0.5333333333, 0.6, 0.0, 0.6666666667, 0.5333333333, 0.6039215686274509, 0.0, + 0.6666666667, 0.5333333333, 0.6078431372549019, 0.4, 1.0, 0.4, 0.611764705882353, 0.4, 1.0, + 0.4, 0.615686274509804, 0.4, 1.0, 0.4, 0.6196078431372549, 0.4, 1.0, 0.4, 0.6235294117647059, + 0.4, 1.0, 0.4, 0.6274509803921569, 0.4, 1.0, 0.4, 0.6313725490196078, 0.4, 1.0, 0.4, + 0.6352941176470588, 0.4, 1.0, 0.4, 0.6392156862745098, 0.4, 1.0, 0.4, 0.6431372549019608, 0.4, + 1.0, 0.4, 0.6470588235294118, 0.4, 1.0, 0.4, 0.6509803921568628, 0.4, 1.0, 0.4, + 0.6549019607843137, 0.4, 1.0, 0.4, 0.6588235294117647, 0.4, 1.0, 0.4, 0.6627450980392157, 0.4, + 1.0, 0.4, 0.6666666666666666, 0.4, 1.0, 0.4, 0.6705882352941176, 0.4, 1.0, 0.4, + 0.6745098039215687, 0.4, 1.0, 0.4, 0.6784313725490196, 0.4, 1.0, 0.4, 0.6823529411764706, 0.4, + 1.0, 0.4, 0.6862745098039216, 0.4, 1.0, 0.4, 0.6901960784313725, 0.4, 1.0, 0.4, + 0.6941176470588235, 0.4, 1.0, 0.4, 0.6980392156862745, 0.4, 1.0, 0.4, 0.7019607843137254, 0.4, + 1.0, 0.4, 0.7058823529411765, 1.0, 0.9490196078, 0.0, 0.7098039215686275, 1.0, 0.9490196078, + 0.0, 0.7137254901960784, 1.0, 0.9490196078, 0.0, 0.7176470588235294, 1.0, 0.9490196078, 0.0, + 0.7215686274509804, 1.0, 0.9490196078, 0.0, 0.7254901960784313, 1.0, 0.9490196078, 0.0, + 0.7294117647058823, 1.0, 0.9490196078, 0.0, 0.7333333333333333, 1.0, 0.9490196078, 0.0, + 0.7372549019607844, 1.0, 0.9490196078, 0.0, 0.7411764705882353, 1.0, 0.9490196078, 0.0, + 0.7450980392156863, 1.0, 0.9490196078, 0.0, 0.7490196078431373, 1.0, 0.9490196078, 0.0, + 0.7529411764705882, 1.0, 0.9490196078, 0.0, 0.7568627450980392, 1.0, 0.9490196078, 0.0, + 0.7607843137254902, 1.0, 0.9490196078, 0.0, 0.7647058823529411, 1.0, 0.9490196078, 0.0, + 0.7686274509803922, 1.0, 0.9490196078, 0.0, 0.7725490196078432, 1.0, 0.9490196078, 0.0, + 0.7764705882352941, 1.0, 0.9490196078, 0.0, 0.7803921568627451, 1.0, 0.9490196078, 0.0, + 0.7843137254901961, 1.0, 0.9490196078, 0.0, 0.788235294117647, 1.0, 0.9490196078, 0.0, + 0.792156862745098, 1.0, 0.9490196078, 0.0, 0.796078431372549, 1.0, 0.9490196078, 0.0, 0.8, + 1.0, 0.9490196078, 0.0, 0.803921568627451, 1.0, 0.9490196078, 0.0, 0.807843137254902, + 0.9490196078, 0.6509803922, 0.2509803922, 0.8117647058823529, 0.9490196078, 0.6509803922, + 0.2509803922, 0.8156862745098039, 0.9490196078, 0.6509803922, 0.2509803922, + 0.8196078431372549, 0.9490196078, 0.6509803922, 0.2509803922, 0.8235294117647058, + 0.9490196078, 0.6509803922, 0.2509803922, 0.8274509803921568, 0.9490196078, 0.6509803922, + 0.2509803922, 0.8313725490196079, 0.9490196078, 0.6509803922, 0.2509803922, + 0.8352941176470589, 0.9490196078, 0.6509803922, 0.2509803922, 0.8392156862745098, + 0.9490196078, 0.6509803922, 0.2509803922, 0.8431372549019608, 0.9490196078, 0.6509803922, + 0.2509803922, 0.8470588235294118, 0.9490196078, 0.6509803922, 0.2509803922, + 0.8509803921568627, 0.9490196078, 0.6509803922, 0.2509803922, 0.8549019607843137, + 0.9490196078, 0.6509803922, 0.2509803922, 0.8588235294117647, 0.9490196078, 0.6509803922, + 0.2509803922, 0.8627450980392157, 0.9490196078, 0.6509803922, 0.2509803922, + 0.8666666666666667, 0.9490196078, 0.6509803922, 0.2509803922, 0.8705882352941177, + 0.9490196078, 0.6509803922, 0.2509803922, 0.8745098039215686, 0.9490196078, 0.6509803922, + 0.2509803922, 0.8784313725490196, 0.9490196078, 0.6509803922, 0.2509803922, + 0.8823529411764706, 0.9490196078, 0.6509803922, 0.2509803922, 0.8862745098039215, + 0.9490196078, 0.6509803922, 0.2509803922, 0.8901960784313725, 0.9490196078, 0.6509803922, + 0.2509803922, 0.8941176470588236, 0.9490196078, 0.6509803922, 0.2509803922, + 0.8980392156862745, 0.9490196078, 0.6509803922, 0.2509803922, 0.9019607843137255, + 0.9490196078, 0.6509803922, 0.2509803922, 0.9058823529411765, 0.9490196078, 0.6509803922, + 0.2509803922, 0.9098039215686274, 1.0, 0.0, 0.0, 0.9137254901960784, 1.0, 0.0, 0.0, + 0.9176470588235294, 1.0, 0.0, 0.0, 0.9215686274509803, 1.0, 0.0, 0.0, 0.9254901960784314, 1.0, + 0.0, 0.0, 0.9294117647058824, 1.0, 0.0, 0.0, 0.9333333333333333, 1.0, 0.0, 0.0, + 0.9372549019607843, 1.0, 0.0, 0.0, 0.9411764705882354, 1.0, 0.0, 0.0, 0.9450980392156864, 1.0, + 0.0, 0.0, 0.9490196078431372, 1.0, 0.0, 0.0, 0.9529411764705882, 1.0, 0.0, 0.0, + 0.9568627450980394, 1.0, 0.0, 0.0, 0.9607843137254903, 1.0, 0.0, 0.0, 0.9647058823529413, 1.0, + 0.0, 0.0, 0.9686274509803922, 1.0, 0.0, 0.0, 0.9725490196078431, 1.0, 0.0, 0.0, + 0.9764705882352941, 1.0, 0.0, 0.0, 0.9803921568627451, 1.0, 0.0, 0.0, 0.984313725490196, 1.0, + 0.0, 0.0, 0.9882352941176471, 1.0, 0.0, 0.0, 0.9921568627450981, 1.0, 0.0, 0.0, + 0.996078431372549, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, + ], + }, + { + ColorSpace: 'RGB', + Name: 'ge_256', + RGBPoints: [ + 0.0, 0.0039215686, 0.0078431373, 0.0078431373, 0.00392156862745098, 0.0039215686, + 0.0078431373, 0.0078431373, 0.00784313725490196, 0.0039215686, 0.0078431373, 0.0117647059, + 0.011764705882352941, 0.0039215686, 0.0117647059, 0.0156862745, 0.01568627450980392, + 0.0039215686, 0.0117647059, 0.0196078431, 0.0196078431372549, 0.0039215686, 0.0156862745, + 0.0235294118, 0.023529411764705882, 0.0039215686, 0.0156862745, 0.0274509804, + 0.027450980392156862, 0.0039215686, 0.0196078431, 0.031372549, 0.03137254901960784, + 0.0039215686, 0.0196078431, 0.0352941176, 0.03529411764705882, 0.0039215686, 0.0235294118, + 0.0392156863, 0.0392156862745098, 0.0039215686, 0.0235294118, 0.0431372549, + 0.043137254901960784, 0.0039215686, 0.0274509804, 0.0470588235, 0.047058823529411764, + 0.0039215686, 0.0274509804, 0.0509803922, 0.050980392156862744, 0.0039215686, 0.031372549, + 0.0549019608, 0.054901960784313725, 0.0039215686, 0.031372549, 0.0588235294, + 0.05882352941176471, 0.0039215686, 0.0352941176, 0.062745098, 0.06274509803921569, + 0.0039215686, 0.0352941176, 0.0666666667, 0.06666666666666667, 0.0039215686, 0.0392156863, + 0.0705882353, 0.07058823529411765, 0.0039215686, 0.0392156863, 0.0745098039, + 0.07450980392156863, 0.0039215686, 0.0431372549, 0.0784313725, 0.0784313725490196, + 0.0039215686, 0.0431372549, 0.0823529412, 0.08235294117647059, 0.0039215686, 0.0470588235, + 0.0862745098, 0.08627450980392157, 0.0039215686, 0.0470588235, 0.0901960784, + 0.09019607843137255, 0.0039215686, 0.0509803922, 0.0941176471, 0.09411764705882353, + 0.0039215686, 0.0509803922, 0.0980392157, 0.09803921568627451, 0.0039215686, 0.0549019608, + 0.1019607843, 0.10196078431372549, 0.0039215686, 0.0549019608, 0.1058823529, + 0.10588235294117647, 0.0039215686, 0.0588235294, 0.1098039216, 0.10980392156862745, + 0.0039215686, 0.0588235294, 0.1137254902, 0.11372549019607843, 0.0039215686, 0.062745098, + 0.1176470588, 0.11764705882352942, 0.0039215686, 0.062745098, 0.1215686275, + 0.12156862745098039, 0.0039215686, 0.0666666667, 0.1254901961, 0.12549019607843137, + 0.0039215686, 0.0666666667, 0.1294117647, 0.12941176470588237, 0.0039215686, 0.0705882353, + 0.1333333333, 0.13333333333333333, 0.0039215686, 0.0705882353, 0.137254902, + 0.13725490196078433, 0.0039215686, 0.0745098039, 0.1411764706, 0.1411764705882353, + 0.0039215686, 0.0745098039, 0.1450980392, 0.1450980392156863, 0.0039215686, 0.0784313725, + 0.1490196078, 0.14901960784313725, 0.0039215686, 0.0784313725, 0.1529411765, + 0.15294117647058825, 0.0039215686, 0.0823529412, 0.1568627451, 0.1568627450980392, + 0.0039215686, 0.0823529412, 0.1607843137, 0.1607843137254902, 0.0039215686, 0.0862745098, + 0.1647058824, 0.16470588235294117, 0.0039215686, 0.0862745098, 0.168627451, + 0.16862745098039217, 0.0039215686, 0.0901960784, 0.1725490196, 0.17254901960784313, + 0.0039215686, 0.0901960784, 0.1764705882, 0.17647058823529413, 0.0039215686, 0.0941176471, + 0.1803921569, 0.1803921568627451, 0.0039215686, 0.0941176471, 0.1843137255, + 0.1843137254901961, 0.0039215686, 0.0980392157, 0.1882352941, 0.18823529411764706, + 0.0039215686, 0.0980392157, 0.1921568627, 0.19215686274509805, 0.0039215686, 0.1019607843, + 0.1960784314, 0.19607843137254902, 0.0039215686, 0.1019607843, 0.2, 0.2, 0.0039215686, + 0.1058823529, 0.2039215686, 0.20392156862745098, 0.0039215686, 0.1058823529, 0.2078431373, + 0.20784313725490197, 0.0039215686, 0.1098039216, 0.2117647059, 0.21176470588235294, + 0.0039215686, 0.1098039216, 0.2156862745, 0.21568627450980393, 0.0039215686, 0.1137254902, + 0.2196078431, 0.2196078431372549, 0.0039215686, 0.1137254902, 0.2235294118, + 0.2235294117647059, 0.0039215686, 0.1176470588, 0.2274509804, 0.22745098039215686, + 0.0039215686, 0.1176470588, 0.231372549, 0.23137254901960785, 0.0039215686, 0.1215686275, + 0.2352941176, 0.23529411764705885, 0.0039215686, 0.1215686275, 0.2392156863, + 0.23921568627450984, 0.0039215686, 0.1254901961, 0.2431372549, 0.24313725490196078, + 0.0039215686, 0.1254901961, 0.2470588235, 0.24705882352941178, 0.0039215686, 0.1294117647, + 0.2509803922, 0.25098039215686274, 0.0039215686, 0.1294117647, 0.2509803922, + 0.2549019607843137, 0.0078431373, 0.1254901961, 0.2549019608, 0.25882352941176473, + 0.0156862745, 0.1254901961, 0.2588235294, 0.2627450980392157, 0.0235294118, 0.1215686275, + 0.262745098, 0.26666666666666666, 0.031372549, 0.1215686275, 0.2666666667, + 0.27058823529411763, 0.0392156863, 0.1176470588, 0.2705882353, 0.27450980392156865, + 0.0470588235, 0.1176470588, 0.2745098039, 0.2784313725490196, 0.0549019608, 0.1137254902, + 0.2784313725, 0.2823529411764706, 0.062745098, 0.1137254902, 0.2823529412, + 0.28627450980392155, 0.0705882353, 0.1098039216, 0.2862745098, 0.2901960784313726, + 0.0784313725, 0.1098039216, 0.2901960784, 0.29411764705882354, 0.0862745098, 0.1058823529, + 0.2941176471, 0.2980392156862745, 0.0941176471, 0.1058823529, 0.2980392157, + 0.30196078431372547, 0.1019607843, 0.1019607843, 0.3019607843, 0.3058823529411765, + 0.1098039216, 0.1019607843, 0.3058823529, 0.30980392156862746, 0.1176470588, 0.0980392157, + 0.3098039216, 0.3137254901960784, 0.1254901961, 0.0980392157, 0.3137254902, + 0.3176470588235294, 0.1333333333, 0.0941176471, 0.3176470588, 0.3215686274509804, + 0.1411764706, 0.0941176471, 0.3215686275, 0.3254901960784314, 0.1490196078, 0.0901960784, + 0.3254901961, 0.32941176470588235, 0.1568627451, 0.0901960784, 0.3294117647, + 0.3333333333333333, 0.1647058824, 0.0862745098, 0.3333333333, 0.33725490196078434, + 0.1725490196, 0.0862745098, 0.337254902, 0.3411764705882353, 0.1803921569, 0.0823529412, + 0.3411764706, 0.34509803921568627, 0.1882352941, 0.0823529412, 0.3450980392, + 0.34901960784313724, 0.1960784314, 0.0784313725, 0.3490196078, 0.35294117647058826, + 0.2039215686, 0.0784313725, 0.3529411765, 0.3568627450980392, 0.2117647059, 0.0745098039, + 0.3568627451, 0.3607843137254902, 0.2196078431, 0.0745098039, 0.3607843137, + 0.36470588235294116, 0.2274509804, 0.0705882353, 0.3647058824, 0.3686274509803922, + 0.2352941176, 0.0705882353, 0.368627451, 0.37254901960784315, 0.2431372549, 0.0666666667, + 0.3725490196, 0.3764705882352941, 0.2509803922, 0.0666666667, 0.3764705882, + 0.3803921568627451, 0.2549019608, 0.062745098, 0.3803921569, 0.3843137254901961, 0.262745098, + 0.062745098, 0.3843137255, 0.38823529411764707, 0.2705882353, 0.0588235294, 0.3882352941, + 0.39215686274509803, 0.2784313725, 0.0588235294, 0.3921568627, 0.396078431372549, + 0.2862745098, 0.0549019608, 0.3960784314, 0.4, 0.2941176471, 0.0549019608, 0.4, + 0.403921568627451, 0.3019607843, 0.0509803922, 0.4039215686, 0.40784313725490196, + 0.3098039216, 0.0509803922, 0.4078431373, 0.4117647058823529, 0.3176470588, 0.0470588235, + 0.4117647059, 0.41568627450980394, 0.3254901961, 0.0470588235, 0.4156862745, + 0.4196078431372549, 0.3333333333, 0.0431372549, 0.4196078431, 0.4235294117647059, + 0.3411764706, 0.0431372549, 0.4235294118, 0.42745098039215684, 0.3490196078, 0.0392156863, + 0.4274509804, 0.43137254901960786, 0.3568627451, 0.0392156863, 0.431372549, + 0.43529411764705883, 0.3647058824, 0.0352941176, 0.4352941176, 0.4392156862745098, + 0.3725490196, 0.0352941176, 0.4392156863, 0.44313725490196076, 0.3803921569, 0.031372549, + 0.4431372549, 0.4470588235294118, 0.3882352941, 0.031372549, 0.4470588235, + 0.45098039215686275, 0.3960784314, 0.0274509804, 0.4509803922, 0.4549019607843137, + 0.4039215686, 0.0274509804, 0.4549019608, 0.4588235294117647, 0.4117647059, 0.0235294118, + 0.4588235294, 0.4627450980392157, 0.4196078431, 0.0235294118, 0.462745098, 0.4666666666666667, + 0.4274509804, 0.0196078431, 0.4666666667, 0.4705882352941177, 0.4352941176, 0.0196078431, + 0.4705882353, 0.4745098039215686, 0.4431372549, 0.0156862745, 0.4745098039, + 0.4784313725490197, 0.4509803922, 0.0156862745, 0.4784313725, 0.48235294117647065, + 0.4588235294, 0.0117647059, 0.4823529412, 0.48627450980392156, 0.4666666667, 0.0117647059, + 0.4862745098, 0.49019607843137253, 0.4745098039, 0.0078431373, 0.4901960784, + 0.49411764705882355, 0.4823529412, 0.0078431373, 0.4941176471, 0.4980392156862745, + 0.4901960784, 0.0039215686, 0.4980392157, 0.5019607843137255, 0.4980392157, 0.0117647059, + 0.4980392157, 0.5058823529411764, 0.5058823529, 0.0156862745, 0.4901960784, + 0.5098039215686274, 0.5137254902, 0.0235294118, 0.4823529412, 0.5137254901960784, + 0.5215686275, 0.0274509804, 0.4745098039, 0.5176470588235295, 0.5294117647, 0.0352941176, + 0.4666666667, 0.5215686274509804, 0.537254902, 0.0392156863, 0.4588235294, 0.5254901960784314, + 0.5450980392, 0.0470588235, 0.4509803922, 0.5294117647058824, 0.5529411765, 0.0509803922, + 0.4431372549, 0.5333333333333333, 0.5607843137, 0.0588235294, 0.4352941176, + 0.5372549019607843, 0.568627451, 0.062745098, 0.4274509804, 0.5411764705882353, 0.5764705882, + 0.0705882353, 0.4196078431, 0.5450980392156862, 0.5843137255, 0.0745098039, 0.4117647059, + 0.5490196078431373, 0.5921568627, 0.0823529412, 0.4039215686, 0.5529411764705883, 0.6, + 0.0862745098, 0.3960784314, 0.5568627450980392, 0.6078431373, 0.0941176471, 0.3882352941, + 0.5607843137254902, 0.6156862745, 0.0980392157, 0.3803921569, 0.5647058823529412, + 0.6235294118, 0.1058823529, 0.3725490196, 0.5686274509803921, 0.631372549, 0.1098039216, + 0.3647058824, 0.5725490196078431, 0.6392156863, 0.1176470588, 0.3568627451, + 0.5764705882352941, 0.6470588235, 0.1215686275, 0.3490196078, 0.5803921568627451, + 0.6549019608, 0.1294117647, 0.3411764706, 0.5843137254901961, 0.662745098, 0.1333333333, + 0.3333333333, 0.5882352941176471, 0.6705882353, 0.1411764706, 0.3254901961, 0.592156862745098, + 0.6784313725, 0.1450980392, 0.3176470588, 0.596078431372549, 0.6862745098, 0.1529411765, + 0.3098039216, 0.6, 0.6941176471, 0.1568627451, 0.3019607843, 0.6039215686274509, 0.7019607843, + 0.1647058824, 0.2941176471, 0.6078431372549019, 0.7098039216, 0.168627451, 0.2862745098, + 0.611764705882353, 0.7176470588, 0.1764705882, 0.2784313725, 0.615686274509804, 0.7254901961, + 0.1803921569, 0.2705882353, 0.6196078431372549, 0.7333333333, 0.1882352941, 0.262745098, + 0.6235294117647059, 0.7411764706, 0.1921568627, 0.2549019608, 0.6274509803921569, + 0.7490196078, 0.2, 0.2509803922, 0.6313725490196078, 0.7529411765, 0.2039215686, 0.2431372549, + 0.6352941176470588, 0.7607843137, 0.2117647059, 0.2352941176, 0.6392156862745098, 0.768627451, + 0.2156862745, 0.2274509804, 0.6431372549019608, 0.7764705882, 0.2235294118, 0.2196078431, + 0.6470588235294118, 0.7843137255, 0.2274509804, 0.2117647059, 0.6509803921568628, + 0.7921568627, 0.2352941176, 0.2039215686, 0.6549019607843137, 0.8, 0.2392156863, 0.1960784314, + 0.6588235294117647, 0.8078431373, 0.2470588235, 0.1882352941, 0.6627450980392157, + 0.8156862745, 0.2509803922, 0.1803921569, 0.6666666666666666, 0.8235294118, 0.2549019608, + 0.1725490196, 0.6705882352941176, 0.831372549, 0.2588235294, 0.1647058824, 0.6745098039215687, + 0.8392156863, 0.2666666667, 0.1568627451, 0.6784313725490196, 0.8470588235, 0.2705882353, + 0.1490196078, 0.6823529411764706, 0.8549019608, 0.2784313725, 0.1411764706, + 0.6862745098039216, 0.862745098, 0.2823529412, 0.1333333333, 0.6901960784313725, 0.8705882353, + 0.2901960784, 0.1254901961, 0.6941176470588235, 0.8784313725, 0.2941176471, 0.1176470588, + 0.6980392156862745, 0.8862745098, 0.3019607843, 0.1098039216, 0.7019607843137254, + 0.8941176471, 0.3058823529, 0.1019607843, 0.7058823529411765, 0.9019607843, 0.3137254902, + 0.0941176471, 0.7098039215686275, 0.9098039216, 0.3176470588, 0.0862745098, + 0.7137254901960784, 0.9176470588, 0.3254901961, 0.0784313725, 0.7176470588235294, + 0.9254901961, 0.3294117647, 0.0705882353, 0.7215686274509804, 0.9333333333, 0.337254902, + 0.062745098, 0.7254901960784313, 0.9411764706, 0.3411764706, 0.0549019608, 0.7294117647058823, + 0.9490196078, 0.3490196078, 0.0470588235, 0.7333333333333333, 0.9568627451, 0.3529411765, + 0.0392156863, 0.7372549019607844, 0.9647058824, 0.3607843137, 0.031372549, 0.7411764705882353, + 0.9725490196, 0.3647058824, 0.0235294118, 0.7450980392156863, 0.9803921569, 0.3725490196, + 0.0156862745, 0.7490196078431373, 0.9882352941, 0.3725490196, 0.0039215686, + 0.7529411764705882, 0.9960784314, 0.3843137255, 0.0156862745, 0.7568627450980392, + 0.9960784314, 0.3921568627, 0.031372549, 0.7607843137254902, 0.9960784314, 0.4039215686, + 0.0470588235, 0.7647058823529411, 0.9960784314, 0.4117647059, 0.062745098, 0.7686274509803922, + 0.9960784314, 0.4235294118, 0.0784313725, 0.7725490196078432, 0.9960784314, 0.431372549, + 0.0941176471, 0.7764705882352941, 0.9960784314, 0.4431372549, 0.1098039216, + 0.7803921568627451, 0.9960784314, 0.4509803922, 0.1254901961, 0.7843137254901961, + 0.9960784314, 0.462745098, 0.1411764706, 0.788235294117647, 0.9960784314, 0.4705882353, + 0.1568627451, 0.792156862745098, 0.9960784314, 0.4823529412, 0.1725490196, 0.796078431372549, + 0.9960784314, 0.4901960784, 0.1882352941, 0.8, 0.9960784314, 0.5019607843, 0.2039215686, + 0.803921568627451, 0.9960784314, 0.5098039216, 0.2196078431, 0.807843137254902, 0.9960784314, + 0.5215686275, 0.2352941176, 0.8117647058823529, 0.9960784314, 0.5294117647, 0.2509803922, + 0.8156862745098039, 0.9960784314, 0.5411764706, 0.262745098, 0.8196078431372549, 0.9960784314, + 0.5490196078, 0.2784313725, 0.8235294117647058, 0.9960784314, 0.5607843137, 0.2941176471, + 0.8274509803921568, 0.9960784314, 0.568627451, 0.3098039216, 0.8313725490196079, 0.9960784314, + 0.5803921569, 0.3254901961, 0.8352941176470589, 0.9960784314, 0.5882352941, 0.3411764706, + 0.8392156862745098, 0.9960784314, 0.6, 0.3568627451, 0.8431372549019608, 0.9960784314, + 0.6078431373, 0.3725490196, 0.8470588235294118, 0.9960784314, 0.6196078431, 0.3882352941, + 0.8509803921568627, 0.9960784314, 0.6274509804, 0.4039215686, 0.8549019607843137, + 0.9960784314, 0.6392156863, 0.4196078431, 0.8588235294117647, 0.9960784314, 0.6470588235, + 0.4352941176, 0.8627450980392157, 0.9960784314, 0.6588235294, 0.4509803922, + 0.8666666666666667, 0.9960784314, 0.6666666667, 0.4666666667, 0.8705882352941177, + 0.9960784314, 0.6784313725, 0.4823529412, 0.8745098039215686, 0.9960784314, 0.6862745098, + 0.4980392157, 0.8784313725490196, 0.9960784314, 0.6980392157, 0.5137254902, + 0.8823529411764706, 0.9960784314, 0.7058823529, 0.5294117647, 0.8862745098039215, + 0.9960784314, 0.7176470588, 0.5450980392, 0.8901960784313725, 0.9960784314, 0.7254901961, + 0.5607843137, 0.8941176470588236, 0.9960784314, 0.737254902, 0.5764705882, 0.8980392156862745, + 0.9960784314, 0.7450980392, 0.5921568627, 0.9019607843137255, 0.9960784314, 0.7529411765, + 0.6078431373, 0.9058823529411765, 0.9960784314, 0.7607843137, 0.6235294118, + 0.9098039215686274, 0.9960784314, 0.7725490196, 0.6392156863, 0.9137254901960784, + 0.9960784314, 0.7803921569, 0.6549019608, 0.9176470588235294, 0.9960784314, 0.7921568627, + 0.6705882353, 0.9215686274509803, 0.9960784314, 0.8, 0.6862745098, 0.9254901960784314, + 0.9960784314, 0.8117647059, 0.7019607843, 0.9294117647058824, 0.9960784314, 0.8196078431, + 0.7176470588, 0.9333333333333333, 0.9960784314, 0.831372549, 0.7333333333, 0.9372549019607843, + 0.9960784314, 0.8392156863, 0.7490196078, 0.9411764705882354, 0.9960784314, 0.8509803922, + 0.7607843137, 0.9450980392156864, 0.9960784314, 0.8588235294, 0.7764705882, + 0.9490196078431372, 0.9960784314, 0.8705882353, 0.7921568627, 0.9529411764705882, + 0.9960784314, 0.8784313725, 0.8078431373, 0.9568627450980394, 0.9960784314, 0.8901960784, + 0.8235294118, 0.9607843137254903, 0.9960784314, 0.8980392157, 0.8392156863, + 0.9647058823529413, 0.9960784314, 0.9098039216, 0.8549019608, 0.9686274509803922, + 0.9960784314, 0.9176470588, 0.8705882353, 0.9725490196078431, 0.9960784314, 0.9294117647, + 0.8862745098, 0.9764705882352941, 0.9960784314, 0.937254902, 0.9019607843, 0.9803921568627451, + 0.9960784314, 0.9490196078, 0.9176470588, 0.984313725490196, 0.9960784314, 0.9568627451, + 0.9333333333, 0.9882352941176471, 0.9960784314, 0.968627451, 0.9490196078, 0.9921568627450981, + 0.9960784314, 0.9764705882, 0.9647058824, 0.996078431372549, 0.9960784314, 0.9882352941, + 0.9803921569, 1.0, 0.9960784314, 0.9882352941, 0.9803921569, + ], + }, + { + ColorSpace: 'RGB', + Name: 'ge', + RGBPoints: [ + 0.0, 0.0078431373, 0.0078431373, 0.0078431373, 0.00392156862745098, 0.0078431373, + 0.0078431373, 0.0078431373, 0.00784313725490196, 0.0078431373, 0.0078431373, 0.0078431373, + 0.011764705882352941, 0.0078431373, 0.0078431373, 0.0078431373, 0.01568627450980392, + 0.0078431373, 0.0078431373, 0.0078431373, 0.0196078431372549, 0.0078431373, 0.0078431373, + 0.0078431373, 0.023529411764705882, 0.0078431373, 0.0078431373, 0.0078431373, + 0.027450980392156862, 0.0078431373, 0.0078431373, 0.0078431373, 0.03137254901960784, + 0.0078431373, 0.0078431373, 0.0078431373, 0.03529411764705882, 0.0078431373, 0.0078431373, + 0.0078431373, 0.0392156862745098, 0.0078431373, 0.0078431373, 0.0078431373, + 0.043137254901960784, 0.0078431373, 0.0078431373, 0.0078431373, 0.047058823529411764, + 0.0078431373, 0.0078431373, 0.0078431373, 0.050980392156862744, 0.0078431373, 0.0078431373, + 0.0078431373, 0.054901960784313725, 0.0078431373, 0.0078431373, 0.0078431373, + 0.05882352941176471, 0.0117647059, 0.0078431373, 0.0078431373, 0.06274509803921569, + 0.0078431373, 0.0156862745, 0.0156862745, 0.06666666666666667, 0.0078431373, 0.0235294118, + 0.0235294118, 0.07058823529411765, 0.0078431373, 0.031372549, 0.031372549, + 0.07450980392156863, 0.0078431373, 0.0392156863, 0.0392156863, 0.0784313725490196, + 0.0078431373, 0.0470588235, 0.0470588235, 0.08235294117647059, 0.0078431373, 0.0549019608, + 0.0549019608, 0.08627450980392157, 0.0078431373, 0.062745098, 0.062745098, + 0.09019607843137255, 0.0078431373, 0.0705882353, 0.0705882353, 0.09411764705882353, + 0.0078431373, 0.0784313725, 0.0784313725, 0.09803921568627451, 0.0078431373, 0.0901960784, + 0.0862745098, 0.10196078431372549, 0.0078431373, 0.0980392157, 0.0941176471, + 0.10588235294117647, 0.0078431373, 0.1058823529, 0.1019607843, 0.10980392156862745, + 0.0078431373, 0.1137254902, 0.1098039216, 0.11372549019607843, 0.0078431373, 0.1215686275, + 0.1176470588, 0.11764705882352942, 0.0078431373, 0.1294117647, 0.1254901961, + 0.12156862745098039, 0.0078431373, 0.137254902, 0.1333333333, 0.12549019607843137, + 0.0078431373, 0.1450980392, 0.1411764706, 0.12941176470588237, 0.0078431373, 0.1529411765, + 0.1490196078, 0.13333333333333333, 0.0078431373, 0.1647058824, 0.1568627451, + 0.13725490196078433, 0.0078431373, 0.1725490196, 0.1647058824, 0.1411764705882353, + 0.0078431373, 0.1803921569, 0.1725490196, 0.1450980392156863, 0.0078431373, 0.1882352941, + 0.1803921569, 0.14901960784313725, 0.0078431373, 0.1960784314, 0.1882352941, + 0.15294117647058825, 0.0078431373, 0.2039215686, 0.1960784314, 0.1568627450980392, + 0.0078431373, 0.2117647059, 0.2039215686, 0.1607843137254902, 0.0078431373, 0.2196078431, + 0.2117647059, 0.16470588235294117, 0.0078431373, 0.2274509804, 0.2196078431, + 0.16862745098039217, 0.0078431373, 0.2352941176, 0.2274509804, 0.17254901960784313, + 0.0078431373, 0.2470588235, 0.2352941176, 0.17647058823529413, 0.0078431373, 0.2509803922, + 0.2431372549, 0.1803921568627451, 0.0078431373, 0.2549019608, 0.2509803922, + 0.1843137254901961, 0.0078431373, 0.262745098, 0.2509803922, 0.18823529411764706, + 0.0078431373, 0.2705882353, 0.2588235294, 0.19215686274509805, 0.0078431373, 0.2784313725, + 0.2666666667, 0.19607843137254902, 0.0078431373, 0.2862745098, 0.2745098039, 0.2, + 0.0078431373, 0.2941176471, 0.2823529412, 0.20392156862745098, 0.0078431373, 0.3019607843, + 0.2901960784, 0.20784313725490197, 0.0078431373, 0.3137254902, 0.2980392157, + 0.21176470588235294, 0.0078431373, 0.3215686275, 0.3058823529, 0.21568627450980393, + 0.0078431373, 0.3294117647, 0.3137254902, 0.2196078431372549, 0.0078431373, 0.337254902, + 0.3215686275, 0.2235294117647059, 0.0078431373, 0.3450980392, 0.3294117647, + 0.22745098039215686, 0.0078431373, 0.3529411765, 0.337254902, 0.23137254901960785, + 0.0078431373, 0.3607843137, 0.3450980392, 0.23529411764705885, 0.0078431373, 0.368627451, + 0.3529411765, 0.23921568627450984, 0.0078431373, 0.3764705882, 0.3607843137, + 0.24313725490196078, 0.0078431373, 0.3843137255, 0.368627451, 0.24705882352941178, + 0.0078431373, 0.3960784314, 0.3764705882, 0.25098039215686274, 0.0078431373, 0.4039215686, + 0.3843137255, 0.2549019607843137, 0.0078431373, 0.4117647059, 0.3921568627, + 0.25882352941176473, 0.0078431373, 0.4196078431, 0.4, 0.2627450980392157, 0.0078431373, + 0.4274509804, 0.4078431373, 0.26666666666666666, 0.0078431373, 0.4352941176, 0.4156862745, + 0.27058823529411763, 0.0078431373, 0.4431372549, 0.4235294118, 0.27450980392156865, + 0.0078431373, 0.4509803922, 0.431372549, 0.2784313725490196, 0.0078431373, 0.4588235294, + 0.4392156863, 0.2823529411764706, 0.0078431373, 0.4705882353, 0.4470588235, + 0.28627450980392155, 0.0078431373, 0.4784313725, 0.4549019608, 0.2901960784313726, + 0.0078431373, 0.4862745098, 0.462745098, 0.29411764705882354, 0.0078431373, 0.4941176471, + 0.4705882353, 0.2980392156862745, 0.0078431373, 0.5019607843, 0.4784313725, + 0.30196078431372547, 0.0117647059, 0.5098039216, 0.4862745098, 0.3058823529411765, + 0.0196078431, 0.5019607843, 0.4941176471, 0.30980392156862746, 0.0274509804, 0.4941176471, + 0.5058823529, 0.3137254901960784, 0.0352941176, 0.4862745098, 0.5137254902, + 0.3176470588235294, 0.0431372549, 0.4784313725, 0.5215686275, 0.3215686274509804, + 0.0509803922, 0.4705882353, 0.5294117647, 0.3254901960784314, 0.0588235294, 0.462745098, + 0.537254902, 0.32941176470588235, 0.0666666667, 0.4549019608, 0.5450980392, + 0.3333333333333333, 0.0745098039, 0.4470588235, 0.5529411765, 0.33725490196078434, + 0.0823529412, 0.4392156863, 0.5607843137, 0.3411764705882353, 0.0901960784, 0.431372549, + 0.568627451, 0.34509803921568627, 0.0980392157, 0.4235294118, 0.5764705882, + 0.34901960784313724, 0.1058823529, 0.4156862745, 0.5843137255, 0.35294117647058826, + 0.1137254902, 0.4078431373, 0.5921568627, 0.3568627450980392, 0.1215686275, 0.4, 0.6, + 0.3607843137254902, 0.1294117647, 0.3921568627, 0.6078431373, 0.36470588235294116, + 0.137254902, 0.3843137255, 0.6156862745, 0.3686274509803922, 0.1450980392, 0.3764705882, + 0.6235294118, 0.37254901960784315, 0.1529411765, 0.368627451, 0.631372549, 0.3764705882352941, + 0.1607843137, 0.3607843137, 0.6392156863, 0.3803921568627451, 0.168627451, 0.3529411765, + 0.6470588235, 0.3843137254901961, 0.1764705882, 0.3450980392, 0.6549019608, + 0.38823529411764707, 0.1843137255, 0.337254902, 0.662745098, 0.39215686274509803, + 0.1921568627, 0.3294117647, 0.6705882353, 0.396078431372549, 0.2, 0.3215686275, 0.6784313725, + 0.4, 0.2078431373, 0.3137254902, 0.6862745098, 0.403921568627451, 0.2156862745, 0.3058823529, + 0.6941176471, 0.40784313725490196, 0.2235294118, 0.2980392157, 0.7019607843, + 0.4117647058823529, 0.231372549, 0.2901960784, 0.7098039216, 0.41568627450980394, + 0.2392156863, 0.2823529412, 0.7176470588, 0.4196078431372549, 0.2470588235, 0.2745098039, + 0.7254901961, 0.4235294117647059, 0.2509803922, 0.2666666667, 0.7333333333, + 0.42745098039215684, 0.2509803922, 0.2588235294, 0.7411764706, 0.43137254901960786, + 0.2588235294, 0.2509803922, 0.7490196078, 0.43529411764705883, 0.2666666667, 0.2509803922, + 0.7490196078, 0.4392156862745098, 0.2745098039, 0.2431372549, 0.7568627451, + 0.44313725490196076, 0.2823529412, 0.2352941176, 0.7647058824, 0.4470588235294118, + 0.2901960784, 0.2274509804, 0.7725490196, 0.45098039215686275, 0.2980392157, 0.2196078431, + 0.7803921569, 0.4549019607843137, 0.3058823529, 0.2117647059, 0.7882352941, + 0.4588235294117647, 0.3137254902, 0.2039215686, 0.7960784314, 0.4627450980392157, + 0.3215686275, 0.1960784314, 0.8039215686, 0.4666666666666667, 0.3294117647, 0.1882352941, + 0.8117647059, 0.4705882352941177, 0.337254902, 0.1803921569, 0.8196078431, 0.4745098039215686, + 0.3450980392, 0.1725490196, 0.8274509804, 0.4784313725490197, 0.3529411765, 0.1647058824, + 0.8352941176, 0.48235294117647065, 0.3607843137, 0.1568627451, 0.8431372549, + 0.48627450980392156, 0.368627451, 0.1490196078, 0.8509803922, 0.49019607843137253, + 0.3764705882, 0.1411764706, 0.8588235294, 0.49411764705882355, 0.3843137255, 0.1333333333, + 0.8666666667, 0.4980392156862745, 0.3921568627, 0.1254901961, 0.8745098039, + 0.5019607843137255, 0.4, 0.1176470588, 0.8823529412, 0.5058823529411764, 0.4078431373, + 0.1098039216, 0.8901960784, 0.5098039215686274, 0.4156862745, 0.1019607843, 0.8980392157, + 0.5137254901960784, 0.4235294118, 0.0941176471, 0.9058823529, 0.5176470588235295, 0.431372549, + 0.0862745098, 0.9137254902, 0.5215686274509804, 0.4392156863, 0.0784313725, 0.9215686275, + 0.5254901960784314, 0.4470588235, 0.0705882353, 0.9294117647, 0.5294117647058824, + 0.4549019608, 0.062745098, 0.937254902, 0.5333333333333333, 0.462745098, 0.0549019608, + 0.9450980392, 0.5372549019607843, 0.4705882353, 0.0470588235, 0.9529411765, + 0.5411764705882353, 0.4784313725, 0.0392156863, 0.9607843137, 0.5450980392156862, + 0.4862745098, 0.031372549, 0.968627451, 0.5490196078431373, 0.4941176471, 0.0235294118, + 0.9764705882, 0.5529411764705883, 0.4980392157, 0.0156862745, 0.9843137255, + 0.5568627450980392, 0.5058823529, 0.0078431373, 0.9921568627, 0.5607843137254902, + 0.5137254902, 0.0156862745, 0.9803921569, 0.5647058823529412, 0.5215686275, 0.0235294118, + 0.9647058824, 0.5686274509803921, 0.5294117647, 0.0352941176, 0.9490196078, + 0.5725490196078431, 0.537254902, 0.0431372549, 0.9333333333, 0.5764705882352941, 0.5450980392, + 0.0509803922, 0.9176470588, 0.5803921568627451, 0.5529411765, 0.062745098, 0.9019607843, + 0.5843137254901961, 0.5607843137, 0.0705882353, 0.8862745098, 0.5882352941176471, 0.568627451, + 0.0784313725, 0.8705882353, 0.592156862745098, 0.5764705882, 0.0901960784, 0.8549019608, + 0.596078431372549, 0.5843137255, 0.0980392157, 0.8392156863, 0.6, 0.5921568627, 0.1098039216, + 0.8235294118, 0.6039215686274509, 0.6, 0.1176470588, 0.8078431373, 0.6078431372549019, + 0.6078431373, 0.1254901961, 0.7921568627, 0.611764705882353, 0.6156862745, 0.137254902, + 0.7764705882, 0.615686274509804, 0.6235294118, 0.1450980392, 0.7607843137, 0.6196078431372549, + 0.631372549, 0.1529411765, 0.7490196078, 0.6235294117647059, 0.6392156863, 0.1647058824, + 0.737254902, 0.6274509803921569, 0.6470588235, 0.1725490196, 0.7215686275, 0.6313725490196078, + 0.6549019608, 0.1843137255, 0.7058823529, 0.6352941176470588, 0.662745098, 0.1921568627, + 0.6901960784, 0.6392156862745098, 0.6705882353, 0.2, 0.6745098039, 0.6431372549019608, + 0.6784313725, 0.2117647059, 0.6588235294, 0.6470588235294118, 0.6862745098, 0.2196078431, + 0.6431372549, 0.6509803921568628, 0.6941176471, 0.2274509804, 0.6274509804, + 0.6549019607843137, 0.7019607843, 0.2392156863, 0.6117647059, 0.6588235294117647, + 0.7098039216, 0.2470588235, 0.5960784314, 0.6627450980392157, 0.7176470588, 0.2509803922, + 0.5803921569, 0.6666666666666666, 0.7254901961, 0.2588235294, 0.5647058824, + 0.6705882352941176, 0.7333333333, 0.2666666667, 0.5490196078, 0.6745098039215687, + 0.7411764706, 0.2784313725, 0.5333333333, 0.6784313725490196, 0.7490196078, 0.2862745098, + 0.5176470588, 0.6823529411764706, 0.7490196078, 0.2941176471, 0.5019607843, + 0.6862745098039216, 0.7529411765, 0.3058823529, 0.4862745098, 0.6901960784313725, + 0.7607843137, 0.3137254902, 0.4705882353, 0.6941176470588235, 0.768627451, 0.3215686275, + 0.4549019608, 0.6980392156862745, 0.7764705882, 0.3333333333, 0.4392156863, + 0.7019607843137254, 0.7843137255, 0.3411764706, 0.4235294118, 0.7058823529411765, + 0.7921568627, 0.3529411765, 0.4078431373, 0.7098039215686275, 0.8, 0.3607843137, 0.3921568627, + 0.7137254901960784, 0.8078431373, 0.368627451, 0.3764705882, 0.7176470588235294, 0.8156862745, + 0.3803921569, 0.3607843137, 0.7215686274509804, 0.8235294118, 0.3882352941, 0.3450980392, + 0.7254901960784313, 0.831372549, 0.3960784314, 0.3294117647, 0.7294117647058823, 0.8392156863, + 0.4078431373, 0.3137254902, 0.7333333333333333, 0.8470588235, 0.4156862745, 0.2980392157, + 0.7372549019607844, 0.8549019608, 0.4274509804, 0.2823529412, 0.7411764705882353, 0.862745098, + 0.4352941176, 0.2666666667, 0.7450980392156863, 0.8705882353, 0.4431372549, 0.2509803922, + 0.7490196078431373, 0.8784313725, 0.4549019608, 0.2431372549, 0.7529411764705882, + 0.8862745098, 0.462745098, 0.2274509804, 0.7568627450980392, 0.8941176471, 0.4705882353, + 0.2117647059, 0.7607843137254902, 0.9019607843, 0.4823529412, 0.1960784314, + 0.7647058823529411, 0.9098039216, 0.4901960784, 0.1803921569, 0.7686274509803922, + 0.9176470588, 0.4980392157, 0.1647058824, 0.7725490196078432, 0.9254901961, 0.5098039216, + 0.1490196078, 0.7764705882352941, 0.9333333333, 0.5176470588, 0.1333333333, + 0.7803921568627451, 0.9411764706, 0.5294117647, 0.1176470588, 0.7843137254901961, + 0.9490196078, 0.537254902, 0.1019607843, 0.788235294117647, 0.9568627451, 0.5450980392, + 0.0862745098, 0.792156862745098, 0.9647058824, 0.5568627451, 0.0705882353, 0.796078431372549, + 0.9725490196, 0.5647058824, 0.0549019608, 0.8, 0.9803921569, 0.5725490196, 0.0392156863, + 0.803921568627451, 0.9882352941, 0.5843137255, 0.0235294118, 0.807843137254902, 0.9921568627, + 0.5921568627, 0.0078431373, 0.8117647058823529, 0.9921568627, 0.6039215686, 0.0274509804, + 0.8156862745098039, 0.9921568627, 0.6117647059, 0.0509803922, 0.8196078431372549, + 0.9921568627, 0.6196078431, 0.0745098039, 0.8235294117647058, 0.9921568627, 0.631372549, + 0.0980392157, 0.8274509803921568, 0.9921568627, 0.6392156863, 0.1215686275, + 0.8313725490196079, 0.9921568627, 0.6470588235, 0.1411764706, 0.8352941176470589, + 0.9921568627, 0.6588235294, 0.1647058824, 0.8392156862745098, 0.9921568627, 0.6666666667, + 0.1882352941, 0.8431372549019608, 0.9921568627, 0.6784313725, 0.2117647059, + 0.8470588235294118, 0.9921568627, 0.6862745098, 0.2352941176, 0.8509803921568627, + 0.9921568627, 0.6941176471, 0.2509803922, 0.8549019607843137, 0.9921568627, 0.7058823529, + 0.2705882353, 0.8588235294117647, 0.9921568627, 0.7137254902, 0.2941176471, + 0.8627450980392157, 0.9921568627, 0.7215686275, 0.3176470588, 0.8666666666666667, + 0.9921568627, 0.7333333333, 0.3411764706, 0.8705882352941177, 0.9921568627, 0.7411764706, + 0.3647058824, 0.8745098039215686, 0.9921568627, 0.7490196078, 0.3843137255, + 0.8784313725490196, 0.9921568627, 0.7529411765, 0.4078431373, 0.8823529411764706, + 0.9921568627, 0.7607843137, 0.431372549, 0.8862745098039215, 0.9921568627, 0.7725490196, + 0.4549019608, 0.8901960784313725, 0.9921568627, 0.7803921569, 0.4784313725, + 0.8941176470588236, 0.9921568627, 0.7882352941, 0.4980392157, 0.8980392156862745, + 0.9921568627, 0.8, 0.5215686275, 0.9019607843137255, 0.9921568627, 0.8078431373, 0.5450980392, + 0.9058823529411765, 0.9921568627, 0.8156862745, 0.568627451, 0.9098039215686274, 0.9921568627, + 0.8274509804, 0.5921568627, 0.9137254901960784, 0.9921568627, 0.8352941176, 0.6156862745, + 0.9176470588235294, 0.9921568627, 0.8470588235, 0.6352941176, 0.9215686274509803, + 0.9921568627, 0.8549019608, 0.6588235294, 0.9254901960784314, 0.9921568627, 0.862745098, + 0.6823529412, 0.9294117647058824, 0.9921568627, 0.8745098039, 0.7058823529, + 0.9333333333333333, 0.9921568627, 0.8823529412, 0.7294117647, 0.9372549019607843, + 0.9921568627, 0.8901960784, 0.7490196078, 0.9411764705882354, 0.9921568627, 0.9019607843, + 0.7647058824, 0.9450980392156864, 0.9921568627, 0.9098039216, 0.7882352941, + 0.9490196078431372, 0.9921568627, 0.9215686275, 0.8117647059, 0.9529411764705882, + 0.9921568627, 0.9294117647, 0.8352941176, 0.9568627450980394, 0.9921568627, 0.937254902, + 0.8588235294, 0.9607843137254903, 0.9921568627, 0.9490196078, 0.8784313725, + 0.9647058823529413, 0.9921568627, 0.9568627451, 0.9019607843, 0.9686274509803922, + 0.9921568627, 0.9647058824, 0.9254901961, 0.9725490196078431, 0.9921568627, 0.9764705882, + 0.9490196078, 0.9764705882352941, 0.9921568627, 0.9843137255, 0.9725490196, + 0.9803921568627451, 0.9921568627, 0.9921568627, 0.9921568627, 0.984313725490196, 0.9921568627, + 0.9921568627, 0.9921568627, 0.9882352941176471, 0.9921568627, 0.9921568627, 0.9921568627, + 0.9921568627450981, 0.9921568627, 0.9921568627, 0.9921568627, 0.996078431372549, 0.9921568627, + 0.9921568627, 0.9921568627, 1.0, 0.9921568627, 0.9921568627, 0.9921568627, + ], + }, + { + ColorSpace: 'RGB', + Name: 'siemens', + RGBPoints: [ + 0.0, 0.0078431373, 0.0039215686, 0.1254901961, 0.00392156862745098, 0.0078431373, + 0.0039215686, 0.1254901961, 0.00784313725490196, 0.0078431373, 0.0039215686, 0.1882352941, + 0.011764705882352941, 0.0117647059, 0.0039215686, 0.2509803922, 0.01568627450980392, + 0.0117647059, 0.0039215686, 0.3098039216, 0.0196078431372549, 0.0156862745, 0.0039215686, + 0.3725490196, 0.023529411764705882, 0.0156862745, 0.0039215686, 0.3725490196, + 0.027450980392156862, 0.0156862745, 0.0039215686, 0.3725490196, 0.03137254901960784, + 0.0156862745, 0.0039215686, 0.3725490196, 0.03529411764705882, 0.0156862745, 0.0039215686, + 0.3725490196, 0.0392156862745098, 0.0156862745, 0.0039215686, 0.3725490196, + 0.043137254901960784, 0.0156862745, 0.0039215686, 0.3725490196, 0.047058823529411764, + 0.0156862745, 0.0039215686, 0.3725490196, 0.050980392156862744, 0.0156862745, 0.0039215686, + 0.3725490196, 0.054901960784313725, 0.0156862745, 0.0039215686, 0.3725490196, + 0.05882352941176471, 0.0156862745, 0.0039215686, 0.3725490196, 0.06274509803921569, + 0.0156862745, 0.0039215686, 0.3882352941, 0.06666666666666667, 0.0156862745, 0.0039215686, + 0.4078431373, 0.07058823529411765, 0.0156862745, 0.0039215686, 0.4235294118, + 0.07450980392156863, 0.0156862745, 0.0039215686, 0.4431372549, 0.0784313725490196, + 0.0156862745, 0.0039215686, 0.462745098, 0.08235294117647059, 0.0156862745, 0.0039215686, + 0.4784313725, 0.08627450980392157, 0.0156862745, 0.0039215686, 0.4980392157, + 0.09019607843137255, 0.0196078431, 0.0039215686, 0.5137254902, 0.09411764705882353, + 0.0196078431, 0.0039215686, 0.5333333333, 0.09803921568627451, 0.0196078431, 0.0039215686, + 0.5529411765, 0.10196078431372549, 0.0196078431, 0.0039215686, 0.568627451, + 0.10588235294117647, 0.0196078431, 0.0039215686, 0.5882352941, 0.10980392156862745, + 0.0196078431, 0.0039215686, 0.6039215686, 0.11372549019607843, 0.0196078431, 0.0039215686, + 0.6235294118, 0.11764705882352942, 0.0196078431, 0.0039215686, 0.6431372549, + 0.12156862745098039, 0.0235294118, 0.0039215686, 0.6588235294, 0.12549019607843137, + 0.0235294118, 0.0039215686, 0.6784313725, 0.12941176470588237, 0.0235294118, 0.0039215686, + 0.6980392157, 0.13333333333333333, 0.0235294118, 0.0039215686, 0.7137254902, + 0.13725490196078433, 0.0235294118, 0.0039215686, 0.7333333333, 0.1411764705882353, + 0.0235294118, 0.0039215686, 0.7490196078, 0.1450980392156863, 0.0235294118, 0.0039215686, + 0.7647058824, 0.14901960784313725, 0.0235294118, 0.0039215686, 0.7843137255, + 0.15294117647058825, 0.0274509804, 0.0039215686, 0.8, 0.1568627450980392, 0.0274509804, + 0.0039215686, 0.8196078431, 0.1607843137254902, 0.0274509804, 0.0039215686, 0.8352941176, + 0.16470588235294117, 0.0274509804, 0.0039215686, 0.8549019608, 0.16862745098039217, + 0.0274509804, 0.0039215686, 0.8745098039, 0.17254901960784313, 0.0274509804, 0.0039215686, + 0.8901960784, 0.17647058823529413, 0.0274509804, 0.0039215686, 0.9098039216, + 0.1803921568627451, 0.031372549, 0.0039215686, 0.9294117647, 0.1843137254901961, 0.031372549, + 0.0039215686, 0.9254901961, 0.18823529411764706, 0.0509803922, 0.0039215686, 0.9098039216, + 0.19215686274509805, 0.0705882353, 0.0039215686, 0.8901960784, 0.19607843137254902, + 0.0901960784, 0.0039215686, 0.8705882353, 0.2, 0.1137254902, 0.0039215686, 0.8509803922, + 0.20392156862745098, 0.1333333333, 0.0039215686, 0.831372549, 0.20784313725490197, + 0.1529411765, 0.0039215686, 0.8117647059, 0.21176470588235294, 0.1725490196, 0.0039215686, + 0.7921568627, 0.21568627450980393, 0.1960784314, 0.0039215686, 0.7725490196, + 0.2196078431372549, 0.2156862745, 0.0039215686, 0.7529411765, 0.2235294117647059, + 0.2352941176, 0.0039215686, 0.737254902, 0.22745098039215686, 0.2509803922, 0.0039215686, + 0.7176470588, 0.23137254901960785, 0.2745098039, 0.0039215686, 0.6980392157, + 0.23529411764705885, 0.2941176471, 0.0039215686, 0.6784313725, 0.23921568627450984, + 0.3137254902, 0.0039215686, 0.6588235294, 0.24313725490196078, 0.3333333333, 0.0039215686, + 0.6392156863, 0.24705882352941178, 0.3568627451, 0.0039215686, 0.6196078431, + 0.25098039215686274, 0.3764705882, 0.0039215686, 0.6, 0.2549019607843137, 0.3960784314, + 0.0039215686, 0.5803921569, 0.25882352941176473, 0.4156862745, 0.0039215686, 0.5607843137, + 0.2627450980392157, 0.4392156863, 0.0039215686, 0.5411764706, 0.26666666666666666, + 0.4588235294, 0.0039215686, 0.5215686275, 0.27058823529411763, 0.4784313725, 0.0039215686, + 0.5019607843, 0.27450980392156865, 0.4980392157, 0.0039215686, 0.4823529412, + 0.2784313725490196, 0.5215686275, 0.0039215686, 0.4666666667, 0.2823529411764706, + 0.5411764706, 0.0039215686, 0.4470588235, 0.28627450980392155, 0.5607843137, 0.0039215686, + 0.4274509804, 0.2901960784313726, 0.5803921569, 0.0039215686, 0.4078431373, + 0.29411764705882354, 0.6039215686, 0.0039215686, 0.3882352941, 0.2980392156862745, + 0.6235294118, 0.0039215686, 0.368627451, 0.30196078431372547, 0.6431372549, 0.0039215686, + 0.3490196078, 0.3058823529411765, 0.662745098, 0.0039215686, 0.3294117647, + 0.30980392156862746, 0.6862745098, 0.0039215686, 0.3098039216, 0.3137254901960784, + 0.7058823529, 0.0039215686, 0.2901960784, 0.3176470588235294, 0.7254901961, 0.0039215686, + 0.2705882353, 0.3215686274509804, 0.7450980392, 0.0039215686, 0.2509803922, + 0.3254901960784314, 0.7647058824, 0.0039215686, 0.2352941176, 0.32941176470588235, + 0.7843137255, 0.0039215686, 0.2156862745, 0.3333333333333333, 0.8039215686, 0.0039215686, + 0.1960784314, 0.33725490196078434, 0.8235294118, 0.0039215686, 0.1764705882, + 0.3411764705882353, 0.8470588235, 0.0039215686, 0.1568627451, 0.34509803921568627, + 0.8666666667, 0.0039215686, 0.137254902, 0.34901960784313724, 0.8862745098, 0.0039215686, + 0.1176470588, 0.35294117647058826, 0.9058823529, 0.0039215686, 0.0980392157, + 0.3568627450980392, 0.9294117647, 0.0039215686, 0.0784313725, 0.3607843137254902, + 0.9490196078, 0.0039215686, 0.0588235294, 0.36470588235294116, 0.968627451, 0.0039215686, + 0.0392156863, 0.3686274509803922, 0.9921568627, 0.0039215686, 0.0235294118, + 0.37254901960784315, 0.9529411765, 0.0039215686, 0.0588235294, 0.3764705882352941, + 0.9529411765, 0.0078431373, 0.0549019608, 0.3803921568627451, 0.9529411765, 0.0156862745, + 0.0549019608, 0.3843137254901961, 0.9529411765, 0.0235294118, 0.0549019608, + 0.38823529411764707, 0.9529411765, 0.031372549, 0.0549019608, 0.39215686274509803, + 0.9529411765, 0.0352941176, 0.0549019608, 0.396078431372549, 0.9529411765, 0.0431372549, + 0.0549019608, 0.4, 0.9529411765, 0.0509803922, 0.0549019608, 0.403921568627451, 0.9529411765, + 0.0588235294, 0.0549019608, 0.40784313725490196, 0.9529411765, 0.062745098, 0.0549019608, + 0.4117647058823529, 0.9529411765, 0.0705882353, 0.0549019608, 0.41568627450980394, + 0.9529411765, 0.0784313725, 0.0509803922, 0.4196078431372549, 0.9529411765, 0.0862745098, + 0.0509803922, 0.4235294117647059, 0.9568627451, 0.0941176471, 0.0509803922, + 0.42745098039215684, 0.9568627451, 0.0980392157, 0.0509803922, 0.43137254901960786, + 0.9568627451, 0.1058823529, 0.0509803922, 0.43529411764705883, 0.9568627451, 0.1137254902, + 0.0509803922, 0.4392156862745098, 0.9568627451, 0.1215686275, 0.0509803922, + 0.44313725490196076, 0.9568627451, 0.1254901961, 0.0509803922, 0.4470588235294118, + 0.9568627451, 0.1333333333, 0.0509803922, 0.45098039215686275, 0.9568627451, 0.1411764706, + 0.0509803922, 0.4549019607843137, 0.9568627451, 0.1490196078, 0.0470588235, + 0.4588235294117647, 0.9568627451, 0.1568627451, 0.0470588235, 0.4627450980392157, + 0.9568627451, 0.1607843137, 0.0470588235, 0.4666666666666667, 0.9568627451, 0.168627451, + 0.0470588235, 0.4705882352941177, 0.9607843137, 0.1764705882, 0.0470588235, + 0.4745098039215686, 0.9607843137, 0.1843137255, 0.0470588235, 0.4784313725490197, + 0.9607843137, 0.1882352941, 0.0470588235, 0.48235294117647065, 0.9607843137, 0.1960784314, + 0.0470588235, 0.48627450980392156, 0.9607843137, 0.2039215686, 0.0470588235, + 0.49019607843137253, 0.9607843137, 0.2117647059, 0.0470588235, 0.49411764705882355, + 0.9607843137, 0.2196078431, 0.0431372549, 0.4980392156862745, 0.9607843137, 0.2235294118, + 0.0431372549, 0.5019607843137255, 0.9607843137, 0.231372549, 0.0431372549, 0.5058823529411764, + 0.9607843137, 0.2392156863, 0.0431372549, 0.5098039215686274, 0.9607843137, 0.2470588235, + 0.0431372549, 0.5137254901960784, 0.9607843137, 0.2509803922, 0.0431372549, + 0.5176470588235295, 0.9647058824, 0.2549019608, 0.0431372549, 0.5215686274509804, + 0.9647058824, 0.262745098, 0.0431372549, 0.5254901960784314, 0.9647058824, 0.2705882353, + 0.0431372549, 0.5294117647058824, 0.9647058824, 0.2745098039, 0.0431372549, + 0.5333333333333333, 0.9647058824, 0.2823529412, 0.0392156863, 0.5372549019607843, + 0.9647058824, 0.2901960784, 0.0392156863, 0.5411764705882353, 0.9647058824, 0.2980392157, + 0.0392156863, 0.5450980392156862, 0.9647058824, 0.3058823529, 0.0392156863, + 0.5490196078431373, 0.9647058824, 0.3098039216, 0.0392156863, 0.5529411764705883, + 0.9647058824, 0.3176470588, 0.0392156863, 0.5568627450980392, 0.9647058824, 0.3254901961, + 0.0392156863, 0.5607843137254902, 0.9647058824, 0.3333333333, 0.0392156863, + 0.5647058823529412, 0.9647058824, 0.337254902, 0.0392156863, 0.5686274509803921, 0.968627451, + 0.3450980392, 0.0392156863, 0.5725490196078431, 0.968627451, 0.3529411765, 0.0352941176, + 0.5764705882352941, 0.968627451, 0.3607843137, 0.0352941176, 0.5803921568627451, 0.968627451, + 0.368627451, 0.0352941176, 0.5843137254901961, 0.968627451, 0.3725490196, 0.0352941176, + 0.5882352941176471, 0.968627451, 0.3803921569, 0.0352941176, 0.592156862745098, 0.968627451, + 0.3882352941, 0.0352941176, 0.596078431372549, 0.968627451, 0.3960784314, 0.0352941176, 0.6, + 0.968627451, 0.4, 0.0352941176, 0.6039215686274509, 0.968627451, 0.4078431373, 0.0352941176, + 0.6078431372549019, 0.968627451, 0.4156862745, 0.0352941176, 0.611764705882353, 0.968627451, + 0.4235294118, 0.031372549, 0.615686274509804, 0.9725490196, 0.431372549, 0.031372549, + 0.6196078431372549, 0.9725490196, 0.4352941176, 0.031372549, 0.6235294117647059, 0.9725490196, + 0.4431372549, 0.031372549, 0.6274509803921569, 0.9725490196, 0.4509803922, 0.031372549, + 0.6313725490196078, 0.9725490196, 0.4588235294, 0.031372549, 0.6352941176470588, 0.9725490196, + 0.462745098, 0.031372549, 0.6392156862745098, 0.9725490196, 0.4705882353, 0.031372549, + 0.6431372549019608, 0.9725490196, 0.4784313725, 0.031372549, 0.6470588235294118, 0.9725490196, + 0.4862745098, 0.031372549, 0.6509803921568628, 0.9725490196, 0.4941176471, 0.0274509804, + 0.6549019607843137, 0.9725490196, 0.4980392157, 0.0274509804, 0.6588235294117647, + 0.9725490196, 0.5058823529, 0.0274509804, 0.6627450980392157, 0.9764705882, 0.5137254902, + 0.0274509804, 0.6666666666666666, 0.9764705882, 0.5215686275, 0.0274509804, + 0.6705882352941176, 0.9764705882, 0.5254901961, 0.0274509804, 0.6745098039215687, + 0.9764705882, 0.5333333333, 0.0274509804, 0.6784313725490196, 0.9764705882, 0.5411764706, + 0.0274509804, 0.6823529411764706, 0.9764705882, 0.5490196078, 0.0274509804, + 0.6862745098039216, 0.9764705882, 0.5529411765, 0.0274509804, 0.6901960784313725, + 0.9764705882, 0.5607843137, 0.0235294118, 0.6941176470588235, 0.9764705882, 0.568627451, + 0.0235294118, 0.6980392156862745, 0.9764705882, 0.5764705882, 0.0235294118, + 0.7019607843137254, 0.9764705882, 0.5843137255, 0.0235294118, 0.7058823529411765, + 0.9764705882, 0.5882352941, 0.0235294118, 0.7098039215686275, 0.9764705882, 0.5960784314, + 0.0235294118, 0.7137254901960784, 0.9803921569, 0.6039215686, 0.0235294118, + 0.7176470588235294, 0.9803921569, 0.6117647059, 0.0235294118, 0.7215686274509804, + 0.9803921569, 0.6156862745, 0.0235294118, 0.7254901960784313, 0.9803921569, 0.6235294118, + 0.0235294118, 0.7294117647058823, 0.9803921569, 0.631372549, 0.0196078431, 0.7333333333333333, + 0.9803921569, 0.6392156863, 0.0196078431, 0.7372549019607844, 0.9803921569, 0.6470588235, + 0.0196078431, 0.7411764705882353, 0.9803921569, 0.6509803922, 0.0196078431, + 0.7450980392156863, 0.9803921569, 0.6588235294, 0.0196078431, 0.7490196078431373, + 0.9803921569, 0.6666666667, 0.0196078431, 0.7529411764705882, 0.9803921569, 0.6745098039, + 0.0196078431, 0.7568627450980392, 0.9803921569, 0.6784313725, 0.0196078431, + 0.7607843137254902, 0.9843137255, 0.6862745098, 0.0196078431, 0.7647058823529411, + 0.9843137255, 0.6941176471, 0.0196078431, 0.7686274509803922, 0.9843137255, 0.7019607843, + 0.0156862745, 0.7725490196078432, 0.9843137255, 0.7098039216, 0.0156862745, + 0.7764705882352941, 0.9843137255, 0.7137254902, 0.0156862745, 0.7803921568627451, + 0.9843137255, 0.7215686275, 0.0156862745, 0.7843137254901961, 0.9843137255, 0.7294117647, + 0.0156862745, 0.788235294117647, 0.9843137255, 0.737254902, 0.0156862745, 0.792156862745098, + 0.9843137255, 0.7411764706, 0.0156862745, 0.796078431372549, 0.9843137255, 0.7490196078, + 0.0156862745, 0.8, 0.9843137255, 0.7529411765, 0.0156862745, 0.803921568627451, 0.9843137255, + 0.7607843137, 0.0156862745, 0.807843137254902, 0.9882352941, 0.768627451, 0.0156862745, + 0.8117647058823529, 0.9882352941, 0.768627451, 0.0156862745, 0.8156862745098039, 0.9843137255, + 0.7843137255, 0.0117647059, 0.8196078431372549, 0.9843137255, 0.8, 0.0117647059, + 0.8235294117647058, 0.9843137255, 0.8156862745, 0.0117647059, 0.8274509803921568, + 0.9803921569, 0.831372549, 0.0117647059, 0.8313725490196079, 0.9803921569, 0.8431372549, + 0.0117647059, 0.8352941176470589, 0.9803921569, 0.8588235294, 0.0078431373, + 0.8392156862745098, 0.9803921569, 0.8745098039, 0.0078431373, 0.8431372549019608, + 0.9764705882, 0.8901960784, 0.0078431373, 0.8470588235294118, 0.9764705882, 0.9058823529, + 0.0078431373, 0.8509803921568627, 0.9764705882, 0.9176470588, 0.0078431373, + 0.8549019607843137, 0.9764705882, 0.9333333333, 0.0039215686, 0.8588235294117647, + 0.9725490196, 0.9490196078, 0.0039215686, 0.8627450980392157, 0.9725490196, 0.9647058824, + 0.0039215686, 0.8666666666666667, 0.9725490196, 0.9803921569, 0.0039215686, + 0.8705882352941177, 0.9725490196, 0.9960784314, 0.0039215686, 0.8745098039215686, + 0.9725490196, 0.9960784314, 0.0039215686, 0.8784313725490196, 0.9725490196, 0.9960784314, + 0.0352941176, 0.8823529411764706, 0.9725490196, 0.9960784314, 0.0666666667, + 0.8862745098039215, 0.9725490196, 0.9960784314, 0.0980392157, 0.8901960784313725, + 0.9725490196, 0.9960784314, 0.1294117647, 0.8941176470588236, 0.9725490196, 0.9960784314, + 0.1647058824, 0.8980392156862745, 0.9764705882, 0.9960784314, 0.1960784314, + 0.9019607843137255, 0.9764705882, 0.9960784314, 0.2274509804, 0.9058823529411765, + 0.9764705882, 0.9960784314, 0.2549019608, 0.9098039215686274, 0.9764705882, 0.9960784314, + 0.2901960784, 0.9137254901960784, 0.9764705882, 0.9960784314, 0.3215686275, + 0.9176470588235294, 0.9803921569, 0.9960784314, 0.3529411765, 0.9215686274509803, + 0.9803921569, 0.9960784314, 0.3843137255, 0.9254901960784314, 0.9803921569, 0.9960784314, + 0.4156862745, 0.9294117647058824, 0.9803921569, 0.9960784314, 0.4509803922, + 0.9333333333333333, 0.9803921569, 0.9960784314, 0.4823529412, 0.9372549019607843, + 0.9843137255, 0.9960784314, 0.5137254902, 0.9411764705882354, 0.9843137255, 0.9960784314, + 0.5450980392, 0.9450980392156864, 0.9843137255, 0.9960784314, 0.5803921569, + 0.9490196078431372, 0.9843137255, 0.9960784314, 0.6117647059, 0.9529411764705882, + 0.9843137255, 0.9960784314, 0.6431372549, 0.9568627450980394, 0.9882352941, 0.9960784314, + 0.6745098039, 0.9607843137254903, 0.9882352941, 0.9960784314, 0.7058823529, + 0.9647058823529413, 0.9882352941, 0.9960784314, 0.7411764706, 0.9686274509803922, + 0.9882352941, 0.9960784314, 0.768627451, 0.9725490196078431, 0.9882352941, 0.9960784314, 0.8, + 0.9764705882352941, 0.9921568627, 0.9960784314, 0.831372549, 0.9803921568627451, 0.9921568627, + 0.9960784314, 0.8666666667, 0.984313725490196, 0.9921568627, 0.9960784314, 0.8980392157, + 0.9882352941176471, 0.9921568627, 0.9960784314, 0.9294117647, 0.9921568627450981, + 0.9921568627, 0.9960784314, 0.9607843137, 0.996078431372549, 0.9960784314, 0.9960784314, + 0.9607843137, 1.0, 0.9960784314, 0.9960784314, 0.9607843137, + ], + }, +]; diff --git a/extensions/tmtv/src/utils/createAndDownloadTMTVReport.js b/extensions/tmtv/src/utils/createAndDownloadTMTVReport.js new file mode 100644 index 0000000..615d384 --- /dev/null +++ b/extensions/tmtv/src/utils/createAndDownloadTMTVReport.js @@ -0,0 +1,45 @@ +export default function createAndDownloadTMTVReport(segReport, additionalReportRows, options = {}) { + const firstReport = segReport[Object.keys(segReport)[0]]; + const columns = Object.keys(firstReport); + const csv = [columns.join(',')]; + + Object.values(segReport).forEach(segmentation => { + const row = []; + columns.forEach(column => { + // if it is array then we need to replace , with space to avoid csv parsing error + row.push( + Array.isArray(segmentation[column]) ? segmentation[column].join(' ') : segmentation[column] + ); + }); + csv.push(row.join(',')); + }); + + csv.push(''); + csv.push(''); + csv.push(''); + + csv.push(`Patient ID,${firstReport.PatientID}`); + csv.push(`Study Date,${firstReport.StudyDate}`); + csv.push(''); + additionalReportRows.forEach(({ key, value: values }) => { + const temp = []; + temp.push(`${key}`); + Object.keys(values).forEach(k => { + temp.push(`${k}`); + temp.push(`${values[k]}`); + }); + + csv.push(temp.join(',')); + }); + + const blob = new Blob([csv.join('\n')], { + type: 'text/csv;charset=utf-8', + }); + + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = options.filename ?? `${firstReport.PatientID}_tmtv.csv`; + a.click(); +} diff --git a/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/dicomRTAnnotationExport.js b/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/dicomRTAnnotationExport.js new file mode 100644 index 0000000..c8de552 --- /dev/null +++ b/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/dicomRTAnnotationExport.js @@ -0,0 +1,19 @@ +import dcmjs from 'dcmjs'; +import { classes, DicomMetadataStore } from '@ohif/core'; +import { adaptersRT } from '@cornerstonejs/adapters'; + +const { datasetToBlob } = dcmjs.data; +const metadataProvider = classes.MetadataProvider; + +export default function dicomRTAnnotationExport(annotations) { + const dataset = adaptersRT.Cornerstone3D.RTSS.generateRTSSFromAnnotations( + annotations, + metadataProvider, + DicomMetadataStore + ); + const reportBlob = datasetToBlob(dataset); + + //Create a URL for the binary. + var objectUrl = URL.createObjectURL(reportBlob); + window.location.assign(objectUrl); +} diff --git a/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/index.js b/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/index.js new file mode 100644 index 0000000..7a915da --- /dev/null +++ b/extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/index.js @@ -0,0 +1,3 @@ +import dicomRTAnnotationExport from './dicomRTAnnotationExport'; + +export default dicomRTAnnotationExport; diff --git a/extensions/tmtv/src/utils/getThresholdValue.ts b/extensions/tmtv/src/utils/getThresholdValue.ts new file mode 100644 index 0000000..137b5b9 --- /dev/null +++ b/extensions/tmtv/src/utils/getThresholdValue.ts @@ -0,0 +1,73 @@ +import * as csTools from '@cornerstonejs/tools'; + +function getRoiStats(referencedVolume, annotations) { + // roiStats + const { imageData } = referencedVolume; + const values = imageData.getPointData().getScalars().getData(); + + // Todo: add support for other strategies + const { fn, baseValue } = _getStrategyFn('max'); + let value = baseValue; + + const boundsIJK = csTools.utilities.rectangleROITool.getBoundsIJKFromRectangleAnnotations( + annotations, + referencedVolume + ); + + const [[iMin, iMax], [jMin, jMax], [kMin, kMax]] = boundsIJK; + + for (let i = iMin; i <= iMax; i++) { + for (let j = jMin; j <= jMax; j++) { + for (let k = kMin; k <= kMax; k++) { + const offset = imageData.computeOffsetIndex([i, j, k]); + value = fn(values[offset], value); + } + } + } + return value; +} + +function getThresholdValues( + annotationUIDs, + referencedVolumes, + config +): { ptLower: number; ptUpper: number; ctLower: number; ctUpper: number } { + if (config.strategy === 'range') { + return { + ptLower: Number(config.ptLower), + ptUpper: Number(config.ptUpper), + ctLower: Number(config.ctLower), + ctUpper: Number(config.ctUpper), + }; + } + + const { weight } = config; + const annotations = annotationUIDs.map(annotationUID => + csTools.annotation.state.getAnnotation(annotationUID) + ); + + const ptValue = getRoiStats(referencedVolumes[0], annotations); + + return { + ctLower: -Infinity, + ctUpper: +Infinity, + ptLower: weight * ptValue, + ptUpper: +Infinity, + }; +} + +function _getStrategyFn(statistic): { + fn: (a: number, b: number) => number; + baseValue: number; +} { + const baseValue = -Infinity; + const fn = (number, maxValue) => { + if (number > maxValue) { + maxValue = number; + } + return maxValue; + }; + return { fn, baseValue }; +} + +export default getThresholdValues; diff --git a/extensions/tmtv/src/utils/handleROIThresholding.ts b/extensions/tmtv/src/utils/handleROIThresholding.ts new file mode 100644 index 0000000..bb5ae0c --- /dev/null +++ b/extensions/tmtv/src/utils/handleROIThresholding.ts @@ -0,0 +1,97 @@ +import { Segment, Segmentation } from '@cornerstonejs/tools/types'; +import { triggerEvent, eventTarget, Enums } from '@cornerstonejs/core'; + +export const handleROIThresholding = async ({ + segmentationId, + commandsManager, + segmentationService, +}: withAppTypes<{ + segmentationId: string; +}>) => { + const segmentation = segmentationService.getSegmentation(segmentationId); + + triggerEvent(eventTarget, Enums.Events.WEB_WORKER_PROGRESS, { + progress: 0, + type: 'Calculate Lesion Stats', + id: segmentationId, + }); + + // re-calculating the cached stats for the active segmentation + const updatedPerSegmentCachedStats = {}; + for (const [segmentIndex, segment] of Object.entries(segmentation.segments)) { + if (!segment) { + continue; + } + + const numericSegmentIndex = Number(segmentIndex); + + const lesionStats = await commandsManager.run('getLesionStats', { + segmentationId, + segmentIndex: numericSegmentIndex, + }); + + const suvPeak = await commandsManager.run('calculateSuvPeak', { + segmentationId, + segmentIndex: numericSegmentIndex, + }); + + const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue; + + // update segDetails with the suv peak for the active segmentation + const cachedStats = { + lesionStats, + suvPeak, + lesionGlyoclysisStats, + }; + + const updatedSegment: Segment = { + ...segment, + cachedStats: { + ...segment.cachedStats, + ...cachedStats, + }, + }; + + updatedPerSegmentCachedStats[numericSegmentIndex] = cachedStats; + + segmentation.segments[segmentIndex] = updatedSegment; + } + + // all available segmentations + const segmentations = segmentationService.getSegmentations(); + const tmtv = await commandsManager.run('calculateTMTV', { segmentations }); + + triggerEvent(eventTarget, Enums.Events.WEB_WORKER_PROGRESS, { + progress: 100, + type: 'Calculate Lesion Stats', + id: segmentationId, + }); + + // add the tmtv to all the segment cachedStats, although it is a global + // value but we don't have any other way to display it for now + // Update all segmentations with the calculated TMTV + segmentations.forEach(segmentation => { + segmentation.cachedStats = { + ...segmentation.cachedStats, + tmtv, + }; + + // Update each segment within the segmentation + Object.keys(segmentation.segments).forEach(segmentIndex => { + segmentation.segments[segmentIndex].cachedStats = { + ...segmentation.segments[segmentIndex].cachedStats, + tmtv, + }; + }); + + // Update the segmentation object + const updatedSegmentation: Segmentation = { + ...segmentation, + segments: { + ...segmentation.segments, + }, + }; + + segmentationService.addOrUpdateSegmentation(updatedSegmentation); + }); +}; diff --git a/extensions/tmtv/src/utils/hpViewports.ts b/extensions/tmtv/src/utils/hpViewports.ts new file mode 100644 index 0000000..c206c5c --- /dev/null +++ b/extensions/tmtv/src/utils/hpViewports.ts @@ -0,0 +1,494 @@ +// Common sync group configurations +const cameraPositionSync = (id: string) => ({ + type: 'cameraPosition', + id, + source: true, + target: true, +}); + +const hydrateSegSync = { + type: 'hydrateseg', + id: 'sameFORId', + source: true, + target: true, + options: { + matchingRules: ['sameFOR'], + }, +}; + +const ctAXIAL: AppTypes.HangingProtocol.Viewport = { + viewportOptions: { + viewportId: 'ctAXIAL', + viewportType: 'volume', + orientation: 'axial', + toolGroupId: 'ctToolGroup', + initialImageOptions: { + // index: 5, + preset: 'first', // 'first', 'last', 'middle' + }, + syncGroups: [ + cameraPositionSync('axialSync'), + { + type: 'voi', + id: 'ctWLSync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + hydrateSegSync, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + ], +}; + +const ctSAGITTAL: AppTypes.HangingProtocol.Viewport = { + viewportOptions: { + viewportId: 'ctSAGITTAL', + viewportType: 'volume', + orientation: 'sagittal', + toolGroupId: 'ctToolGroup', + syncGroups: [ + cameraPositionSync('sagittalSync'), + { + type: 'voi', + id: 'ctWLSync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + hydrateSegSync, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + ], +}; + +const ctCORONAL: AppTypes.HangingProtocol.Viewport = { + viewportOptions: { + viewportId: 'ctCORONAL', + viewportType: 'volume', + orientation: 'coronal', + toolGroupId: 'ctToolGroup', + syncGroups: [ + cameraPositionSync('coronalSync'), + { + type: 'voi', + id: 'ctWLSync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + hydrateSegSync, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + ], +}; + +const ptAXIAL: AppTypes.HangingProtocol.Viewport = { + viewportOptions: { + viewportId: 'ptAXIAL', + viewportType: 'volume', + background: [1, 1, 1], + orientation: 'axial', + toolGroupId: 'ptToolGroup', + initialImageOptions: { + // index: 5, + preset: 'first', // 'first', 'last', 'middle' + }, + syncGroups: [ + cameraPositionSync('axialSync'), + { + type: 'voi', + id: 'ptWLSync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'voi', + id: 'ptFusionWLSync', + source: true, + target: false, + options: { + syncColormap: false, + syncInvertState: false, + }, + }, + hydrateSegSync, + ], + }, + displaySets: [ + { + options: { + voi: { + custom: 'getPTVOIRange', + }, + voiInverted: true, + }, + id: 'ptDisplaySet', + }, + ], +}; + +const ptSAGITTAL: AppTypes.HangingProtocol.Viewport = { + viewportOptions: { + viewportId: 'ptSAGITTAL', + viewportType: 'volume', + orientation: 'sagittal', + background: [1, 1, 1], + toolGroupId: 'ptToolGroup', + syncGroups: [ + cameraPositionSync('sagittalSync'), + { + type: 'voi', + id: 'ptWLSync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'voi', + id: 'ptFusionWLSync', + source: true, + target: false, + options: { + syncColormap: false, + syncInvertState: false, + }, + }, + hydrateSegSync, + ], + }, + displaySets: [ + { + options: { + voi: { + custom: 'getPTVOIRange', + }, + voiInverted: true, + }, + id: 'ptDisplaySet', + }, + ], +}; + +const ptCORONAL: AppTypes.HangingProtocol.Viewport = { + viewportOptions: { + viewportId: 'ptCORONAL', + viewportType: 'volume', + orientation: 'coronal', + background: [1, 1, 1], + toolGroupId: 'ptToolGroup', + syncGroups: [ + cameraPositionSync('coronalSync'), + { + type: 'voi', + id: 'ptWLSync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'voi', + id: 'ptFusionWLSync', + source: true, + target: false, + options: { + syncColormap: false, + syncInvertState: false, + }, + }, + hydrateSegSync, + ], + }, + displaySets: [ + { + options: { + voi: { + custom: 'getPTVOIRange', + }, + voiInverted: true, + }, + id: 'ptDisplaySet', + }, + ], +}; + +const fusionAXIAL: AppTypes.HangingProtocol.Viewport = { + viewportOptions: { + viewportId: 'fusionAXIAL', + viewportType: 'volume', + orientation: 'axial', + toolGroupId: 'fusionToolGroup', + initialImageOptions: { + // index: 5, + preset: 'first', // 'first', 'last', 'middle' + }, + syncGroups: [ + cameraPositionSync('axialSync'), + { + type: 'voi', + id: 'ctWLSync', + source: false, + target: true, + }, + { + type: 'voi', + id: 'fusionWLSync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'voi', + id: 'ptFusionWLSync', + source: false, + target: true, + options: { + syncColormap: false, + syncInvertState: false, + }, + }, + hydrateSegSync, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + { + id: 'ptDisplaySet', + options: { + colormap: { + name: 'hsv', + opacity: [ + { value: 0, opacity: 0 }, + { value: 0.1, opacity: 0.8 }, + { value: 1, opacity: 0.9 }, + ], + }, + voi: { + custom: 'getPTVOIRange', + }, + }, + }, + ], +}; + +const fusionSAGITTAL = { + viewportOptions: { + viewportId: 'fusionSAGITTAL', + viewportType: 'volume', + orientation: 'sagittal', + toolGroupId: 'fusionToolGroup', + // initialImageOptions: { + // index: 180, + // preset: 'middle', // 'first', 'last', 'middle' + // }, + syncGroups: [ + cameraPositionSync('sagittalSync'), + { + type: 'voi', + id: 'ctWLSync', + source: false, + target: true, + }, + { + type: 'voi', + id: 'fusionWLSync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'voi', + id: 'ptFusionWLSync', + source: false, + target: true, + options: { + syncColormap: false, + syncInvertState: false, + }, + }, + hydrateSegSync, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + { + id: 'ptDisplaySet', + options: { + colormap: { + name: 'hsv', + opacity: [ + { value: 0, opacity: 0 }, + { value: 0.1, opacity: 0.8 }, + { value: 1, opacity: 0.9 }, + ], + }, + voi: { + custom: 'getPTVOIRange', + }, + }, + }, + ], +}; + +const fusionCORONAL = { + viewportOptions: { + viewportId: 'fusionCoronal', + viewportType: 'volume', + orientation: 'coronal', + toolGroupId: 'fusionToolGroup', + // initialImageOptions: { + // index: 180, + // preset: 'middle', // 'first', 'last', 'middle' + // }, + syncGroups: [ + cameraPositionSync('coronalSync'), + { + type: 'voi', + id: 'ctWLSync', + source: false, + target: true, + }, + { + type: 'voi', + id: 'fusionWLSync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'voi', + id: 'ptFusionWLSync', + source: false, + target: true, + options: { + syncColormap: false, + syncInvertState: false, + }, + }, + hydrateSegSync, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + { + id: 'ptDisplaySet', + options: { + colormap: { + name: 'hsv', + opacity: [ + { value: 0, opacity: 0 }, + { value: 0.1, opacity: 0.8 }, + { value: 1, opacity: 0.9 }, + ], + }, + voi: { + custom: 'getPTVOIRange', + }, + }, + }, + ], +}; + +const mipSAGITTAL: AppTypes.HangingProtocol.Viewport = { + viewportOptions: { + viewportId: 'mipSagittal', + viewportType: 'volume', + orientation: 'sagittal', + background: [1, 1, 1], + toolGroupId: 'mipToolGroup', + syncGroups: [ + { + type: 'voi', + id: 'ptWLSync', + source: true, + target: true, + options: { + syncColormap: true, + }, + }, + { + type: 'voi', + id: 'ptFusionWLSync', + source: true, + target: false, + options: { + syncColormap: false, + syncInvertState: false, + }, + }, + hydrateSegSync, + ], + + // Custom props can be used to set custom properties which extensions + // can react on. + customViewportProps: { + // We use viewportDisplay to filter the viewports which are displayed + // in mip and we set the scrollbar according to their rotation index + // in the cornerstone extension. + hideOverlays: true, + }, + }, + displaySets: [ + { + options: { + blendMode: 'MIP', + slabThickness: 'fullVolume', + voi: { + custom: 'getPTVOIRange', + }, + voiInverted: true, + }, + id: 'ptDisplaySet', + }, + ], +}; + +export { + ctAXIAL, + ctSAGITTAL, + ctCORONAL, + ptAXIAL, + ptSAGITTAL, + ptCORONAL, + fusionAXIAL, + fusionSAGITTAL, + fusionCORONAL, + mipSAGITTAL, +}; diff --git a/extensions/tmtv/src/utils/measurementServiceMappings/CircleROIStartEndThreshold.js b/extensions/tmtv/src/utils/measurementServiceMappings/CircleROIStartEndThreshold.js new file mode 100644 index 0000000..d844256 --- /dev/null +++ b/extensions/tmtv/src/utils/measurementServiceMappings/CircleROIStartEndThreshold.js @@ -0,0 +1,67 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import { getSOPInstanceAttributes } from '@ohif/extension-cornerstone'; + +const CircleROIStartEndThreshold = { + toAnnotation: (measurement, definition) => {}, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} cornerstone Cornerstone event data + * @return {Measurement} Measurement instance + */ + toMeasurement: (csToolsEventDetail, displaySetService, cornerstoneViewportService) => { + const { annotation, viewportId } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + if (!metadata || !data) { + console.warn('Length tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType) { + throw new Error('Tool not supported'); + } + + const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes( + referencedImageId, + cornerstoneViewportService, + viewportId + ); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID); + } + + const { cachedStats } = data; + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + // points, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: metadata.label, + // displayText: displayText, + data: data.cachedStats, + type: 'CircleROIStartEndThreshold', + // getReport, + }; + }, +}; + +export default CircleROIStartEndThreshold; diff --git a/extensions/tmtv/src/utils/measurementServiceMappings/RectangleROIStartEndThreshold.js b/extensions/tmtv/src/utils/measurementServiceMappings/RectangleROIStartEndThreshold.js new file mode 100644 index 0000000..57ecd7b --- /dev/null +++ b/extensions/tmtv/src/utils/measurementServiceMappings/RectangleROIStartEndThreshold.js @@ -0,0 +1,63 @@ +import SUPPORTED_TOOLS from './constants/supportedTools'; +import { getSOPInstanceAttributes } from '@ohif/extension-cornerstone'; + +const RectangleROIStartEndThreshold = { + toAnnotation: (measurement, definition) => {}, + + /** + * Maps cornerstone annotation event data to measurement service format. + * + * @param {Object} cornerstone Cornerstone event data + * @return {Measurement} Measurement instance + */ + toMeasurement: (csToolsEventDetail, displaySetService, cornerstoneViewportService) => { + const { annotation, viewportId } = csToolsEventDetail; + const { metadata, data, annotationUID } = annotation; + + if (!metadata || !data) { + console.warn('Length tool: Missing metadata or data'); + return null; + } + + const { toolName, referencedImageId, FrameOfReferenceUID } = metadata; + const validToolType = SUPPORTED_TOOLS.includes(toolName); + + if (!validToolType) { + throw new Error('Tool not supported'); + } + + const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes( + referencedImageId, + cornerstoneViewportService, + viewportId + ); + + let displaySet; + + if (SOPInstanceUID) { + displaySet = displaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + } else { + displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID); + } + + return { + uid: annotationUID, + SOPInstanceUID, + FrameOfReferenceUID, + // points, + metadata, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + toolName: metadata.toolName, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: metadata.label, + data: data.cachedStats, + type: 'RectangleROIStartEndThreshold', + }; + }, +}; + +export default RectangleROIStartEndThreshold; diff --git a/extensions/tmtv/src/utils/measurementServiceMappings/constants/supportedTools.js b/extensions/tmtv/src/utils/measurementServiceMappings/constants/supportedTools.js new file mode 100644 index 0000000..31cc105 --- /dev/null +++ b/extensions/tmtv/src/utils/measurementServiceMappings/constants/supportedTools.js @@ -0,0 +1 @@ +export default ['RectangleROIStartEndThreshold']; diff --git a/extensions/tmtv/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js b/extensions/tmtv/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js new file mode 100644 index 0000000..7537297 --- /dev/null +++ b/extensions/tmtv/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js @@ -0,0 +1,41 @@ +import RectangleROIStartEndThreshold from './RectangleROIStartEndThreshold'; +import CircleROIStartEndThreshold from './CircleROIStartEndThreshold'; + +const measurementServiceMappingsFactory = ( + measurementService, + displaySetService, + cornerstoneViewportService +) => { + return { + RectangleROIStartEndThreshold: { + toAnnotation: RectangleROIStartEndThreshold.toAnnotation, + toMeasurement: csToolsAnnotation => + RectangleROIStartEndThreshold.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService + ), + matchingCriteria: [ + { + valueType: measurementService.VALUE_TYPES.ROI_THRESHOLD_MANUAL, + }, + ], + }, + CircleROIStartEndThreshold: { + toAnnotation: CircleROIStartEndThreshold.toAnnotation, + toMeasurement: csToolsAnnotation => + CircleROIStartEndThreshold.toMeasurement( + csToolsAnnotation, + displaySetService, + cornerstoneViewportService + ), + matchingCriteria: [ + { + valueType: measurementService.VALUE_TYPES.ROI_THRESHOLD_MANUAL, + }, + ], + }, + }; +}; + +export default measurementServiceMappingsFactory; diff --git a/jest.config.base.js b/jest.config.base.js new file mode 100644 index 0000000..dbd563f --- /dev/null +++ b/jest.config.base.js @@ -0,0 +1,40 @@ +// https://github.com/facebook/jest/issues/3613 +// Yarn Doctor: `npx @yarnpkg/doctor .` --> +// '' warning: +// Strings should avoid referencing the node_modules directory (prefer require.resolve) + +module.exports = { + verbose: true, + // roots: ['/src'], + testMatch: ['/src/**/*.test.js'], + testPathIgnorePatterns: ['/node_modules/'], + testEnvironment: 'jsdom', + moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'], + moduleNameMapper: { + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + '/src/__mocks__/fileMock.js', + '\\.(css|less)$': 'identity-obj-proxy', + }, + // Setup + // setupFiles: ["jest-canvas-mock/lib/index.js"], + // Coverage + reporters: [ + 'default', + // Docs: https://www.npmjs.com/package/jest-junit + [ + 'jest-junit', + { + addFileAttribute: true, // CircleCI Only + }, + ], + ], + collectCoverage: false, + collectCoverageFrom: [ + '/src/**/*.{js,jsx}', + // Not + '!/src/**/*.test.js', + '!**/node_modules/**', + '!**/__tests__/**', + '!/dist/**', + ], +}; diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..c19684d --- /dev/null +++ b/jest.config.js @@ -0,0 +1,17 @@ +// Initiate all tests from root, but allow tests from each package root. +// Share as much config as possible to reduce duplication. +// +// Borrowing from here: +// https://github.com/facebook/jest/issues/3112#issuecomment-398581705 +const base = require('./jest.config.base.js'); + +module.exports = { + ...base, + // https://jestjs.io/docs/en/configuration#projects-array-string-projectconfig + projects: [ + '/platform/*/jest.config.js', + '/extensions/*/jest.config.js', + //'/modes/*/jest.config.js' // Enable if any mode definitions start including tests + ], + coverageDirectory: '/coverage/', +}; diff --git a/lerna-debug.log b/lerna-debug.log new file mode 100644 index 0000000..80ec479 --- /dev/null +++ b/lerna-debug.log @@ -0,0 +1,20 @@ +0 silly argv { +0 silly argv _: [ 'run' ], +0 silly argv stream: true, +0 silly argv lernaVersion: '7.4.2', +0 silly argv '$0': 'node_modules/.bin/lerna', +0 silly argv script: 'build:viewer' +0 silly argv } +1 notice cli v7.4.2 +2 verbose packageConfigs Explicit "packages" configuration found in lerna.json. Resolving packages using the configured glob(s): ["extensions/*","platform/*","modes/*","addOns/externals/*"] +3 verbose rootPath /home/pacs/Viewers +4 error Error: Target project does not exist: npm:@nx/nx-darwin-arm64@16.10.0 +4 error at validateCommonDependencyRules (/home/pacs/Viewers/node_modules/nx/src/project-graph/project-graph-builder.js:323:15) +4 error at validateDependency (/home/pacs/Viewers/node_modules/nx/src/project-graph/project-graph-builder.js:313:5) +4 error at /home/pacs/Viewers/node_modules/nx/src/plugins/js/lock-file/yarn-parser.js:214:80 +4 error at Array.forEach () +4 error at /home/pacs/Viewers/node_modules/nx/src/plugins/js/lock-file/yarn-parser.js:205:49 +4 error at Array.forEach () +4 error at /home/pacs/Viewers/node_modules/nx/src/plugins/js/lock-file/yarn-parser.js:203:72 +4 error at Array.forEach () +4 error at /home/pacs/Viewers/node_modules/nx/src/plugins/js/lock-file/yarn-parser.js:200:26 diff --git a/lerna.json b/lerna.json new file mode 100644 index 0000000..df4463e --- /dev/null +++ b/lerna.json @@ -0,0 +1,5 @@ +{ + "version": "3.10.0-beta.111", + "packages": ["extensions/*", "platform/*", "modes/*", "addOns/externals/*"], + "npmClient": "yarn" +} diff --git a/modes/basic-dev-mode/.webpack/webpack.dev.js b/modes/basic-dev-mode/.webpack/webpack.dev.js new file mode 100644 index 0000000..1b8e34c --- /dev/null +++ b/modes/basic-dev-mode/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.ts`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/modes/basic-dev-mode/.webpack/webpack.prod.js b/modes/basic-dev-mode/.webpack/webpack.prod.js new file mode 100644 index 0000000..a835b9a --- /dev/null +++ b/modes/basic-dev-mode/.webpack/webpack.prod.js @@ -0,0 +1,47 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const pkg = require('./../package.json'); + +const ROOT_DIR = path.join(__dirname, './..'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.ts`, +}; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: false, + }, + output: { + path: ROOT_DIR, + library: 'ohif-mode-basic-dev', + libraryTarget: 'umd', + filename: pkg.main, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + ], + }); +}; diff --git a/modes/basic-dev-mode/CHANGELOG.md b/modes/basic-dev-mode/CHANGELOG.md new file mode 100644 index 0000000..e75fcda --- /dev/null +++ b/modes/basic-dev-mode/CHANGELOG.md @@ -0,0 +1,3027 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + + +### Features + +* **hotkeys:** Migrate hotkeys to customization service and fix issues with overrides ([#4777](https://github.com/OHIF/Viewers/issues/4777)) ([3e6913b](https://github.com/OHIF/Viewers/commit/3e6913b097569280a5cc2fa5bbe4add52f149305)) + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + + +### Bug Fixes + +* right panel for the create mode cli command ([#4788](https://github.com/OHIF/Viewers/issues/4788)) ([5712e91](https://github.com/OHIF/Viewers/commit/5712e91ca1d939ff3c36615d3cf1a1f6f0051c4f)) + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + + +### Features + +* **panels:** responsive thumbnails based on panel size ([#4723](https://github.com/OHIF/Viewers/issues/4723)) ([d9abc3d](https://github.com/OHIF/Viewers/commit/d9abc3da8d94d6c5ab0cc5af25a5f61849905a35)) + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + + +### Features + +* **dev:** move to rsbuild for dev - faster ([#4674](https://github.com/OHIF/Viewers/issues/4674)) ([d4a4267](https://github.com/OHIF/Viewers/commit/d4a4267429c02916dd51f6aefb290d96dd1c3b04)) + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + + +### Bug Fixes + +* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30)) + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + + +### Features + +* **hp:** Add displayArea option for Hanging protocols and example with Mamo([#3808](https://github.com/OHIF/Viewers/issues/3808)) ([18ac08e](https://github.com/OHIF/Viewers/commit/18ac08ed860d119721c52e4ffc270332259100b6)) + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + + +### Features + +* **layout:** new layout selector with 3D volume rendering ([#3923](https://github.com/OHIF/Viewers/issues/3923)) ([617043f](https://github.com/OHIF/Viewers/commit/617043fe0da5de91fbea4ac33a27f1df16ae1ca6)) + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + + +### Features + +* **worklist:** New worklist buttons and tooltips ([#3989](https://github.com/OHIF/Viewers/issues/3989)) ([9bcd1ae](https://github.com/OHIF/Viewers/commit/9bcd1ae6f51d61786cc1e99624f396b56a47cd69)) + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + + +### Features + +* **config:** Add activateViewportBeforeInteraction parameter for viewport interaction customization ([#3847](https://github.com/OHIF/Viewers/issues/3847)) ([f707b4e](https://github.com/OHIF/Viewers/commit/f707b4ebc996f379cd30337badc06b07e6e35ac5)) +* **i18n:** enhanced i18n support ([#3761](https://github.com/OHIF/Viewers/issues/3761)) ([d14a8f0](https://github.com/OHIF/Viewers/commit/d14a8f0199db95cd9e85866a011b64d6bf830d57)) + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + + +### Features + +* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f)) + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + +**Note:** Version bump only for package @ohif/mode-basic-dev-mode diff --git a/modes/basic-dev-mode/LICENSE b/modes/basic-dev-mode/LICENSE new file mode 100644 index 0000000..19e20dd --- /dev/null +++ b/modes/basic-dev-mode/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/modes/basic-dev-mode/babel.config.js b/modes/basic-dev-mode/babel.config.js new file mode 100644 index 0000000..325ca2a --- /dev/null +++ b/modes/basic-dev-mode/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/modes/basic-dev-mode/package.json b/modes/basic-dev-mode/package.json new file mode 100644 index 0000000..ce41fbc --- /dev/null +++ b/modes/basic-dev-mode/package.json @@ -0,0 +1,49 @@ +{ + "name": "@ohif/mode-basic-dev-mode", + "version": "3.10.0-beta.111", + "description": "Basic OHIF Viewer Using Cornerstone", + "author": "OHIF", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/ohif-mode-basic-dev-mode.umd.js", + "module": "src/index.ts", + "engines": { + "node": ">=10", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "files": [ + "dist", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:cornerstone": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.111", + "@ohif/extension-cornerstone": "3.10.0-beta.111", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.111", + "@ohif/extension-default": "3.10.0-beta.111", + "@ohif/extension-dicom-pdf": "3.10.0-beta.111", + "@ohif/extension-dicom-video": "3.10.0-beta.111" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "i18next": "^17.0.3" + }, + "devDependencies": { + "webpack": "5.94.0", + "webpack-merge": "^5.7.3" + } +} diff --git a/modes/basic-dev-mode/src/id.js b/modes/basic-dev-mode/src/id.js new file mode 100644 index 0000000..ebe5acd --- /dev/null +++ b/modes/basic-dev-mode/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/modes/basic-dev-mode/src/index.ts b/modes/basic-dev-mode/src/index.ts new file mode 100644 index 0000000..a1ca916 --- /dev/null +++ b/modes/basic-dev-mode/src/index.ts @@ -0,0 +1,183 @@ +import toolbarButtons from './toolbarButtons'; +import { hotkeys } from '@ohif/core'; +import { id } from './id'; +import i18n from 'i18next'; + +const configs = { + Length: {}, + // +}; + +const ohif = { + layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', + sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', + measurements: '@ohif/extension-cornerstone.panelModule.panelMeasurement', + thumbnailList: '@ohif/extension-default.panelModule.seriesList', +}; + +const cs3d = { + viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', +}; + +const dicomsr = { + sopClassHandler: '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr', + viewport: '@ohif/extension-cornerstone-dicom-sr.viewportModule.dicom-sr', +}; + +const dicomvideo = { + sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video', + viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video', +}; + +const dicompdf = { + sopClassHandler: '@ohif/extension-dicom-pdf.sopClassHandlerModule.dicom-pdf', + viewport: '@ohif/extension-dicom-pdf.viewportModule.dicom-pdf', +}; + +const extensionDependencies = { + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', + '@ohif/extension-cornerstone-dicom-sr': '^3.0.0', + '@ohif/extension-dicom-pdf': '^3.0.1', + '@ohif/extension-dicom-video': '^3.0.1', +}; + +function modeFactory({ modeConfiguration }) { + return { + id, + routeName: 'dev', + displayName: i18n.t('Modes:Basic Dev Viewer'), + /** + * Lifecycle hooks + */ + onModeEnter: ({ servicesManager, extensionManager }: withAppTypes) => { + const { toolbarService, toolGroupService } = servicesManager.services; + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + + const { toolNames, Enums } = utilityModule.exports; + + const tools = { + active: [ + { + toolName: toolNames.WindowLevel, + bindings: [{ mouseButton: Enums.MouseBindings.Primary }], + }, + { + toolName: toolNames.Pan, + bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }], + }, + { + toolName: toolNames.Zoom, + bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], + }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, + ], + passive: [ + { toolName: toolNames.Length }, + { toolName: toolNames.Bidirectional }, + { toolName: toolNames.Probe }, + { toolName: toolNames.EllipticalROI }, + { toolName: toolNames.CircleROI }, + { toolName: toolNames.RectangleROI }, + { toolName: toolNames.StackScroll }, + { toolName: toolNames.CalibrationLine }, + ], + // enabled + enabled: [{ toolName: toolNames.ImageOverlayViewer }], + // disabled + }; + + toolGroupService.createToolGroupAndAddTools('default', tools); + + toolbarService.addButtons(toolbarButtons); + toolbarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'WindowLevel', + 'Pan', + 'Layout', + 'MoreTools', + ]); + }, + onModeExit: ({ servicesManager }: withAppTypes) => { + const { + toolGroupService, + measurementService, + toolbarService, + uiDialogService, + uiModalService, + } = servicesManager.services; + uiDialogService.dismissAll(); + uiModalService.hide(); + toolGroupService.destroy(); + }, + validationTags: { + study: [], + series: [], + }, + isValidMode: ({ modalities }) => { + const modalities_list = modalities.split('\\'); + + // Slide Microscopy modality not supported by basic mode yet + return { + valid: !modalities_list.includes('SM'), + description: 'The mode does not support the following modalities: SM', + }; + }, + routes: [ + { + path: 'viewer-cs3d', + /*init: ({ servicesManager, extensionManager }) => { + //defaultViewerRouteInit + },*/ + layoutTemplate: ({ location, servicesManager }) => { + return { + id: ohif.layout, + props: { + // TODO: Should be optional, or required to pass empty array for slots? + leftPanels: [ohif.thumbnailList], + leftPanelResizable: true, + rightPanels: [ohif.measurements], + rightPanelResizable: true, + viewports: [ + { + namespace: cs3d.viewport, + displaySetsToDisplay: [ohif.sopClassHandler], + }, + { + namespace: dicomvideo.viewport, + displaySetsToDisplay: [dicomvideo.sopClassHandler], + }, + { + namespace: dicompdf.viewport, + displaySetsToDisplay: [dicompdf.sopClassHandler], + }, + ], + }, + }; + }, + }, + ], + extensions: extensionDependencies, + hangingProtocol: 'default', + sopClassHandlers: [ + dicomvideo.sopClassHandler, + ohif.sopClassHandler, + dicompdf.sopClassHandler, + dicomsr.sopClassHandler, + ], + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; diff --git a/modes/basic-dev-mode/src/toolbarButtons.ts b/modes/basic-dev-mode/src/toolbarButtons.ts new file mode 100644 index 0000000..8fd6e34 --- /dev/null +++ b/modes/basic-dev-mode/src/toolbarButtons.ts @@ -0,0 +1,261 @@ +import { WindowLevelMenuItem } from '@ohif/ui'; +import { defaults, ToolbarService } from '@ohif/core'; +import type { Button } from '@ohif/core/types'; + +const { windowLevelPresets } = defaults; + +function _createWwwcPreset(preset, title, subtitle) { + return { + id: preset.toString(), + title, + subtitle, + commands: [ + { + commandName: 'setWindowLevel', + commandOptions: { + ...windowLevelPresets[preset], + }, + context: 'CORNERSTONE', + }, + ], + }; +} + +function _createSetToolActiveCommands(toolName, toolGroupIds = ['default', 'mpr']) { + return toolGroupIds.map(toolGroupId => ({ + commandName: 'setToolActive', + commandOptions: { + toolGroupId, + toolName, + }, + context: 'CORNERSTONE', + })); +} + +const toolbarButtons: Button[] = [ + { + id: 'MeasurementTools', + uiType: 'ohif.toolButtonList', + props: { + groupId: 'MeasurementTools', + evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList', + primary: ToolbarService.createButton({ + id: 'Length', + icon: 'tool-length', + label: 'Length', + tooltip: 'Length Tool', + commands: _createSetToolActiveCommands('Length'), + evaluate: 'evaluate.cornerstoneTool', + }), + secondary: { + icon: 'chevron-down', + tooltip: 'More Measure Tools', + }, + items: [ + ToolbarService.createButton({ + id: 'Bidirectional', + icon: 'tool-bidirectional', + label: 'Bidirectional', + tooltip: 'Bidirectional Tool', + commands: _createSetToolActiveCommands('Bidirectional'), + evaluate: 'evaluate.cornerstoneTool', + }), + ToolbarService.createButton({ + id: 'EllipticalROI', + icon: 'tool-ellipse', + label: 'Ellipse', + tooltip: 'Ellipse ROI', + commands: _createSetToolActiveCommands('EllipticalROI'), + evaluate: 'evaluate.cornerstoneTool', + }), + ToolbarService.createButton({ + id: 'CircleROI', + icon: 'tool-circle', + label: 'Circle', + tooltip: 'Circle Tool', + commands: _createSetToolActiveCommands('CircleROI'), + evaluate: 'evaluate.cornerstoneTool', + }), + ], + }, + }, + { + id: 'Zoom', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-zoom', + label: 'Zoom', + commands: _createSetToolActiveCommands('Zoom'), + evaluate: 'evaluate.cornerstoneTool', + }, + }, + { + id: 'WindowLevel', + uiType: 'ohif.toolButtonList', + props: { + groupId: 'WindowLevel', + primary: ToolbarService.createButton({ + id: 'WindowLevel', + icon: 'tool-window-level', + label: 'Window Level', + tooltip: 'Window Level', + commands: _createSetToolActiveCommands('WindowLevel'), + evaluate: 'evaluate.cornerstoneTool', + }), + secondary: { + icon: 'chevron-down', + tooltip: 'W/L Presets', + }, + renderer: WindowLevelMenuItem, + items: [ + _createWwwcPreset(1, 'Soft tissue', '400 / 40'), + _createWwwcPreset(2, 'Lung', '1500 / -600'), + _createWwwcPreset(3, 'Liver', '150 / 90'), + _createWwwcPreset(4, 'Bone', '2500 / 480'), + _createWwwcPreset(5, 'Brain', '80 / 40'), + ], + }, + }, + { + id: 'Pan', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-move', + label: 'Pan', + commands: _createSetToolActiveCommands('Pan'), + evaluate: 'evaluate.cornerstoneTool', + }, + }, + { + id: 'Capture', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-capture', + label: 'Capture', + commands: [ + { + commandName: 'showDownloadViewportModal', + context: 'CORNERSTONE', + }, + ], + evaluate: [ + 'evaluate.action', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video', 'wholeSlide'], + }, + ], + }, + }, + { + id: 'Layout', + uiType: 'ohif.layoutSelector', + props: { + rows: 3, + columns: 4, + evaluate: 'evaluate.action', + commands: [ + { + commandName: 'setViewportGridLayout', + }, + ], + }, + }, + { + id: 'MoreTools', + uiType: 'ohif.toolButtonList', + props: { + groupId: 'MoreTools', + evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList', + primary: ToolbarService.createButton({ + id: 'Reset', + icon: 'tool-reset', + label: 'Reset View', + tooltip: 'Reset View', + commands: [ + { + commandName: 'resetViewport', + context: 'CORNERSTONE', + }, + ], + evaluate: 'evaluate.action', + }), + secondary: { + icon: 'chevron-down', + tooltip: 'More Tools', + }, + items: [ + ToolbarService.createButton({ + id: 'Reset', + icon: 'tool-reset', + label: 'Reset View', + tooltip: 'Reset View', + commands: [ + { + commandName: 'resetViewport', + context: 'CORNERSTONE', + }, + ], + evaluate: 'evaluate.action', + }), + ToolbarService.createButton({ + id: 'RotateRight', + icon: 'tool-rotate-right', + label: 'Rotate Right', + tooltip: 'Rotate Right +90', + commands: [ + { + commandName: 'rotateViewportCW', + context: 'CORNERSTONE', + }, + ], + evaluate: 'evaluate.action', + }), + ToolbarService.createButton({ + id: 'FlipHorizontal', + icon: 'tool-flip-horizontal', + label: 'Flip Horizontally', + tooltip: 'Flip Horizontally', + commands: [ + { + commandName: 'flipViewportHorizontal', + context: 'CORNERSTONE', + }, + ], + evaluate: 'evaluate.action', + }), + ToolbarService.createButton({ + id: 'StackScroll', + icon: 'tool-stack-scroll', + label: 'Stack Scroll', + tooltip: 'Stack Scroll', + commands: _createSetToolActiveCommands('StackScroll'), + evaluate: 'evaluate.cornerstoneTool', + }), + ToolbarService.createButton({ + id: 'Invert', + icon: 'tool-invert', + label: 'Invert Colors', + tooltip: 'Invert Colors', + commands: [ + { + commandName: 'invertViewport', + context: 'CORNERSTONE', + }, + ], + evaluate: 'evaluate.action', + }), + ToolbarService.createButton({ + id: 'CalibrationLine', + icon: 'tool-calibration', + label: 'Calibration Line', + tooltip: 'Calibration Line', + commands: _createSetToolActiveCommands('CalibrationLine'), + evaluate: 'evaluate.cornerstoneTool', + }), + ], + }, + }, +]; + +export default toolbarButtons; diff --git a/modes/basic-test-mode/.webpack/webpack.dev.js b/modes/basic-test-mode/.webpack/webpack.dev.js new file mode 100644 index 0000000..1b8e34c --- /dev/null +++ b/modes/basic-test-mode/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.ts`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/modes/basic-test-mode/.webpack/webpack.prod.js b/modes/basic-test-mode/.webpack/webpack.prod.js new file mode 100644 index 0000000..0a7d7f8 --- /dev/null +++ b/modes/basic-test-mode/.webpack/webpack.prod.js @@ -0,0 +1,54 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +const pkg = require('./../package.json'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); + +const ROOT_DIR = path.join(__dirname, './../'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.ts`, +}; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: false, + }, + output: { + path: ROOT_DIR, + library: 'ohif-mode-basic-test', + libraryTarget: 'umd', + libraryExport: 'default', + filename: pkg.main, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + // new MiniCssExtractPlugin({ + // filename: './dist/[name].css', + // chunkFilename: './dist/[id].css', + // }), + ], + }); +}; diff --git a/modes/basic-test-mode/CHANGELOG.md b/modes/basic-test-mode/CHANGELOG.md new file mode 100644 index 0000000..777754e --- /dev/null +++ b/modes/basic-test-mode/CHANGELOG.md @@ -0,0 +1,3090 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + + +### Features + +* **hotkeys:** Migrate hotkeys to customization service and fix issues with overrides ([#4777](https://github.com/OHIF/Viewers/issues/4777)) ([3e6913b](https://github.com/OHIF/Viewers/commit/3e6913b097569280a5cc2fa5bbe4add52f149305)) + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + + +### Features + +* **customization:** new customization service api ([#4688](https://github.com/OHIF/Viewers/issues/4688)) ([55ad8ef](https://github.com/OHIF/Viewers/commit/55ad8efbabc3fabd8031fc08927b2f92ae5aec69)) + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + + +### Features + +* **panels:** responsive thumbnails based on panel size ([#4723](https://github.com/OHIF/Viewers/issues/4723)) ([d9abc3d](https://github.com/OHIF/Viewers/commit/d9abc3da8d94d6c5ab0cc5af25a5f61849905a35)) + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + + +### Features + +* Start using group filtering to define measurements table layout ([#4501](https://github.com/OHIF/Viewers/issues/4501)) ([82440e8](https://github.com/OHIF/Viewers/commit/82440e88d5debe808f0b14281b77e430c2489779)) + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + + +### Bug Fixes + +* **tools:** enable additional tools in volume viewport ([#4620](https://github.com/OHIF/Viewers/issues/4620)) ([1992002](https://github.com/OHIF/Viewers/commit/1992002d2dced171c17b9a0163baf707fc551e3d)) + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + + +### Bug Fixes + +* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30)) + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + + +### Features + +* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3)) + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + + +### Features + +* Add CS3D WSI and Video Viewports and add annotation navigation for MPR ([#4182](https://github.com/OHIF/Viewers/issues/4182)) ([7599ec9](https://github.com/OHIF/Viewers/commit/7599ec9421129dcade94e6fa6ec7908424ab3134)) + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + + +### Features + +* **pmap:** added support for parametric map ([#4284](https://github.com/OHIF/Viewers/issues/4284)) ([fc0064f](https://github.com/OHIF/Viewers/commit/fc0064fd9d8cdc8fde81b81f0e71fd5d077ca22b)) + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + + +### Bug Fixes + +* Cobb angle not working in basic-test mode and open contour ([#4280](https://github.com/OHIF/Viewers/issues/4280)) ([6fd3c7e](https://github.com/OHIF/Viewers/commit/6fd3c7e293fec851dd30e650c1347cc0bc7a99ee)) + + +### Features + +* Add interleaved HTJ2K and volume progressive loading ([#4276](https://github.com/OHIF/Viewers/issues/4276)) ([a2084f3](https://github.com/OHIF/Viewers/commit/a2084f319b731d98b59485799fb80357094f8c38)) + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + + +### Bug Fixes + +* **window-level:** move window level region to more tools menu ([#4215](https://github.com/OHIF/Viewers/issues/4215)) ([33f4c18](https://github.com/OHIF/Viewers/commit/33f4c18f2687d30a250fe7183df3daae8394a984)) + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + + +### Features + +* **window-level-region:** add window level region tool ([#4127](https://github.com/OHIF/Viewers/issues/4127)) ([ab1a18a](https://github.com/OHIF/Viewers/commit/ab1a18af5a5b0f9086c080ed81c8fda9bfaa975b)) + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + + +### Features + +* **hp:** Add displayArea option for Hanging protocols and example with Mamo([#3808](https://github.com/OHIF/Viewers/issues/3808)) ([18ac08e](https://github.com/OHIF/Viewers/commit/18ac08ed860d119721c52e4ffc270332259100b6)) + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + + +### Bug Fixes + +* **bugs:** enhancements and bugs in several areas ([#4086](https://github.com/OHIF/Viewers/issues/4086)) ([730f434](https://github.com/OHIF/Viewers/commit/730f4349100f21b4489a21707dbb2dca9dbfbba2)) + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + + +### Bug Fixes + +* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af)) + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + + +### Bug Fixes + +* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025)) + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + + +### Bug Fixes + +* **general:** enhancements and bug fixes ([#4018](https://github.com/OHIF/Viewers/issues/4018)) ([2b83393](https://github.com/OHIF/Viewers/commit/2b83393f91cb16ea06821d79d14ff60f80c29c90)) + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + + +### Features + +* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2)) + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + + +### Features + +* **advanced-roi-tools:** new tools and icon updates and overlay bug fixes ([#4014](https://github.com/OHIF/Viewers/issues/4014)) ([cea27d4](https://github.com/OHIF/Viewers/commit/cea27d438d1de2c1ec90cbaefdc2b31a1d9980a1)) + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + + +### Features + +* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1)) + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + + +### Bug Fixes + +* **new layout:** address black screen bugs ([#4008](https://github.com/OHIF/Viewers/issues/4008)) ([158a181](https://github.com/OHIF/Viewers/commit/158a1816703e0ad66cae08cb9bd1ffb93bbd8d43)) + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + + +### Features + +* **layout:** new layout selector with 3D volume rendering ([#3923](https://github.com/OHIF/Viewers/issues/3923)) ([617043f](https://github.com/OHIF/Viewers/commit/617043fe0da5de91fbea4ac33a27f1df16ae1ca6)) + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + + +### Features + +* **worklist:** New worklist buttons and tooltips ([#3989](https://github.com/OHIF/Viewers/issues/3989)) ([9bcd1ae](https://github.com/OHIF/Viewers/commit/9bcd1ae6f51d61786cc1e99624f396b56a47cd69)) + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + + +### Bug Fixes + +* **viewport-sync:** remember synced viewports bw stack and volume and RENAME StackImageSync to ImageSliceSync ([#3849](https://github.com/OHIF/Viewers/issues/3849)) ([e4a116b](https://github.com/OHIF/Viewers/commit/e4a116b074fcb85c8cbcc9db44fdec565f3386db)) + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + + +### Features + +* **config:** Add activateViewportBeforeInteraction parameter for viewport interaction customization ([#3847](https://github.com/OHIF/Viewers/issues/3847)) ([f707b4e](https://github.com/OHIF/Viewers/commit/f707b4ebc996f379cd30337badc06b07e6e35ac5)) +* **i18n:** enhanced i18n support ([#3761](https://github.com/OHIF/Viewers/issues/3761)) ([d14a8f0](https://github.com/OHIF/Viewers/commit/d14a8f0199db95cd9e85866a011b64d6bf830d57)) + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + + +### Bug Fixes + +* **overlay:** Overlays aren't shown on undefined origin ([#3781](https://github.com/OHIF/Viewers/issues/3781)) ([fd1251f](https://github.com/OHIF/Viewers/commit/fd1251f751d8147b8a78c7f4d81c67ba69769afa)) + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + + +### Bug Fixes + +* **toolbar:** allow customizable toolbar for active viewport and allow active tool to be deactivated via a click ([#3608](https://github.com/OHIF/Viewers/issues/3608)) ([dd6d976](https://github.com/OHIF/Viewers/commit/dd6d9768bbca1d3cc472e8c1e6d85822500b96ef)) + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + + +### Features + +* **locale:** add German translations - community PR ([#3697](https://github.com/OHIF/Viewers/issues/3697)) ([ebe8f71](https://github.com/OHIF/Viewers/commit/ebe8f71da22c1d24b58f889c5d803951e19817b6)) + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + + +### Bug Fixes + +* **dicom overlay:** Handle special cases of ArrayBuffer for various DICOM overlay attributes. ([#3684](https://github.com/OHIF/Viewers/issues/3684)) ([e36a604](https://github.com/OHIF/Viewers/commit/e36a6043315e900eeb6ce183772c7f852f478e96)) +* **StackSync:** Miscellaneous fixes for stack image sync ([#3663](https://github.com/OHIF/Viewers/issues/3663)) ([8a335bd](https://github.com/OHIF/Viewers/commit/8a335bd03d14ba87d65d7468d93f74040aa828d9)) + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + +**Note:** Version bump only for package @ohif/mode-test + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + +**Note:** Version bump only for package @ohif/mode-test diff --git a/modes/basic-test-mode/LICENSE b/modes/basic-test-mode/LICENSE new file mode 100644 index 0000000..983c5ef --- /dev/null +++ b/modes/basic-test-mode/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2023 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/modes/basic-test-mode/README.md b/modes/basic-test-mode/README.md new file mode 100644 index 0000000..318df91 --- /dev/null +++ b/modes/basic-test-mode/README.md @@ -0,0 +1,5 @@ +# Test mode + +This mode is used to test the basic functionality of the OHIF viewer +in a controlled environment. It is not intended to be used for +development or production. diff --git a/modes/basic-test-mode/babel.config.js b/modes/basic-test-mode/babel.config.js new file mode 100644 index 0000000..325ca2a --- /dev/null +++ b/modes/basic-test-mode/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/modes/basic-test-mode/package.json b/modes/basic-test-mode/package.json new file mode 100644 index 0000000..2805068 --- /dev/null +++ b/modes/basic-test-mode/package.json @@ -0,0 +1,54 @@ +{ + "name": "@ohif/mode-test", + "version": "3.10.0-beta.111", + "description": "Basic mode for testing", + "author": "OHIF", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/ohif-mode-test.umd.js", + "module": "src/index.ts", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "files": [ + "dist", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "keywords": [ + "ohif-mode" + ], + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:cornerstone": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.111", + "@ohif/extension-cornerstone": "3.10.0-beta.111", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.111", + "@ohif/extension-default": "3.10.0-beta.111", + "@ohif/extension-dicom-pdf": "3.10.0-beta.111", + "@ohif/extension-dicom-video": "3.10.0-beta.111", + "@ohif/extension-measurement-tracking": "3.10.0-beta.111", + "@ohif/extension-test": "3.10.0-beta.111" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "i18next": "^17.0.3" + }, + "devDependencies": { + "webpack": "5.94.0", + "webpack-merge": "^5.7.3" + } +} diff --git a/modes/basic-test-mode/src/id.js b/modes/basic-test-mode/src/id.js new file mode 100644 index 0000000..ebe5acd --- /dev/null +++ b/modes/basic-test-mode/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/modes/basic-test-mode/src/index.ts b/modes/basic-test-mode/src/index.ts new file mode 100644 index 0000000..1ad5c62 --- /dev/null +++ b/modes/basic-test-mode/src/index.ts @@ -0,0 +1,238 @@ +import { hotkeys } from '@ohif/core'; +import toolbarButtons from './toolbarButtons'; +import { id } from './id'; +import initToolGroups from './initToolGroups'; +import moreTools from './moreTools'; +import i18n from 'i18next'; + +// Allow this mode by excluding non-imaging modalities such as SR, SEG +// Also, SM is not a simple imaging modalities, so exclude it. +const NON_IMAGE_MODALITIES = ['ECG', 'SR', 'SEG', 'RTSTRUCT']; + +const ohif = { + layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', + sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', + wsiSopClassHandler: + '@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler', + thumbnailList: '@ohif/extension-default.panelModule.seriesList', +}; + +const tracked = { + measurements: '@ohif/extension-measurement-tracking.panelModule.trackedMeasurements', + thumbnailList: '@ohif/extension-measurement-tracking.panelModule.seriesList', + viewport: '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked', +}; + +const dicomsr = { + sopClassHandler: '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr', + viewport: '@ohif/extension-cornerstone-dicom-sr.viewportModule.dicom-sr', +}; + +const dicomvideo = { + sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video', + viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video', +}; + +const dicompdf = { + sopClassHandler: '@ohif/extension-dicom-pdf.sopClassHandlerModule.dicom-pdf', + viewport: '@ohif/extension-dicom-pdf.viewportModule.dicom-pdf', +}; + +const dicomSeg = { + sopClassHandler: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg', + viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg', +}; + +const cornerstone = { + panel: '@ohif/extension-cornerstone.panelModule.panelSegmentation', + measurements: '@ohif/extension-cornerstone.panelModule.panelMeasurement', +}; + +const dicomPmap = { + sopClassHandler: '@ohif/extension-cornerstone-dicom-pmap.sopClassHandlerModule.dicom-pmap', + viewport: '@ohif/extension-cornerstone-dicom-pmap.viewportModule.dicom-pmap', +}; + +const extensionDependencies = { + // Can derive the versions at least process.env.from npm_package_version + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', + '@ohif/extension-measurement-tracking': '^3.0.0', + '@ohif/extension-cornerstone-dicom-sr': '^3.0.0', + '@ohif/extension-cornerstone-dicom-seg': '^3.0.0', + '@ohif/extension-cornerstone-dicom-pmap': '^3.0.0', + '@ohif/extension-dicom-pdf': '^3.0.1', + '@ohif/extension-dicom-video': '^3.0.1', + '@ohif/extension-test': '^0.0.1', +}; + +function modeFactory() { + return { + // TODO: We're using this as a route segment + // We should not be. + id, + routeName: 'basic-test', + displayName: i18n.t('Modes:Basic Test Mode'), + /** + * Lifecycle hooks + */ + onModeEnter: ({ servicesManager, extensionManager, commandsManager }: withAppTypes) => { + const { measurementService, toolbarService, toolGroupService, customizationService } = + servicesManager.services; + + measurementService.clearMeasurements(); + + // Init Default and SR ToolGroups + initToolGroups(extensionManager, toolGroupService, commandsManager); + + // init customizations + customizationService.setCustomizations([ + '@ohif/extension-test.customizationModule.custom-context-menu', + ]); + + toolbarService.addButtons([...toolbarButtons, ...moreTools]); + toolbarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'WindowLevel', + 'Pan', + 'Capture', + 'Layout', + 'MPR', + 'Crosshairs', + 'MoreTools', + ]); + + customizationService.setCustomizations( + { + 'ohif.hotkeyBindings': { + $push: [ + { + commandName: 'undo', + label: 'Undo', + keys: ['ctrl+z'], + isEditable: true, + }, + ], + }, + }, + 'mode' + ); + }, + onModeExit: ({ servicesManager }: withAppTypes) => { + const { + toolGroupService, + syncGroupService, + segmentationService, + cornerstoneViewportService, + uiDialogService, + uiModalService, + } = servicesManager.services; + + uiDialogService.dismissAll(); + uiModalService.hide(); + toolGroupService.destroy(); + syncGroupService.destroy(); + segmentationService.destroy(); + cornerstoneViewportService.destroy(); + }, + validationTags: { + study: [], + series: [], + }, + + isValidMode: function ({ modalities }) { + const modalities_list = modalities.split('\\'); + + // Exclude non-image modalities + return { + valid: !!modalities_list.filter(modality => NON_IMAGE_MODALITIES.indexOf(modality) === -1) + .length, + description: + 'The mode does not support studies that ONLY include the following modalities: SM, ECG, SR, SEG', + }; + }, + routes: [ + { + path: 'basic-test', + /*init: ({ servicesManager, extensionManager }) => { + //defaultViewerRouteInit + },*/ + layoutTemplate: () => { + return { + id: ohif.layout, + props: { + // Use the first two for an untracked view + // leftPanels: [ohif.thumbnailList], + // rightPanels: [dicomSeg.panel, ohif.measurements], + leftPanels: [tracked.thumbnailList], + leftPanelResizable: true, + // Can use cornerstone.measurements for all measurements + rightPanels: [cornerstone.panel, tracked.measurements, cornerstone.measurements], + rightPanelResizable: true, + // rightPanelClosed: true, // optional prop to start with collapse panels + viewports: [ + { + namespace: tracked.viewport, + displaySetsToDisplay: [ + ohif.sopClassHandler, + dicomvideo.sopClassHandler, + ohif.wsiSopClassHandler, + ], + }, + { + namespace: dicomsr.viewport, + displaySetsToDisplay: [dicomsr.sopClassHandler], + }, + { + namespace: dicomvideo.viewport, + displaySetsToDisplay: [dicomvideo.sopClassHandler], + }, + { + namespace: dicompdf.viewport, + displaySetsToDisplay: [dicompdf.sopClassHandler], + }, + { + namespace: dicomSeg.viewport, + displaySetsToDisplay: [dicomSeg.sopClassHandler], + }, + { + namespace: dicomPmap.viewport, + displaySetsToDisplay: [dicomPmap.sopClassHandler], + }, + ], + }, + }; + }, + }, + ], + extensions: extensionDependencies, + // Default protocol gets self-registered by default in the init + hangingProtocol: 'default', + // Order is important in sop class handlers when two handlers both use + // the same sop class under different situations. In that case, the more + // general handler needs to come last. For this case, the dicomvideo must + // come first to remove video transfer syntax before ohif uses images + sopClassHandlers: [ + dicomvideo.sopClassHandler, + dicomSeg.sopClassHandler, + ohif.wsiSopClassHandler, + ohif.sopClassHandler, + dicompdf.sopClassHandler, + dicomsr.sopClassHandler, + ], + hotkeys: { + // Don't store the hotkeys for basic-test-mode under the same key + // because they get customized by tests + name: 'basic-test-hotkeys', + }, + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; diff --git a/modes/basic-test-mode/src/initToolGroups.ts b/modes/basic-test-mode/src/initToolGroups.ts new file mode 100644 index 0000000..4574284 --- /dev/null +++ b/modes/basic-test-mode/src/initToolGroups.ts @@ -0,0 +1,276 @@ +const colours = { + 'viewport-0': 'rgb(200, 0, 0)', + 'viewport-1': 'rgb(200, 200, 0)', + 'viewport-2': 'rgb(0, 200, 0)', +}; + +const colorsByOrientation = { + axial: 'rgb(200, 0, 0)', + sagittal: 'rgb(200, 200, 0)', + coronal: 'rgb(0, 200, 0)', +}; + +function initDefaultToolGroup(extensionManager, toolGroupService, commandsManager, toolGroupId) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + + const { toolNames, Enums } = utilityModule.exports; + + const tools = { + active: [ + { + toolName: toolNames.WindowLevel, + bindings: [{ mouseButton: Enums.MouseBindings.Primary }], + }, + { + toolName: toolNames.Pan, + bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }], + }, + { + toolName: toolNames.Zoom, + bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], + }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, + ], + passive: [ + { toolName: toolNames.Length }, + { + toolName: toolNames.ArrowAnnotate, + configuration: { + getTextCallback: (callback, eventDetails) => + commandsManager.runCommand('arrowTextCallback', { + callback, + eventDetails, + }), + + changeTextCallback: (data, eventDetails, callback) => + commandsManager.runCommand('arrowTextCallback', { + callback, + data, + eventDetails, + }), + }, + }, + { toolName: toolNames.Bidirectional }, + { toolName: toolNames.DragProbe }, + { toolName: toolNames.Probe }, + { toolName: toolNames.EllipticalROI }, + { toolName: toolNames.CircleROI }, + { toolName: toolNames.RectangleROI }, + { toolName: toolNames.StackScroll }, + { toolName: toolNames.Angle }, + { toolName: toolNames.CobbAngle }, + { toolName: toolNames.Magnify }, + { toolName: toolNames.WindowLevelRegion }, + { toolName: toolNames.UltrasoundDirectional }, + { toolName: toolNames.PlanarFreehandROI }, + { toolName: toolNames.SplineROI }, + { toolName: toolNames.LivewireContour }, + ], + // enabled + enabled: [{ toolName: toolNames.ImageOverlayViewer }], + // disabled + disabled: [{ toolName: toolNames.ReferenceLines }, { toolName: toolNames.AdvancedMagnify }], + }; + + toolGroupService.createToolGroupAndAddTools(toolGroupId, tools); +} + +function initSRToolGroup(extensionManager, toolGroupService, commandsManager) { + const SRUtilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone-dicom-sr.utilityModule.tools' + ); + + const CS3DUtilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + + const { toolNames: SRToolNames } = SRUtilityModule.exports; + const { toolNames, Enums } = CS3DUtilityModule.exports; + const tools = { + active: [ + { + toolName: toolNames.WindowLevel, + bindings: [ + { + mouseButton: Enums.MouseBindings.Primary, + }, + ], + }, + { + toolName: toolNames.Pan, + bindings: [ + { + mouseButton: Enums.MouseBindings.Auxiliary, + }, + ], + }, + { + toolName: toolNames.Zoom, + bindings: [ + { + mouseButton: Enums.MouseBindings.Secondary, + }, + ], + }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, + ], + passive: [ + { toolName: SRToolNames.SRLength }, + { toolName: SRToolNames.SRArrowAnnotate }, + { toolName: SRToolNames.SRBidirectional }, + { toolName: SRToolNames.SREllipticalROI }, + { toolName: SRToolNames.SRCircleROI }, + { toolName: toolNames.WindowLevelRegion }, + ], + enabled: [ + { + toolName: SRToolNames.DICOMSRDisplay, + bindings: [], + }, + ], + // disabled + }; + + const toolGroupId = 'SRToolGroup'; + toolGroupService.createToolGroupAndAddTools(toolGroupId, tools); +} + +function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + + const serviceManager = extensionManager._servicesManager; + const { cornerstoneViewportService } = serviceManager.services; + + const { toolNames, Enums } = utilityModule.exports; + + const tools = { + active: [ + { + toolName: toolNames.WindowLevel, + bindings: [{ mouseButton: Enums.MouseBindings.Primary }], + }, + { + toolName: toolNames.Pan, + bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }], + }, + { + toolName: toolNames.Zoom, + bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], + }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, + ], + passive: [ + { toolName: toolNames.Length }, + { + toolName: toolNames.ArrowAnnotate, + configuration: { + getTextCallback: (callback, eventDetails) => + commandsManager.runCommand('arrowTextCallback', { + callback, + eventDetails, + }), + + changeTextCallback: (data, eventDetails, callback) => + commandsManager.runCommand('arrowTextCallback', { + callback, + data, + eventDetails, + }), + }, + }, + { toolName: toolNames.Bidirectional }, + { toolName: toolNames.DragProbe }, + { toolName: toolNames.Probe }, + { toolName: toolNames.EllipticalROI }, + { toolName: toolNames.CircleROI }, + { toolName: toolNames.RectangleROI }, + { toolName: toolNames.StackScroll }, + { toolName: toolNames.Angle }, + { toolName: toolNames.WindowLevelRegion }, + { toolName: toolNames.PlanarFreehandROI }, + { toolName: toolNames.SplineROI }, + { toolName: toolNames.LivewireContour }, + ], + disabled: [ + { + toolName: toolNames.Crosshairs, + configuration: { + viewportIndicators: false, + autoPan: { + enabled: false, + panSize: 10, + }, + getReferenceLineColor: viewportId => { + const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); + const viewportOptions = viewportInfo?.viewportOptions; + if (viewportOptions) { + return ( + colours[viewportOptions.id] || + colorsByOrientation[viewportOptions.orientation] || + '#0c0' + ); + } else { + console.warn('missing viewport?', viewportId); + return '#0c0'; + } + }, + }, + }, + { toolName: toolNames.ReferenceLines }, + ], + + // enabled + // disabled + }; + + toolGroupService.createToolGroupAndAddTools('mpr', tools); +} + +function initVolume3DToolGroup(extensionManager, toolGroupService) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + + const { toolNames, Enums } = utilityModule.exports; + + const tools = { + active: [ + { + toolName: toolNames.TrackballRotateTool, + bindings: [{ mouseButton: Enums.MouseBindings.Primary }], + }, + { + toolName: toolNames.Zoom, + bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], + }, + { + toolName: toolNames.Pan, + bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }], + }, + ], + }; + + toolGroupService.createToolGroupAndAddTools('volume3d', tools); +} + +function initToolGroups(extensionManager, toolGroupService, commandsManager) { + initDefaultToolGroup(extensionManager, toolGroupService, commandsManager, 'default'); + initSRToolGroup(extensionManager, toolGroupService, commandsManager); + initMPRToolGroup(extensionManager, toolGroupService, commandsManager); + initVolume3DToolGroup(extensionManager, toolGroupService); +} + +export default initToolGroups; diff --git a/modes/basic-test-mode/src/moreTools.ts b/modes/basic-test-mode/src/moreTools.ts new file mode 100644 index 0000000..f56edbc --- /dev/null +++ b/modes/basic-test-mode/src/moreTools.ts @@ -0,0 +1,212 @@ +import type { RunCommand } from '@ohif/core/types'; +import { EVENTS } from '@cornerstonejs/core'; +import { ToolbarService, ViewportGridService } from '@ohif/core'; +import { setToolActiveToolbar } from './toolbarButtons'; +const { createButton } = ToolbarService; + +const ReferenceLinesListeners: RunCommand = [ + { + commandName: 'setSourceViewportForReferenceLinesTool', + context: 'CORNERSTONE', + }, +]; + +const moreTools = [ + { + id: 'MoreTools', + uiType: 'ohif.toolButtonList', + props: { + groupId: 'MoreTools', + evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList', + primary: createButton({ + id: 'Reset', + icon: 'tool-reset', + tooltip: 'Reset View', + label: 'Reset', + commands: 'resetViewport', + evaluate: 'evaluate.action', + }), + secondary: { + icon: 'chevron-down', + label: '', + tooltip: 'More Tools', + }, + items: [ + createButton({ + id: 'Reset', + icon: 'tool-reset', + label: 'Reset View', + tooltip: 'Reset View', + commands: 'resetViewport', + evaluate: 'evaluate.action', + }), + createButton({ + id: 'rotate-right', + icon: 'tool-rotate-right', + label: 'Rotate Right', + tooltip: 'Rotate +90', + commands: 'rotateViewportCW', + evaluate: 'evaluate.action', + }), + createButton({ + id: 'flipHorizontal', + icon: 'tool-flip-horizontal', + label: 'Flip Horizontal', + tooltip: 'Flip Horizontally', + commands: 'flipViewportHorizontal', + evaluate: 'evaluate.viewportProperties.toggle', + }), + createButton({ + id: 'ImageSliceSync', + icon: 'link', + label: 'Image Slice Sync', + tooltip: 'Enable position synchronization on stack viewports', + commands: { + commandName: 'toggleSynchronizer', + commandOptions: { + type: 'imageSlice', + }, + }, + listeners: { + [EVENTS.VIEWPORT_NEW_IMAGE_SET]: { + commandName: 'toggleImageSliceSync', + commandOptions: { toggledState: true }, + }, + }, + evaluate: 'evaluate.cornerstone.synchronizer', + }), + createButton({ + id: 'ReferenceLines', + icon: 'tool-referenceLines', + label: 'Reference Lines', + tooltip: 'Show Reference Lines', + commands: 'toggleEnabledDisabledToolbar', + listeners: { + [ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners, + [ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners, + }, + evaluate: 'evaluate.cornerstoneTool.toggle', + }), + createButton({ + id: 'ImageOverlayViewer', + icon: 'toggle-dicom-overlay', + label: 'Image Overlay', + tooltip: 'Toggle Image Overlay', + commands: 'toggleEnabledDisabledToolbar', + evaluate: 'evaluate.cornerstoneTool.toggle', + }), + createButton({ + id: 'StackScroll', + icon: 'tool-stack-scroll', + label: 'Stack Scroll', + tooltip: 'Stack Scroll', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'invert', + icon: 'tool-invert', + label: 'Invert', + tooltip: 'Invert Colors', + commands: 'invertViewport', + evaluate: 'evaluate.viewportProperties.toggle', + }), + createButton({ + id: 'Probe', + icon: 'tool-probe', + label: 'Probe', + tooltip: 'Probe', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'Cine', + icon: 'tool-cine', + label: 'Cine', + tooltip: 'Cine', + commands: 'toggleCine', + evaluate: 'evaluate.cine', + }), + createButton({ + id: 'Angle', + icon: 'tool-angle', + label: 'Angle', + tooltip: 'Angle', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'CobbAngle', + icon: 'icon-tool-cobb-angle', + label: 'Cobb Angle', + tooltip: 'Cobb Angle', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'Magnify', + icon: 'tool-magnify', + label: 'Zoom-in', + tooltip: 'Zoom-in', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'RectangleROI', + icon: 'tool-rectangle', + label: 'Rectangle', + tooltip: 'Rectangle', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'CalibrationLine', + icon: 'tool-calibration', + label: 'Calibration', + tooltip: 'Calibration Line', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'TagBrowser', + icon: 'dicom-tag-browser', + label: 'Dicom Tag Browser', + tooltip: 'Dicom Tag Browser', + commands: 'openDICOMTagViewer', + }), + createButton({ + id: 'AdvancedMagnify', + icon: 'icon-tool-loupe', + label: 'Magnify Probe', + tooltip: 'Magnify Probe', + commands: 'toggleActiveDisabledToolbar', + evaluate: 'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled', + }), + createButton({ + id: 'UltrasoundDirectionalTool', + icon: 'icon-tool-ultrasound-bidirectional', + label: 'Ultrasound Directional', + tooltip: 'Ultrasound Directional', + commands: setToolActiveToolbar, + evaluate: [ + 'evaluate.cornerstoneTool', + { + name: 'evaluate.modality.supported', + supportedModalities: ['US'], + }, + ], + }), + createButton({ + id: 'WindowLevelRegion', + icon: 'icon-tool-window-region', + label: 'Window Level Region', + tooltip: 'Window Level Region', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + ], + }, + }, +]; + +export default moreTools; diff --git a/modes/basic-test-mode/src/toolbarButtons.ts b/modes/basic-test-mode/src/toolbarButtons.ts new file mode 100644 index 0000000..fc27d37 --- /dev/null +++ b/modes/basic-test-mode/src/toolbarButtons.ts @@ -0,0 +1,253 @@ +// TODO: torn, can either bake this here; or have to create a whole new button type +// Only ways that you can pass in a custom React component for render :l +import { + // ListMenu, + WindowLevelMenuItem, +} from '@ohif/ui'; +import { defaults, ToolbarService } from '@ohif/core'; +import type { Button } from '@ohif/core/types'; + +const { windowLevelPresets } = defaults; +const { createButton } = ToolbarService; + +/** + * + * @param {*} preset - preset number (from above import) + * @param {*} title + * @param {*} subtitle + */ +function _createWwwcPreset(preset, title, subtitle) { + return { + id: preset.toString(), + title, + subtitle, + commands: [ + { + commandName: 'setWindowLevel', + commandOptions: { + ...windowLevelPresets[preset], + }, + context: 'CORNERSTONE', + }, + ], + }; +} + +export const setToolActiveToolbar = { + commandName: 'setToolActiveToolbar', + commandOptions: { + toolGroupIds: ['default', 'mpr', 'SRToolGroup'], + }, +}; + +const toolbarButtons: Button[] = [ + { + id: 'MeasurementTools', + uiType: 'ohif.toolButtonList', + props: { + groupId: 'MeasurementTools', + // group evaluate to determine which item should move to the top + evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList', + primary: createButton({ + id: 'Length', + icon: 'tool-length', + label: 'Length', + tooltip: 'Length Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + secondary: { + icon: 'chevron-down', + tooltip: 'More Measure Tools', + }, + items: [ + createButton({ + id: 'Length', + icon: 'tool-length', + label: 'Length', + tooltip: 'Length Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'Bidirectional', + icon: 'tool-bidirectional', + label: 'Bidirectional', + tooltip: 'Bidirectional Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'ArrowAnnotate', + icon: 'tool-annotate', + label: 'Annotation', + tooltip: 'Arrow Annotate', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'EllipticalROI', + icon: 'tool-ellipse', + label: 'Ellipse', + tooltip: 'Ellipse ROI', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'CircleROI', + icon: 'tool-circle', + label: 'Circle', + tooltip: 'Circle Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'PlanarFreehandROI', + icon: 'icon-tool-freehand-roi', + label: 'Freehand ROI', + tooltip: 'Freehand ROI', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'SplineROI', + icon: 'icon-tool-spline-roi', + label: 'Spline ROI', + tooltip: 'Spline ROI', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'LivewireContour', + icon: 'icon-tool-livewire', + label: 'Livewire tool', + tooltip: 'Livewire tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + ], + }, + }, + { + id: 'Zoom', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-zoom', + label: 'Zoom', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + // Window Level + { + id: 'WindowLevel', + uiType: 'ohif.toolButtonList', + props: { + groupId: 'WindowLevel', + primary: createButton({ + id: 'WindowLevel', + icon: 'tool-window-level', + label: 'Window Level', + tooltip: 'Window Level', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + secondary: { + icon: 'chevron-down', + label: 'W/L Manual', + tooltip: 'W/L Presets', + }, + renderer: WindowLevelMenuItem, + items: [ + _createWwwcPreset(1, 'Soft tissue', '400 / 40'), + _createWwwcPreset(2, 'Lung', '1500 / -600'), + _createWwwcPreset(3, 'Liver', '150 / 90'), + _createWwwcPreset(4, 'Bone', '2500 / 480'), + _createWwwcPreset(5, 'Brain', '80 / 40'), + ], + }, + }, + // Pan... + { + id: 'Pan', + uiType: 'ohif.toolButton', + props: { + type: 'tool', + icon: 'tool-move', + label: 'Pan', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + { + id: 'MPR', + uiType: 'ohif.toolButton', + props: { + icon: 'icon-mpr', + label: 'MPR', + commands: [ + { + commandName: 'toggleHangingProtocol', + commandOptions: { + protocolId: 'mpr', + }, + }, + ], + evaluate: 'evaluate.mpr', + }, + }, + { + id: 'TrackBallRotate', + type: 'ohif.toolButton', + props: { + type: 'tool', + icon: 'tool-3d-rotate', + label: '3D Rotate', + commands: setToolActiveToolbar, + }, + }, + { + id: 'Capture', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-capture', + label: 'Capture', + commands: 'showDownloadViewportModal', + evaluate: [ + 'evaluate.action', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video', 'wholeSlide'], + }, + ], + }, + }, + { + id: 'Layout', + uiType: 'ohif.layoutSelector', + props: { + rows: 3, + columns: 4, + evaluate: 'evaluate.action', + commands: 'setViewportGridLayout', + }, + }, + { + id: 'Crosshairs', + uiType: 'ohif.toolButton', + props: { + type: 'tool', + icon: 'tool-crosshair', + label: 'Crosshairs', + commands: { + commandName: 'setToolActiveToolbar', + commandOptions: { + toolGroupIds: ['mpr'], + }, + }, + evaluate: 'evaluate.cornerstoneTool', + }, + }, +]; + +export default toolbarButtons; \ No newline at end of file diff --git a/modes/longitudinal/.webpack/webpack.dev.js b/modes/longitudinal/.webpack/webpack.dev.js new file mode 100644 index 0000000..1b8e34c --- /dev/null +++ b/modes/longitudinal/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.ts`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/modes/longitudinal/.webpack/webpack.prod.js b/modes/longitudinal/.webpack/webpack.prod.js new file mode 100644 index 0000000..b60b890 --- /dev/null +++ b/modes/longitudinal/.webpack/webpack.prod.js @@ -0,0 +1,53 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +const pkg = require('./../package.json'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); + +const ROOT_DIR = path.join(__dirname, './../'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); +const ENTRY = { + app: `${SRC_DIR}/index.ts`, +}; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: false, + }, + output: { + path: ROOT_DIR, + library: 'ohif-mode-longitudinal', + libraryTarget: 'umd', + libraryExport: 'default', + filename: pkg.main, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + // new MiniCssExtractPlugin({ + // filename: './dist/[name].css', + // chunkFilename: './dist/[id].css', + // }), + ], + }); +}; diff --git a/modes/longitudinal/CHANGELOG.md b/modes/longitudinal/CHANGELOG.md new file mode 100644 index 0000000..05420a8 --- /dev/null +++ b/modes/longitudinal/CHANGELOG.md @@ -0,0 +1,3114 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + + +### Features + +* **hotkeys:** Migrate hotkeys to customization service and fix issues with overrides ([#4777](https://github.com/OHIF/Viewers/issues/4777)) ([3e6913b](https://github.com/OHIF/Viewers/commit/3e6913b097569280a5cc2fa5bbe4add52f149305)) + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + + +### Bug Fixes + +* **measurement label auto-completion:** Customization of measurement label auto-completion fails for measurements following arrow annotations. ([#4739](https://github.com/OHIF/Viewers/issues/4739)) ([e035ef1](https://github.com/OHIF/Viewers/commit/e035ef1dcc72ecbe2a757e3b814551d768d7e610)) + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + + +### Features + +* **customization:** new customization service api ([#4688](https://github.com/OHIF/Viewers/issues/4688)) ([55ad8ef](https://github.com/OHIF/Viewers/commit/55ad8efbabc3fabd8031fc08927b2f92ae5aec69)) + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + + +### Features + +* **resizable-side-panels:** Make the left and right side panels (optionally) resizable. ([#4672](https://github.com/OHIF/Viewers/issues/4672)) ([d90a4cf](https://github.com/OHIF/Viewers/commit/d90a4cfb16cc0daed9b905de9780f44cca1323f9)) + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + + +### Bug Fixes + +* **tools:** enable additional tools in volume viewport ([#4620](https://github.com/OHIF/Viewers/issues/4620)) ([1992002](https://github.com/OHIF/Viewers/commit/1992002d2dced171c17b9a0163baf707fc551e3d)) + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + + +### Bug Fixes + +* **multiframe:** metadata handling of NM studies and loading order ([#4554](https://github.com/OHIF/Viewers/issues/4554)) ([7624ccb](https://github.com/OHIF/Viewers/commit/7624ccb5e495c0a151227a458d8d5bfb8babb22c)) + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + + +### Features + +* **segmentation:** Enhance dropdown menu functionality in SegmentationTable ([#4553](https://github.com/OHIF/Viewers/issues/4553)) ([397fd85](https://github.com/OHIF/Viewers/commit/397fd856539cd3b949a9614a9ea32d0d04a90000)) + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + + +### Bug Fixes + +* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed)) + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + + +### Bug Fixes + +* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30)) + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + + +### Features + +* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3)) + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + + +### Features + +* Add CS3D WSI and Video Viewports and add annotation navigation for MPR ([#4182](https://github.com/OHIF/Viewers/issues/4182)) ([7599ec9](https://github.com/OHIF/Viewers/commit/7599ec9421129dcade94e6fa6ec7908424ab3134)) + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + + +### Features + +* **pmap:** added support for parametric map ([#4284](https://github.com/OHIF/Viewers/issues/4284)) ([fc0064f](https://github.com/OHIF/Viewers/commit/fc0064fd9d8cdc8fde81b81f0e71fd5d077ca22b)) + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + + +### Features + +* **SR:** text structured report (TEXT, CODE, NUM, PNAME, DATE, TIME and DATETIME) ([#4287](https://github.com/OHIF/Viewers/issues/4287)) ([246ebab](https://github.com/OHIF/Viewers/commit/246ebab6ebf5431a704a1861a5804045b9644ba4)) + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Features + +* **sort:** custom series sort in study panel ([#4214](https://github.com/OHIF/Viewers/issues/4214)) ([a433d40](https://github.com/OHIF/Viewers/commit/a433d406e2cac13f644203996c682260b54e8865)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + + +### Bug Fixes + +* **window-level:** move window level region to more tools menu ([#4215](https://github.com/OHIF/Viewers/issues/4215)) ([33f4c18](https://github.com/OHIF/Viewers/commit/33f4c18f2687d30a250fe7183df3daae8394a984)) + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + + +### Features + +* **window-level-region:** add window level region tool ([#4127](https://github.com/OHIF/Viewers/issues/4127)) ([ab1a18a](https://github.com/OHIF/Viewers/commit/ab1a18af5a5b0f9086c080ed81c8fda9bfaa975b)) + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + + +### Features + +* **hp:** Add displayArea option for Hanging protocols and example with Mamo([#3808](https://github.com/OHIF/Viewers/issues/3808)) ([18ac08e](https://github.com/OHIF/Viewers/commit/18ac08ed860d119721c52e4ffc270332259100b6)) + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + + +### Bug Fixes + +* **crosshairs:** reset angle, position, and slabthickness for crosshairs when reset viewport tool is used ([#4113](https://github.com/OHIF/Viewers/issues/4113)) ([73d9e99](https://github.com/OHIF/Viewers/commit/73d9e99d5d6f38ab6c36f4471d54f18798feacb4)) + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + + +### Features + +* **rt:** allow rendering of points in RT Struct ([#4128](https://github.com/OHIF/Viewers/issues/4128)) ([5903b07](https://github.com/OHIF/Viewers/commit/5903b0749aa41112d2e991bf53ed29b1fd7bd13f)) + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + + +### Bug Fixes + +* **bugs:** enhancements and bugs in several areas ([#4086](https://github.com/OHIF/Viewers/issues/4086)) ([730f434](https://github.com/OHIF/Viewers/commit/730f4349100f21b4489a21707dbb2dca9dbfbba2)) + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + + +### Bug Fixes + +* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af)) + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + + +### Bug Fixes + +* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025)) + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + + +### Bug Fixes + +* **general:** enhancements and bug fixes ([#4018](https://github.com/OHIF/Viewers/issues/4018)) ([2b83393](https://github.com/OHIF/Viewers/commit/2b83393f91cb16ea06821d79d14ff60f80c29c90)) + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + + +### Features + +* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2)) + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + + +### Bug Fixes + +* **cornerstone-dicom-sr:** Freehand SR hydration support ([#3996](https://github.com/OHIF/Viewers/issues/3996)) ([5645ac1](https://github.com/OHIF/Viewers/commit/5645ac1b271e1ed8c57f5d71100809362447267e)) + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + + +### Features + +* **advanced-roi-tools:** new tools and icon updates and overlay bug fixes ([#4014](https://github.com/OHIF/Viewers/issues/4014)) ([cea27d4](https://github.com/OHIF/Viewers/commit/cea27d438d1de2c1ec90cbaefdc2b31a1d9980a1)) + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + + +### Features + +* **measurement:** Add support measurement label autocompletion ([#3855](https://github.com/OHIF/Viewers/issues/3855)) ([56b1eae](https://github.com/OHIF/Viewers/commit/56b1eae6356a6534960df1196bdd1e95b0a9a470)) + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + + +### Features + +* **segmentation:** Enhanced segmentation panel design for TMTV ([#3988](https://github.com/OHIF/Viewers/issues/3988)) ([9f3235f](https://github.com/OHIF/Viewers/commit/9f3235ff096636aafa88d8a42859e8dc85d9036d)) + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + + +### Features + +* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1)) + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + + +### Bug Fixes + +* **new layout:** address black screen bugs ([#4008](https://github.com/OHIF/Viewers/issues/4008)) ([158a181](https://github.com/OHIF/Viewers/commit/158a1816703e0ad66cae08cb9bd1ffb93bbd8d43)) + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + + +### Features + +* **layout:** new layout selector with 3D volume rendering ([#3923](https://github.com/OHIF/Viewers/issues/3923)) ([617043f](https://github.com/OHIF/Viewers/commit/617043fe0da5de91fbea4ac33a27f1df16ae1ca6)) + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + + +### Features + +* **worklist:** New worklist buttons and tooltips ([#3989](https://github.com/OHIF/Viewers/issues/3989)) ([9bcd1ae](https://github.com/OHIF/Viewers/commit/9bcd1ae6f51d61786cc1e99624f396b56a47cd69)) + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + + +### Bug Fixes + +* **cli:** mode creation template ([#3876](https://github.com/OHIF/Viewers/issues/3876)) ([#3981](https://github.com/OHIF/Viewers/issues/3981)) ([e485d68](https://github.com/OHIF/Viewers/commit/e485d68fd4619ce7187113cbe59e47f9523dbcc8)) + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + + +### Bug Fixes + +* **viewport-sync:** remember synced viewports bw stack and volume and RENAME StackImageSync to ImageSliceSync ([#3849](https://github.com/OHIF/Viewers/issues/3849)) ([e4a116b](https://github.com/OHIF/Viewers/commit/e4a116b074fcb85c8cbcc9db44fdec565f3386db)) + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + + +### Features + +* **config:** Add activateViewportBeforeInteraction parameter for viewport interaction customization ([#3847](https://github.com/OHIF/Viewers/issues/3847)) ([f707b4e](https://github.com/OHIF/Viewers/commit/f707b4ebc996f379cd30337badc06b07e6e35ac5)) +* **i18n:** enhanced i18n support ([#3761](https://github.com/OHIF/Viewers/issues/3761)) ([d14a8f0](https://github.com/OHIF/Viewers/commit/d14a8f0199db95cd9e85866a011b64d6bf830d57)) + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + + +### Bug Fixes + +* **toolbar:** allow customizable toolbar for active viewport and allow active tool to be deactivated via a click ([#3608](https://github.com/OHIF/Viewers/issues/3608)) ([dd6d976](https://github.com/OHIF/Viewers/commit/dd6d9768bbca1d3cc472e8c1e6d85822500b96ef)) + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + + +### Bug Fixes + +* **modules:** add stylus loader as an option to be uncommented ([#3710](https://github.com/OHIF/Viewers/issues/3710)) ([7c57f67](https://github.com/OHIF/Viewers/commit/7c57f67844b790fc6e47ac3f9708bf9d576389c8)) + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + + +### Bug Fixes + +* **StackSync:** Miscellaneous fixes for stack image sync ([#3663](https://github.com/OHIF/Viewers/issues/3663)) ([8a335bd](https://github.com/OHIF/Viewers/commit/8a335bd03d14ba87d65d7468d93f74040aa828d9)) + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + + +### Features + +* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f)) + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + +**Note:** Version bump only for package @ohif/mode-longitudinal + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + +**Note:** Version bump only for package @ohif/mode-longitudinal diff --git a/modes/longitudinal/LICENSE b/modes/longitudinal/LICENSE new file mode 100644 index 0000000..19e20dd --- /dev/null +++ b/modes/longitudinal/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/modes/longitudinal/README.md b/modes/longitudinal/README.md new file mode 100644 index 0000000..4b6b907 --- /dev/null +++ b/modes/longitudinal/README.md @@ -0,0 +1,60 @@ +# Measurement Tracking Mode + + + +## Introduction +Measurement tracking mode allows you to: + +- Draw annotations and have them shown in the measurement panel +- Create a report from the tracked measurement and export them as DICOM SR +- Use already exported DICOM SR to re-hydrate the measurements in the viewer + +![preview](https://user-images.githubusercontent.com/7490180/171255703-e6d46da8-8d12-4685-b358-0c8d4d5cb5fe.png) + +## Workflow + + +### Status Icon +Each viewport has a left icon indicating whether the series within the viewport contains: + +- tracked measurement OR +- untracked measurement OR +- Structured Report OR +- Locked (uneditable) Structured Report + +In the following, we will discuss each category. + +![tracked](https://user-images.githubusercontent.com/7490180/171255750-c6903338-c295-4553-b8aa-8cb6a8d63943.png) + +### Tracked vs Untracked Measurements + +OHIF-v3 implements a workflow for measurement tracking that can be seen below. +In summary, when you create an annotation, a prompt will be shown whether to start tracking or not. If you start the tracking, the annotation style will change to a solid line, and annotation details get displayed on the measurement panel. On the other hand, if you decline the tracking prompt, the measurement will be considered "temporary," and annotation style remains as a dashed line and not shown on the right panel, and cannot be exported. + +Below, you can see different icons that appear for a tracked vs. untracked series in OHIF-v3. + + +![workflow](https://user-images.githubusercontent.com/7490180/171255780-dd249cbf-dd61-4e02-8d46-b91e01d53529.png) + + +### Reading and Writing DICOM SR +OHIF-v3 provides full support for reading, writing and mapping the DICOM Structured Report (SR) to interactable Cornerstone Tools. When you load an already exported DICOM SR into the viewer, you will be prompted whether to track the measurements for the series or not. + + +![preview](https://user-images.githubusercontent.com/7490180/171255797-6c374780-8e94-4a7f-a125-69b67c18c18c.png) + +If you click Yes, DICOM SR measurements gets re-hydrated into the viewer and the series become a tracked series. However, If you say no and later decide to say track the measurements, you can always click on the SR button that will prompt you with the same message again. + + +![restore](https://user-images.githubusercontent.com/7490180/171255813-8d460bd7-e64d-4bce-9467-ad5cf2615c56.png) + +The full workflow for saving measurements to SR and loading SR into the viewer is shown below. + +![sr-import](https://user-images.githubusercontent.com/7490180/171255826-c308ead6-9dad-4e91-9411-df62658cc839.png) + + +### Loading DICOM SR into an Already Tracked Series + +If you have an already tracked series and try to load a DICOM SR measurements, you will be shown the following lock icon. This means that, you can review the DICOM SR measurement, manipulate image and draw "temporary" measurements; however, you cannot edit the DICOM SR measurement. + +![locked](https://user-images.githubusercontent.com/7490180/171255842-91b84f91-4e1c-4a20-b4a2-cf9653560c43.png) diff --git a/modes/longitudinal/assets/locked.png b/modes/longitudinal/assets/locked.png new file mode 100644 index 0000000..40e7820 Binary files /dev/null and b/modes/longitudinal/assets/locked.png differ diff --git a/modes/longitudinal/assets/preview.png b/modes/longitudinal/assets/preview.png new file mode 100644 index 0000000..2b8cfb3 Binary files /dev/null and b/modes/longitudinal/assets/preview.png differ diff --git a/modes/longitudinal/assets/restore.png b/modes/longitudinal/assets/restore.png new file mode 100644 index 0000000..cfd6622 Binary files /dev/null and b/modes/longitudinal/assets/restore.png differ diff --git a/modes/longitudinal/assets/sr-import.png b/modes/longitudinal/assets/sr-import.png new file mode 100644 index 0000000..0d31e4c Binary files /dev/null and b/modes/longitudinal/assets/sr-import.png differ diff --git a/modes/longitudinal/assets/tracked.png b/modes/longitudinal/assets/tracked.png new file mode 100644 index 0000000..7da69fb Binary files /dev/null and b/modes/longitudinal/assets/tracked.png differ diff --git a/modes/longitudinal/assets/workflow.png b/modes/longitudinal/assets/workflow.png new file mode 100644 index 0000000..2291ac3 Binary files /dev/null and b/modes/longitudinal/assets/workflow.png differ diff --git a/modes/longitudinal/babel.config.js b/modes/longitudinal/babel.config.js new file mode 100644 index 0000000..325ca2a --- /dev/null +++ b/modes/longitudinal/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/modes/longitudinal/package.json b/modes/longitudinal/package.json new file mode 100644 index 0000000..c0186aa --- /dev/null +++ b/modes/longitudinal/package.json @@ -0,0 +1,55 @@ +{ + "name": "@ohif/mode-longitudinal", + "version": "3.10.0-beta.111", + "description": "Longitudinal Workflow", + "author": "OHIF", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/ohif-mode-longitudinal.js", + "module": "src/index.ts", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "files": [ + "dist", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "keywords": [ + "ohif-mode" + ], + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:cornerstone": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.111", + "@ohif/extension-cornerstone": "3.10.0-beta.111", + "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.111", + "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.111", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.111", + "@ohif/extension-default": "3.10.0-beta.111", + "@ohif/extension-dicom-pdf": "3.10.0-beta.111", + "@ohif/extension-dicom-video": "3.10.0-beta.111", + "@ohif/extension-measurement-tracking": "3.10.0-beta.111" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "i18next": "^17.0.3" + }, + "devDependencies": { + "webpack": "5.94.0", + "webpack-merge": "^5.7.3" + } +} diff --git a/modes/longitudinal/src/id.js b/modes/longitudinal/src/id.js new file mode 100644 index 0000000..ebe5acd --- /dev/null +++ b/modes/longitudinal/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/modes/longitudinal/src/index.ts b/modes/longitudinal/src/index.ts new file mode 100644 index 0000000..11f85eb --- /dev/null +++ b/modes/longitudinal/src/index.ts @@ -0,0 +1,253 @@ +import { hotkeys } from '@ohif/core'; +import i18n from 'i18next'; +import { id } from './id'; +import initToolGroups from './initToolGroups'; +import toolbarButtons from './toolbarButtons'; +import moreTools from './moreTools'; + +// Allow this mode by excluding non-imaging modalities such as SR, SEG +// Also, SM is not a simple imaging modalities, so exclude it. +const NON_IMAGE_MODALITIES = ['ECG', 'SEG', 'RTSTRUCT', 'RTPLAN', 'PR']; + +const ohif = { + layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', + sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', + thumbnailList: '@ohif/extension-default.panelModule.seriesList', + wsiSopClassHandler: + '@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler', +}; + +const cornerstone = { + measurements: '@ohif/extension-cornerstone.panelModule.panelMeasurement', + segmentation: '@ohif/extension-cornerstone.panelModule.panelSegmentation', +}; + +const tracked = { + measurements: '@ohif/extension-measurement-tracking.panelModule.trackedMeasurements', + thumbnailList: '@ohif/extension-measurement-tracking.panelModule.seriesList', + viewport: '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked', +}; + +const dicomsr = { + sopClassHandler: '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr', + sopClassHandler3D: '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr-3d', + viewport: '@ohif/extension-cornerstone-dicom-sr.viewportModule.dicom-sr', +}; + +const dicomvideo = { + sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video', + viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video', +}; + +const dicompdf = { + sopClassHandler: '@ohif/extension-dicom-pdf.sopClassHandlerModule.dicom-pdf', + viewport: '@ohif/extension-dicom-pdf.viewportModule.dicom-pdf', +}; + +const dicomSeg = { + sopClassHandler: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg', + viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg', +}; + +const dicomPmap = { + sopClassHandler: '@ohif/extension-cornerstone-dicom-pmap.sopClassHandlerModule.dicom-pmap', + viewport: '@ohif/extension-cornerstone-dicom-pmap.viewportModule.dicom-pmap', +}; + +const dicomRT = { + viewport: '@ohif/extension-cornerstone-dicom-rt.viewportModule.dicom-rt', + sopClassHandler: '@ohif/extension-cornerstone-dicom-rt.sopClassHandlerModule.dicom-rt', +}; + +const extensionDependencies = { + // Can derive the versions at least process.env.from npm_package_version + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', + '@ohif/extension-measurement-tracking': '^3.0.0', + '@ohif/extension-cornerstone-dicom-sr': '^3.0.0', + '@ohif/extension-cornerstone-dicom-seg': '^3.0.0', + '@ohif/extension-cornerstone-dicom-pmap': '^3.0.0', + '@ohif/extension-cornerstone-dicom-rt': '^3.0.0', + '@ohif/extension-dicom-pdf': '^3.0.1', + '@ohif/extension-dicom-video': '^3.0.1', +}; + +function modeFactory({ modeConfiguration }) { + let _activatePanelTriggersSubscriptions = []; + return { + // TODO: We're using this as a route segment + // We should not be. + id, + routeName: 'viewer', + displayName: i18n.t('Modes:Basic Viewer'), + /** + * Lifecycle hooks + */ + onModeEnter: function ({ servicesManager, extensionManager, commandsManager }: withAppTypes) { + const { measurementService, toolbarService, toolGroupService, customizationService } = + servicesManager.services; + + measurementService.clearMeasurements(); + + // Init Default and SR ToolGroups + initToolGroups(extensionManager, toolGroupService, commandsManager); + + toolbarService.addButtons([...toolbarButtons, ...moreTools]); + toolbarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'Pan', + 'TrackballRotate', + 'WindowLevel', + 'Capture', + 'Layout', + 'Crosshairs', + 'MoreTools', + ]); + + // // ActivatePanel event trigger for when a segmentation or measurement is added. + // // Do not force activation so as to respect the state the user may have left the UI in. + // _activatePanelTriggersSubscriptions = [ + // ...panelService.addActivatePanelTriggers( + // cornerstone.segmentation, + // [ + // { + // sourcePubSubService: segmentationService, + // sourceEvents: [segmentationService.EVENTS.SEGMENTATION_ADDED], + // }, + // ], + // true + // ), + // ...panelService.addActivatePanelTriggers( + // tracked.measurements, + // [ + // { + // sourcePubSubService: measurementService, + // sourceEvents: [ + // measurementService.EVENTS.MEASUREMENT_ADDED, + // measurementService.EVENTS.RAW_MEASUREMENT_ADDED, + // ], + // }, + // ], + // true + // ), + // true, + // ]; + }, + onModeExit: ({ servicesManager }: withAppTypes) => { + const { + toolGroupService, + syncGroupService, + segmentationService, + cornerstoneViewportService, + uiDialogService, + uiModalService, + } = servicesManager.services; + + _activatePanelTriggersSubscriptions.forEach(sub => sub.unsubscribe()); + _activatePanelTriggersSubscriptions = []; + + uiDialogService.dismissAll(); + uiModalService.hide(); + toolGroupService.destroy(); + syncGroupService.destroy(); + segmentationService.destroy(); + cornerstoneViewportService.destroy(); + }, + validationTags: { + study: [], + series: [], + }, + + isValidMode: function ({ modalities }) { + const modalities_list = modalities.split('\\'); + + // Exclude non-image modalities + return { + valid: !!modalities_list.filter(modality => NON_IMAGE_MODALITIES.indexOf(modality) === -1) + .length, + description: + 'The mode does not support studies that ONLY include the following modalities: SM, ECG, SEG, RTSTRUCT', + }; + }, + routes: [ + { + path: 'longitudinal', + /*init: ({ servicesManager, extensionManager }) => { + //defaultViewerRouteInit + },*/ + layoutTemplate: () => { + return { + id: ohif.layout, + props: { + leftPanels: [tracked.thumbnailList], + leftPanelResizable: true, + rightPanels: [cornerstone.segmentation, tracked.measurements], + rightPanelClosed: true, + rightPanelResizable: true, + viewports: [ + { + namespace: tracked.viewport, + displaySetsToDisplay: [ + ohif.sopClassHandler, + dicomvideo.sopClassHandler, + dicomsr.sopClassHandler3D, + ohif.wsiSopClassHandler, + ], + }, + { + namespace: dicomsr.viewport, + displaySetsToDisplay: [dicomsr.sopClassHandler], + }, + { + namespace: dicompdf.viewport, + displaySetsToDisplay: [dicompdf.sopClassHandler], + }, + { + namespace: dicomSeg.viewport, + displaySetsToDisplay: [dicomSeg.sopClassHandler], + }, + { + namespace: dicomPmap.viewport, + displaySetsToDisplay: [dicomPmap.sopClassHandler], + }, + { + namespace: dicomRT.viewport, + displaySetsToDisplay: [dicomRT.sopClassHandler], + }, + ], + }, + }; + }, + }, + ], + extensions: extensionDependencies, + // Default protocol gets self-registered by default in the init + hangingProtocol: 'default', + // Order is important in sop class handlers when two handlers both use + // the same sop class under different situations. In that case, the more + // general handler needs to come last. For this case, the dicomvideo must + // come first to remove video transfer syntax before ohif uses images + sopClassHandlers: [ + dicomvideo.sopClassHandler, + dicomSeg.sopClassHandler, + dicomPmap.sopClassHandler, + ohif.sopClassHandler, + ohif.wsiSopClassHandler, + dicompdf.sopClassHandler, + dicomsr.sopClassHandler3D, + dicomsr.sopClassHandler, + dicomRT.sopClassHandler, + ], + ...modeConfiguration, + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; +export { initToolGroups, moreTools, toolbarButtons }; diff --git a/modes/longitudinal/src/initToolGroups.js b/modes/longitudinal/src/initToolGroups.js new file mode 100644 index 0000000..63d1f61 --- /dev/null +++ b/modes/longitudinal/src/initToolGroups.js @@ -0,0 +1,322 @@ +import { toolNames as SRToolNames } from '@ohif/extension-cornerstone-dicom-sr'; + +const colours = { + 'viewport-0': 'rgb(200, 0, 0)', + 'viewport-1': 'rgb(200, 200, 0)', + 'viewport-2': 'rgb(0, 200, 0)', +}; + +const colorsByOrientation = { + axial: 'rgb(200, 0, 0)', + sagittal: 'rgb(200, 200, 0)', + coronal: 'rgb(0, 200, 0)', +}; + +function initDefaultToolGroup( + extensionManager, + toolGroupService, + commandsManager, + toolGroupId +) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + + const { toolNames, Enums } = utilityModule.exports; + + const tools = { + active: [ + { + toolName: toolNames.WindowLevel, + bindings: [{ mouseButton: Enums.MouseBindings.Primary }], + }, + { + toolName: toolNames.Pan, + bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }], + }, + { + toolName: toolNames.Zoom, + bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], + }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, + ], + passive: [ + { toolName: toolNames.Length }, + { + toolName: toolNames.ArrowAnnotate, + configuration: { + getTextCallback: (callback, eventDetails) => { + commandsManager.runCommand('arrowTextCallback', { + callback, + eventDetails, + }); + }, + changeTextCallback: (data, eventDetails, callback) => { + commandsManager.runCommand('arrowTextCallback', { + callback, + data, + eventDetails, + }); + }, + }, + }, + { toolName: toolNames.Bidirectional }, + { toolName: toolNames.DragProbe }, + { toolName: toolNames.Probe }, + { toolName: toolNames.EllipticalROI }, + { toolName: toolNames.CircleROI }, + { toolName: toolNames.RectangleROI }, + { toolName: toolNames.StackScroll }, + { toolName: toolNames.Angle }, + { toolName: toolNames.CobbAngle }, + { toolName: toolNames.Magnify }, + { toolName: toolNames.CalibrationLine }, + { + toolName: toolNames.PlanarFreehandContourSegmentation, + configuration: { + displayOnePointAsCrosshairs: true, + }, + }, + { toolName: toolNames.UltrasoundDirectional }, + { toolName: toolNames.PlanarFreehandROI }, + { toolName: toolNames.SplineROI }, + { toolName: toolNames.LivewireContour }, + { toolName: toolNames.WindowLevelRegion }, + ], + enabled: [ + { toolName: toolNames.ImageOverlayViewer }, + { toolName: toolNames.ReferenceLines }, + { + toolName: SRToolNames.SRSCOORD3DPoint, + }, + ], + disabled: [ + { + toolName: toolNames.AdvancedMagnify, + }, + ], + }; + + toolGroupService.createToolGroupAndAddTools(toolGroupId, tools); +} + +function initSRToolGroup(extensionManager, toolGroupService) { + const SRUtilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone-dicom-sr.utilityModule.tools' + ); + + if (!SRUtilityModule) { + return; + } + + const CS3DUtilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + + const { toolNames: SRToolNames } = SRUtilityModule.exports; + const { toolNames, Enums } = CS3DUtilityModule.exports; + const tools = { + active: [ + { + toolName: toolNames.WindowLevel, + bindings: [ + { + mouseButton: Enums.MouseBindings.Primary, + }, + ], + }, + { + toolName: toolNames.Pan, + bindings: [ + { + mouseButton: Enums.MouseBindings.Auxiliary, + }, + ], + }, + { + toolName: toolNames.Zoom, + bindings: [ + { + mouseButton: Enums.MouseBindings.Secondary, + }, + ], + }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, + ], + passive: [ + { toolName: SRToolNames.SRLength }, + { toolName: SRToolNames.SRArrowAnnotate }, + { toolName: SRToolNames.SRBidirectional }, + { toolName: SRToolNames.SREllipticalROI }, + { toolName: SRToolNames.SRCircleROI }, + { toolName: SRToolNames.SRPlanarFreehandROI }, + { toolName: SRToolNames.SRRectangleROI }, + { toolName: toolNames.WindowLevelRegion }, + ], + enabled: [ + { + toolName: SRToolNames.DICOMSRDisplay, + }, + ], + // disabled + }; + + const toolGroupId = 'SRToolGroup'; + toolGroupService.createToolGroupAndAddTools(toolGroupId, tools); +} + +function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + + const serviceManager = extensionManager._servicesManager; + const { cornerstoneViewportService } = serviceManager.services; + + const { toolNames, Enums } = utilityModule.exports; + + const tools = { + active: [ + { + toolName: toolNames.WindowLevel, + bindings: [{ mouseButton: Enums.MouseBindings.Primary }], + }, + { + toolName: toolNames.Pan, + bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }], + }, + { + toolName: toolNames.Zoom, + bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], + }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, + ], + passive: [ + { toolName: toolNames.Length }, + { + toolName: toolNames.ArrowAnnotate, + configuration: { + getTextCallback: (callback, eventDetails) => { + commandsManager.runCommand('arrowTextCallback', { + callback, + eventDetails, + }); + }, + changeTextCallback: (data, eventDetails, callback) => { + commandsManager.runCommand('arrowTextCallback', { + callback, + data, + eventDetails, + }); + }, + }, + }, + { toolName: toolNames.Bidirectional }, + { toolName: toolNames.DragProbe }, + { toolName: toolNames.Probe }, + { toolName: toolNames.EllipticalROI }, + { toolName: toolNames.CircleROI }, + { toolName: toolNames.RectangleROI }, + { toolName: toolNames.StackScroll }, + { toolName: toolNames.Angle }, + { toolName: toolNames.CobbAngle }, + { toolName: toolNames.PlanarFreehandROI }, + { toolName: toolNames.SplineROI }, + { toolName: toolNames.LivewireContour }, + { toolName: toolNames.WindowLevelRegion }, + { + toolName: toolNames.PlanarFreehandContourSegmentation, + configuration: { + displayOnePointAsCrosshairs: true, + }, + }, + ], + disabled: [ + { + toolName: toolNames.Crosshairs, + configuration: { + viewportIndicators: true, + viewportIndicatorsConfig: { + circleRadius: 5, + xOffset: 0.95, + yOffset: 0.05, + }, + disableOnPassive: true, + autoPan: { + enabled: false, + panSize: 10, + }, + getReferenceLineColor: viewportId => { + const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); + const viewportOptions = viewportInfo?.viewportOptions; + if (viewportOptions) { + return ( + colours[viewportOptions.id] || + colorsByOrientation[viewportOptions.orientation] || + '#0c0' + ); + } else { + console.warn('missing viewport?', viewportId); + return '#0c0'; + } + }, + }, + }, + { + toolName: toolNames.AdvancedMagnify, + }, + { toolName: toolNames.ReferenceLines }, + ], + }; + + toolGroupService.createToolGroupAndAddTools('mpr', tools); +} +function initVolume3DToolGroup(extensionManager, toolGroupService) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + + const { toolNames, Enums } = utilityModule.exports; + + const tools = { + active: [ + { + toolName: toolNames.TrackballRotateTool, + bindings: [{ mouseButton: Enums.MouseBindings.Primary }], + }, + { + toolName: toolNames.Zoom, + bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], + }, + { + toolName: toolNames.Pan, + bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }], + }, + ], + }; + + toolGroupService.createToolGroupAndAddTools('volume3d', tools); +} + +function initToolGroups(extensionManager, toolGroupService, commandsManager) { + initDefaultToolGroup( + extensionManager, + toolGroupService, + commandsManager, + 'default' + ); + initSRToolGroup(extensionManager, toolGroupService); + initMPRToolGroup(extensionManager, toolGroupService, commandsManager); + initVolume3DToolGroup(extensionManager, toolGroupService); +} + +export default initToolGroups; diff --git a/modes/longitudinal/src/moreTools.ts b/modes/longitudinal/src/moreTools.ts new file mode 100644 index 0000000..77265e3 --- /dev/null +++ b/modes/longitudinal/src/moreTools.ts @@ -0,0 +1,270 @@ +import type { RunCommand } from '@ohif/core/types'; +import { EVENTS } from '@cornerstonejs/core'; +import { ToolbarService, ViewportGridService } from '@ohif/core'; +import { setToolActiveToolbar } from './toolbarButtons'; +const { createButton } = ToolbarService; + +const ReferenceLinesListeners: RunCommand = [ + { + commandName: 'setSourceViewportForReferenceLinesTool', + context: 'CORNERSTONE', + }, +]; + +const moreTools = [ + { + id: 'MoreTools', + uiType: 'ohif.toolButtonList', + props: { + groupId: 'MoreTools', + evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList', + primary: createButton({ + id: 'Reset', + icon: 'tool-reset', + tooltip: 'Reset View', + label: 'Reset', + commands: 'resetViewport', + evaluate: 'evaluate.action', + }), + secondary: { + icon: 'chevron-down', + label: '', + tooltip: 'More Tools', + }, + items: [ + createButton({ + id: 'Reset', + icon: 'tool-reset', + label: 'Reset View', + tooltip: 'Reset View', + commands: 'resetViewport', + evaluate: 'evaluate.action', + }), + createButton({ + id: 'rotate-right', + icon: 'tool-rotate-right', + label: 'Rotate Right', + tooltip: 'Rotate +90', + commands: 'rotateViewportCW', + evaluate: [ + 'evaluate.action', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + }, + ], + }), + createButton({ + id: 'flipHorizontal', + icon: 'tool-flip-horizontal', + label: 'Flip Horizontal', + tooltip: 'Flip Horizontally', + commands: 'flipViewportHorizontal', + evaluate: [ + 'evaluate.viewportProperties.toggle', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video', 'volume3d'], + }, + ], + }), + createButton({ + id: 'ImageSliceSync', + icon: 'link', + label: 'Image Slice Sync', + tooltip: 'Enable position synchronization on stack viewports', + commands: { + commandName: 'toggleSynchronizer', + commandOptions: { + type: 'imageSlice', + }, + }, + listeners: { + [EVENTS.VIEWPORT_NEW_IMAGE_SET]: { + commandName: 'toggleImageSliceSync', + commandOptions: { toggledState: true }, + }, + }, + evaluate: [ + 'evaluate.cornerstone.synchronizer', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video', 'volume3d'], + }, + ], + }), + createButton({ + id: 'ReferenceLines', + icon: 'tool-referenceLines', + label: 'Reference Lines', + tooltip: 'Show Reference Lines', + commands: 'toggleEnabledDisabledToolbar', + listeners: { + [ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners, + [ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners, + }, + evaluate: [ + 'evaluate.cornerstoneTool.toggle', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + }, + ], + }), + createButton({ + id: 'ImageOverlayViewer', + icon: 'toggle-dicom-overlay', + label: 'Image Overlay', + tooltip: 'Toggle Image Overlay', + commands: 'toggleEnabledDisabledToolbar', + evaluate: [ + 'evaluate.cornerstoneTool.toggle', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + }, + ], + }), + createButton({ + id: 'StackScroll', + icon: 'tool-stack-scroll', + label: 'Stack Scroll', + tooltip: 'Stack Scroll', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'invert', + icon: 'tool-invert', + label: 'Invert', + tooltip: 'Invert Colors', + commands: 'invertViewport', + evaluate: [ + 'evaluate.viewportProperties.toggle', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + }, + ], + }), + createButton({ + id: 'Probe', + icon: 'tool-probe', + label: 'Probe', + tooltip: 'Probe', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'Cine', + icon: 'tool-cine', + label: 'Cine', + tooltip: 'Cine', + commands: 'toggleCine', + evaluate: [ + 'evaluate.cine', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d'], + }, + ], + }), + createButton({ + id: 'Angle', + icon: 'tool-angle', + label: 'Angle', + tooltip: 'Angle', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'CobbAngle', + icon: 'icon-tool-cobb-angle', + label: 'Cobb Angle', + tooltip: 'Cobb Angle', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'Magnify', + icon: 'tool-magnify', + label: 'Zoom-in', + tooltip: 'Zoom-in', + commands: setToolActiveToolbar, + evaluate: [ + 'evaluate.cornerstoneTool', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + }, + ], + }), + createButton({ + id: 'CalibrationLine', + icon: 'tool-calibration', + label: 'Calibration', + tooltip: 'Calibration Line', + commands: setToolActiveToolbar, + evaluate: [ + 'evaluate.cornerstoneTool', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + }, + ], + }), + createButton({ + id: 'TagBrowser', + icon: 'dicom-tag-browser', + label: 'Dicom Tag Browser', + tooltip: 'Dicom Tag Browser', + commands: 'openDICOMTagViewer', + }), + createButton({ + id: 'AdvancedMagnify', + icon: 'icon-tool-loupe', + label: 'Magnify Probe', + tooltip: 'Magnify Probe', + commands: 'toggleActiveDisabledToolbar', + evaluate: [ + 'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + }, + ], + }), + createButton({ + id: 'UltrasoundDirectionalTool', + icon: 'icon-tool-ultrasound-bidirectional', + label: 'Ultrasound Directional', + tooltip: 'Ultrasound Directional', + commands: setToolActiveToolbar, + evaluate: [ + 'evaluate.cornerstoneTool', + { + name: 'evaluate.modality.supported', + supportedModalities: ['US'], + }, + ], + }), + createButton({ + id: 'WindowLevelRegion', + icon: 'icon-tool-window-region', + label: 'Window Level Region', + tooltip: 'Window Level Region', + commands: setToolActiveToolbar, + evaluate: [ + 'evaluate.cornerstoneTool', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + }, + ], + }), + ], + }, + }, +]; + +export default moreTools; diff --git a/modes/longitudinal/src/toolbarButtons.ts b/modes/longitudinal/src/toolbarButtons.ts new file mode 100644 index 0000000..0036202 --- /dev/null +++ b/modes/longitudinal/src/toolbarButtons.ts @@ -0,0 +1,236 @@ +// TODO: torn, can either bake this here; or have to create a whole new button type +// Only ways that you can pass in a custom React component for render :l +import { ToolbarService } from '@ohif/core'; +import type { Button } from '@ohif/core/types'; + +const { createButton } = ToolbarService; + +export const setToolActiveToolbar = { + commandName: 'setToolActiveToolbar', + commandOptions: { + toolGroupIds: ['default', 'mpr', 'SRToolGroup', 'volume3d'], + }, +}; + +const toolbarButtons: Button[] = [ + { + id: 'MeasurementTools', + uiType: 'ohif.toolButtonList', + props: { + groupId: 'MeasurementTools', + // group evaluate to determine which item should move to the top + evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList', + primary: createButton({ + id: 'Length', + icon: 'tool-length', + label: 'Length', + tooltip: 'Length Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + secondary: { + icon: 'chevron-down', + tooltip: 'More Measure Tools', + }, + items: [ + createButton({ + id: 'Length', + icon: 'tool-length', + label: 'Length', + tooltip: 'Length Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'Bidirectional', + icon: 'tool-bidirectional', + label: 'Bidirectional', + tooltip: 'Bidirectional Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'ArrowAnnotate', + icon: 'tool-annotate', + label: 'Annotation', + tooltip: 'Arrow Annotate', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'EllipticalROI', + icon: 'tool-ellipse', + label: 'Ellipse', + tooltip: 'Ellipse ROI', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'RectangleROI', + icon: 'tool-rectangle', + label: 'Rectangle', + tooltip: 'Rectangle ROI', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'CircleROI', + icon: 'tool-circle', + label: 'Circle', + tooltip: 'Circle Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'PlanarFreehandROI', + icon: 'icon-tool-freehand-roi', + label: 'Freehand ROI', + tooltip: 'Freehand ROI', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'SplineROI', + icon: 'icon-tool-spline-roi', + label: 'Spline ROI', + tooltip: 'Spline ROI', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'LivewireContour', + icon: 'icon-tool-livewire', + label: 'Livewire tool', + tooltip: 'Livewire tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + ], + }, + }, + { + id: 'Zoom', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-zoom', + label: 'Zoom', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + // Window Level + { + id: 'WindowLevel', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-window-level', + label: 'Window Level', + commands: setToolActiveToolbar, + evaluate: [ + 'evaluate.cornerstoneTool', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['wholeSlide'], + }, + ], + }, + }, + // Pan... + { + id: 'Pan', + uiType: 'ohif.toolButton', + props: { + type: 'tool', + icon: 'tool-move', + label: 'Pan', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + { + id: 'TrackballRotate', + uiType: 'ohif.toolButton', + props: { + type: 'tool', + icon: 'tool-3d-rotate', + label: '3D Rotate', + commands: setToolActiveToolbar, + evaluate: { + name: 'evaluate.cornerstoneTool', + disabledText: 'Select a 3D viewport to enable this tool', + }, + }, + }, + { + id: 'Capture', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-capture', + label: 'Capture', + commands: 'showDownloadViewportModal', + evaluate: [ + 'evaluate.action', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video', 'wholeSlide'], + }, + ], + }, + }, + { + id: 'Layout', + uiType: 'ohif.layoutSelector', + props: { + rows: 3, + columns: 4, + evaluate: 'evaluate.action', + }, + }, + { + id: 'Crosshairs', + uiType: 'ohif.toolButton', + props: { + type: 'tool', + icon: 'tool-crosshair', + label: 'Crosshairs', + commands: { + commandName: 'setToolActiveToolbar', + commandOptions: { + toolGroupIds: ['mpr'], + }, + }, + evaluate: { + name: 'evaluate.cornerstoneTool', + disabledText: 'Select an MPR viewport to enable this tool', + }, + }, + }, + // { + // id: 'Undo', + // uiType: 'ohif.toolButton', + // props: { + // type: 'tool', + // icon: 'prev-arrow', + // label: 'Undo', + // commands: { + // commandName: 'undo', + // }, + // evaluate: 'evaluate.action', + // }, + // }, + // { + // id: 'Redo', + // uiType: 'ohif.toolButton', + // props: { + // type: 'tool', + // icon: 'next-arrow', + // label: 'Redo', + // commands: { + // commandName: 'redo', + // }, + // evaluate: 'evaluate.action', + // }, + // }, +]; + +export default toolbarButtons; diff --git a/modes/microscopy/.gitignore b/modes/microscopy/.gitignore new file mode 100644 index 0000000..6704566 --- /dev/null +++ b/modes/microscopy/.gitignore @@ -0,0 +1,104 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/modes/microscopy/.prettierrc b/modes/microscopy/.prettierrc new file mode 100644 index 0000000..ef83baa --- /dev/null +++ b/modes/microscopy/.prettierrc @@ -0,0 +1,11 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"], + "trailingComma": "es5", + "printWidth": 100, + "proseWrap": "always", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/modes/microscopy/.webpack/webpack.dev.js b/modes/microscopy/.webpack/webpack.dev.js new file mode 100644 index 0000000..4bf848b --- /dev/null +++ b/modes/microscopy/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.js`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/modes/microscopy/.webpack/webpack.prod.js b/modes/microscopy/.webpack/webpack.prod.js new file mode 100644 index 0000000..f578122 --- /dev/null +++ b/modes/microscopy/.webpack/webpack.prod.js @@ -0,0 +1,53 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +const pkg = require('./../package.json'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); + +const ROOT_DIR = path.join(__dirname, './../'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: false, + }, + output: { + path: ROOT_DIR, + library: 'ohif-mode-microscopy', + libraryTarget: 'umd', + libraryExport: 'default', + filename: pkg.main, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + // new MiniCssExtractPlugin({ + // filename: './dist/[name].css', + // chunkFilename: './dist/[id].css', + // }), + ], + }); +}; diff --git a/modes/microscopy/CHANGELOG.md b/modes/microscopy/CHANGELOG.md new file mode 100644 index 0000000..ff1c67f --- /dev/null +++ b/modes/microscopy/CHANGELOG.md @@ -0,0 +1,3033 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + + +### Features + +* **hotkeys:** Migrate hotkeys to customization service and fix issues with overrides ([#4777](https://github.com/OHIF/Viewers/issues/4777)) ([3e6913b](https://github.com/OHIF/Viewers/commit/3e6913b097569280a5cc2fa5bbe4add52f149305)) + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + + +### Bug Fixes + +* right panel for the create mode cli command ([#4788](https://github.com/OHIF/Viewers/issues/4788)) ([5712e91](https://github.com/OHIF/Viewers/commit/5712e91ca1d939ff3c36615d3cf1a1f6f0051c4f)) + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + + +### Features + +* **panels:** responsive thumbnails based on panel size ([#4723](https://github.com/OHIF/Viewers/issues/4723)) ([d9abc3d](https://github.com/OHIF/Viewers/commit/d9abc3da8d94d6c5ab0cc5af25a5f61849905a35)) + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + + +### Features + +* **dev:** move to rsbuild for dev - faster ([#4674](https://github.com/OHIF/Viewers/issues/4674)) ([d4a4267](https://github.com/OHIF/Viewers/commit/d4a4267429c02916dd51f6aefb290d96dd1c3b04)) + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + + +### Bug Fixes + +* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30)) + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + + +### Features + +* Add CS3D WSI and Video Viewports and add annotation navigation for MPR ([#4182](https://github.com/OHIF/Viewers/issues/4182)) ([7599ec9](https://github.com/OHIF/Viewers/commit/7599ec9421129dcade94e6fa6ec7908424ab3134)) + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + + +### Bug Fixes + +* **bugs:** enhancements and bugs in several areas ([#4086](https://github.com/OHIF/Viewers/issues/4086)) ([730f434](https://github.com/OHIF/Viewers/commit/730f4349100f21b4489a21707dbb2dca9dbfbba2)) + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + + +### Features + +* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2)) + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + + +### Features + +* **worklist:** New worklist buttons and tooltips ([#3989](https://github.com/OHIF/Viewers/issues/3989)) ([9bcd1ae](https://github.com/OHIF/Viewers/commit/9bcd1ae6f51d61786cc1e99624f396b56a47cd69)) + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + + +### Features + +* **config:** Add activateViewportBeforeInteraction parameter for viewport interaction customization ([#3847](https://github.com/OHIF/Viewers/issues/3847)) ([f707b4e](https://github.com/OHIF/Viewers/commit/f707b4ebc996f379cd30337badc06b07e6e35ac5)) +* **i18n:** enhanced i18n support ([#3761](https://github.com/OHIF/Viewers/issues/3761)) ([d14a8f0](https://github.com/OHIF/Viewers/commit/d14a8f0199db95cd9e85866a011b64d6bf830d57)) + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + + +### Bug Fixes + +* **measurement and microscopy:** various small fixes for measurement and microscopy side panel ([#3696](https://github.com/OHIF/Viewers/issues/3696)) ([c1d5ee7](https://github.com/OHIF/Viewers/commit/c1d5ee7e3f7f4c0c6bed9ae81eba5519741c5155)) + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + +**Note:** Version bump only for package @ohif/mode-microscopy + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + +**Note:** Version bump only for package @ohif/mode-microscopy diff --git a/modes/microscopy/LICENSE b/modes/microscopy/LICENSE new file mode 100644 index 0000000..ccd0edc --- /dev/null +++ b/modes/microscopy/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2023 microscopy (26860200+md-prog@users.noreply.github.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/modes/microscopy/README.md b/modes/microscopy/README.md new file mode 100644 index 0000000..e4bcca9 --- /dev/null +++ b/modes/microscopy/README.md @@ -0,0 +1,13 @@ +# OHIF mode for microscopy +Mode for *DICOM VL Whole Slide Microscopy Image*. +This mode uses [OHIF extension for microscopy](../../extensions/dicom-microscopy/). + + +## Acknowledgements + +- [DICOM Microscopy Viewer](https://github.com/ImagingDataCommons/dicom-microscopy-viewer) is a Vanilla JS library for web-based visualization of DICOM VL Whole Slide Microscopy Image datasets and derived information. +- [SLIM Viewer](https://github.com/imagingdatacommons/slim) is a single-page application for interactive visualization and annotation of digital whole slide microscopy images and derived image analysis results in standard DICOM format. The application is based on the dicom-microscopy-viewer JavaScript library and runs fully client side without any custom server components. + + +## License +MIT diff --git a/modes/microscopy/babel.config.js b/modes/microscopy/babel.config.js new file mode 100644 index 0000000..a35080a --- /dev/null +++ b/modes/microscopy/babel.config.js @@ -0,0 +1,43 @@ +module.exports = { + plugins: ['@babel/plugin-proposal-class-properties'], + env: { + test: { + presets: [ + [ + // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2 + '@babel/preset-env', + { + modules: 'commonjs', + debug: false, + }, + '@babel/preset-typescript', + ], + '@babel/preset-react', + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-regenerator', + '@babel/plugin-transform-runtime', + ], + }, + production: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + development: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + }, +}; diff --git a/modes/microscopy/package.json b/modes/microscopy/package.json new file mode 100644 index 0000000..e11b898 --- /dev/null +++ b/modes/microscopy/package.json @@ -0,0 +1,45 @@ +{ + "name": "@ohif/mode-microscopy", + "version": "3.10.0-beta.111", + "description": "OHIF mode for DICOM microscopy", + "author": "OHIF", + "license": "MIT", + "main": "dist/ohif-mode-microscopy.umd.js", + "files": [ + "dist/**", + "public/**", + "README.md" + ], + "repository": "OHIF/Viewers", + "keywords": [ + "ohif-mode" + ], + "publishConfig": { + "access": "public" + }, + "module": "src/index.tsx", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:cornerstone": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.111", + "@ohif/extension-dicom-microscopy": "3.10.0-beta.111" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "i18next": "^17.0.3" + } +} diff --git a/modes/microscopy/src/id.js b/modes/microscopy/src/id.js new file mode 100644 index 0000000..ebe5acd --- /dev/null +++ b/modes/microscopy/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/modes/microscopy/src/index.tsx b/modes/microscopy/src/index.tsx new file mode 100644 index 0000000..bdef6ef --- /dev/null +++ b/modes/microscopy/src/index.tsx @@ -0,0 +1,136 @@ +import { hotkeys } from '@ohif/core'; +import i18n from 'i18next'; + +import { id } from './id'; +import toolbarButtons from './toolbarButtons'; + +const ohif = { + layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', + sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', + hangingProtocols: '@ohif/extension-default.hangingProtocolModule.default', + leftPanel: '@ohif/extension-default.panelModule.seriesList', + rightPanel: '@ohif/extension-dicom-microscopy.panelModule.measure', +}; + +export const cornerstone = { + viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', +}; + +const dicomvideo = { + sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video', + viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video', +}; + +const dicompdf = { + sopClassHandler: '@ohif/extension-dicom-pdf.sopClassHandlerModule.dicom-pdf', + viewport: '@ohif/extension-dicom-pdf.viewportModule.dicom-pdf', +}; + +const extensionDependencies = { + // Can derive the versions at least process.env.from npm_package_version + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', + '@ohif/extension-cornerstone-dicom-sr': '^3.0.0', + '@ohif/extension-dicom-pdf': '^3.0.1', + '@ohif/extension-dicom-video': '^3.0.1', + '@ohif/extension-dicom-microscopy': '^3.0.0', +}; + +function modeFactory({ modeConfiguration }) { + return { + // TODO: We're using this as a route segment + // We should not be. + id, + routeName: 'microscopy', + displayName: i18n.t('Modes:Microscopy'), + + /** + * Lifecycle hooks + */ + onModeEnter: ({ servicesManager, extensionManager, commandsManager }: withAppTypes) => { + const { toolbarService } = servicesManager.services; + + toolbarService.addButtons(toolbarButtons); + toolbarService.createButtonSection('primary', ['MeasurementTools', 'dragPan', 'TagBrowser']); + }, + + onModeExit: ({ servicesManager }: withAppTypes) => { + const { toolbarService, uiDialogService, uiModalService } = servicesManager.services; + + uiDialogService.dismissAll(); + uiModalService.hide(); + toolbarService.reset(); + }, + + validationTags: { + study: [], + series: [], + }, + + isValidMode: ({ modalities }) => { + const modalities_list = modalities.split('\\'); + + return { + valid: modalities_list.includes('SM'), + description: 'Microscopy mode only supports the SM modality', + }; + }, + + routes: [ + { + path: 'microscopy', + /*init: ({ servicesManager, extensionManager }) => { + //defaultViewerRouteInit + },*/ + layoutTemplate: ({ location, servicesManager }) => { + return { + id: ohif.layout, + props: { + leftPanels: [ohif.leftPanel], + leftPanelResizable: true, + leftPanelClosed: true, // we have problem with rendering thumbnails for microscopy images + rightPanelClosed: true, // we do not have the save microscopy measurements yet + rightPanels: [ohif.rightPanel], + rightPanelResizable: true, + viewports: [ + { + namespace: '@ohif/extension-dicom-microscopy.viewportModule.microscopy-dicom', + displaySetsToDisplay: [ + // Share the sop class handler with cornerstone version of it + '@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler', + '@ohif/extension-dicom-microscopy.sopClassHandlerModule.DicomMicroscopySRSopClassHandler', + ], + }, + { + namespace: dicomvideo.viewport, + displaySetsToDisplay: [dicomvideo.sopClassHandler], + }, + { + namespace: dicompdf.viewport, + displaySetsToDisplay: [dicompdf.sopClassHandler], + }, + ], + }, + }; + }, + }, + ], + extensions: extensionDependencies, + hangingProtocol: 'default', + sopClassHandlers: [ + '@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler', + '@ohif/extension-dicom-microscopy.sopClassHandlerModule.DicomMicroscopySRSopClassHandler', + dicomvideo.sopClassHandler, + dicompdf.sopClassHandler, + ], + ...modeConfiguration, + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; diff --git a/modes/microscopy/src/toolbarButtons.js b/modes/microscopy/src/toolbarButtons.js new file mode 100644 index 0000000..9edb10d --- /dev/null +++ b/modes/microscopy/src/toolbarButtons.js @@ -0,0 +1,164 @@ +import { ToolbarService } from '@ohif/core'; + +const toolbarButtons = [ + { + id: 'MeasurementTools', + uiType: 'ohif.toolButtonList', + props: { + groupId: 'MeasurementTools', + // group evaluate to determine which item should move to the top + evaluate: 'evaluate.group.promoteToPrimary', + primary: ToolbarService.createButton({ + id: 'line', + icon: 'tool-length', + label: 'Line', + tooltip: 'Line', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { toolName: 'line' }, + context: 'MICROSCOPY', + }, + ], + evaluate: 'evaluate.microscopyTool', + }), + secondary: { + icon: 'chevron-down', + tooltip: 'More Measure Tools', + }, + items: [ + ToolbarService.createButton({ + id: 'line', + icon: 'tool-length', + label: 'Line', + tooltip: 'Line', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { toolName: 'line' }, + context: 'MICROSCOPY', + }, + ], + evaluate: 'evaluate.microscopyTool', + }), + ToolbarService.createButton({ + id: 'point', + icon: 'tool-point', + label: 'Point', + tooltip: 'Point Tool', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { toolName: 'point' }, + context: 'MICROSCOPY', + }, + ], + evaluate: 'evaluate.microscopyTool', + }), + // Point Tool was previously defined + ToolbarService.createButton({ + id: 'polygon', + icon: 'tool-polygon', + label: 'Polygon', + tooltip: 'Polygon Tool', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { toolName: 'polygon' }, + context: 'MICROSCOPY', + }, + ], + evaluate: 'evaluate.microscopyTool', + }), + ToolbarService.createButton({ + id: 'circle', + icon: 'tool-circle', + label: 'Circle', + tooltip: 'Circle Tool', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { toolName: 'circle' }, + context: 'MICROSCOPY', + }, + ], + evaluate: 'evaluate.microscopyTool', + }), + ToolbarService.createButton({ + id: 'box', + icon: 'tool-rectangle', + label: 'Box', + tooltip: 'Box Tool', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { toolName: 'box' }, + context: 'MICROSCOPY', + }, + ], + evaluate: 'evaluate.microscopyTool', + }), + ToolbarService.createButton({ + id: 'freehandpolygon', + icon: 'tool-freehand-polygon', + label: 'Freehand Polygon', + tooltip: 'Freehand Polygon Tool', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { toolName: 'freehandpolygon' }, + context: 'MICROSCOPY', + }, + ], + evaluate: 'evaluate.microscopyTool', + }), + ToolbarService.createButton({ + id: 'freehandline', + icon: 'tool-freehand-line', + label: 'Freehand Line', + tooltip: 'Freehand Line Tool', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { toolName: 'freehandline' }, + context: 'MICROSCOPY', + }, + ], + evaluate: 'evaluate.microscopyTool', + }), + ], + }, + }, + { + id: 'dragPan', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-move', + label: 'Pan', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { toolName: 'dragPan' }, + context: 'MICROSCOPY', + }, + ], + evaluate: 'evaluate.microscopyTool', + }, + }, + { + id: 'TagBrowser', + uiType: 'ohif.toolButton', + props: { + icon: 'dicom-tag-browser', + label: 'Dicom Tag Browser', + commands: [ + { + commandName: 'openDICOMTagViewer', + }, + ], + evaluate: 'evaluate.action', + }, + }, +]; + +export default toolbarButtons; \ No newline at end of file diff --git a/modes/preclinical-4d/.webpack/webpack.dev.js b/modes/preclinical-4d/.webpack/webpack.dev.js new file mode 100644 index 0000000..6aea859 --- /dev/null +++ b/modes/preclinical-4d/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/modes/preclinical-4d/.webpack/webpack.prod.js b/modes/preclinical-4d/.webpack/webpack.prod.js new file mode 100644 index 0000000..f8b0a79 --- /dev/null +++ b/modes/preclinical-4d/.webpack/webpack.prod.js @@ -0,0 +1,53 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +const pkg = require('./../package.json'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); + +const ROOT_DIR = path.join(__dirname, './../'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: false, + }, + output: { + path: ROOT_DIR, + library: 'ohif-mode-preclinical-4d', + libraryTarget: 'umd', + libraryExport: 'default', + filename: pkg.main, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + // new MiniCssExtractPlugin({ + // filename: './dist/[name].css', + // chunkFilename: './dist/[id].css', + // }), + ], + }); +}; diff --git a/modes/preclinical-4d/CHANGELOG.md b/modes/preclinical-4d/CHANGELOG.md new file mode 100644 index 0000000..325162c --- /dev/null +++ b/modes/preclinical-4d/CHANGELOG.md @@ -0,0 +1,2017 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + + +### Features + +* **hotkeys:** Migrate hotkeys to customization service and fix issues with overrides ([#4777](https://github.com/OHIF/Viewers/issues/4777)) ([3e6913b](https://github.com/OHIF/Viewers/commit/3e6913b097569280a5cc2fa5bbe4add52f149305)) + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + + +### Bug Fixes + +* right panel for the create mode cli command ([#4788](https://github.com/OHIF/Viewers/issues/4788)) ([5712e91](https://github.com/OHIF/Viewers/commit/5712e91ca1d939ff3c36615d3cf1a1f6f0051c4f)) + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + + +### Features + +* **customization:** new customization service api ([#4688](https://github.com/OHIF/Viewers/issues/4688)) ([55ad8ef](https://github.com/OHIF/Viewers/commit/55ad8efbabc3fabd8031fc08927b2f92ae5aec69)) + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + + +### Features + +* **panels:** responsive thumbnails based on panel size ([#4723](https://github.com/OHIF/Viewers/issues/4723)) ([d9abc3d](https://github.com/OHIF/Viewers/commit/d9abc3da8d94d6c5ab0cc5af25a5f61849905a35)) + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + + +### Features + +* **dev:** move to rsbuild for dev - faster ([#4674](https://github.com/OHIF/Viewers/issues/4674)) ([d4a4267](https://github.com/OHIF/Viewers/commit/d4a4267429c02916dd51f6aefb290d96dd1c3b04)) + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + + +### Bug Fixes + +* **cli:** publish 4D preclincial mode on NPM so it can be used in the OHIF cli commands ([#4557](https://github.com/OHIF/Viewers/issues/4557)) ([085590a](https://github.com/OHIF/Viewers/commit/085590a4ca64bebad9ef60503411e1a6dd4d93f9)) + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + + +### Bug Fixes + +* **multiframe:** metadata handling of NM studies and loading order ([#4554](https://github.com/OHIF/Viewers/issues/4554)) ([7624ccb](https://github.com/OHIF/Viewers/commit/7624ccb5e495c0a151227a458d8d5bfb8babb22c)) + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + + +### Bug Fixes + +* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30)) + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + + +### Features + +* **new-study-panel:** default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser ([#4417](https://github.com/OHIF/Viewers/issues/4417)) ([a7fd9fa](https://github.com/OHIF/Viewers/commit/a7fd9fa5bfff7a1b533d99cb96f7147a35fd528f)) + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + + +### Features + +* **studies-panel:** New OHIF study panel - under experimental flag ([#4254](https://github.com/OHIF/Viewers/issues/4254)) ([7a96406](https://github.com/OHIF/Viewers/commit/7a96406a116e46e62c396855fa64f434e2984b58)) + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + + +### Bug Fixes + +* **crosshairs:** reset angle, position, and slabthickness for crosshairs when reset viewport tool is used ([#4113](https://github.com/OHIF/Viewers/issues/4113)) ([73d9e99](https://github.com/OHIF/Viewers/commit/73d9e99d5d6f38ab6c36f4471d54f18798feacb4)) + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + + +### Bug Fixes + +* **tmtv:** threshold was crashing the side panel ([#4119](https://github.com/OHIF/Viewers/issues/4119)) ([8d5c676](https://github.com/OHIF/Viewers/commit/8d5c676a5e1f3eda664071c8aece313de766bd59)) + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + + +### Bug Fixes + +* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f)) + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + + +### Features + +* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb)) + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + + +### Bug Fixes + +* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025)) + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-preclinical-4d + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + + +### Features + +* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2)) diff --git a/modes/preclinical-4d/LICENSE b/modes/preclinical-4d/LICENSE new file mode 100644 index 0000000..5f35ab7 --- /dev/null +++ b/modes/preclinical-4d/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2023 4d () + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/modes/preclinical-4d/README.md b/modes/preclinical-4d/README.md new file mode 100644 index 0000000..0f6870d --- /dev/null +++ b/modes/preclinical-4d/README.md @@ -0,0 +1,7 @@ +# 4d +## Description + +## Author +OHIF +## License +MIT \ No newline at end of file diff --git a/modes/preclinical-4d/babel.config.js b/modes/preclinical-4d/babel.config.js new file mode 100644 index 0000000..a35080a --- /dev/null +++ b/modes/preclinical-4d/babel.config.js @@ -0,0 +1,43 @@ +module.exports = { + plugins: ['@babel/plugin-proposal-class-properties'], + env: { + test: { + presets: [ + [ + // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2 + '@babel/preset-env', + { + modules: 'commonjs', + debug: false, + }, + '@babel/preset-typescript', + ], + '@babel/preset-react', + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-regenerator', + '@babel/plugin-transform-runtime', + ], + }, + production: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + development: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + }, +}; diff --git a/modes/preclinical-4d/package.json b/modes/preclinical-4d/package.json new file mode 100644 index 0000000..d4f6b95 --- /dev/null +++ b/modes/preclinical-4d/package.json @@ -0,0 +1,50 @@ +{ + "name": "@ohif/mode-preclinical-4d", + "version": "3.10.0-beta.111", + "description": "4D Workflow", + "author": "OHIF", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/index.umd.js", + "module": "src/index.tsx", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "files": [ + "dist/**", + "public/**", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "keywords": [ + "ohif-mode" + ], + "scripts": { + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo", + "dev:cornerstone": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.111", + "@ohif/extension-cornerstone": "3.10.0-beta.111", + "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.111", + "@ohif/extension-cornerstone-dynamic-volume": "3.10.0-beta.111", + "@ohif/extension-default": "3.10.0-beta.111", + "@ohif/extension-tmtv": "3.10.0-beta.111" + }, + "dependencies": { + "@babel/runtime": "^7.20.13" + }, + "devDependencies": { + "webpack": "5.94.0", + "webpack-merge": "^5.7.3" + } +} diff --git a/modes/preclinical-4d/src/getWorkflowSettings.ts b/modes/preclinical-4d/src/getWorkflowSettings.ts new file mode 100644 index 0000000..624175f --- /dev/null +++ b/modes/preclinical-4d/src/getWorkflowSettings.ts @@ -0,0 +1,103 @@ +const dynamicVolume = { + sopClassHandler: + '@ohif/extension-cornerstone-dynamic-volume.sopClassHandlerModule.dynamic-volume', + leftPanel: '@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-volume', + segmentation: '@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-segmentation', +}; + +const cornerstone = { + segmentation: '@ohif/extension-cornerstone.panelModule.panelSegmentationNoHeader', + activeViewportWindowLevel: '@ohif/extension-cornerstone.panelModule.activeViewportWindowLevel', +}; + +const defaultButtons = { + buttonSection: 'primary', + buttons: ['MeasurementTools', 'Zoom', 'WindowLevel', 'Crosshairs', 'Pan'], +}; + +const defaultLeftPanel = [[dynamicVolume.leftPanel, cornerstone.activeViewportWindowLevel]]; + +const defaultLayout = { + panels: { + left: defaultLeftPanel, + right: [], + }, +}; + +function getWorkflowSettings({ servicesManager }) { + return { + steps: [ + { + id: 'dataPreparation', + name: 'Data Preparation', + layout: { + panels: { + left: defaultLeftPanel, + }, + }, + toolbarButtons: defaultButtons, + hangingProtocol: { + protocolId: 'default4D', + stageId: 'dataPreparation', + }, + info: 'In the Data Preparation step, you can visualize the dynamic PT volume data in three orthogonal views: axial, sagittal, and coronal. Use the left panel controls to adjust the visualization settings, such as playback speed, or navigate between different frames. This step allows you to assess the quality of the PT data and prepare for further analysis or registration with other modalities.', + }, + { + id: 'registration', + name: 'Registration', + layout: defaultLayout, + toolbarButtons: defaultButtons, + hangingProtocol: { + protocolId: 'default4D', + stageId: 'registration', + }, + info: 'The Registration step provides a comprehensive view of the CT, PT, and fused CT-PT volume data in multiple orientations. The fusion viewports display the CT and PT volumes overlaid, allowing you to visually assess the alignment and registration between the two modalities. The individual CT and PT viewports are also available for side-by-side comparison. This step is crucial for ensuring proper registration before proceeding with further analysis or quantification.', + }, + { + id: 'roiQuantification', + name: 'ROI Quantification', + layout: { + panels: { + left: defaultLeftPanel, + right: [[dynamicVolume.segmentation]], + }, + options: { + leftPanelClosed: false, + rightPanelClosed: false, + }, + }, + toolbarButtons: [ + defaultButtons, + { + buttonSection: 'dynamic-toolbox', + buttons: ['BrushTools', 'RectangleROIStartEndThreshold'], + }, + ], + hangingProtocol: { + protocolId: 'default4D', + stageId: 'roiQuantification', + }, + info: 'The ROI quantification step allows you to define regions of interest (ROIs) with labelmap segmentations, on the fused CT-PT volume data using the labelmap tools. The left panel provides controls for adjusting the dynamic volume visualization, while the right panel offers tools for segmentation, editing, and exporting the ROI data. This step enables you to quantify the uptake or other measures within the defined ROIs for further analysis.', + }, + { + id: 'kineticAnalysis', + name: 'Kinetic Analysis', + layout: defaultLayout, + toolbarButtons: defaultButtons, + hangingProtocol: { + protocolId: 'default4D', + stageId: 'kineticAnalysis', + }, + onEnter: [ + { + commandName: 'updateSegmentationsChartDisplaySet', + options: { servicesManager }, + }, + ], + info: 'The Kinetic Analysis step provides a comprehensive view for visualizing and analyzing the dynamic data derived from the ROI segmentations. The fusion viewports display the combined CT-PT volume data, while a dedicated viewport shows a series chart representing the data over time. This step allows you to explore the temporal dynamics of the uptake or other kinetic measures within the defined regions of interest, enabling further quantitative analysis and modeling.', + }, + ], + }; +} + +export { getWorkflowSettings as default }; diff --git a/modes/preclinical-4d/src/id.js b/modes/preclinical-4d/src/id.js new file mode 100644 index 0000000..ebe5acd --- /dev/null +++ b/modes/preclinical-4d/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/modes/preclinical-4d/src/index.tsx b/modes/preclinical-4d/src/index.tsx new file mode 100644 index 0000000..a0c494d --- /dev/null +++ b/modes/preclinical-4d/src/index.tsx @@ -0,0 +1,179 @@ +import { id } from './id'; +import { hotkeys } from '@ohif/core'; +import initWorkflowSteps from './initWorkflowSteps'; +import initToolGroups from './initToolGroups'; +import toolbarButtons from './toolbarButtons'; +import segmentationButtons from './segmentationButtons'; + +const extensionDependencies = { + '@ohif/extension-default': '3.7.0-beta.76', + '@ohif/extension-cornerstone': '3.7.0-beta.76', + '@ohif/extension-cornerstone-dynamic-volume': '3.7.0-beta.76', + '@ohif/extension-cornerstone-dicom-seg': '3.7.0-beta.76', + '@ohif/extension-tmtv': '3.7.0-beta.76', +}; + +const ohif = { + layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', + defaultSopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', + chartSopClassHandler: '@ohif/extension-default.sopClassHandlerModule.chart', + hangingProtocol: '@ohif/extension-default.hangingProtocolModule.default', + leftPanel: '@ohif/extension-default.panelModule.seriesList', + chartViewport: '@ohif/extension-default.viewportModule.chartViewport', +}; + +const dynamicVolume = { + leftPanel: '@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-volume', +}; + +const cornerstone = { + viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', + activeViewportWindowLevel: '@ohif/extension-cornerstone.panelModule.activeViewportWindowLevel', +}; + +function modeFactory({ modeConfiguration }) { + return { + id, + routeName: 'dynamic-volume', + displayName: 'Preclinical 4D', + onModeEnter: function ({ servicesManager, extensionManager, commandsManager }: withAppTypes) { + const { + measurementService, + toolbarService, + cineService, + cornerstoneViewportService, + toolGroupService, + customizationService, + viewportGridService, + } = servicesManager.services; + + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + + const { toolNames, Enums } = utilityModule.exports; + + measurementService.clearMeasurements(); + initToolGroups({ toolNames, Enums, toolGroupService, commandsManager, servicesManager }); + + toolbarService.addButtons([...toolbarButtons, ...segmentationButtons]); + toolbarService.createButtonSection('secondary', ['ProgressDropdown']); + + // the primary button section is created in the workflow steps + // specific to the step + customizationService.setCustomizations({ + 'panelSegmentation.tableMode': { + $set: 'expanded', + }, + 'panelSegmentation.onSegmentationAdd': { + $set: () => { + commandsManager.run('createNewLabelMapForDynamicVolume'); + }, + }, + 'panelSegmentation.showAddSegment': { + $set: false, + }, + }); + + // Auto play the clip initially when the volumes are loaded + const { unsubscribe } = cornerstoneViewportService.subscribe( + cornerstoneViewportService.EVENTS.VIEWPORT_VOLUMES_CHANGED, + () => { + const viewportId = viewportGridService.getActiveViewportId(); + const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); + cineService.playClip(csViewport.element, { viewportId }); + // cineService.setIsCineEnabled(true); + + unsubscribe(); + } + ); + }, + onSetupRouteComplete: ({ servicesManager }: withAppTypes) => { + // This needs to run after hanging protocol matching process because + // it may change the protocol/stage based on workflow stage settings + initWorkflowSteps({ servicesManager }); + }, + onModeExit: ({ servicesManager }: withAppTypes) => { + const { + toolGroupService, + syncGroupService, + segmentationService, + cornerstoneViewportService, + } = servicesManager.services; + + toolGroupService.destroy(); + syncGroupService.destroy(); + segmentationService.destroy(); + cornerstoneViewportService.destroy(); + }, + get validationTags() { + return { + study: [], + series: [], + }; + }, + isValidMode: ({ modalities, study }) => { + // Todo: we need to find a better way to validate the mode + return { + valid: study.mrn === 'M1', + description: 'This mode is only available for 4D PET/CT studies.', + }; + }, + + /** + * Mode Routes are used to define the mode's behavior. A list of Mode Route + * that includes the mode's path and the layout to be used. The layout will + * include the components that are used in the layout. For instance, if the + * default layoutTemplate is used (id: '@ohif/extension-default.layoutTemplateModule.viewerLayout') + * it will include the leftPanels, rightPanels, and viewports. However, if + * you define another layoutTemplate that includes a Footer for instance, + * you should provide the Footer component here too. Note: We use Strings + * to reference the component's ID as they are registered in the internal + * ExtensionManager. The template for the string is: + * `${extensionId}.{moduleType}.${componentId}`. + */ + routes: [ + { + path: 'preclinical-4d', + layoutTemplate: ({ location, servicesManager }) => { + return { + id: ohif.layout, + props: { + leftPanels: [[dynamicVolume.leftPanel, cornerstone.activeViewportWindowLevel]], + leftPanelResizable: true, + rightPanels: [], + rightPanelResizable: true, + rightPanelClosed: true, + viewports: [ + { + namespace: cornerstone.viewport, + displaySetsToDisplay: [ohif.defaultSopClassHandler], + }, + { + namespace: ohif.chartViewport, + displaySetsToDisplay: [ohif.chartSopClassHandler], + }, + ], + }, + }; + }, + }, + ], + extensions: extensionDependencies, + // Default protocol gets self-registered by default in the init + hangingProtocol: 'default4D', + // Order is important in sop class handlers when two handlers both use + // the same sop class under different situations. In that case, the more + // general handler needs to come last. For this case, the dicomvideo must + // come first to remove video transfer syntax before ohif uses images + sopClassHandlers: [ohif.chartSopClassHandler, ohif.defaultSopClassHandler], + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; diff --git a/modes/preclinical-4d/src/initToolGroups.tsx b/modes/preclinical-4d/src/initToolGroups.tsx new file mode 100644 index 0000000..6f8dd0e --- /dev/null +++ b/modes/preclinical-4d/src/initToolGroups.tsx @@ -0,0 +1,164 @@ +const toolGroupIds = { + default: 'dynamic4D-default', + PT: 'dynamic4D-pt', + Fusion: 'dynamic4D-fusion', + CT: 'dynamic4D-ct', +}; + +const colours = { + 'viewport-0': 'rgb(200, 0, 0)', + 'viewport-1': 'rgb(200, 200, 0)', + 'viewport-2': 'rgb(0, 200, 0)', +}; + +const colorsByOrientation = { + axial: 'rgb(200, 0, 0)', + sagittal: 'rgb(200, 200, 0)', + coronal: 'rgb(0, 200, 0)', +}; + +function _initToolGroups(toolNames, Enums, toolGroupService, commandsManager, servicesManager) { + const { cornerstoneViewportService } = servicesManager.services; + const tools = { + active: [ + { + toolName: toolNames.WindowLevel, + bindings: [{ mouseButton: Enums.MouseBindings.Primary }], + }, + { + toolName: toolNames.Pan, + bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }], + }, + { + toolName: toolNames.Zoom, + bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], + }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, + ], + passive: [ + { toolName: toolNames.Length }, + { toolName: toolNames.ArrowAnnotate }, + { toolName: toolNames.Bidirectional }, + { toolName: toolNames.Probe }, + { toolName: toolNames.EllipticalROI }, + { toolName: toolNames.RectangleROI }, + { toolName: toolNames.RectangleROIThreshold }, + { toolName: toolNames.RectangleScissors }, + { toolName: toolNames.PaintFill }, + { toolName: toolNames.StackScroll }, + { toolName: toolNames.Magnify }, + { + toolName: 'CircularBrush', + parentTool: 'Brush', + configuration: { + activeStrategy: 'FILL_INSIDE_CIRCLE', + brushSize: 7, + }, + }, + { + toolName: 'CircularEraser', + parentTool: 'Brush', + configuration: { + activeStrategy: 'ERASE_INSIDE_CIRCLE', + brushSize: 7, + }, + }, + { + toolName: 'SphereBrush', + parentTool: 'Brush', + configuration: { + activeStrategy: 'FILL_INSIDE_SPHERE', + brushSize: 7, + }, + }, + { + toolName: 'SphereEraser', + parentTool: 'Brush', + configuration: { + activeStrategy: 'ERASE_INSIDE_SPHERE', + brushSize: 7, + }, + }, + { + toolName: 'ThresholdCircularBrush', + parentTool: 'Brush', + configuration: { + activeStrategy: 'THRESHOLD_INSIDE_CIRCLE', + brushSize: 7, + }, + }, + { + toolName: 'ThresholdSphereBrush', + parentTool: 'Brush', + configuration: { + activeStrategy: 'THRESHOLD_INSIDE_SPHERE', + brushSize: 7, + }, + }, + { toolName: toolNames.CircleScissors }, + { toolName: toolNames.RectangleScissors }, + { toolName: toolNames.SphereScissors }, + { toolName: toolNames.StackScroll }, + { toolName: toolNames.Magnify }, + ], + enabled: [], + disabled: [ + { + toolName: toolNames.Crosshairs, + configuration: { + viewportIndicators: true, + viewportIndicatorsConfig: { + circleRadius: 5, + xOffset: 0.95, + yOffset: 0.05, + }, + disableOnPassive: true, + autoPan: { + enabled: false, + panSize: 10, + }, + getReferenceLineColor: viewportId => { + const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); + const viewportOptions = viewportInfo?.viewportOptions; + if (viewportOptions) { + return ( + colours[viewportOptions.id] || + colorsByOrientation[viewportOptions.orientation] || + '#0c0' + ); + } else { + console.warn('missing viewport?', viewportId); + return '#0c0'; + } + }, + }, + }, + ], + }; + + toolGroupService.createToolGroupAndAddTools(toolGroupIds.PT, { + ...tools, + passive: [...tools.passive, { toolName: 'RectangleROIStartEndThreshold' }], + }); + + toolGroupService.createToolGroupAndAddTools(toolGroupIds.CT, { + ...tools, + passive: [...tools.passive, { toolName: 'RectangleROIStartEndThreshold' }], + }); + + toolGroupService.createToolGroupAndAddTools(toolGroupIds.Fusion, { + ...tools, + passive: [...tools.passive, { toolName: 'RectangleROIStartEndThreshold' }], + }); + + toolGroupService.createToolGroupAndAddTools(toolGroupIds.default, tools); +} + +function initToolGroups({ toolNames, Enums, toolGroupService, commandsManager, servicesManager }) { + _initToolGroups(toolNames, Enums, toolGroupService, commandsManager, servicesManager); +} + +export { initToolGroups as default, toolGroupIds }; diff --git a/modes/preclinical-4d/src/initWorkflowSteps.ts b/modes/preclinical-4d/src/initWorkflowSteps.ts new file mode 100644 index 0000000..47b423a --- /dev/null +++ b/modes/preclinical-4d/src/initWorkflowSteps.ts @@ -0,0 +1,9 @@ +import getWorkflowSettings from './getWorkflowSettings'; + +export default function initWorkflowSteps({ servicesManager }: withAppTypes): void { + const { workflowStepsService } = servicesManager.services; + const workflowSettings = getWorkflowSettings({ servicesManager }); + + workflowStepsService.addWorkflowSteps(workflowSettings.steps); + workflowStepsService.setActiveWorkflowStep(workflowSettings.steps[0].id); +} diff --git a/modes/preclinical-4d/src/segmentationButtons.ts b/modes/preclinical-4d/src/segmentationButtons.ts new file mode 100644 index 0000000..ec6f600 --- /dev/null +++ b/modes/preclinical-4d/src/segmentationButtons.ts @@ -0,0 +1,164 @@ +import type { Button } from '@ohif/core/types'; + +const toolbarButtons: Button[] = [ + { + id: 'BrushTools', + uiType: 'ohif.toolBoxButtonGroup', + props: { + groupId: 'BrushTools', + evaluate: 'evaluate.cornerstone.hasSegmentation', + items: [ + { + id: 'Brush', + icon: 'icon-tool-brush', + label: 'Brush', + evaluate: { + name: 'evaluate.cornerstone.segmentation', + toolNames: ['CircularBrush', 'SphereBrush'], + }, + options: [ + { + name: 'Size (mm)', + id: 'brush-radius', + type: 'range', + min: 0.5, + max: 99.5, + step: 0.5, + value: 7, + commands: { + commandName: 'setBrushSize', + commandOptions: { toolNames: ['CircularBrush', 'SphereBrush'] }, + }, + }, + { + name: 'Shape', + type: 'radio', + id: 'brush-mode', + value: 'CircularBrush', + values: [ + { value: 'CircularBrush', label: 'Circle' }, + { value: 'SphereBrush', label: 'Sphere' }, + ], + commands: 'setToolActiveToolbar', + }, + ], + }, + { + id: 'Eraser', + icon: 'icon-tool-eraser', + label: 'Eraser', + evaluate: { + name: 'evaluate.cornerstone.segmentation', + toolNames: ['CircularEraser', 'SphereEraser'], + }, + options: [ + { + name: 'Radius (mm)', + id: 'eraser-radius', + type: 'range', + min: 0.5, + max: 99.5, + step: 0.5, + value: 7, + commands: { + commandName: 'setBrushSize', + commandOptions: { toolNames: ['CircularEraser', 'SphereEraser'] }, + }, + }, + { + name: 'Shape', + type: 'radio', + id: 'eraser-mode', + value: 'CircularEraser', + values: [ + { value: 'CircularEraser', label: 'Circle' }, + { value: 'SphereEraser', label: 'Sphere' }, + ], + commands: 'setToolActiveToolbar', + }, + ], + }, + { + id: 'Threshold', + icon: 'icon-tool-threshold', + label: 'Eraser', + evaluate: { + name: 'evaluate.cornerstone.segmentation', + toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'], + }, + options: [ + { + name: 'Radius (mm)', + id: 'threshold-radius', + type: 'range', + min: 0.5, + max: 99.5, + step: 0.5, + value: 7, + commands: { + commandName: 'setBrushSize', + commandOptions: { + toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'], + }, + }, + }, + { + name: 'Shape', + type: 'radio', + id: 'eraser-mode', + value: 'ThresholdCircularBrush', + values: [ + { value: 'ThresholdCircularBrush', label: 'Circle' }, + { value: 'ThresholdSphereBrush', label: 'Sphere' }, + ], + commands: 'setToolActiveToolbar', + }, + { + name: 'ThresholdRange', + type: 'double-range', + id: 'threshold-range', + min: 0, + max: 100, + step: 0.5, + value: [2, 50], + commands: { + commandName: 'setThresholdRange', + commandOptions: { + toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'], + }, + }, + }, + ], + }, + ], + }, + }, + { + id: 'Shapes', + uiType: 'ohif.toolBoxButton', + props: { + label: 'Shapes', + evaluate: { + name: 'evaluate.cornerstone.segmentation', + toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'], + }, + icon: 'icon-tool-shape', + options: [ + { + name: 'Shape', + type: 'radio', + value: 'CircleScissor', + id: 'shape-mode', + values: [ + { value: 'CircleScissor', label: 'Circle' }, + { value: 'SphereScissor', label: 'Sphere' }, + { value: 'RectangleScissor', label: 'Rectangle' }, + ], + commands: 'setToolActiveToolbar', + }, + ], + }, + }, +]; + +export default toolbarButtons; diff --git a/modes/preclinical-4d/src/toolbarButtons.tsx b/modes/preclinical-4d/src/toolbarButtons.tsx new file mode 100644 index 0000000..4d8632a --- /dev/null +++ b/modes/preclinical-4d/src/toolbarButtons.tsx @@ -0,0 +1,166 @@ +import { defaults, ToolbarService } from '@ohif/core'; +import { toolGroupIds } from './initToolGroups'; + +const { createButton } = ToolbarService; + +const setToolActiveToolbar = { + commandName: 'setToolActiveToolbar', + commandOptions: { + toolGroupIds: [toolGroupIds.PT, toolGroupIds.CT, toolGroupIds.Fusion, toolGroupIds.default], + }, +}; + +const toolbarButtons = [ + { + id: 'MeasurementTools', + uiType: 'ohif.toolButtonList', + props: { + groupId: 'MeasurementTools', + evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList', + primary: createButton({ + id: 'Length', + icon: 'tool-length', + label: 'Length', + tooltip: 'Length Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + secondary: { + icon: 'chevron-down', + tooltip: 'More Measure Tools', + }, + items: [ + { + id: 'Length', + icon: 'tool-length', + label: 'Length', + tooltip: 'Length Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + { + id: 'Bidirectional', + icon: 'tool-bidirectional', + label: 'Bidirectional', + tooltip: 'Bidirectional Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + { + id: 'ArrowAnnotate', + icon: 'tool-annotate', + label: 'Annotation', + tooltip: 'Arrow Annotate', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + { + id: 'EllipticalROI', + icon: 'tool-ellipse', + label: 'Ellipse', + tooltip: 'Ellipse ROI', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + ], + }, + }, + { + id: 'Zoom', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-zoom', + label: 'Zoom', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + { + id: 'WindowLevel', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-window-level', + label: 'Window Level', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + { + id: 'Pan', + uiType: 'ohif.toolButton', + props: { + type: 'tool', + icon: 'tool-move', + label: 'Pan', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + { + id: 'TrackballRotate', + uiType: 'ohif.toolButton', + props: { + type: 'tool', + icon: 'tool-3d-rotate', + label: '3D Rotate', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + { + id: 'Capture', + uiType: 'ohif.radioGroup', + props: { + icon: 'tool-capture', + label: 'Capture', + commands: 'showDownloadViewportModal', + evaluate: [ + 'evaluate.action', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video', 'wholeSlide'], + }, + ], + }, + }, + { + id: 'Layout', + uiType: 'ohif.layoutSelector', + props: { + rows: 3, + columns: 4, + evaluate: 'evaluate.action', + }, + }, + { + id: 'Crosshairs', + uiType: 'ohif.toolButton', + props: { + type: 'tool', + icon: 'tool-crosshair', + label: 'Crosshairs', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + { + id: 'ProgressDropdown', + uiType: 'ohif.progressDropdown', + }, + { + id: 'RectangleROIStartEndThreshold', + uiType: 'ohif.radioGroup', + props: { + icon: 'tool-create-threshold', + label: 'Rectangle ROI Threshold', + commands: setToolActiveToolbar, + evaluate: { + name: 'evaluate.cornerstone.segmentation', + toolNames: ['RectangleROIStartEndThreshold'], + }, + options: 'tmtv.RectangleROIThresholdOptions', + }, + }, +]; + +export default toolbarButtons; diff --git a/modes/segmentation/.gitignore b/modes/segmentation/.gitignore new file mode 100644 index 0000000..6704566 --- /dev/null +++ b/modes/segmentation/.gitignore @@ -0,0 +1,104 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/modes/segmentation/.prettierrc b/modes/segmentation/.prettierrc new file mode 100644 index 0000000..28448ab --- /dev/null +++ b/modes/segmentation/.prettierrc @@ -0,0 +1,11 @@ +{ + "trailingComma": "es5", + "printWidth": 100, + "proseWrap": "always", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "arrowParens": "avoid", + "singleAttributePerLine": true, + "endOfLine": "auto" +} diff --git a/modes/segmentation/.webpack/webpack.prod.js b/modes/segmentation/.webpack/webpack.prod.js new file mode 100644 index 0000000..95d7bfe --- /dev/null +++ b/modes/segmentation/.webpack/webpack.prod.js @@ -0,0 +1,94 @@ +const path = require('path'); +const pkg = require('../package.json'); + +const outputFile = 'index.umd.js'; +const rootDir = path.resolve(__dirname, '../'); +const outputFolder = path.join(__dirname, `../dist/umd/${pkg.name}/`); + +// Todo: add ESM build for the mode in addition to umd build +const config = { + mode: 'production', + entry: rootDir + '/' + pkg.module, + devtool: 'source-map', + output: { + path: outputFolder, + filename: outputFile, + library: pkg.name, + libraryTarget: 'umd', + chunkFilename: '[name].chunk.js', + umdNamedDefine: true, + globalObject: "typeof self !== 'undefined' ? self : this", + }, + externals: [ + { + react: { + root: 'React', + commonjs2: 'react', + commonjs: 'react', + amd: 'react', + }, + '@ohif/core': { + commonjs2: '@ohif/core', + commonjs: '@ohif/core', + amd: '@ohif/core', + root: '@ohif/core', + }, + '@ohif/ui': { + commonjs2: '@ohif/ui', + commonjs: '@ohif/ui', + amd: '@ohif/ui', + root: '@ohif/ui', + }, + }, + ], + module: { + rules: [ + { + test: /(\.jsx|\.js|\.tsx|\.ts)$/, + loader: 'babel-loader', + exclude: /(node_modules|bower_components)/, + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + }, + { + test: /\.svg?$/, + oneOf: [ + { + use: [ + { + loader: '@svgr/webpack', + options: { + svgoConfig: { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false + }, + }, + }, + ] + }, + prettier: false, + svgo: true, + titleProp: true, + }, + }, + ], + issuer: { + and: [/\.(ts|tsx|js|jsx|md|mdx)$/], + }, + }, + ], + }, + ], + }, + resolve: { + modules: [path.resolve('./node_modules'), path.resolve('./src')], + extensions: ['.json', '.js', '.jsx', '.tsx', '.ts'], + }, +}; + +module.exports = config; diff --git a/modes/segmentation/CHANGELOG.md b/modes/segmentation/CHANGELOG.md new file mode 100644 index 0000000..305936e --- /dev/null +++ b/modes/segmentation/CHANGELOG.md @@ -0,0 +1,2908 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + + +### Features + +* **hotkeys:** Migrate hotkeys to customization service and fix issues with overrides ([#4777](https://github.com/OHIF/Viewers/issues/4777)) ([3e6913b](https://github.com/OHIF/Viewers/commit/3e6913b097569280a5cc2fa5bbe4add52f149305)) + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + + +### Bug Fixes + +* right panel for the create mode cli command ([#4788](https://github.com/OHIF/Viewers/issues/4788)) ([5712e91](https://github.com/OHIF/Viewers/commit/5712e91ca1d939ff3c36615d3cf1a1f6f0051c4f)) + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + + +### Features + +* **panels:** responsive thumbnails based on panel size ([#4723](https://github.com/OHIF/Viewers/issues/4723)) ([d9abc3d](https://github.com/OHIF/Viewers/commit/d9abc3da8d94d6c5ab0cc5af25a5f61849905a35)) + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + + +### Features + +* **dev:** move to rsbuild for dev - faster ([#4674](https://github.com/OHIF/Viewers/issues/4674)) ([d4a4267](https://github.com/OHIF/Viewers/commit/d4a4267429c02916dd51f6aefb290d96dd1c3b04)) + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + + +### Bug Fixes + +* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30)) + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + + +### Bug Fixes + +* Re-enable hpScale module ([#4237](https://github.com/OHIF/Viewers/issues/4237)) ([2eab049](https://github.com/OHIF/Viewers/commit/2eab049d7993bb834f7736093941c175f16d61fc)) + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + + +### Bug Fixes + +* **window-level:** move window level region to more tools menu ([#4215](https://github.com/OHIF/Viewers/issues/4215)) ([33f4c18](https://github.com/OHIF/Viewers/commit/33f4c18f2687d30a250fe7183df3daae8394a984)) + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + + +### Bug Fixes + +* **seg:** maintain algorithm name and algorithm type when DICOM seg is exported or downloaded ([#4203](https://github.com/OHIF/Viewers/issues/4203)) ([a29e94d](https://github.com/OHIF/Viewers/commit/a29e94de803f79bbb3372d00ad8eb14b4224edc2)) + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + + +### Features + +* **window-level-region:** add window level region tool ([#4127](https://github.com/OHIF/Viewers/issues/4127)) ([ab1a18a](https://github.com/OHIF/Viewers/commit/ab1a18af5a5b0f9086c080ed81c8fda9bfaa975b)) + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + + +### Bug Fixes + +* **tmtv:** crosshairs should not have viewport indicators ([#4197](https://github.com/OHIF/Viewers/issues/4197)) ([f85da32](https://github.com/OHIF/Viewers/commit/f85da32f34389ef7cecae03c07e0af26468b52a6)) + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + + +### Features + +* **hp:** Add displayArea option for Hanging protocols and example with Mamo([#3808](https://github.com/OHIF/Viewers/issues/3808)) ([18ac08e](https://github.com/OHIF/Viewers/commit/18ac08ed860d119721c52e4ffc270332259100b6)) + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + + +### Bug Fixes + +* **crosshairs:** reset angle, position, and slabthickness for crosshairs when reset viewport tool is used ([#4113](https://github.com/OHIF/Viewers/issues/4113)) ([73d9e99](https://github.com/OHIF/Viewers/commit/73d9e99d5d6f38ab6c36f4471d54f18798feacb4)) + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + + +### Bug Fixes + +* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f)) + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + + +### Bug Fixes + +* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af)) + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + + +### Bug Fixes + +* **bugs:** and replace seriesInstanceUID and seriesInstanceUIDs URL with seriesInstanceUIDs ([#4049](https://github.com/OHIF/Viewers/issues/4049)) ([da7c1a5](https://github.com/OHIF/Viewers/commit/da7c1a5d8c54bfa1d3f97bbc500386bf76e7fd9d)) + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + + +### Bug Fixes + +* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025)) + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + + +### Bug Fixes + +* **general:** enhancements and bug fixes ([#4018](https://github.com/OHIF/Viewers/issues/4018)) ([2b83393](https://github.com/OHIF/Viewers/commit/2b83393f91cb16ea06821d79d14ff60f80c29c90)) + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + + +### Features + +* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2)) + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + + +### Features + +* **advanced-roi-tools:** new tools and icon updates and overlay bug fixes ([#4014](https://github.com/OHIF/Viewers/issues/4014)) ([cea27d4](https://github.com/OHIF/Viewers/commit/cea27d438d1de2c1ec90cbaefdc2b31a1d9980a1)) + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + + +### Features + +* **segmentation:** Enhanced segmentation panel design for TMTV ([#3988](https://github.com/OHIF/Viewers/issues/3988)) ([9f3235f](https://github.com/OHIF/Viewers/commit/9f3235ff096636aafa88d8a42859e8dc85d9036d)) + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + + +### Features + +* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1)) + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + + +### Features + +* **layout:** new layout selector with 3D volume rendering ([#3923](https://github.com/OHIF/Viewers/issues/3923)) ([617043f](https://github.com/OHIF/Viewers/commit/617043fe0da5de91fbea4ac33a27f1df16ae1ca6)) + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + + +### Features + +* **worklist:** New worklist buttons and tooltips ([#3989](https://github.com/OHIF/Viewers/issues/3989)) ([9bcd1ae](https://github.com/OHIF/Viewers/commit/9bcd1ae6f51d61786cc1e99624f396b56a47cd69)) + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + + +### Bug Fixes + +* **viewport-sync:** remember synced viewports bw stack and volume and RENAME StackImageSync to ImageSliceSync ([#3849](https://github.com/OHIF/Viewers/issues/3849)) ([e4a116b](https://github.com/OHIF/Viewers/commit/e4a116b074fcb85c8cbcc9db44fdec565f3386db)) + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + + +### Bug Fixes + +* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6)) + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + + +### Bug Fixes + +* address and improve system vulnerabilities ([#3851](https://github.com/OHIF/Viewers/issues/3851)) ([805c532](https://github.com/OHIF/Viewers/commit/805c53270f243ec61f142a3ffa0af500021cd5ec)) + + +### Features + +* **config:** Add activateViewportBeforeInteraction parameter for viewport interaction customization ([#3847](https://github.com/OHIF/Viewers/issues/3847)) ([f707b4e](https://github.com/OHIF/Viewers/commit/f707b4ebc996f379cd30337badc06b07e6e35ac5)) +* **i18n:** enhanced i18n support ([#3761](https://github.com/OHIF/Viewers/issues/3761)) ([d14a8f0](https://github.com/OHIF/Viewers/commit/d14a8f0199db95cd9e85866a011b64d6bf830d57)) + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + + +### Bug Fixes + +* **SM:** drag and drop is now fixed for SM ([#3813](https://github.com/OHIF/Viewers/issues/3813)) ([f1a6764](https://github.com/OHIF/Viewers/commit/f1a67647aed635437b188cea7cf5d5a8fb974bbe)) + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + + +### Bug Fixes + +* **modules:** add stylus loader as an option to be uncommented ([#3710](https://github.com/OHIF/Viewers/issues/3710)) ([7c57f67](https://github.com/OHIF/Viewers/commit/7c57f67844b790fc6e47ac3f9708bf9d576389c8)) + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-segmentation + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) diff --git a/modes/segmentation/LICENSE b/modes/segmentation/LICENSE new file mode 100644 index 0000000..c58f059 --- /dev/null +++ b/modes/segmentation/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2023 @ohif-segmentation-mode (contact@ohif.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/modes/segmentation/README.md b/modes/segmentation/README.md new file mode 100644 index 0000000..5bf905d --- /dev/null +++ b/modes/segmentation/README.md @@ -0,0 +1,7 @@ +# @ohif-segmentation-mode +## Description +OHIF segmentation mode which enables labelmap segmentation read/edit/export +## Author +@ohif +## License +MIT \ No newline at end of file diff --git a/modes/segmentation/babel.config.js b/modes/segmentation/babel.config.js new file mode 100644 index 0000000..a35080a --- /dev/null +++ b/modes/segmentation/babel.config.js @@ -0,0 +1,43 @@ +module.exports = { + plugins: ['@babel/plugin-proposal-class-properties'], + env: { + test: { + presets: [ + [ + // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2 + '@babel/preset-env', + { + modules: 'commonjs', + debug: false, + }, + '@babel/preset-typescript', + ], + '@babel/preset-react', + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-regenerator', + '@babel/plugin-transform-runtime', + ], + }, + production: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + development: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + }, +}; diff --git a/modes/segmentation/package.json b/modes/segmentation/package.json new file mode 100644 index 0000000..0ab0ba1 --- /dev/null +++ b/modes/segmentation/package.json @@ -0,0 +1,77 @@ +{ + "name": "@ohif/mode-segmentation", + "version": "3.10.0-beta.111", + "description": "OHIF segmentation mode which enables labelmap segmentation read/edit/export", + "author": "@ohif", + "license": "MIT", + "main": "dist/umd/@ohif/mode-segmentation/index.umd.js", + "files": [ + "dist/**", + "public/**", + "README.md" + ], + "repository": "OHIF/Viewers", + "keywords": [ + "ohif-mode" + ], + "publishConfig": { + "access": "public" + }, + "module": "src/index.tsx", + "engines": { + "node": ">=14", + "npm": ">=7", + "yarn": ">=1.16.0" + }, + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:cornerstone": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.111", + "@ohif/extension-cornerstone": "3.10.0-beta.111", + "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.111", + "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.111", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.111", + "@ohif/extension-default": "3.10.0-beta.111", + "@ohif/extension-dicom-pdf": "3.10.0-beta.111", + "@ohif/extension-dicom-video": "3.10.0-beta.111" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "i18next": "^17.0.3" + }, + "devDependencies": { + "@babel/core": "7.24.7", + "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-proposal-object-rest-spread": "^7.17.3", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.16.7", + "@babel/plugin-transform-regenerator": "^7.16.7", + "@babel/plugin-transform-runtime": "7.24.7", + "@babel/plugin-transform-typescript": "^7.13.0", + "@babel/preset-env": "7.23.2", + "@babel/preset-react": "^7.16.7", + "@babel/preset-typescript": "^7.13.0", + "@svgr/webpack": "^8.1.0", + "babel-eslint": "^10.1.0", + "babel-loader": "^8.0.0-beta.4", + "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^10.2.0", + "cross-env": "^7.0.3", + "dotenv": "^14.1.0", + "eslint": "^8.39.0", + "eslint-loader": "^2.0.0", + "webpack": "5.94.0", + "webpack-cli": "^4.7.2", + "webpack-merge": "^5.7.3" + } +} diff --git a/modes/segmentation/src/id.js b/modes/segmentation/src/id.js new file mode 100644 index 0000000..ebe5acd --- /dev/null +++ b/modes/segmentation/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/modes/segmentation/src/index.tsx b/modes/segmentation/src/index.tsx new file mode 100644 index 0000000..80e7ce2 --- /dev/null +++ b/modes/segmentation/src/index.tsx @@ -0,0 +1,170 @@ +import { id } from './id'; +import toolbarButtons from './toolbarButtons'; +import segmentationButtons from './segmentationButtons'; +import initToolGroups from './initToolGroups'; + +const ohif = { + layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', + sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', + hangingProtocol: '@ohif/extension-default.hangingProtocolModule.default', + leftPanel: '@ohif/extension-default.panelModule.seriesList', +}; + +const cornerstone = { + viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', + panelTool: '@ohif/extension-cornerstone.panelModule.panelSegmentationWithTools', +}; + +const segmentation = { + sopClassHandler: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg', + viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg', +}; + +/** + * Just two dependencies to be able to render a viewport with panels in order + * to make sure that the mode is working. + */ +const extensionDependencies = { + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', + '@ohif/extension-cornerstone-dicom-seg': '^3.0.0', +}; + +function modeFactory({ modeConfiguration }) { + return { + /** + * Mode ID, which should be unique among modes used by the viewer. This ID + * is used to identify the mode in the viewer's state. + */ + id, + routeName: 'segmentation', + /** + * Mode name, which is displayed in the viewer's UI in the workList, for the + * user to select the mode. + */ + displayName: 'Segmentation', + /** + * Runs when the Mode Route is mounted to the DOM. Usually used to initialize + * Services and other resources. + */ + onModeEnter: ({ servicesManager, extensionManager, commandsManager }: withAppTypes) => { + const { measurementService, toolbarService, toolGroupService, customizationService } = + servicesManager.services; + + measurementService.clearMeasurements(); + + // Init Default and SR ToolGroups + initToolGroups(extensionManager, toolGroupService, commandsManager); + + toolbarService.addButtons(toolbarButtons); + toolbarService.addButtons(segmentationButtons); + + toolbarService.createButtonSection('primary', [ + 'WindowLevel', + 'Pan', + 'Zoom', + 'TrackballRotate', + 'Capture', + 'Layout', + 'Crosshairs', + 'MoreTools', + ]); + toolbarService.createButtonSection('segmentationToolbox', ['BrushTools', 'Shapes']); + }, + onModeExit: ({ servicesManager }: withAppTypes) => { + const { + toolGroupService, + syncGroupService, + segmentationService, + cornerstoneViewportService, + uiDialogService, + uiModalService, + } = servicesManager.services; + + uiDialogService.dismissAll(); + uiModalService.hide(); + toolGroupService.destroy(); + syncGroupService.destroy(); + segmentationService.destroy(); + cornerstoneViewportService.destroy(); + }, + /** */ + validationTags: { + study: [], + series: [], + }, + /** + * A boolean return value that indicates whether the mode is valid for the + * modalities of the selected studies. Currently we don't have stack viewport + * segmentations and we should exclude them + */ + isValidMode: ({ modalities }) => { + // Don't show the mode if the selected studies have only one modality + // that is not supported by the mode + const modalitiesArray = modalities.split('\\'); + return { + valid: + modalitiesArray.length === 1 + ? !['SM', 'ECG', 'OT', 'DOC'].includes(modalitiesArray[0]) + : true, + description: + 'The mode does not support studies that ONLY include the following modalities: SM, OT, DOC', + }; + }, + /** + * Mode Routes are used to define the mode's behavior. A list of Mode Route + * that includes the mode's path and the layout to be used. The layout will + * include the components that are used in the layout. For instance, if the + * default layoutTemplate is used (id: '@ohif/extension-default.layoutTemplateModule.viewerLayout') + * it will include the leftPanels, rightPanels, and viewports. However, if + * you define another layoutTemplate that includes a Footer for instance, + * you should provide the Footer component here too. Note: We use Strings + * to reference the component's ID as they are registered in the internal + * ExtensionManager. The template for the string is: + * `${extensionId}.{moduleType}.${componentId}`. + */ + routes: [ + { + path: 'template', + layoutTemplate: ({ location, servicesManager }) => { + return { + id: ohif.layout, + props: { + leftPanels: [ohif.leftPanel], + leftPanelResizable: true, + rightPanels: [cornerstone.panelTool], + rightPanelResizable: true, + // leftPanelClosed: true, + viewports: [ + { + namespace: cornerstone.viewport, + displaySetsToDisplay: [ohif.sopClassHandler], + }, + { + namespace: segmentation.viewport, + displaySetsToDisplay: [segmentation.sopClassHandler], + }, + ], + }, + }; + }, + }, + ], + /** List of extensions that are used by the mode */ + extensions: extensionDependencies, + /** HangingProtocol used by the mode */ + // Commented out to just use the most applicable registered hanging protocol + // The example is used for a grid layout to specify that as a preferred layout + // hangingProtocol: ['@ohif/mnGrid'], + /** SopClassHandlers used by the mode */ + sopClassHandlers: [ohif.sopClassHandler, segmentation.sopClassHandler], + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; diff --git a/modes/segmentation/src/initToolGroups.ts b/modes/segmentation/src/initToolGroups.ts new file mode 100644 index 0000000..387a4b0 --- /dev/null +++ b/modes/segmentation/src/initToolGroups.ts @@ -0,0 +1,181 @@ +const colours = { + 'viewport-0': 'rgb(200, 0, 0)', + 'viewport-1': 'rgb(200, 200, 0)', + 'viewport-2': 'rgb(0, 200, 0)', +}; + +const colorsByOrientation = { + axial: 'rgb(200, 0, 0)', + sagittal: 'rgb(200, 200, 0)', + coronal: 'rgb(0, 200, 0)', +}; + +function createTools(utilityModule) { + const { toolNames, Enums } = utilityModule.exports; + return { + active: [ + { toolName: toolNames.WindowLevel, bindings: [{ mouseButton: Enums.MouseBindings.Primary }] }, + { toolName: toolNames.Pan, bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }] }, + { toolName: toolNames.Zoom, bindings: [{ mouseButton: Enums.MouseBindings.Secondary }] }, + { toolName: toolNames.StackScroll, bindings: [{ mouseButton: Enums.MouseBindings.Wheel }] }, + ], + passive: [ + { + toolName: 'CircularBrush', + parentTool: 'Brush', + configuration: { + activeStrategy: 'FILL_INSIDE_CIRCLE', + }, + }, + { + toolName: 'CircularEraser', + parentTool: 'Brush', + configuration: { + activeStrategy: 'ERASE_INSIDE_CIRCLE', + }, + }, + { + toolName: 'SphereBrush', + parentTool: 'Brush', + configuration: { + activeStrategy: 'FILL_INSIDE_SPHERE', + }, + }, + { + toolName: 'SphereEraser', + parentTool: 'Brush', + configuration: { + activeStrategy: 'ERASE_INSIDE_SPHERE', + }, + }, + { + toolName: 'ThresholdCircularBrush', + parentTool: 'Brush', + configuration: { + activeStrategy: 'THRESHOLD_INSIDE_CIRCLE', + }, + }, + { + toolName: 'ThresholdSphereBrush', + parentTool: 'Brush', + configuration: { + activeStrategy: 'THRESHOLD_INSIDE_SPHERE', + }, + }, + { + toolName: 'ThresholdCircularBrushDynamic', + parentTool: 'Brush', + configuration: { + activeStrategy: 'THRESHOLD_INSIDE_CIRCLE', + // preview: { + // enabled: true, + // }, + strategySpecificConfiguration: { + // to use the use the center segment index to determine + // if inside -> same segment, if outside -> eraser + // useCenterSegmentIndex: true, + THRESHOLD: { + isDynamic: true, + dynamicRadius: 3, + }, + }, + }, + }, + { toolName: toolNames.CircleScissors }, + { toolName: toolNames.RectangleScissors }, + { toolName: toolNames.SphereScissors }, + { toolName: toolNames.StackScroll }, + { toolName: toolNames.Magnify }, + { toolName: toolNames.WindowLevelRegion }, + + { toolName: toolNames.UltrasoundDirectional }, + ], + disabled: [{ toolName: toolNames.ReferenceLines }, { toolName: toolNames.AdvancedMagnify }], + }; +} + +function initDefaultToolGroup(extensionManager, toolGroupService, commandsManager, toolGroupId) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + const tools = createTools(utilityModule); + toolGroupService.createToolGroupAndAddTools(toolGroupId, tools); +} + +function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + const servicesManager = extensionManager._servicesManager; + const { cornerstoneViewportService } = servicesManager.services; + const tools = createTools(utilityModule); + tools.disabled.push( + { + toolName: utilityModule.exports.toolNames.Crosshairs, + configuration: { + viewportIndicators: true, + viewportIndicatorsConfig: { + circleRadius: 5, + xOffset: 0.95, + yOffset: 0.05, + }, + disableOnPassive: true, + autoPan: { + enabled: false, + panSize: 10, + }, + getReferenceLineColor: viewportId => { + const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); + const viewportOptions = viewportInfo?.viewportOptions; + if (viewportOptions) { + return ( + colours[viewportOptions.id] || + colorsByOrientation[viewportOptions.orientation] || + '#0c0' + ); + } else { + console.warn('missing viewport?', viewportId); + return '#0c0'; + } + }, + }, + }, + { toolName: utilityModule.exports.toolNames.ReferenceLines } + ); + toolGroupService.createToolGroupAndAddTools('mpr', tools); +} + +function initVolume3DToolGroup(extensionManager, toolGroupService) { + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + + const { toolNames, Enums } = utilityModule.exports; + + const tools = { + active: [ + { + toolName: toolNames.TrackballRotateTool, + bindings: [{ mouseButton: Enums.MouseBindings.Primary }], + }, + { + toolName: toolNames.Zoom, + bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], + }, + { + toolName: toolNames.Pan, + bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }], + }, + ], + }; + + toolGroupService.createToolGroupAndAddTools('volume3d', tools); +} + +function initToolGroups(extensionManager, toolGroupService, commandsManager) { + initDefaultToolGroup(extensionManager, toolGroupService, commandsManager, 'default'); + initMPRToolGroup(extensionManager, toolGroupService, commandsManager); + initVolume3DToolGroup(extensionManager, toolGroupService); +} + +export default initToolGroups; diff --git a/modes/segmentation/src/segmentationButtons.ts b/modes/segmentation/src/segmentationButtons.ts new file mode 100644 index 0000000..103a2e3 --- /dev/null +++ b/modes/segmentation/src/segmentationButtons.ts @@ -0,0 +1,204 @@ +import type { Button } from '@ohif/core/types'; + +const toolbarButtons: Button[] = [ + { + id: 'BrushTools', + uiType: 'ohif.toolBoxButtonGroup', + props: { + groupId: 'BrushTools', + evaluate: 'evaluate.cornerstone.hasSegmentation', + items: [ + { + id: 'Brush', + icon: 'icon-tool-brush', + label: 'Brush', + evaluate: { + name: 'evaluate.cornerstone.segmentation', + toolNames: ['CircularBrush', 'SphereBrush'], + disabledText: 'Create new segmentation to enable this tool.', + }, + options: [ + { + name: 'Radius (mm)', + id: 'brush-radius', + type: 'range', + min: 0.5, + max: 99.5, + step: 0.5, + value: 25, + commands: { + commandName: 'setBrushSize', + commandOptions: { toolNames: ['CircularBrush', 'SphereBrush'] }, + }, + }, + { + name: 'Shape', + type: 'radio', + id: 'brush-mode', + value: 'CircularBrush', + values: [ + { value: 'CircularBrush', label: 'Circle' }, + { value: 'SphereBrush', label: 'Sphere' }, + ], + commands: 'setToolActiveToolbar', + }, + ], + }, + { + id: 'Eraser', + icon: 'icon-tool-eraser', + label: 'Eraser', + evaluate: { + name: 'evaluate.cornerstone.segmentation', + toolNames: ['CircularEraser', 'SphereEraser'], + }, + options: [ + { + name: 'Radius (mm)', + id: 'eraser-radius', + type: 'range', + min: 0.5, + max: 99.5, + step: 0.5, + value: 25, + commands: { + commandName: 'setBrushSize', + commandOptions: { toolNames: ['CircularEraser', 'SphereEraser'] }, + }, + }, + { + name: 'Shape', + type: 'radio', + id: 'eraser-mode', + value: 'CircularEraser', + values: [ + { value: 'CircularEraser', label: 'Circle' }, + { value: 'SphereEraser', label: 'Sphere' }, + ], + commands: 'setToolActiveToolbar', + }, + ], + }, + { + id: 'Threshold', + icon: 'icon-tool-threshold', + label: 'Threshold Tool', + evaluate: { + name: 'evaluate.cornerstone.segmentation', + toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'], + }, + options: [ + { + name: 'Radius (mm)', + id: 'threshold-radius', + type: 'range', + min: 0.5, + max: 99.5, + step: 0.5, + value: 25, + commands: { + commandName: 'setBrushSize', + commandOptions: { + toolNames: [ + 'ThresholdCircularBrush', + 'ThresholdSphereBrush', + 'ThresholdCircularBrushDynamic', + ], + }, + }, + }, + + { + name: 'Threshold', + type: 'radio', + id: 'dynamic-mode', + value: 'ThresholdRange', + values: [ + { value: 'ThresholdDynamic', label: 'Dynamic' }, + { value: 'ThresholdRange', label: 'Range' }, + ], + commands: ({ value, commandsManager, options }) => { + if (value === 'ThresholdDynamic') { + commandsManager.run('setToolActive', { + toolName: 'ThresholdCircularBrushDynamic', + }); + + return; + } + + // check the condition of the threshold-range option + const thresholdRangeOption = options.find( + option => option.id === 'threshold-shape' + ); + + commandsManager.run('setToolActiveToolbar', { + toolName: thresholdRangeOption.value, + }); + }, + }, + { + name: 'Shape', + type: 'radio', + id: 'threshold-shape', + value: 'ThresholdCircularBrush', + values: [ + { value: 'ThresholdCircularBrush', label: 'Circle' }, + { value: 'ThresholdSphereBrush', label: 'Sphere' }, + ], + condition: ({ options }) => + options.find(option => option.id === 'dynamic-mode').value === 'ThresholdRange', + commands: 'setToolActiveToolbar', + }, + { + name: 'ThresholdRange', + type: 'double-range', + id: 'threshold-range', + min: -1000, + max: 1000, + step: 1, + value: [100, 600], + condition: ({ options }) => + options.find(option => option.id === 'dynamic-mode').value === 'ThresholdRange', + commands: { + commandName: 'setThresholdRange', + commandOptions: { + toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'], + }, + }, + }, + ], + }, + ], + }, + }, + { + id: 'Shapes', + uiType: 'ohif.toolBoxButton', + props: { + id: 'Shapes', + icon: 'icon-tool-shape', + label: 'Shapes', + evaluate: { + name: 'evaluate.cornerstone.segmentation', + toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'], + disabledText: 'Create new segmentation to enable shapes tool.', + }, + options: [ + { + name: 'Shape', + type: 'radio', + value: 'CircleScissor', + id: 'shape-mode', + values: [ + { value: 'CircleScissor', label: 'Circle' }, + { value: 'SphereScissor', label: 'Sphere' }, + { value: 'RectangleScissor', label: 'Rectangle' }, + ], + commands: 'setToolActiveToolbar', + }, + ], + }, + }, +]; + +export default toolbarButtons; diff --git a/modes/segmentation/src/toolbarButtons.ts b/modes/segmentation/src/toolbarButtons.ts new file mode 100644 index 0000000..f03eefe --- /dev/null +++ b/modes/segmentation/src/toolbarButtons.ts @@ -0,0 +1,259 @@ +import type { Button } from '@ohif/core/types'; +import { ToolbarService, ViewportGridService } from '@ohif/core'; + +const { createButton } = ToolbarService; + +const ReferenceLinesListeners: RunCommand = [ + { + commandName: 'setSourceViewportForReferenceLinesTool', + context: 'CORNERSTONE', + }, +]; + +export const setToolActiveToolbar = { + commandName: 'setToolActiveToolbar', + commandOptions: { + toolGroupIds: ['default', 'mpr', 'SRToolGroup', 'volume3d'], + }, +}; + +const toolbarButtons: Button[] = [ + { + id: 'Zoom', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-zoom', + label: 'Zoom', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + { + id: 'WindowLevel', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-window-level', + label: 'Window Level', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + { + id: 'Pan', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-move', + label: 'Pan', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + { + id: 'TrackballRotate', + uiType: 'ohif.toolButton', + props: { + type: 'tool', + icon: 'tool-3d-rotate', + label: '3D Rotate', + commands: setToolActiveToolbar, + evaluate: { + name: 'evaluate.cornerstoneTool', + disabledText: 'Select a 3D viewport to enable this tool', + }, + }, + }, + { + id: 'Capture', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-capture', + label: 'Capture', + commands: 'showDownloadViewportModal', + evaluate: [ + 'evaluate.action', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video', 'wholeSlide'], + }, + ], + }, + }, + { + id: 'Layout', + uiType: 'ohif.layoutSelector', + props: { + rows: 3, + columns: 4, + evaluate: 'evaluate.action', + commands: 'setViewportGridLayout', + }, + }, + { + id: 'Crosshairs', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-crosshair', + label: 'Crosshairs', + commands: { + commandName: 'setToolActiveToolbar', + commandOptions: { + toolGroupIds: ['mpr'], + }, + }, + evaluate: { + name: 'evaluate.cornerstoneTool', + disabledText: 'Select an MPR viewport to enable this tool', + }, + }, + }, + { + id: 'MoreTools', + uiType: 'ohif.toolButtonList', + props: { + groupId: 'MoreTools', + evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList', + primary: createButton({ + id: 'Reset', + icon: 'tool-reset', + tooltip: 'Reset View', + label: 'Reset', + commands: 'resetViewport', + evaluate: 'evaluate.action', + }), + secondary: { + icon: 'chevron-down', + label: '', + tooltip: 'More Tools', + }, + items: [ + createButton({ + id: 'Reset', + icon: 'tool-reset', + label: 'Reset View', + tooltip: 'Reset View', + commands: 'resetViewport', + evaluate: 'evaluate.action', + }), + createButton({ + id: 'rotate-right', + icon: 'tool-rotate-right', + label: 'Rotate Right', + tooltip: 'Rotate +90', + commands: 'rotateViewportCW', + evaluate: 'evaluate.action', + }), + createButton({ + id: 'flipHorizontal', + icon: 'tool-flip-horizontal', + label: 'Flip Horizontal', + tooltip: 'Flip Horizontally', + commands: 'flipViewportHorizontal', + evaluate: [ + 'evaluate.viewportProperties.toggle', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d'], + }, + ], + }), + createButton({ + id: 'ReferenceLines', + icon: 'tool-referenceLines', + label: 'Reference Lines', + tooltip: 'Show Reference Lines', + commands: 'toggleEnabledDisabledToolbar', + listeners: { + [ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners, + [ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners, + }, + evaluate: 'evaluate.cornerstoneTool.toggle', + }), + createButton({ + id: 'ImageOverlayViewer', + icon: 'toggle-dicom-overlay', + label: 'Image Overlay', + tooltip: 'Toggle Image Overlay', + commands: 'toggleEnabledDisabledToolbar', + evaluate: 'evaluate.cornerstoneTool.toggle', + }), + createButton({ + id: 'StackScroll', + icon: 'tool-stack-scroll', + label: 'Stack Scroll', + tooltip: 'Stack Scroll', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'invert', + icon: 'tool-invert', + label: 'Invert', + tooltip: 'Invert Colors', + commands: 'invertViewport', + evaluate: 'evaluate.viewportProperties.toggle', + }), + createButton({ + id: 'Cine', + icon: 'tool-cine', + label: 'Cine', + tooltip: 'Cine', + commands: 'toggleCine', + evaluate: [ + 'evaluate.cine', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d'], + }, + ], + }), + createButton({ + id: 'Magnify', + icon: 'tool-magnify', + label: 'Zoom-in', + tooltip: 'Zoom-in', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + createButton({ + id: 'TagBrowser', + icon: 'dicom-tag-browser', + label: 'Dicom Tag Browser', + tooltip: 'Dicom Tag Browser', + commands: 'openDICOMTagViewer', + }), + createButton({ + id: 'AdvancedMagnify', + icon: 'icon-tool-loupe', + label: 'Magnify Probe', + tooltip: 'Magnify Probe', + commands: 'toggleActiveDisabledToolbar', + evaluate: 'evaluate.cornerstoneTool.toggle.ifStrictlyDisabled', + }), + createButton({ + id: 'UltrasoundDirectionalTool', + icon: 'icon-tool-ultrasound-bidirectional', + label: 'Ultrasound Directional', + tooltip: 'Ultrasound Directional', + commands: setToolActiveToolbar, + evaluate: [ + 'evaluate.cornerstoneTool', + { + name: 'evaluate.modality.supported', + supportedModalities: ['US'], + }, + ], + }), + createButton({ + id: 'WindowLevelRegion', + icon: 'icon-tool-window-region', + label: 'Window Level Region', + tooltip: 'Window Level Region', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + ], + }, + }, +]; + +export default toolbarButtons; diff --git a/modes/tmtv/.webpack/webpack.dev.js b/modes/tmtv/.webpack/webpack.dev.js new file mode 100644 index 0000000..1b8e34c --- /dev/null +++ b/modes/tmtv/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.ts`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/modes/tmtv/.webpack/webpack.prod.js b/modes/tmtv/.webpack/webpack.prod.js new file mode 100644 index 0000000..f2edb43 --- /dev/null +++ b/modes/tmtv/.webpack/webpack.prod.js @@ -0,0 +1,54 @@ +const webpack = require('webpack'); +const { merge } = require('webpack-merge'); +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +const pkg = require('./../package.json'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); + +const ROOT_DIR = path.join(__dirname, './../'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.ts`, +}; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: false, + }, + output: { + path: ROOT_DIR, + library: 'ohif-mode-tmtv', + libraryTarget: 'umd', + libraryExport: 'default', + filename: pkg.main, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + // new MiniCssExtractPlugin({ + // filename: './dist/[name].css', + // chunkFilename: './dist/[id].css', + // }), + ], + }); +}; diff --git a/modes/tmtv/CHANGELOG.md b/modes/tmtv/CHANGELOG.md new file mode 100644 index 0000000..6514e3e --- /dev/null +++ b/modes/tmtv/CHANGELOG.md @@ -0,0 +1,3072 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + + +### Features + +* **hotkeys:** Migrate hotkeys to customization service and fix issues with overrides ([#4777](https://github.com/OHIF/Viewers/issues/4777)) ([3e6913b](https://github.com/OHIF/Viewers/commit/3e6913b097569280a5cc2fa5bbe4add52f149305)) + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + + +### Bug Fixes + +* **measurement label auto-completion:** Customization of measurement label auto-completion fails for measurements following arrow annotations. ([#4739](https://github.com/OHIF/Viewers/issues/4739)) ([e035ef1](https://github.com/OHIF/Viewers/commit/e035ef1dcc72ecbe2a757e3b814551d768d7e610)) + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + + +### Features + +* **customization:** new customization service api ([#4688](https://github.com/OHIF/Viewers/issues/4688)) ([55ad8ef](https://github.com/OHIF/Viewers/commit/55ad8efbabc3fabd8031fc08927b2f92ae5aec69)) + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + + +### Features + +* **panels:** responsive thumbnails based on panel size ([#4723](https://github.com/OHIF/Viewers/issues/4723)) ([d9abc3d](https://github.com/OHIF/Viewers/commit/d9abc3da8d94d6c5ab0cc5af25a5f61849905a35)) + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + + +### Bug Fixes + +* **multiframe:** metadata handling of NM studies and loading order ([#4554](https://github.com/OHIF/Viewers/issues/4554)) ([7624ccb](https://github.com/OHIF/Viewers/commit/7624ccb5e495c0a151227a458d8d5bfb8babb22c)) + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + + +### Features + +* **new-study-panel:** default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser ([#4417](https://github.com/OHIF/Viewers/issues/4417)) ([a7fd9fa](https://github.com/OHIF/Viewers/commit/a7fd9fa5bfff7a1b533d99cb96f7147a35fd528f)) + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + + +### Features + +* **studies-panel:** New OHIF study panel - under experimental flag ([#4254](https://github.com/OHIF/Viewers/issues/4254)) ([7a96406](https://github.com/OHIF/Viewers/commit/7a96406a116e46e62c396855fa64f434e2984b58)) + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + + +### Bug Fixes + +* **tmtv:** crosshairs should not have viewport indicators ([#4197](https://github.com/OHIF/Viewers/issues/4197)) ([f85da32](https://github.com/OHIF/Viewers/commit/f85da32f34389ef7cecae03c07e0af26468b52a6)) + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + + +### Features + +* **hp:** Add displayArea option for Hanging protocols and example with Mamo([#3808](https://github.com/OHIF/Viewers/issues/3808)) ([18ac08e](https://github.com/OHIF/Viewers/commit/18ac08ed860d119721c52e4ffc270332259100b6)) + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + + +### Bug Fixes + +* **crosshairs:** reset angle, position, and slabthickness for crosshairs when reset viewport tool is used ([#4113](https://github.com/OHIF/Viewers/issues/4113)) ([73d9e99](https://github.com/OHIF/Viewers/commit/73d9e99d5d6f38ab6c36f4471d54f18798feacb4)) + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + + +### Bug Fixes + +* **tmtv:** threshold was crashing the side panel ([#4119](https://github.com/OHIF/Viewers/issues/4119)) ([8d5c676](https://github.com/OHIF/Viewers/commit/8d5c676a5e1f3eda664071c8aece313de766bd59)) + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + + +### Bug Fixes + +* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f)) + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + + +### Features + +* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb)) + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + + +### Bug Fixes + +* **bugs:** and replace seriesInstanceUID and seriesInstanceUIDs URL with seriesInstanceUIDs ([#4049](https://github.com/OHIF/Viewers/issues/4049)) ([da7c1a5](https://github.com/OHIF/Viewers/commit/da7c1a5d8c54bfa1d3f97bbc500386bf76e7fd9d)) + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + + +### Bug Fixes + +* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025)) + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + + +### Features + +* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13)) + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + + +### Features + +* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2)) + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + + +### Features + +* **measurement:** Add support measurement label autocompletion ([#3855](https://github.com/OHIF/Viewers/issues/3855)) ([56b1eae](https://github.com/OHIF/Viewers/commit/56b1eae6356a6534960df1196bdd1e95b0a9a470)) + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + + +### Features + +* **segmentation:** Enhanced segmentation panel design for TMTV ([#3988](https://github.com/OHIF/Viewers/issues/3988)) ([9f3235f](https://github.com/OHIF/Viewers/commit/9f3235ff096636aafa88d8a42859e8dc85d9036d)) + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + + +### Features + +* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1)) + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + + +### Features + +* **layout:** new layout selector with 3D volume rendering ([#3923](https://github.com/OHIF/Viewers/issues/3923)) ([617043f](https://github.com/OHIF/Viewers/commit/617043fe0da5de91fbea4ac33a27f1df16ae1ca6)) + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + + +### Features + +* **worklist:** New worklist buttons and tooltips ([#3989](https://github.com/OHIF/Viewers/issues/3989)) ([9bcd1ae](https://github.com/OHIF/Viewers/commit/9bcd1ae6f51d61786cc1e99624f396b56a47cd69)) + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + + +### Bug Fixes + +* convert radian to degree value for mip rotation ([#3881](https://github.com/OHIF/Viewers/issues/3881)) ([bf846c9](https://github.com/OHIF/Viewers/commit/bf846c94c378f04b9f44dcd71be3f056dbcfe0b5)) + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + + +### Features + +* **config:** Add activateViewportBeforeInteraction parameter for viewport interaction customization ([#3847](https://github.com/OHIF/Viewers/issues/3847)) ([f707b4e](https://github.com/OHIF/Viewers/commit/f707b4ebc996f379cd30337badc06b07e6e35ac5)) +* **i18n:** enhanced i18n support ([#3761](https://github.com/OHIF/Viewers/issues/3761)) ([d14a8f0](https://github.com/OHIF/Viewers/commit/d14a8f0199db95cd9e85866a011b64d6bf830d57)) + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + + +### Bug Fixes + +* **segmentation:** Various fixes for segmentation mode and other ([#3709](https://github.com/OHIF/Viewers/issues/3709)) ([a9a6ad5](https://github.com/OHIF/Viewers/commit/a9a6ad50eae67b43b8b34efc07182d788cacdcfe)) + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + +**Note:** Version bump only for package @ohif/mode-tmtv + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + +**Note:** Version bump only for package @ohif/mode-tmtv diff --git a/modes/tmtv/LICENSE b/modes/tmtv/LICENSE new file mode 100644 index 0000000..19e20dd --- /dev/null +++ b/modes/tmtv/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/modes/tmtv/README.md b/modes/tmtv/README.md new file mode 100644 index 0000000..00c35e9 --- /dev/null +++ b/modes/tmtv/README.md @@ -0,0 +1,78 @@ +# Total Metabolic Tumor Volume + +## Introduction + +Total Metabolic Tumor Volume (TMTV) workflow mode enables quantitatively measurement of a tumor volume in a patient's body. +This mode is accessible in any study that has a PT and CT image series as you can see below + + +![modeValid](https://user-images.githubusercontent.com/7490180/171256138-7a948654-6836-460c-817a-fa9a1929926b.png) + +Note: If the study does not have a PT and CT image series, the TMTV workflow mode will not be available +and will become grayed out. + +## Layout +The designed layout for the viewports follows a predefined hanging protocol which will place +10 viewports containing CT, PT, Fusion and Maximum Intensity Projection (MIP) PT scenes. + +The hanging protocol will match the CT and PT displaySets based on series description. In terms +of PT displaySets, the hanging protocol will match the PT displaySet that has attenuated +corrected PET image data. + +As seen in the image below, the first row contains CT volume in 3 different views of Axial, +Sagittal and Coronal. The second row contains PT volume in the same views as the first row. +The last row contains the fusion volume and the viewport to the right is a MIP of the PT +Volume in the Sagittal view. + + + +![modeLayout](https://user-images.githubusercontent.com/7490180/171256159-1e94edac-985f-4de3-8759-27a077541f8f.png) + +## Synchronization + +The viewports in the 3 rows are synchronized both for the Camera and WindowLevel. +It means that when you interact with the CT viewport (pan, zoom, scroll), +the PT and Fusion viewports will be synchronized to the same view. In addition +to camera synchronization, the window level of the CT viewport will be synchronized +with the fusion viewport. + + +### MIP +The tools that are activated on each viewport is unique to its data. For instance, +the mouse scroll tool for PT, CT and Fusion viewports are scrolling through the image data +(in different directions); however, the mouse scroll tool for the MIP viewport will +rotate the camera to match the usecase for the MIP. + + +## Panels +There are two panels that are available in the TMTV workflow mode and we will +discuss them in detail below. + +### SUV Panel +This panel shows the PT metadata derived from the matched PT displaySet. The user +can edit/change the metadata if needed, and by reloading the data the new +metadata will be applied to the PT volume. + + +## ROI Threshold Panel +The ROI Threshold panel is a panel that allows the user to use the `RectangleROIStartEnd` +tool from Cornerstone to define and edit a region of interest. Then, the user can +apply a threshold to the pixels in the ROI and save the result as a segmentation volume. + +By applying each threshold to the ROI, the Total Metabolic Tumor Volume (TMTV), and +the SUV Peak values will get calculated for the labelmap segments and shown in the +panel. + + +## Export Report + +Finally, the results can be saved in the CSV format. The RectangleROI annotations +can also be extracted as a dicom RT Structure Set and saved as a DICOM file. + + +## Video Tutorial + +Below you can see a video tutorial on how to use the TMTV workflow mode. + + +https://user-images.githubusercontent.com/7490180/171065443-35369fba-e955-48ac-94da-d262e0fccb6b.mp4 diff --git a/modes/tmtv/assets/modeLayout.png b/modes/tmtv/assets/modeLayout.png new file mode 100644 index 0000000..028b579 Binary files /dev/null and b/modes/tmtv/assets/modeLayout.png differ diff --git a/modes/tmtv/assets/modeValid.png b/modes/tmtv/assets/modeValid.png new file mode 100644 index 0000000..69ea706 Binary files /dev/null and b/modes/tmtv/assets/modeValid.png differ diff --git a/modes/tmtv/babel.config.js b/modes/tmtv/babel.config.js new file mode 100644 index 0000000..325ca2a --- /dev/null +++ b/modes/tmtv/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/modes/tmtv/package.json b/modes/tmtv/package.json new file mode 100644 index 0000000..c618e26 --- /dev/null +++ b/modes/tmtv/package.json @@ -0,0 +1,53 @@ +{ + "name": "@ohif/mode-tmtv", + "version": "3.10.0-beta.111", + "description": "Total Metabolic Tumor Volume Workflow", + "author": "OHIF", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/ohif-mode-tmtv.umd.js", + "module": "src/index.ts", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "files": [ + "dist", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "keywords": [ + "ohif-mode" + ], + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:cornerstone": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "3.10.0-beta.111", + "@ohif/extension-cornerstone": "3.10.0-beta.111", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.111", + "@ohif/extension-default": "3.10.0-beta.111", + "@ohif/extension-dicom-pdf": "3.10.0-beta.111", + "@ohif/extension-dicom-video": "3.10.0-beta.111", + "@ohif/extension-measurement-tracking": "3.10.0-beta.111" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "i18next": "^17.0.3" + }, + "devDependencies": { + "webpack": "5.94.0", + "webpack-merge": "^5.7.3" + } +} diff --git a/modes/tmtv/src/id.js b/modes/tmtv/src/id.js new file mode 100644 index 0000000..ebe5acd --- /dev/null +++ b/modes/tmtv/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/modes/tmtv/src/index.ts b/modes/tmtv/src/index.ts new file mode 100644 index 0000000..eb070f0 --- /dev/null +++ b/modes/tmtv/src/index.ts @@ -0,0 +1,229 @@ +import { hotkeys, classes } from '@ohif/core'; +import toolbarButtons from './toolbarButtons.js'; +import { id } from './id.js'; +import initToolGroups from './initToolGroups.js'; +import setCrosshairsConfiguration from './utils/setCrosshairsConfiguration.js'; +import setFusionActiveVolume from './utils/setFusionActiveVolume.js'; +import i18n from 'i18next'; + +const { MetadataProvider } = classes; + +const ohif = { + layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', + sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', + thumbnailList: '@ohif/extension-default.panelModule.seriesList', +}; + +const cs3d = { + viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', + segPanel: '@ohif/extension-cornerstone.panelModule.panelSegmentationNoHeader', + measurements: '@ohif/extension-cornerstone.panelModule.measurements', +}; + +const tmtv = { + hangingProtocol: '@ohif/extension-tmtv.hangingProtocolModule.ptCT', + petSUV: '@ohif/extension-tmtv.panelModule.petSUV', + tmtv: '@ohif/extension-tmtv.panelModule.tmtv', +}; + +const extensionDependencies = { + // Can derive the versions at least process.env.from npm_package_version + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', + '@ohif/extension-cornerstone-dicom-seg': '^3.0.0', + '@ohif/extension-tmtv': '^3.0.0', +}; + +const unsubscriptions = []; +function modeFactory({ modeConfiguration }) { + return { + // TODO: We're using this as a route segment + // We should not be. + id, + routeName: 'tmtv', + displayName: i18n.t('Modes:Total Metabolic Tumor Volume'), + /** + * Lifecycle hooks + */ + onModeEnter: ({ servicesManager, extensionManager, commandsManager }: withAppTypes) => { + const { + toolbarService, + toolGroupService, + customizationService, + hangingProtocolService, + displaySetService, + } = servicesManager.services; + + const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.tools' + ); + + const { toolNames, Enums } = utilityModule.exports; + + // Init Default and SR ToolGroups + initToolGroups(toolNames, Enums, toolGroupService, commandsManager); + + const { unsubscribe } = toolGroupService.subscribe( + toolGroupService.EVENTS.VIEWPORT_ADDED, + () => { + // For fusion toolGroup we need to add the volumeIds for the crosshairs + // since in the fusion viewport we don't want both PT and CT to render MIP + // when slabThickness is modified + const { displaySetMatchDetails } = hangingProtocolService.getMatchDetails(); + + setCrosshairsConfiguration( + displaySetMatchDetails, + toolNames, + toolGroupService, + displaySetService + ); + + setFusionActiveVolume( + displaySetMatchDetails, + toolNames, + toolGroupService, + displaySetService + ); + } + ); + + unsubscriptions.push(unsubscribe); + toolbarService.addButtons(toolbarButtons); + toolbarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'WindowLevel', + 'Crosshairs', + 'Pan', + ]); + toolbarService.createButtonSection('ROIThresholdToolbox', [ + 'RectangleROIStartEndThreshold', + 'BrushTools', + ]); + + customizationService.setCustomizations({ + 'panelSegmentation.tableMode': { + $set: 'expanded', + }, + 'panelSegmentation.onSegmentationAdd': { + $set: () => { + commandsManager.run('createNewLabelmapFromPT'); + }, + }, + }); + + // For the hanging protocol we need to decide on the window level + // based on whether the SUV is corrected or not, hence we can't hard + // code the window level in the hanging protocol but we add a custom + // attribute to the hanging protocol that will be used to get the + // window level based on the metadata + hangingProtocolService.addCustomAttribute( + 'getPTVOIRange', + 'get PT VOI based on corrected or not', + props => { + const ptDisplaySet = props.find(imageSet => imageSet.Modality === 'PT'); + + if (!ptDisplaySet) { + return; + } + + const { imageId } = ptDisplaySet.images[0]; + const imageIdScalingFactor = MetadataProvider.get('scalingModule', imageId); + + const isSUVAvailable = imageIdScalingFactor && imageIdScalingFactor.suvbw; + + if (isSUVAvailable) { + return { + windowWidth: 5, + windowCenter: 2.5, + }; + } + + return; + } + ); + }, + onModeExit: ({ servicesManager }: withAppTypes) => { + const { + toolGroupService, + syncGroupService, + segmentationService, + cornerstoneViewportService, + uiDialogService, + uiModalService, + } = servicesManager.services; + + unsubscriptions.forEach(unsubscribe => unsubscribe()); + uiDialogService.dismissAll(); + uiModalService.hide(); + toolGroupService.destroy(); + syncGroupService.destroy(); + segmentationService.destroy(); + cornerstoneViewportService.destroy(); + }, + validationTags: { + study: [], + series: [], + }, + isValidMode: ({ modalities, study }) => { + const modalities_list = modalities.split('\\'); + const invalidModalities = ['SM']; + + const isValid = + modalities_list.includes('CT') && + study.mrn !== 'M1' && + modalities_list.includes('PT') && + !invalidModalities.some(modality => modalities_list.includes(modality)) && + // This is study is a 4D study with PT and CT and not a 3D study for the tmtv + // mode, until we have a better way to identify 4D studies we will use the + // StudyInstanceUID to identify the study + // Todo: when we add the 4D mode which comes with a mechanism to identify + // 4D studies we can use that + study.studyInstanceUid !== '1.3.6.1.4.1.12842.1.1.14.3.20220915.105557.468.2963630849'; + + // there should be both CT and PT modalities and the modality should not be SM + return { + valid: isValid, + description: 'The mode requires both PT and CT series in the study', + }; + }, + routes: [ + { + path: 'tmtv', + /*init: ({ servicesManager, extensionManager }) => { + //defaultViewerRouteInit + },*/ + layoutTemplate: () => { + return { + id: ohif.layout, + props: { + leftPanels: [ohif.thumbnailList], + leftPanelResizable: true, + leftPanelClosed: true, + rightPanels: [tmtv.tmtv, tmtv.petSUV], + rightPanelResizable: true, + viewports: [ + { + namespace: cs3d.viewport, + displaySetsToDisplay: [ohif.sopClassHandler], + }, + ], + }, + }; + }, + }, + ], + extensions: extensionDependencies, + hangingProtocol: tmtv.hangingProtocol, + sopClassHandlers: [ohif.sopClassHandler], + ...modeConfiguration, + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; diff --git a/modes/tmtv/src/initToolGroups.js b/modes/tmtv/src/initToolGroups.js new file mode 100644 index 0000000..00447ce --- /dev/null +++ b/modes/tmtv/src/initToolGroups.js @@ -0,0 +1,181 @@ +export const toolGroupIds = { + CT: 'ctToolGroup', + PT: 'ptToolGroup', + Fusion: 'fusionToolGroup', + MIP: 'mipToolGroup', + default: 'default', +}; + +function _initToolGroups(toolNames, Enums, toolGroupService, commandsManager) { + const tools = { + active: [ + { + toolName: toolNames.WindowLevel, + bindings: [{ mouseButton: Enums.MouseBindings.Primary }], + }, + { + toolName: toolNames.Pan, + bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }], + }, + { + toolName: toolNames.Zoom, + bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], + }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, + ], + passive: [ + { toolName: toolNames.Length }, + { + toolName: toolNames.ArrowAnnotate, + configuration: { + getTextCallback: (callback, eventDetails) => { + commandsManager.runCommand('arrowTextCallback', { + callback, + eventDetails, + }); + }, + changeTextCallback: (data, eventDetails, callback) => { + commandsManager.runCommand('arrowTextCallback', { + callback, + data, + eventDetails, + }); + }, + }, + }, + { toolName: toolNames.Bidirectional }, + { toolName: toolNames.DragProbe }, + { toolName: toolNames.Probe }, + { toolName: toolNames.EllipticalROI }, + { toolName: toolNames.RectangleROI }, + { toolName: toolNames.StackScroll }, + { toolName: toolNames.Angle }, + { toolName: toolNames.CobbAngle }, + { toolName: toolNames.Magnify }, + { + toolName: 'CircularBrush', + parentTool: 'Brush', + configuration: { + activeStrategy: 'FILL_INSIDE_CIRCLE', + }, + }, + { + toolName: 'CircularEraser', + parentTool: 'Brush', + configuration: { + activeStrategy: 'ERASE_INSIDE_CIRCLE', + }, + }, + { + toolName: 'SphereBrush', + parentTool: 'Brush', + configuration: { + activeStrategy: 'FILL_INSIDE_SPHERE', + }, + }, + { + toolName: 'SphereEraser', + parentTool: 'Brush', + configuration: { + activeStrategy: 'ERASE_INSIDE_SPHERE', + }, + }, + { + toolName: 'ThresholdCircularBrush', + parentTool: 'Brush', + configuration: { + activeStrategy: 'THRESHOLD_INSIDE_CIRCLE', + }, + }, + { + toolName: 'ThresholdSphereBrush', + parentTool: 'Brush', + configuration: { + activeStrategy: 'THRESHOLD_INSIDE_SPHERE', + }, + }, + { + toolName: 'ThresholdCircularBrushDynamic', + parentTool: 'Brush', + configuration: { + activeStrategy: 'THRESHOLD_INSIDE_CIRCLE', + // preview: { + // enabled: true, + // }, + strategySpecificConfiguration: { + // to use the use the center segment index to determine + // if inside -> same segment, if outside -> eraser + // useCenterSegmentIndex: true, + THRESHOLD: { + isDynamic: true, + dynamicRadius: 3, + }, + }, + }, + }, + ], + enabled: [], + disabled: [ + { + toolName: toolNames.Crosshairs, + configuration: { + disableOnPassive: true, + autoPan: { + enabled: false, + panSize: 10, + }, + }, + }, + ], + }; + + toolGroupService.createToolGroupAndAddTools(toolGroupIds.CT, tools); + toolGroupService.createToolGroupAndAddTools(toolGroupIds.PT, { + active: tools.active, + passive: [...tools.passive, { toolName: 'RectangleROIStartEndThreshold' }], + enabled: tools.enabled, + disabled: tools.disabled, + }); + toolGroupService.createToolGroupAndAddTools(toolGroupIds.Fusion, tools); + toolGroupService.createToolGroupAndAddTools(toolGroupIds.default, tools); + + const mipTools = { + active: [ + { + toolName: toolNames.VolumeRotate, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + configuration: { + rotateIncrementDegrees: 5, + }, + }, + { + toolName: toolNames.MipJumpToClick, + configuration: { + toolGroupId: toolGroupIds.PT, + }, + bindings: [{ mouseButton: Enums.MouseBindings.Primary }], + }, + ], + enabled: [ + { + toolName: toolNames.OrientationMarker, + configuration: { + orientationWidget: { + viewportCorner: 'BOTTOM_LEFT', + }, + }, + }, + ], + }; + + toolGroupService.createToolGroupAndAddTools(toolGroupIds.MIP, mipTools); +} + +function initToolGroups(toolNames, Enums, toolGroupService, commandsManager) { + _initToolGroups(toolNames, Enums, toolGroupService, commandsManager); +} + +export default initToolGroups; diff --git a/modes/tmtv/src/toolbarButtons.js b/modes/tmtv/src/toolbarButtons.js new file mode 100644 index 0000000..6e63b21 --- /dev/null +++ b/modes/tmtv/src/toolbarButtons.js @@ -0,0 +1,285 @@ +import { ToolbarService } from '@ohif/core'; +import { toolGroupIds } from './initToolGroups'; + +const setToolActiveToolbar = { + commandName: 'setToolActiveToolbar', + commandOptions: { + toolGroupIds: [toolGroupIds.CT, toolGroupIds.PT, toolGroupIds.Fusion], + }, +}; + +const toolbarButtons = [ + { + id: 'MeasurementTools', + uiType: 'ohif.toolButtonList', + props: { + groupId: 'MeasurementTools', + primary: ToolbarService.createButton({ + id: 'Length', + icon: 'tool-length', + label: 'Length', + tooltip: 'Length Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + secondary: { + icon: 'chevron-down', + tooltip: 'More Measure Tools', + }, + items: [ + ToolbarService.createButton({ + id: 'Bidirectional', + icon: 'tool-bidirectional', + label: 'Bidirectional', + tooltip: 'Bidirectional Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + ToolbarService.createButton({ + id: 'ArrowAnnotate', + icon: 'tool-annotate', + label: 'Arrow Annotate', + tooltip: 'Arrow Annotate Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + ToolbarService.createButton({ + id: 'EllipticalROI', + icon: 'tool-ellipse', + label: 'Ellipse', + tooltip: 'Ellipse Tool', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }), + ], + }, + }, + { + id: 'Zoom', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-zoom', + label: 'Zoom', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + // Window Level + Presets + { + id: 'WindowLevel', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-window-level', + label: 'Window Level', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + // Crosshairs Button + { + id: 'Crosshairs', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-crosshair', + label: 'Crosshairs', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + // Pan Button + { + id: 'Pan', + uiType: 'ohif.toolButton', + props: { + icon: 'tool-move', + label: 'Pan', + commands: setToolActiveToolbar, + evaluate: 'evaluate.cornerstoneTool', + }, + }, + // Rectangle ROI Start End Threshold Button + { + id: 'RectangleROIStartEndThreshold', + uiType: 'ohif.toolBoxButton', + props: { + icon: 'tool-create-threshold', + label: 'Rectangle ROI Threshold', + commands: setToolActiveToolbar, + evaluate: [ + 'evaluate.cornerstone.segmentation', + // need to put the disabled text last, since each evaluator will + // merge the result text into the final result + { + name: 'evaluate.cornerstoneTool', + disabledText: 'Select the PT Axial to enable this tool', + }, + ], + options: 'tmtv.RectangleROIThresholdOptions', + }, + }, + { + id: 'BrushTools', + uiType: 'ohif.toolBoxButtonGroup', + props: { + groupId: 'BrushTools', + evaluate: 'evaluate.cornerstone.hasSegmentation', + items: [ + { + id: 'Brush', + icon: 'icon-tool-brush', + label: 'Brush', + evaluate: { + name: 'evaluate.cornerstone.segmentation', + toolNames: ['CircularBrush', 'SphereBrush'], + disabledText: 'Create new segmentation to enable this tool.', + }, + options: [ + { + name: 'Radius (mm)', + id: 'brush-radius', + type: 'range', + min: 0.5, + max: 99.5, + step: 0.5, + value: 25, + commands: { + commandName: 'setBrushSize', + commandOptions: { toolNames: ['CircularBrush', 'SphereBrush'] }, + }, + }, + { + name: 'Shape', + type: 'radio', + id: 'brush-mode', + value: 'CircularBrush', + values: [ + { value: 'CircularBrush', label: 'Circle' }, + { value: 'SphereBrush', label: 'Sphere' }, + ], + commands: 'setToolActiveToolbar', + }, + ], + }, + { + id: 'Eraser', + icon: 'icon-tool-eraser', + label: 'Eraser', + evaluate: { + name: 'evaluate.cornerstone.segmentation', + toolNames: ['CircularEraser', 'SphereEraser'], + }, + options: [ + { + name: 'Radius (mm)', + id: 'eraser-radius', + type: 'range', + min: 0.5, + max: 99.5, + step: 0.5, + value: 25, + commands: { + commandName: 'setBrushSize', + commandOptions: { toolNames: ['CircularEraser', 'SphereEraser'] }, + }, + }, + { + name: 'Shape', + type: 'radio', + id: 'eraser-mode', + value: 'CircularEraser', + values: [ + { value: 'CircularEraser', label: 'Circle' }, + { value: 'SphereEraser', label: 'Sphere' }, + ], + commands: 'setToolActiveToolbar', + }, + ], + }, + { + id: 'Threshold', + icon: 'icon-tool-threshold', + label: 'Threshold Tool', + evaluate: { + name: 'evaluate.cornerstone.segmentation', + toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'], + }, + options: [ + { + name: 'Radius (mm)', + id: 'threshold-radius', + type: 'range', + min: 0.5, + max: 99.5, + step: 0.5, + value: 25, + commands: { + commandName: 'setBrushSize', + commandOptions: { + toolNames: [ + 'ThresholdCircularBrush', + 'ThresholdSphereBrush', + 'ThresholdCircularBrushDynamic', + ], + }, + }, + }, + + { + name: 'Threshold', + type: 'radio', + id: 'dynamic-mode', + value: 'ThresholdRange', + values: [ + { value: 'ThresholdDynamic', label: 'Dynamic' }, + { value: 'ThresholdRange', label: 'Range' }, + ], + commands: ({ value, commandsManager }) => { + if (value === 'ThresholdDynamic') { + commandsManager.run('setToolActive', { + toolName: 'ThresholdCircularBrushDynamic', + }); + } else { + commandsManager.run('setToolActive', { + toolName: 'ThresholdCircularBrush', + }); + } + }, + }, + { + name: 'Shape', + type: 'radio', + id: 'eraser-mode', + value: 'ThresholdCircularBrush', + values: [ + { value: 'ThresholdCircularBrush', label: 'Circle' }, + { value: 'ThresholdSphereBrush', label: 'Sphere' }, + ], + condition: ({ options }) => + options.find(option => option.id === 'dynamic-mode').value === 'ThresholdRange', + commands: 'setToolActiveToolbar', + }, + { + name: 'ThresholdRange', + type: 'double-range', + id: 'threshold-range', + min: 0, + max: 50, + step: 0.5, + value: [2.5, 50], + condition: ({ options }) => + options.find(option => option.id === 'dynamic-mode').value === 'ThresholdRange', + commands: { + commandName: 'setThresholdRange', + commandOptions: { + toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'], + }, + }, + }, + ], + }, + ], + }, + }, +]; + +export default toolbarButtons; diff --git a/modes/tmtv/src/utils/setCrosshairsConfiguration.js b/modes/tmtv/src/utils/setCrosshairsConfiguration.js new file mode 100644 index 0000000..072d944 --- /dev/null +++ b/modes/tmtv/src/utils/setCrosshairsConfiguration.js @@ -0,0 +1,33 @@ +import { toolGroupIds } from '../initToolGroups'; + +export default function setCrosshairsConfiguration( + matches, + toolNames, + toolGroupService, + displaySetService +) { + const matchDetails = matches.get('ctDisplaySet'); + + if (!matchDetails) { + return; + } + + const { SeriesInstanceUID } = matchDetails; + const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID); + + const toolConfig = toolGroupService.getToolConfiguration( + toolGroupIds.Fusion, + toolNames.Crosshairs + ); + + const crosshairsConfig = { + ...toolConfig, + filterActorUIDsToSetSlabThickness: [displaySets[0].displaySetInstanceUID], + }; + + toolGroupService.setToolConfiguration( + toolGroupIds.Fusion, + toolNames.Crosshairs, + crosshairsConfig + ); +} diff --git a/modes/tmtv/src/utils/setFusionActiveVolume.js b/modes/tmtv/src/utils/setFusionActiveVolume.js new file mode 100644 index 0000000..75f4538 --- /dev/null +++ b/modes/tmtv/src/utils/setFusionActiveVolume.js @@ -0,0 +1,61 @@ +import { toolGroupIds } from '../initToolGroups'; + +export default function setFusionActiveVolume( + matches, + toolNames, + toolGroupService, + displaySetService +) { + const matchDetails = matches.get('ptDisplaySet'); + const matchDetails2 = matches.get('ctDisplaySet'); + + if (!matchDetails) { + return; + } + + const { SeriesInstanceUID } = matchDetails; + + const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID); + + if (!displaySets || displaySets.length === 0) { + return; + } + + const wlToolConfig = toolGroupService.getToolConfiguration( + toolGroupIds.Fusion, + toolNames.WindowLevel + ); + + const ellipticalToolConfig = toolGroupService.getToolConfiguration( + toolGroupIds.Fusion, + toolNames.EllipticalROI + ); + + // Todo: this should not take into account the loader id + const volumeId = `cornerstoneStreamingImageVolume:${displaySets[0].displaySetInstanceUID}`; + const { SeriesInstanceUID: SeriesInstanceUID2 } = matchDetails2; + const ctDisplaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID2); + const ctVolumeId = `cornerstoneStreamingImageVolume:${ctDisplaySets[0].displaySetInstanceUID}`; + + const windowLevelConfig = { + ...wlToolConfig, + volumeId: ctVolumeId, + }; + + const ellipticalROIConfig = { + ...ellipticalToolConfig, + volumeId, + }; + + toolGroupService.setToolConfiguration( + toolGroupIds.Fusion, + toolNames.WindowLevel, + windowLevelConfig + ); + + toolGroupService.setToolConfiguration( + toolGroupIds.Fusion, + toolNames.EllipticalROI, + ellipticalROIConfig + ); +} diff --git a/netlify-lerna-cache.sh b/netlify-lerna-cache.sh new file mode 100755 index 0000000..dd22f42 --- /dev/null +++ b/netlify-lerna-cache.sh @@ -0,0 +1,26 @@ +#!/bin/sh +NODE_MODULES_CACHE="./node_modules" +LERNA_CACHE="$NODE_MODULES_CACHE/lerna-cache" + +echo "Running netlify-lerna-cache.sh" +mkdir -p "$NODE_MODULES_CACHE/lerna-cache" + +cache_deps() { + PACKAGES=$(ls -1 $1) + + for PKG in $PACKAGES + do + PKG_NODE_MODULES="$1/$PKG/node_modules" + if [ -d $PKG_NODE_MODULES ]; + then + mv $PKG_NODE_MODULES $LERNA_CACHE/$PKG + echo "Cached node modules for $PKG" + else + echo "Unable to cache node modules for $PKG" + fi + done +} + +cache_deps platform +cache_deps extensions +cache_deps modes \ No newline at end of file diff --git a/netlify-lerna-restore.sh b/netlify-lerna-restore.sh new file mode 100755 index 0000000..44ce194 --- /dev/null +++ b/netlify-lerna-restore.sh @@ -0,0 +1,27 @@ +#!/bin/sh +NODE_MODULES_CACHE="./node_modules" +LERNA_CACHE="$NODE_MODULES_CACHE/lerna-cache" + +echo "Running netlify-lerna-restore.sh" +mkdir -p "$NODE_MODULES_CACHE/lerna-cache" +echo "$NODE_MODULES_CACHE/lerna-cache/*" + +restore_deps() { + PACKAGES=$(ls -1 $1) + + for PKG in $PACKAGES + do + PKG_CACHE="$LERNA_CACHE/$PKG" + if [ -d $PKG_CACHE ]; + then + mv $PKG_CACHE $1/$PKG/node_modules + echo "Restored node modules for $PKG" + else + echo "Unable to restore cache for $PKG" + fi + done +} + +restore_deps platform +restore_deps extensions +restore_deps modes \ No newline at end of file diff --git a/nx.json b/nx.json new file mode 100644 index 0000000..ce4a806 --- /dev/null +++ b/nx.json @@ -0,0 +1,85 @@ +{ + "tasksRunnerOptions": { + "default": { + "runner": "nx/tasks-runners/default", + "options": { + "cacheableOperations": [ + "dev", + "build", + "test:unit", + "test:unit:ci", + "test", + "test:e2e", + "test:e2e:local", + "test:e2e:dist", + "test:e2e:serve", + "build:viewer", + "build:dev", + "build:aws", + "build:viewer:ci", + "build:viewer:qa", + "build:viewer:demo", + "build" + ] + } + } + }, + "targetDefaults": { + "test:unit": { + "dependsOn": [ + "^test:unit" + ] + }, + "test:unit:ci": { + "dependsOn": [ + "^test:unit:ci" + ] + }, + "test": { + "dependsOn": [ + "^test" + ] + }, + "test:e2e": { + "dependsOn": [ + "^test:e2e" + ] + }, + "test:e2e:headed": { + "dependsOn": [ + "^test:e2e:headed" + ] + }, + "test:e2e:local": { + "dependsOn": [ + "^test:e2e:local" + ] + }, + "test:e2e:dist": { + "dependsOn": [ + "^test:e2e:dist" + ] + }, + "test:e2e:serve": { + "dependsOn": [ + "^test:e2e:serve" + ] + }, + "build": { + "outputs": [ + "{projectRoot}/platform/app/dist" + ] + } + }, + "$schema": "./node_modules/nx/schemas/nx-schema.json", + "namedInputs": { + "default": [ + "{projectRoot}/**/*", + "sharedGlobals" + ], + "sharedGlobals": [], + "production": [ + "default" + ] + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..035f377 --- /dev/null +++ b/package.json @@ -0,0 +1,122 @@ +{ + "name": "ohif-monorepo-root", + "private": true, + "workspaces": { + "packages": [ + "platform/app", + "platform/cli", + "platform/ui-next", + "platform/ui", + "platform/core", + "platform/i18n", + "extensions/*", + "modes/*", + "addOns/externals/*" + ], + "nohoist": [ + "**/html-minifier-terser" + ] + }, + "engines": { + "node": ">=18", + "npm": ">=6", + "yarn": ">=1.20.0" + }, + "scripts": { + "cm": "npx git-cz", + "build": "lerna run build:viewer --stream", + "build:dev": "lerna run build:dev --stream", + "build:ci": "lerna run build:viewer:ci --stream", + "build:qa": "lerna run build:viewer:qa --stream", + "clean": "npx lerna run clean --stream", + "clean:deep": "npx lerna run clean:deep --stream", + "cli": "node ./platform/cli/src/index.js", + "build:ui:deploy-preview": "lerna run build:ui:deploy-preview --stream", + "build:demo": "lerna run build:viewer:demo --stream", + "build:package-all": "lerna run build:package --parallel --stream", + "build:package-all-1": "lerna run build:package-1 --parallel --stream", + "dev:fast": "cd platform/app && yarn run dev:fast", + "show:config": "echo Config is $APP_CONFIG on $PUBLIC_URL", + "dev": "lerna run dev:viewer --stream", + "dev:no:cache": "lerna run dev:no:cache --stream", + "dev:project": ".scripts/dev.sh", + "dev:orthanc": "lerna run dev:orthanc --stream", + "dev:orthanc:no:cache": "lerna run dev:orthanc:no:cache --stream", + "dev:dcm4chee": "lerna run dev:dcm4chee --stream", + "dev:static": "lerna run dev:static --stream", + "orthanc:up": "docker compose -f platform/app/.recipes/Nginx-Orthanc/docker-compose.yml up", + "install:dev": "cp -f yarn.lock addOns/yarn.lock && cd addOns && yarn install --modules-folder ../node_modules", + "preinstall": "node preinstall.js", + "start": "yarn run dev", + "test": "yarn run test:unit", + "test:data": "git submodule update --init -f testdata", + "test-watch": "jest --collectCoverage --watchAll", + "test:unit": "jest --collectCoverage", + "test:unit:ci": "lerna run test:unit:ci --parallel --stream", + "test:e2e": "lerna run test:e2e --stream", + "test:e2e:ci": "npx playwright test", + "test:e2e:ui": "npx playwright test --ui", + "test:e2e:headed": "npx playwright test --headed", + "test:e2e:debug": "npx playwright test --debug", + "test:e2e:dist": "lerna run test:e2e:dist --stream", + "test:e2e:serve": "yarn test:data && lerna run test:e2e:serve --stream", + "see-changed": "lerna changed", + "docs:preview": "lerna run docs:preview --stream", + "docs:publish": "chmod +x ./build-and-publish-docs.sh && ./build-and-publish-docs.sh", + "release": "yarn run lerna:version && yarn run lerna:publish", + "lerna:clean": "lerna clean", + "lerna:cache": "./netlify-lerna-cache.sh", + "lerna:restore": "./netlify-lerna-restore.sh", + "lerna:customVersion": "node version.mjs", + "link-list": "npm ls --depth=0 --link=true" + }, + "dependencies": { + "execa": "^8.0.1" + }, + "optionalDependencies": { + "@percy/cypress": "^3.1.1", + "@playwright/test": "^1.48.0", + "cypress": "^14.0.0", + "cypress-file-upload": "^5.0.8" + }, + "devDependencies": { + "@babel/core": "7.24.7", + "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-proposal-object-rest-spread": "^7.17.3", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.16.7", + "@babel/plugin-transform-regenerator": "^7.16.7", + "@babel/plugin-transform-runtime": "7.24.7", + "@babel/plugin-transform-typescript": "^7.13.0", + "@babel/preset-env": "7.24.7", + "@babel/preset-react": "^7.16.7", + "@babel/preset-typescript": "^7.13.0", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "0.6.9" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "src/**/*.{js,json,css}": [ + "prettier --write", + "git add" + ] + }, + "resolutions": { + "commander": "8.3.0", + "path-to-regexp": "0.1.12", + "nth-check": "^2.1.1", + "trim-newlines": "^5.0.0", + "glob-parent": "^6.0.2", + "trim": "^1.0.0", + "package-json": "^8.1.0", + "sharp": "^0.32.6", + "rollup": "2.79.2", + "body-parser": "1.20.3" + } +} diff --git a/platform/app/.all-contributorsrc b/platform/app/.all-contributorsrc new file mode 100644 index 0000000..b12263a --- /dev/null +++ b/platform/app/.all-contributorsrc @@ -0,0 +1,82 @@ +{ + "files": ["README.md"], + "imageSize": 100, + "commit": false, + "contributors": [ + { + "login": "swederik", + "name": "Erik Ziegler", + "avatar_url": "https://avatars3.githubusercontent.com/u/607793?v=4", + "profile": "https://github.com/swederik", + "contributions": ["code", "infra"] + }, + { + "login": "evren217", + "name": "Evren Ozkan", + "avatar_url": "https://avatars1.githubusercontent.com/u/4920551?v=4", + "profile": "https://github.com/evren217", + "contributions": ["code"] + }, + { + "login": "galelis", + "name": "Gustavo Andrรฉ Lelis", + "avatar_url": "https://avatars3.githubusercontent.com/u/2378326?v=4", + "profile": "https://github.com/galelis", + "contributions": ["code"] + }, + { + "login": "dannyrb", + "name": "Danny Brown", + "avatar_url": "https://avatars1.githubusercontent.com/u/5797588?v=4", + "profile": "http://dannyrb.com/", + "contributions": ["code", "infra"] + }, + { + "login": "allcontributors", + "name": "allcontributors[bot]", + "avatar_url": "https://avatars3.githubusercontent.com/u/46843839?v=4", + "profile": "https://github.com/all-contributors/all-contributors-bot", + "contributions": ["doc"] + }, + { + "login": "EsrefDurna", + "name": "Esref Durna", + "avatar_url": "https://avatars0.githubusercontent.com/u/1230575?v=4", + "profile": "https://www.linkedin.com/in/siliconvalleynextgeneration/", + "contributions": ["question"] + }, + { + "login": "diego0020", + "name": "diego0020", + "avatar_url": "https://avatars3.githubusercontent.com/u/7297450?v=4", + "profile": "https://github.com/diego0020", + "contributions": ["code"] + }, + { + "login": "dlwire", + "name": "David Wire", + "avatar_url": "https://avatars3.githubusercontent.com/u/1167291?v=4", + "profile": "https://github.com/dlwire", + "contributions": ["code"] + }, + { + "login": "jfmedeiros1820", + "name": "Joรฃo Felipe de Medeiros Moreira", + "avatar_url": "https://avatars1.githubusercontent.com/u/2211708?v=4", + "profile": "https://github.com/jfmedeiros1820", + "contributions": ["test"] + }, + { + "login": "pavertomato", + "name": "Egor Lezhnin", + "avatar_url": "https://avatars0.githubusercontent.com/u/878990?v=4", + "profile": "http://egor.lezhn.in", + "contributions": ["code"] + } + ], + "contributorsPerLine": 7, + "projectName": "Viewers", + "projectOwner": "OHIF", + "repoType": "github", + "repoHost": "https://github.com" +} diff --git a/platform/app/.browserslistrc b/platform/app/.browserslistrc new file mode 100644 index 0000000..c9285e5 --- /dev/null +++ b/platform/app/.browserslistrc @@ -0,0 +1,7 @@ +# Browsers that we support + +> 1% +IE 11 +not IE < 11 +not dead +not op_mini all diff --git a/platform/app/.dockerignore b/platform/app/.dockerignore new file mode 100644 index 0000000..fcdccdb --- /dev/null +++ b/platform/app/.dockerignore @@ -0,0 +1,23 @@ +# Output +dist/ +build/ + +# Dependencies +node_modules/ + +# Root +README.md +Dockerfile +dockerfile + +# Misc. Config +.git +.DS_Store +.gitignore +.vscode +.circleci + +# Unnecessary things to pull into container +extensions +docs +cypress \ No newline at end of file diff --git a/platform/app/.env.example b/platform/app/.env.example new file mode 100644 index 0000000..14e42e0 --- /dev/null +++ b/platform/app/.env.example @@ -0,0 +1,10 @@ +## +# EXAMPLE +# +# Read more about .env files when using create-react-app here: +# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-development-environment-variables-in-env +# + +PUBLIC_URL=/demo/ +APP_CONFIG=config/netlify.js +USE_HASH_ROUTER=false diff --git a/platform/app/.eslintignore b/platform/app/.eslintignore new file mode 100644 index 0000000..3f1cb5d --- /dev/null +++ b/platform/app/.eslintignore @@ -0,0 +1,3 @@ +config/** +docs/** +img/** diff --git a/platform/app/.gitignore b/platform/app/.gitignore new file mode 100644 index 0000000..9062b1c --- /dev/null +++ b/platform/app/.gitignore @@ -0,0 +1,3 @@ +.vercel +test-results +store.json diff --git a/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/.gitignore b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/.gitignore new file mode 100644 index 0000000..088f9a9 --- /dev/null +++ b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/.gitignore @@ -0,0 +1,6 @@ +logs/* +volumes/* +config/letsencrypt/* +config/certbot/* +!config/letsencrypt/.gitkeep +!config/certbot/.gitkeep diff --git a/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/config/entrypoint.sh b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/config/entrypoint.sh new file mode 100644 index 0000000..8648d7c --- /dev/null +++ b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/config/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Start oauth2-proxy +oauth2-proxy --config=/etc/oauth2-proxy/oauth2-proxy.cfg & + +# Start nginx +nginx -g "daemon off;" diff --git a/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/config/nginx.conf b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/config/nginx.conf new file mode 100644 index 0000000..79043d9 --- /dev/null +++ b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/config/nginx.conf @@ -0,0 +1,238 @@ +worker_processes auto; +error_log /var/logs/nginx/error.log debug; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include '/etc/nginx/mime.types'; + default_type application/octet-stream; + + keepalive_timeout 65; + keepalive_requests 100000; + tcp_nopush on; + tcp_nodelay on; + + proxy_buffers 16 16k; + proxy_buffer_size 32k; + proxy_busy_buffers_size 64k; + proxy_max_temp_file_size 128k; + + + gzip on; + gzip_disable "msie6"; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; + + server { + listen 80; + server_name YOUR_DOMAIN; + + client_max_body_size 0; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } + } + + server { + listen 443 ssl; + server_name YOUR_DOMAIN; + ssl_certificate /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem; + root /var/www/html; + + gzip on; + gzip_types text/css application/javascript application/json image/svg+xml; + gzip_comp_level 9; + etag on; + + location /sw.js { + add_header Cache-Control "no-cache"; + proxy_cache_bypass $http_pragma; + proxy_cache_revalidate on; + expires off; + access_log off; + } + + + location /oauth2 { + expires -1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Auth-Request-Redirect $request_uri; + proxy_pass http://localhost:4180$uri$is_args$args; + } + + location /oauth2/callback { + proxy_pass http://localhost:4180; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /oauth2/sign_out { + expires -1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Auth-Request-Redirect /oauth2/sign_in; + proxy_pass http://localhost:4180; + } + + + location /pacs/ { + auth_request /oauth2/auth; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + expires 0; + add_header Cache-Control private; + + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept' always; + + if ($request_method = OPTIONS) { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept'; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain; charset=utf-8'; + add_header 'Content-Length' 0; + return 204; + } + + rewrite ^/pacs/(.*) /dcm4chee-arc/aets/DCM4CHEE/rs/$1 break; + proxy_pass http://arc:8080; + } + + location /pacs-admin { + return 301 /pacs-admin/; + } + + # Redirect /pacs-admin to /dcm4chee-arc/ui2/ + location = /pacs-admin { + return 301 $scheme://$host/dcm4chee-arc/ui2/; + } + + # Handle /pacs-admin/ requests + location /pacs-admin/ { + return 301 $scheme://$host/dcm4chee-arc/ui2/; + } + + # Proxy pass for /dcm4chee-arc/ui2/ + location /dcm4chee-arc/ui2/ { + error_page 401 = /oauth2/sign_in?rd=$scheme://$host$request_uri; + auth_request /oauth2/auth?allowed_groups=pacsadmin; + + auth_request_set $user $upstream_http_x_auth_request_user; + auth_request_set $token $upstream_http_x_auth_request_access_token; + auth_request_set $auth_cookie $upstream_http_set_cookie; + + proxy_set_header X-User $user; + proxy_set_header X-Access-Token $token; + add_header Set-Cookie $auth_cookie; + + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + expires 0; + add_header Cache-Control private; + + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept' always; + + if ($request_method = OPTIONS) { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept'; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain; charset=utf-8'; + add_header 'Content-Length' 0; + return 204; + } + + proxy_pass http://arc:8080; + } + + # Proxy pass for other /dcm4chee-arc/ requests + location /dcm4chee-arc/ { + proxy_pass http://arc:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + + location /pacs { + return 301 /pacs/; + } + + + location /ohif-viewer/ { + expires -1; + error_page 401 = /oauth2/sign_in?rd=$scheme://$host$request_uri; + auth_request /oauth2/auth; + + auth_request_set $user $upstream_http_x_auth_request_user; + auth_request_set $token $upstream_http_x_auth_request_access_token; + auth_request_set $auth_cookie $upstream_http_set_cookie; + + proxy_set_header X-User $user; + proxy_set_header X-Access-Token $token; + add_header Set-Cookie $auth_cookie; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-Proto $scheme; + + index index.html; + try_files $uri $uri/ /index.html; + } + + + location /ohif-viewer { + return 301 /ohif-viewer/; + } + + location = / { + return 301 /ohif-viewer/; + } + + location / { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } + + location /keycloak/ { + proxy_pass http://keycloak:8080/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /keycloak { + return 301 /keycloak/; + } + } +} diff --git a/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/config/oauth2-proxy.cfg b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/config/oauth2-proxy.cfg new file mode 100644 index 0000000..985525a --- /dev/null +++ b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/config/oauth2-proxy.cfg @@ -0,0 +1,22 @@ +http_address="0.0.0.0:4180" +cookie_secret="GENERATEACOOKIESECRET----------------------=" +email_domains=["*"] +cookie_secure="false" +cookie_expire="9m30s" +cookie_refresh="5m" +client_secret="2Xtlde7aozdkzzYHdIxQNfPDr0wNPTgg" +client_id="ohif_viewer" +redirect_url="http://YOUR_DOMAIN/oauth2/callback" + +ssl_insecure_skip_verify = true +insecure_oidc_allow_unverified_email = true +pass_access_token = true +provider="keycloak-oidc" +provider_display_name="Keycloak" +user_id_claim="oid" +oidc_email_claim="sub" +scope="openid" +pass_host_header=true +code_challenge_method="S256" +oidc_issuer_url="http://YOUR_DOMAIN/keycloak/realms/ohif" +insecure_oidc_skip_issuer_verification = true diff --git a/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/config/ohif-keycloak-realm.json b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/config/ohif-keycloak-realm.json new file mode 100644 index 0000000..3064ee7 --- /dev/null +++ b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/config/ohif-keycloak-realm.json @@ -0,0 +1,2315 @@ +{ + "id": "37c16268-9c83-41c3-b452-3fbc86e6966d", + "realm": "ohif", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "none", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxTemporaryLockouts": 0, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "b886fa27-974b-446f-adaa-a2c96342ce05", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "37c16268-9c83-41c3-b452-3fbc86e6966d", + "attributes": {} + }, + { + "id": "d8bba2d8-ac65-46cb-a1b3-bbee4850333f", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "37c16268-9c83-41c3-b452-3fbc86e6966d", + "attributes": {} + }, + { + "id": "4d80d451-18c2-4982-b2b7-f43aad1b54aa", + "name": "default-roles-ohif", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "account": [ + "manage-account", + "view-profile" + ] + } + }, + "clientRole": false, + "containerId": "37c16268-9c83-41c3-b452-3fbc86e6966d", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "0c749b40-bda4-463a-b1c2-edd014606c8c", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "70605217-ff53-4895-8686-2f87bb81cecd", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "1482af22-41e6-4850-b5a3-3d479edd334b", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-users", + "query-groups" + ] + } + }, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "7b2b0fcd-3539-4322-bd12-379398ff9c63", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "f5856ab0-17fe-49bb-8e04-27d1f37c53c7", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "5cd89fbb-7860-4505-90b8-ad99b28a7ce2", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "c53a6ea6-9c79-485f-9402-b2607df09f53", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "696a4591-d966-446c-a1f6-9a8a8de41f41", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "3b0c1a20-0692-4594-973f-bd7c0d42631b", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "3279ff3a-ae5c-4d59-99a3-3b2791391745", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "56767ac1-1098-41c9-8b94-b9efe47fa17a", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "e231ed30-1c79-4786-80dc-c16d87866b03", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "388304d0-befe-4498-99cd-c9821dfe5ff6", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "manage-identity-providers", + "view-realm", + "view-users", + "query-groups", + "manage-realm", + "manage-users", + "view-authorization", + "query-clients", + "manage-authorization", + "query-realms", + "create-client", + "query-users", + "view-events", + "view-identity-providers", + "manage-clients", + "manage-events", + "view-clients", + "impersonation" + ] + } + }, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "901b8b02-4525-4ed9-b6c5-ee822a874236", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "d9c612fa-421a-4463-8837-4bcc059649ea", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "efbd010a-67a1-472c-b308-a91723ebe819", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "60cbee1d-9fa4-47ee-8f86-f71ae22482c1", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "84219236-4eaa-4ea7-a94c-52d6f8e1bb79", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "d46f77b0-ed82-4580-994f-8a6a8fc22480", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + } + ], + "security-admin-console": [], + "ohif_viewer": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "3e2b2c4a-416e-463d-8907-b919d17a4592", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "8d52074d-c27d-4917-8280-a33bcae47f59", + "attributes": {} + } + ], + "account": [ + { + "id": "1bd17278-c076-41df-9559-7f3524d5cd5e", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "50a0e32a-f503-497b-b3c3-ff127cc56a56", + "attributes": {} + }, + { + "id": "302c47ca-5e53-4cbe-9670-4e10a281d2bf", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "50a0e32a-f503-497b-b3c3-ff127cc56a56", + "attributes": {} + }, + { + "id": "a6bd8131-df31-4922-b7da-7011d7e967db", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "50a0e32a-f503-497b-b3c3-ff127cc56a56", + "attributes": {} + }, + { + "id": "eed205fe-b606-4562-8b65-503e3d4d7d89", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "50a0e32a-f503-497b-b3c3-ff127cc56a56", + "attributes": {} + }, + { + "id": "ea20e235-92da-4409-9a82-2dc4244bc342", + "name": "view-groups", + "description": "${role_view-groups}", + "composite": false, + "clientRole": true, + "containerId": "50a0e32a-f503-497b-b3c3-ff127cc56a56", + "attributes": {} + }, + { + "id": "ccca6524-fbc9-4712-a703-f4311b94e757", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "50a0e32a-f503-497b-b3c3-ff127cc56a56", + "attributes": {} + }, + { + "id": "1b2fa98f-7ff0-4bf6-974c-c3e8b36d8dc3", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "50a0e32a-f503-497b-b3c3-ff127cc56a56", + "attributes": {} + }, + { + "id": "dc213956-175c-4ef1-99e8-0033818761f4", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "50a0e32a-f503-497b-b3c3-ff127cc56a56", + "attributes": {} + } + ] + } + }, + "groups": [ + { + "id": "3249b4f1-6572-4b59-a206-7e707d1e45f6", + "name": "pacsadmin", + "path": "/pacsadmin", + "subGroups": [], + "attributes": {}, + "realmRoles": [], + "clientRoles": {} + } + ], + "defaultRole": { + "id": "4d80d451-18c2-4982-b2b7-f43aad1b54aa", + "name": "default-roles-ohif", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "37c16268-9c83-41c3-b452-3fbc86e6966d" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName", + "totpAppMicrosoftAuthenticatorName" + ], + "localizationTexts": {}, + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyExtraOrigins": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "webAuthnPolicyPasswordlessExtraOrigins": [], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account", + "view-groups" + ] + } + ] + }, + "clients": [ + { + "id": "50a0e32a-f503-497b-b3c3-ff127cc56a56", + "clientId": "account", + "name": "${client_account}", + "description": "", + "rootUrl": "${authAdminUrl}", + "adminUrl": "", + "baseUrl": "/realms/ohif/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/ohif/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "post.logout.redirect.uris": "+", + "display.on.consent.screen": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "5c719a4e-2eed-4b9a-9536-edafb7763e6e", + "clientId": "account-console", + "name": "${client_account-console}", + "description": "", + "rootUrl": "${authAdminUrl}", + "adminUrl": "", + "baseUrl": "/realms/ohif/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/ohif/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "post.logout.redirect.uris": "+", + "display.on.consent.screen": "false", + "oauth2.device.authorization.grant.enabled": "false", + "pkce.code.challenge.method": "S256", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "2a172f00-5ae2-400a-8aba-0c8f267844a3", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "5f86b71b-9a75-465a-9007-9cadd861a1c5", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "8d52074d-c27d-4917-8280-a33bcae47f59", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "ceddadb2-f4b6-4a1d-a0c8-efdd95fd2e9a", + "clientId": "ohif_viewer", + "name": "", + "description": "", + "rootUrl": "http://127.0.0.1", + "adminUrl": "http://127.0.0.1", + "baseUrl": "http://127.0.0.1", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "2Xtlde7aozdkzzYHdIxQNfPDr0wNPTgg", + "redirectUris": [ + "http://127.0.0.1/oauth2/callback" + ], + "webOrigins": [ + "http://127.0.0.1" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1718045868", + "backchannel.logout.session.required": "true", + "display.on.consent.screen": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "b405129c-ed1b-4c4e-b712-1f736be6440b", + "name": "Audience", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "consentRequired": false, + "config": { + "included.client.audience": "ohif_viewer", + "id.token.claim": "true", + "lightweight.claim": "false", + "access.token.claim": "true", + "introspection.token.claim": "true" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "groups", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "ace68c1f-eae4-4d93-ab98-0005a071177d", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/ohif/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/ohif/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "fdee0223-2998-486a-b591-ae6712ae7831", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "73b70709-cb2f-4d06-86af-0b04425970ee", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "0c8df10b-097f-4b13-85b8-3ec3b4d3ef03", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "6cff88ad-73ed-4699-86f9-c7ac5e473a53", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "4098872d-e896-4042-a715-be23f0a1437c", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "06ba9538-581f-4c0b-a7ca-b56eaa8c1e59", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "d992a9b9-ec09-49fd-b052-65942ff5ba58", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "id": "9b2089a4-ce6f-4dcb-abd8-67844a8e17b5", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "0e37cc54-2ff9-4976-9f66-f6f949652d40", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "f2d87384-cc84-4d8a-bef7-379491ba1430", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "03c9b60a-32a9-4bd2-9173-ee7048f8face", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "6db8e846-d71d-4573-89be-cc249c93c4fd", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "aacde81e-9462-446f-abf9-25bb83d3c442", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "44dc5e2c-de20-4e46-94ed-e5fbecee8021", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "74ab1492-ecbb-4a52-a5dd-820d7bc1dec5", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "69aab70e-94d5-45f0-ab8f-14c973492636", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "611957f1-cdfe-43b9-acc6-d2d37b5b6908", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "90e9af6c-7a44-4b8b-bdea-020ccd7cf79b", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "d1ceeaef-bebc-4be6-b5b7-85e361b91ce5", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "d74b3e90-c391-4fea-806d-98b24e598ade", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "077a40b6-ae18-440a-9858-57cb93483f4d", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "f7a704cc-7466-41b4-ada4-3efe4dceb599", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "16085075-08fa-4fe8-aa9e-c429b26df40e", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "1cb74178-9ba0-4225-8ede-a263f0cf0f9d", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "id": "020299f1-b469-41fb-b80d-5cea4ceb4e7d", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String" + } + }, + { + "id": "6759edaf-7d37-44b0-ad7b-50a3383e9cd9", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "userinfo.token.claim": "false", + "user.attribute": "foo", + "lightweight.claim": "false", + "id.token.claim": "false", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}. roles", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "905660b4-380a-4a8c-81a9-bd16b3b3ce3f", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "4e2f2bd8-602f-4ae1-a431-fa92bdedd386", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "f1e69dc4-f19e-4c21-bbf9-609016339710", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "24748647-8a70-422e-8532-f8d231a006db", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "fd884f9e-e345-4823-b463-beb39020ca23", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "db5c28fd-e142-40e1-926c-017d0d58c04a", + "name": "groups", + "description": "", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "gui.order": "", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "1f5afdb5-1de2-40c7-8526-d1ad0585720b", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-group-membership-mapper", + "consentRequired": false, + "config": { + "full.path": "false", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "multivalued": "true", + "id.token.claim": "true", + "lightweight.claim": "false", + "access.token.claim": "true", + "claim.name": "groups" + } + } + ] + }, + { + "id": "c1678c18-641f-4eeb-af7e-c5327adee009", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "bec886da-1100-4af1-a83f-b322a23e9856", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "ead6084e-d9f1-4388-80c6-aead5b3ddfff", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "3a4ceb4e-e597-49e4-8d0c-d41a46a2ab38", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "introspection.token.claim": "true", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "df3a89a6-3ed7-45ad-97e1-598b93a643e7", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "9bbff1aa-ee20-4c33-b40f-d0076a2fe305", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "726d1ea0-ee59-49fe-bd01-416b06276ae9", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "referrerPolicy": "no-referrer", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "e037fff2-9457-47c7-81c7-762fbe02d23c", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "cbd25133-ce87-483c-b2e3-a3c2947b23e9", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "oidc-usermodel-property-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-address-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-role-list-mapper", + "saml-user-property-mapper", + "oidc-full-name-mapper" + ] + } + }, + { + "id": "2fa7da7b-1138-44f9-8236-7e31f5f72695", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "d2e6c9ff-9c36-40cc-bc12-0055d5ea61c3", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "269704c3-de60-4233-a845-6efd83408eb0", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-role-list-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-full-name-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-property-mapper", + "saml-user-attribute-mapper", + "saml-user-property-mapper", + "oidc-address-mapper" + ] + } + }, + { + "id": "28cb7c8a-2fd1-4a3d-8ab8-17e4e83c2648", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "3a77be15-e696-49aa-9dac-64a79d8e443f", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "f662e962-d5bd-4bbe-bc43-bddc7f4faf57", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "5a307075-97e6-4c78-8e05-10e21c097d53", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "78a25e8e-b46c-44c2-8fd4-4b09529890e7", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "bf0f6364-43f0-4b1c-bde2-7646ceb56a63", + "name": "hmac-generated-hs512", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS512" + ] + } + }, + { + "id": "28485c9c-96bc-49c7-a9a2-8941c9067f7d", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "bbb2d80e-a134-4933-afe9-7cb655d70791", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "651f23c0-4ef1-4762-b186-fc2b985ead9d", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "e8dd531a-b8b7-4416-984f-9226cfcc8800", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "e4fc59c9-1048-45b5-9069-57d578d0f125", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "12ea6433-d1b6-4094-b3e2-1ef356b91d92", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "5d6c3ca7-3581-4200-b592-9c714129044f", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "b7712f6e-fbe8-4f15-936d-939e28efd279", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "61a450b5-3007-4e98-a7b2-dba743ea54a7", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "d7566b94-731b-4e84-a2de-a1c8130af38b", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "399c5ce6-4308-4524-89bc-4dee1afd6cbb", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "c8e80f39-d8d7-4fa5-8944-33fcdd366721", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "79251c55-7de3-457e-99e3-92395f2b69a7", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "60a12c1d-d4ce-40c1-a83f-faba71145f0c", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "e36da2db-a596-4680-a5ca-16c4e5a1a9c1", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "2b1b59ff-5417-48cd-8d7a-2ab5ecfedc9f", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "3e4f2042-a809-490c-a1a6-9bb63b4b0e4a", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-terms-and-conditions", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 70, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "0b59d69a-95b0-4c82-bd21-908c019a4974", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "accf37f2-1a52-40b0-a92f-f5a931c57126", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "4eaad8bb-f86b-483d-99a4-4cba184ef573", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "eb0a326f-17e0-49b7-b25c-575b0cd3888f", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 90, + "config": {} + }, + { + "alias": "delete_credential", + "name": "Delete Credential", + "providerId": "delete_credential", + "enabled": true, + "defaultAction": false, + "priority": 100, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "firstBrokerLoginFlow": "first broker login", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "oauth2DevicePollingInterval": "5", + "parRequestUriLifespan": "60", + "cibaInterval": "5", + "realmReusableOtpCode": "false" + }, + "keycloakVersion": "24.0.5", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + }, + "users": [ + { + "username": "viewer", + "enabled": true, + "emailVerified": true, + "firstName": "viewer", + "lastName": "viewer", + "email": "viewer@mail.com", + "credentials": [ + { + "type": "password", + "value": "viewer" + } + ] + }, + { + "username": "pacsadmin", + "enabled": true, + "emailVerified": true, + "firstName": "pacsadmin", + "lastName": "pacsadmin", + "email": "pacsadmin@mail.com", + "credentials": [ + { + "type": "password", + "value": "pacsadmin" + } + ], + "groups": ["pacsadmin"] + } + ] +} diff --git a/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/docker-compose.env b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/docker-compose.env new file mode 100644 index 0000000..54961c3 --- /dev/null +++ b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/docker-compose.env @@ -0,0 +1,4 @@ +STORAGE_DIR=/storage/fs1 +POSTGRES_DB=pacsdb +POSTGRES_USER=pacs +POSTGRES_PASSWORD=pacs diff --git a/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/docker-compose.yml b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/docker-compose.yml new file mode 100644 index 0000000..1c3aa0e --- /dev/null +++ b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/docker-compose.yml @@ -0,0 +1,161 @@ +version: '3.8' + +services: + ldap: + image: dcm4che/slapd-dcm4chee:2.6.3-29.0 + logging: + driver: json-file + options: + max-size: "10m" + ports: + - "389:389" + env_file: docker-compose.env + volumes: + - ~/dcm4chee-arc/ldap:/var/lib/ldap + - ~/dcm4chee-arc/slapd.d:/etc/ldap/slapd.d + + db: + image: dcm4che/postgres-dcm4chee:14.5-29 + logging: + driver: json-file + options: + max-size: "10m" + ports: + - "5432:5432" + env_file: docker-compose.env + volumes: + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro + - ~/dcm4chee-arc/db:/var/lib/postgresql/data + + arc: + image: dcm4che/dcm4chee-arc-psql:5.29.0 + logging: + driver: json-file + options: + max-size: "10m" + ports: + - "8080:8080" + - "8443:8443" + - "9990:9990" + - "9993:9993" + - "11112:11112" + - "2762:2762" + - "2575:2575" + - "12575:12575" + env_file: docker-compose.env + environment: + WILDFLY_CHOWN: /opt/wildfly/standalone /storage + WILDFLY_WAIT_FOR: ldap:389 db:5432 + depends_on: + - ldap + - db + volumes: + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro + - ~/dcm4chee-arc/wildfly:/opt/wildfly/standalone + - ~/dcm4chee-arc/storage:/storage + + ohif_viewer: + build: + context: ./../../../../ + dockerfile: ./platform/app/.recipes/Nginx-Dcm4chee-Keycloak/dockerfile + image: webapp:latest + container_name: webapp + ports: + - '443:443' # SSL + - '80:80' # Web + depends_on: + keycloak: + condition: service_healthy + restart: on-failure + networks: + - default + extra_hosts: + - 'host.docker.internal:host-gateway' + environment: + - OAUTH2_PROXY_SKIP_PROVIDER_BUTTON=true + volumes: + - ./config/nginx.conf:/etc/nginx/nginx.conf + - ./config/oauth2-proxy.cfg:/etc/oauth2-proxy/oauth2-proxy.cfg + - ./config/letsencrypt:/etc/letsencrypt + - ./config/certbot:/var/www/certbot + + keycloak: + image: quay.io/keycloak/keycloak:24.0.5 + command: 'start-dev --import-realm' + hostname: keycloak + container_name: keycloak + volumes: + - ./config/ohif-keycloak-realm.json:/opt/keycloak/data/import/ohif-keycloak-realm.json + environment: + KC_DB_URL_HOST: postgres + KC_DB: postgres + KC_DB_URL: 'jdbc:postgresql://postgres:5432/keycloak' + KC_DB_SCHEMA: public + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: password + KC_HOSTNAME_ADMIN_URL: http://YOUR_DOMAIN/keycloak/ + KC_HOSTNAME_URL: http://YOUR_DOMAIN/keycloak/ + KC_HOSTNAME_STRICT_BACKCHANNEL: true + KC_HOSTNAME_STRICT_HTTPS: false + KC_HTTP_ENABLED: true + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + KC_HEALTH_ENABLED: true + KC_METRICS_ENABLED: true + KC_PROXY: edge + KC_PROXY_HEADERS: xforwarded + KEYCLOAK_JDBC_PARAMS: connectTimeout=40000 + KC_LOG_LEVEL: INFO + KC_HOSTNAME_DEBUG: true + PROXY_ADDRESS_FORWARDING: true + ports: + - 8081:8080 + depends_on: + - postgres + restart: unless-stopped + networks: + - default + extra_hosts: + - 'host.docker.internal:host-gateway' + healthcheck: + test: + [ + "CMD-SHELL", + "exec 3<>/dev/tcp/YOUR_DOMAIN/8080;echo -e \"GET /health/ready HTTP/1.1\r\nhost: http://localhost\r\nConnection: close\r\n\r\n\" >&3;grep \"HTTP/1.1 200 OK\" <&3" + ] + interval: 1s + timeout: 5s + retries: 10 + start_period: 60s + + postgres: + image: postgres:15 + hostname: postgres + container_name: postgres + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: keycloak + POSTGRES_PASSWORD: password + restart: unless-stopped + networks: + - default + + certbot: + image: certbot/certbot + container_name: certbot + volumes: + - ./config/letsencrypt:/etc/letsencrypt + - ./config/certbot:/var/www/certbot + entrypoint: /bin/sh -c "trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;" + +volumes: + postgres_data: + driver: local + +networks: + default: + driver: bridge diff --git a/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/dockerfile b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/dockerfile new file mode 100644 index 0000000..4182de8 --- /dev/null +++ b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/dockerfile @@ -0,0 +1,50 @@ +# Stage 1: Build the application +FROM node:20.18.1-slim as builder + +# Setup the working directory +RUN mkdir /usr/src/app +WORKDIR /usr/src/app + +# Install dependencies +RUN apt-get update && apt-get install -y build-essential python3 + +# Copy the entire project +COPY ./ /usr/src/app/ + +# Install node dependencies +RUN yarn config set workspaces-experimental true +RUN yarn install + +# Set the environment for the build +ENV APP_CONFIG=config/docker-nginx-dcm4chee-keycloak.js + +# Build the application +RUN yarn run build + +# Stage 2: Setup the NGINX environment with OAuth2 Proxy +FROM nginx:alpine + +# Install dependencies for oauth2-proxy +RUN apk add --no-cache curl + +# Create necessary directories +RUN mkdir -p /var/logs/nginx /var/www/html /etc/oauth2-proxy + +# Download and install oauth2-proxy +RUN curl -L https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v7.4.0/oauth2-proxy-v7.4.0.linux-amd64.tar.gz -o oauth2-proxy.tar.gz && \ + tar -xvzf oauth2-proxy.tar.gz && \ + mv oauth2-proxy-v7.4.0.linux-amd64/oauth2-proxy /usr/local/bin/ && \ + rm -rf oauth2-proxy-v7.4.0.linux-amd64 oauth2-proxy.tar.gz + +# Copy the built application +COPY --from=builder /usr/src/app/platform/app/dist /var/www/html + +# Copy the entrypoint script +COPY ./platform/app/.recipes/Nginx-Dcm4chee-Keycloak/config/entrypoint.sh /entrypoint.sh + +# Expose necessary ports +EXPOSE 80 443 4180 + +# Set the entrypoint script as the entrypoint +RUN chmod +x /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] diff --git a/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/etc/localtime b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/etc/localtime new file mode 100644 index 0000000..e69de29 diff --git a/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/etc/timezone b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/etc/timezone new file mode 100644 index 0000000..27f725e --- /dev/null +++ b/platform/app/.recipes/Nginx-Dcm4chee-Keycloak/etc/timezone @@ -0,0 +1 @@ +America/New_York \ No newline at end of file diff --git a/platform/app/.recipes/Nginx-Dcm4chee/config/nginx.conf b/platform/app/.recipes/Nginx-Dcm4chee/config/nginx.conf new file mode 100644 index 0000000..ae7e84a --- /dev/null +++ b/platform/app/.recipes/Nginx-Dcm4chee/config/nginx.conf @@ -0,0 +1,84 @@ +worker_processes auto; +error_log /var/logs/nginx/error.log debug; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/logs/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + gzip on; + gzip_disable "msie6"; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; + + server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + + client_max_body_size 0; + + + # Handle /pacs requests and rewrite them to the correct dcm4chee-arc UI path + # This allows accessing the dcm4chee-arc UI through the /pacs URL + location /pacs { + rewrite ^/pacs(.*)$ /dcm4chee-arc/ui2$1 break; + proxy_pass http://arc:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_request_buffering off; + expires 0; + add_header Cache-Control private; + } + + # Proxy all dcm4chee-arc requests + # This block handles all API requests and general dcm4chee-arc paths + location /dcm4chee-arc/ { + proxy_pass http://arc:8080/dcm4chee-arc/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_request_buffering off; + } + + + location /sw.js { + add_header Cache-Control "no-cache"; + proxy_cache_bypass $http_pragma; + proxy_cache_revalidate on; + expires off; + access_log off; + } + + location / { + root /var/www/html; + index index.html; + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } + } +} diff --git a/platform/app/.recipes/Nginx-Dcm4chee/docker-compose.env b/platform/app/.recipes/Nginx-Dcm4chee/docker-compose.env new file mode 100644 index 0000000..54961c3 --- /dev/null +++ b/platform/app/.recipes/Nginx-Dcm4chee/docker-compose.env @@ -0,0 +1,4 @@ +STORAGE_DIR=/storage/fs1 +POSTGRES_DB=pacsdb +POSTGRES_USER=pacs +POSTGRES_PASSWORD=pacs diff --git a/platform/app/.recipes/Nginx-Dcm4chee/docker-compose.yml b/platform/app/.recipes/Nginx-Dcm4chee/docker-compose.yml new file mode 100644 index 0000000..88aad58 --- /dev/null +++ b/platform/app/.recipes/Nginx-Dcm4chee/docker-compose.yml @@ -0,0 +1,73 @@ +services: + ldap: + image: dcm4che/slapd-dcm4chee:2.6.3-29.0 + logging: + driver: json-file + options: + max-size: "10m" + ports: + - "389:389" + env_file: docker-compose.env + volumes: + - ~/dcm4chee-arc/ldap:/var/lib/ldap + - ~/dcm4chee-arc/slapd.d:/etc/ldap/slapd.d + db: + image: dcm4che/postgres-dcm4chee:14.5-29 + logging: + driver: json-file + options: + max-size: "10m" + ports: + - "5432:5432" + env_file: docker-compose.env + volumes: + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro + - ~/dcm4chee-arc/db:/var/lib/postgresql/data + arc: + image: dcm4che/dcm4chee-arc-psql:5.29.0 + logging: + driver: json-file + options: + max-size: "10m" + ports: + - "8080:8080" + - "8443:8443" + - "9990:9990" + - "9993:9993" + - "11112:11112" + - "2762:2762" + - "2575:2575" + - "12575:12575" + env_file: docker-compose.env + environment: + WILDFLY_CHOWN: /opt/wildfly/standalone /storage + WILDFLY_WAIT_FOR: ldap:389 db:5432 + depends_on: + - ldap + - db + volumes: + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro + - ~/dcm4chee-arc/wildfly:/opt/wildfly/standalone + - ~/dcm4chee-arc/storage:/storage + ohif_viewer: + build: + # Project root + context: ./../../../../ + # Relative to context + dockerfile: ./platform/app/.recipes/Nginx-Dcm4chee/dockerfile + image: webapp:latest + container_name: ohif_dcm4chee + volumes: + # Nginx config + - ./config/nginx.conf:/etc/nginx/nginx.conf + # Logs + - ./logs/nginx:/var/logs/nginx + # Let's Encrypt + # - letsencrypt_certificates:/etc/letsencrypt + # - letsencrypt_challenges:/var/www/letsencrypt + ports: + - '443:443' # SSL + - '80:80' # Web + restart: on-failure diff --git a/platform/app/.recipes/Nginx-Dcm4chee/dockerfile b/platform/app/.recipes/Nginx-Dcm4chee/dockerfile new file mode 100644 index 0000000..bbc6f89 --- /dev/null +++ b/platform/app/.recipes/Nginx-Dcm4chee/dockerfile @@ -0,0 +1,43 @@ +# Stage 1: Build the application +FROM node:20.18.1-slim as builder + +# Setup the working directory +RUN mkdir /usr/src/app +WORKDIR /usr/src/app + +# Install dependencies +# apt-get update is combined with apt-get install to avoid using outdated packages +RUN apt-get update && apt-get install -y build-essential python3 + +# Copy package.json and other dependency-related files first +# Assuming your package.json and yarn.lock or similar are located in the project root + +COPY ./ /usr/src/app/ + +# Install node dependencies +RUN yarn config set workspaces-experimental true +RUN yarn install + +# Copy the rest of the application code + +# set QUICK_BUILD to true to make the build faster for dev +ENV APP_CONFIG=config/docker-nginx-dcm4chee.js + +# Build the application +RUN yarn run build + +# # Stage 2: Bundle the built application into a Docker container which runs NGINX using Alpine Linux +FROM nginx:alpine + +# # Create directories for logs and html content if they don't already exist +RUN mkdir -p /var/log/nginx /var/www/html + + +# # Copy build output to serve static files +COPY --from=builder /usr/src/app/platform/app/dist /var/www/html + +# # Expose HTTP and HTTPS ports +EXPOSE 80 443 + +# # Start NGINX +CMD ["nginx", "-g", "daemon off;"] diff --git a/platform/app/.recipes/Nginx-Dcm4chee/etc/localtime b/platform/app/.recipes/Nginx-Dcm4chee/etc/localtime new file mode 100644 index 0000000..e69de29 diff --git a/platform/app/.recipes/Nginx-Dcm4chee/etc/timezone b/platform/app/.recipes/Nginx-Dcm4chee/etc/timezone new file mode 100644 index 0000000..27f725e --- /dev/null +++ b/platform/app/.recipes/Nginx-Dcm4chee/etc/timezone @@ -0,0 +1 @@ +America/New_York \ No newline at end of file diff --git a/platform/app/.recipes/Nginx-Orthanc-Keycloak/.gitignore b/platform/app/.recipes/Nginx-Orthanc-Keycloak/.gitignore new file mode 100644 index 0000000..088f9a9 --- /dev/null +++ b/platform/app/.recipes/Nginx-Orthanc-Keycloak/.gitignore @@ -0,0 +1,6 @@ +logs/* +volumes/* +config/letsencrypt/* +config/certbot/* +!config/letsencrypt/.gitkeep +!config/certbot/.gitkeep diff --git a/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/certbot/.gitkeep b/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/certbot/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/entrypoint.sh b/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/entrypoint.sh new file mode 100644 index 0000000..8648d7c --- /dev/null +++ b/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Start oauth2-proxy +oauth2-proxy --config=/etc/oauth2-proxy/oauth2-proxy.cfg & + +# Start nginx +nginx -g "daemon off;" diff --git a/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/letsencrypt/.gitkeep b/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/letsencrypt/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/nginx.conf b/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/nginx.conf new file mode 100644 index 0000000..272a478 --- /dev/null +++ b/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/nginx.conf @@ -0,0 +1,208 @@ +worker_processes 2; +error_log /var/logs/nginx/mydomain.error.log; +pid /var/run/nginx.pid; +include /usr/share/nginx/modules/*.conf; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include '/etc/nginx/mime.types'; + default_type application/octet-stream; + + keepalive_timeout 65; + keepalive_requests 100000; + tcp_nopush on; + tcp_nodelay on; + + proxy_buffers 16 16k; + proxy_buffer_size 32k; + proxy_busy_buffers_size 64k; + proxy_max_temp_file_size 128k; + + server { + listen 80; + server_name YOUR_DOMAIN; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } + } + + server { + listen 443 ssl; + server_name YOUR_DOMAIN; + + ssl_certificate /etc/letsencrypt/live/ohifviewer.duckdns.org/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/ohifviewer.duckdns.org/privkey.pem; + + root /var/www/html; + + gzip on; + gzip_types text/css application/javascript application/json image/svg+xml; + gzip_comp_level 9; + etag on; + + location /sw.js { + add_header Cache-Control "no-cache"; + proxy_cache_bypass $http_pragma; + proxy_cache_revalidate on; + expires off; + access_log off; + } + + location /oauth2 { + expires -1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Auth-Request-Redirect $request_uri; + proxy_pass http://localhost:4180$uri$is_args$args; + } + + location /oauth2/callback { + proxy_pass http://localhost:4180; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /oauth2/sign_out { + expires -1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Auth-Request-Redirect /oauth2/sign_in; + proxy_pass http://localhost:4180; + } + + location /pacs-admin/ { + error_page 401 = /oauth2/sign_in?rd=$scheme://$host$request_uri; + auth_request /oauth2/auth?allowed_groups=pacsadmin; + + auth_request_set $user $upstream_http_x_auth_request_user; + auth_request_set $token $upstream_http_x_auth_request_access_token; + auth_request_set $auth_cookie $upstream_http_set_cookie; + + proxy_set_header X-User $user; + proxy_set_header X-Access-Token $token; + add_header Set-Cookie $auth_cookie; + + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + expires 0; + add_header Cache-Control private; + + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept' always; + + if ($request_method = OPTIONS) { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept'; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain; charset=utf-8'; + add_header 'Content-Length' 0; + return 204; + } + + proxy_pass http://orthanc:8042/; + } + + location /pacs-admin { + return 301 /pacs-admin/; + } + + location /pacs/ { + auth_request /oauth2/auth; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + expires 0; + add_header Cache-Control private; + + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept' always; + + if ($request_method = OPTIONS) { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept'; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain; charset=utf-8'; + add_header 'Content-Length' 0; + return 204; + } + + proxy_pass http://orthanc:8042/dicom-web/; + } + + location /pacs { + return 301 /pacs/; + } + + location /ohif-viewer/ { + expires -1; + error_page 401 = /oauth2/sign_in?rd=$scheme://$host$request_uri; + auth_request /oauth2/auth; + + auth_request_set $user $upstream_http_x_auth_request_user; + auth_request_set $token $upstream_http_x_auth_request_access_token; + auth_request_set $auth_cookie $upstream_http_set_cookie; + + proxy_set_header X-User $user; + proxy_set_header X-Access-Token $token; + add_header Set-Cookie $auth_cookie; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-Proto $scheme; + + index index.html; + try_files $uri $uri/ /index.html; + } + + location /ohif-viewer { + return 301 /ohif-viewer/; + } + + location = / { + return 301 /ohif-viewer/; + } + + location / { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } + + location /keycloak/ { + proxy_pass http://keycloak:8080/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /keycloak { + return 301 /keycloak/; + } + } +} diff --git a/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/oauth2-proxy.cfg b/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/oauth2-proxy.cfg new file mode 100644 index 0000000..985525a --- /dev/null +++ b/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/oauth2-proxy.cfg @@ -0,0 +1,22 @@ +http_address="0.0.0.0:4180" +cookie_secret="GENERATEACOOKIESECRET----------------------=" +email_domains=["*"] +cookie_secure="false" +cookie_expire="9m30s" +cookie_refresh="5m" +client_secret="2Xtlde7aozdkzzYHdIxQNfPDr0wNPTgg" +client_id="ohif_viewer" +redirect_url="http://YOUR_DOMAIN/oauth2/callback" + +ssl_insecure_skip_verify = true +insecure_oidc_allow_unverified_email = true +pass_access_token = true +provider="keycloak-oidc" +provider_display_name="Keycloak" +user_id_claim="oid" +oidc_email_claim="sub" +scope="openid" +pass_host_header=true +code_challenge_method="S256" +oidc_issuer_url="http://YOUR_DOMAIN/keycloak/realms/ohif" +insecure_oidc_skip_issuer_verification = true diff --git a/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/ohif-keycloak-realm.json b/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/ohif-keycloak-realm.json new file mode 100644 index 0000000..3064ee7 --- /dev/null +++ b/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/ohif-keycloak-realm.json @@ -0,0 +1,2315 @@ +{ + "id": "37c16268-9c83-41c3-b452-3fbc86e6966d", + "realm": "ohif", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "none", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxTemporaryLockouts": 0, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "b886fa27-974b-446f-adaa-a2c96342ce05", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "37c16268-9c83-41c3-b452-3fbc86e6966d", + "attributes": {} + }, + { + "id": "d8bba2d8-ac65-46cb-a1b3-bbee4850333f", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "37c16268-9c83-41c3-b452-3fbc86e6966d", + "attributes": {} + }, + { + "id": "4d80d451-18c2-4982-b2b7-f43aad1b54aa", + "name": "default-roles-ohif", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "account": [ + "manage-account", + "view-profile" + ] + } + }, + "clientRole": false, + "containerId": "37c16268-9c83-41c3-b452-3fbc86e6966d", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "0c749b40-bda4-463a-b1c2-edd014606c8c", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "70605217-ff53-4895-8686-2f87bb81cecd", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "1482af22-41e6-4850-b5a3-3d479edd334b", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-users", + "query-groups" + ] + } + }, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "7b2b0fcd-3539-4322-bd12-379398ff9c63", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "f5856ab0-17fe-49bb-8e04-27d1f37c53c7", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "5cd89fbb-7860-4505-90b8-ad99b28a7ce2", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "c53a6ea6-9c79-485f-9402-b2607df09f53", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "696a4591-d966-446c-a1f6-9a8a8de41f41", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "3b0c1a20-0692-4594-973f-bd7c0d42631b", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "3279ff3a-ae5c-4d59-99a3-3b2791391745", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "56767ac1-1098-41c9-8b94-b9efe47fa17a", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "e231ed30-1c79-4786-80dc-c16d87866b03", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "388304d0-befe-4498-99cd-c9821dfe5ff6", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "manage-identity-providers", + "view-realm", + "view-users", + "query-groups", + "manage-realm", + "manage-users", + "view-authorization", + "query-clients", + "manage-authorization", + "query-realms", + "create-client", + "query-users", + "view-events", + "view-identity-providers", + "manage-clients", + "manage-events", + "view-clients", + "impersonation" + ] + } + }, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "901b8b02-4525-4ed9-b6c5-ee822a874236", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "d9c612fa-421a-4463-8837-4bcc059649ea", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "efbd010a-67a1-472c-b308-a91723ebe819", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "60cbee1d-9fa4-47ee-8f86-f71ae22482c1", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "84219236-4eaa-4ea7-a94c-52d6f8e1bb79", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + }, + { + "id": "d46f77b0-ed82-4580-994f-8a6a8fc22480", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "attributes": {} + } + ], + "security-admin-console": [], + "ohif_viewer": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "3e2b2c4a-416e-463d-8907-b919d17a4592", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "8d52074d-c27d-4917-8280-a33bcae47f59", + "attributes": {} + } + ], + "account": [ + { + "id": "1bd17278-c076-41df-9559-7f3524d5cd5e", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "50a0e32a-f503-497b-b3c3-ff127cc56a56", + "attributes": {} + }, + { + "id": "302c47ca-5e53-4cbe-9670-4e10a281d2bf", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "50a0e32a-f503-497b-b3c3-ff127cc56a56", + "attributes": {} + }, + { + "id": "a6bd8131-df31-4922-b7da-7011d7e967db", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "50a0e32a-f503-497b-b3c3-ff127cc56a56", + "attributes": {} + }, + { + "id": "eed205fe-b606-4562-8b65-503e3d4d7d89", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "50a0e32a-f503-497b-b3c3-ff127cc56a56", + "attributes": {} + }, + { + "id": "ea20e235-92da-4409-9a82-2dc4244bc342", + "name": "view-groups", + "description": "${role_view-groups}", + "composite": false, + "clientRole": true, + "containerId": "50a0e32a-f503-497b-b3c3-ff127cc56a56", + "attributes": {} + }, + { + "id": "ccca6524-fbc9-4712-a703-f4311b94e757", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "50a0e32a-f503-497b-b3c3-ff127cc56a56", + "attributes": {} + }, + { + "id": "1b2fa98f-7ff0-4bf6-974c-c3e8b36d8dc3", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "50a0e32a-f503-497b-b3c3-ff127cc56a56", + "attributes": {} + }, + { + "id": "dc213956-175c-4ef1-99e8-0033818761f4", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "50a0e32a-f503-497b-b3c3-ff127cc56a56", + "attributes": {} + } + ] + } + }, + "groups": [ + { + "id": "3249b4f1-6572-4b59-a206-7e707d1e45f6", + "name": "pacsadmin", + "path": "/pacsadmin", + "subGroups": [], + "attributes": {}, + "realmRoles": [], + "clientRoles": {} + } + ], + "defaultRole": { + "id": "4d80d451-18c2-4982-b2b7-f43aad1b54aa", + "name": "default-roles-ohif", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "37c16268-9c83-41c3-b452-3fbc86e6966d" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName", + "totpAppMicrosoftAuthenticatorName" + ], + "localizationTexts": {}, + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyExtraOrigins": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "webAuthnPolicyPasswordlessExtraOrigins": [], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account", + "view-groups" + ] + } + ] + }, + "clients": [ + { + "id": "50a0e32a-f503-497b-b3c3-ff127cc56a56", + "clientId": "account", + "name": "${client_account}", + "description": "", + "rootUrl": "${authAdminUrl}", + "adminUrl": "", + "baseUrl": "/realms/ohif/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/ohif/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "post.logout.redirect.uris": "+", + "display.on.consent.screen": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "5c719a4e-2eed-4b9a-9536-edafb7763e6e", + "clientId": "account-console", + "name": "${client_account-console}", + "description": "", + "rootUrl": "${authAdminUrl}", + "adminUrl": "", + "baseUrl": "/realms/ohif/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/ohif/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "post.logout.redirect.uris": "+", + "display.on.consent.screen": "false", + "oauth2.device.authorization.grant.enabled": "false", + "pkce.code.challenge.method": "S256", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "2a172f00-5ae2-400a-8aba-0c8f267844a3", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "5f86b71b-9a75-465a-9007-9cadd861a1c5", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "8d52074d-c27d-4917-8280-a33bcae47f59", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "ceddadb2-f4b6-4a1d-a0c8-efdd95fd2e9a", + "clientId": "ohif_viewer", + "name": "", + "description": "", + "rootUrl": "http://127.0.0.1", + "adminUrl": "http://127.0.0.1", + "baseUrl": "http://127.0.0.1", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "2Xtlde7aozdkzzYHdIxQNfPDr0wNPTgg", + "redirectUris": [ + "http://127.0.0.1/oauth2/callback" + ], + "webOrigins": [ + "http://127.0.0.1" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1718045868", + "backchannel.logout.session.required": "true", + "display.on.consent.screen": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "b405129c-ed1b-4c4e-b712-1f736be6440b", + "name": "Audience", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-mapper", + "consentRequired": false, + "config": { + "included.client.audience": "ohif_viewer", + "id.token.claim": "true", + "lightweight.claim": "false", + "access.token.claim": "true", + "introspection.token.claim": "true" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "groups", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "fac6a1ed-5b50-4e83-b4a5-dff4f6499abc", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "ace68c1f-eae4-4d93-ab98-0005a071177d", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/ohif/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/ohif/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "fdee0223-2998-486a-b591-ae6712ae7831", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "73b70709-cb2f-4d06-86af-0b04425970ee", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "0c8df10b-097f-4b13-85b8-3ec3b4d3ef03", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "6cff88ad-73ed-4699-86f9-c7ac5e473a53", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "4098872d-e896-4042-a715-be23f0a1437c", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "06ba9538-581f-4c0b-a7ca-b56eaa8c1e59", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "d992a9b9-ec09-49fd-b052-65942ff5ba58", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "id": "9b2089a4-ce6f-4dcb-abd8-67844a8e17b5", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "0e37cc54-2ff9-4976-9f66-f6f949652d40", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "f2d87384-cc84-4d8a-bef7-379491ba1430", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "03c9b60a-32a9-4bd2-9173-ee7048f8face", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "6db8e846-d71d-4573-89be-cc249c93c4fd", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "aacde81e-9462-446f-abf9-25bb83d3c442", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "44dc5e2c-de20-4e46-94ed-e5fbecee8021", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "74ab1492-ecbb-4a52-a5dd-820d7bc1dec5", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "69aab70e-94d5-45f0-ab8f-14c973492636", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "611957f1-cdfe-43b9-acc6-d2d37b5b6908", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "90e9af6c-7a44-4b8b-bdea-020ccd7cf79b", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "d1ceeaef-bebc-4be6-b5b7-85e361b91ce5", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "d74b3e90-c391-4fea-806d-98b24e598ade", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "077a40b6-ae18-440a-9858-57cb93483f4d", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "f7a704cc-7466-41b4-ada4-3efe4dceb599", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "16085075-08fa-4fe8-aa9e-c429b26df40e", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "1cb74178-9ba0-4225-8ede-a263f0cf0f9d", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "id": "020299f1-b469-41fb-b80d-5cea4ceb4e7d", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String" + } + }, + { + "id": "6759edaf-7d37-44b0-ad7b-50a3383e9cd9", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "userinfo.token.claim": "false", + "user.attribute": "foo", + "lightweight.claim": "false", + "id.token.claim": "false", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}. roles", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "905660b4-380a-4a8c-81a9-bd16b3b3ce3f", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "4e2f2bd8-602f-4ae1-a431-fa92bdedd386", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "f1e69dc4-f19e-4c21-bbf9-609016339710", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "24748647-8a70-422e-8532-f8d231a006db", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "fd884f9e-e345-4823-b463-beb39020ca23", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "db5c28fd-e142-40e1-926c-017d0d58c04a", + "name": "groups", + "description": "", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "gui.order": "", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "1f5afdb5-1de2-40c7-8526-d1ad0585720b", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-group-membership-mapper", + "consentRequired": false, + "config": { + "full.path": "false", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "multivalued": "true", + "id.token.claim": "true", + "lightweight.claim": "false", + "access.token.claim": "true", + "claim.name": "groups" + } + } + ] + }, + { + "id": "c1678c18-641f-4eeb-af7e-c5327adee009", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "bec886da-1100-4af1-a83f-b322a23e9856", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "ead6084e-d9f1-4388-80c6-aead5b3ddfff", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "3a4ceb4e-e597-49e4-8d0c-d41a46a2ab38", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "introspection.token.claim": "true", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "df3a89a6-3ed7-45ad-97e1-598b93a643e7", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "9bbff1aa-ee20-4c33-b40f-d0076a2fe305", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "726d1ea0-ee59-49fe-bd01-416b06276ae9", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "referrerPolicy": "no-referrer", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "e037fff2-9457-47c7-81c7-762fbe02d23c", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "cbd25133-ce87-483c-b2e3-a3c2947b23e9", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "oidc-usermodel-property-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-address-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-role-list-mapper", + "saml-user-property-mapper", + "oidc-full-name-mapper" + ] + } + }, + { + "id": "2fa7da7b-1138-44f9-8236-7e31f5f72695", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "d2e6c9ff-9c36-40cc-bc12-0055d5ea61c3", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "269704c3-de60-4233-a845-6efd83408eb0", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-role-list-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-full-name-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-property-mapper", + "saml-user-attribute-mapper", + "saml-user-property-mapper", + "oidc-address-mapper" + ] + } + }, + { + "id": "28cb7c8a-2fd1-4a3d-8ab8-17e4e83c2648", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "3a77be15-e696-49aa-9dac-64a79d8e443f", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "f662e962-d5bd-4bbe-bc43-bddc7f4faf57", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "5a307075-97e6-4c78-8e05-10e21c097d53", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "78a25e8e-b46c-44c2-8fd4-4b09529890e7", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "bf0f6364-43f0-4b1c-bde2-7646ceb56a63", + "name": "hmac-generated-hs512", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS512" + ] + } + }, + { + "id": "28485c9c-96bc-49c7-a9a2-8941c9067f7d", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "bbb2d80e-a134-4933-afe9-7cb655d70791", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "651f23c0-4ef1-4762-b186-fc2b985ead9d", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "e8dd531a-b8b7-4416-984f-9226cfcc8800", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "e4fc59c9-1048-45b5-9069-57d578d0f125", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "12ea6433-d1b6-4094-b3e2-1ef356b91d92", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "5d6c3ca7-3581-4200-b592-9c714129044f", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "b7712f6e-fbe8-4f15-936d-939e28efd279", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "61a450b5-3007-4e98-a7b2-dba743ea54a7", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "d7566b94-731b-4e84-a2de-a1c8130af38b", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "399c5ce6-4308-4524-89bc-4dee1afd6cbb", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "c8e80f39-d8d7-4fa5-8944-33fcdd366721", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "79251c55-7de3-457e-99e3-92395f2b69a7", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "60a12c1d-d4ce-40c1-a83f-faba71145f0c", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "e36da2db-a596-4680-a5ca-16c4e5a1a9c1", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "2b1b59ff-5417-48cd-8d7a-2ab5ecfedc9f", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "3e4f2042-a809-490c-a1a6-9bb63b4b0e4a", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-terms-and-conditions", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 70, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "0b59d69a-95b0-4c82-bd21-908c019a4974", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "accf37f2-1a52-40b0-a92f-f5a931c57126", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "4eaad8bb-f86b-483d-99a4-4cba184ef573", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "eb0a326f-17e0-49b7-b25c-575b0cd3888f", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 90, + "config": {} + }, + { + "alias": "delete_credential", + "name": "Delete Credential", + "providerId": "delete_credential", + "enabled": true, + "defaultAction": false, + "priority": 100, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "firstBrokerLoginFlow": "first broker login", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "oauth2DevicePollingInterval": "5", + "parRequestUriLifespan": "60", + "cibaInterval": "5", + "realmReusableOtpCode": "false" + }, + "keycloakVersion": "24.0.5", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + }, + "users": [ + { + "username": "viewer", + "enabled": true, + "emailVerified": true, + "firstName": "viewer", + "lastName": "viewer", + "email": "viewer@mail.com", + "credentials": [ + { + "type": "password", + "value": "viewer" + } + ] + }, + { + "username": "pacsadmin", + "enabled": true, + "emailVerified": true, + "firstName": "pacsadmin", + "lastName": "pacsadmin", + "email": "pacsadmin@mail.com", + "credentials": [ + { + "type": "password", + "value": "pacsadmin" + } + ], + "groups": ["pacsadmin"] + } + ] +} diff --git a/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/orthanc.json b/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/orthanc.json new file mode 100644 index 0000000..2e10723 --- /dev/null +++ b/platform/app/.recipes/Nginx-Orthanc-Keycloak/config/orthanc.json @@ -0,0 +1,89 @@ +{ + "Name": "Orthanc inside Docker", + "StorageDirectory": "/var/lib/orthanc/db", + "IndexDirectory": "/var/lib/orthanc/db", + "StorageCompression": false, + "MaximumStorageSize": 0, + "MaximumPatientCount": 0, + "LuaScripts": [], + "Plugins": ["/usr/share/orthanc/plugins", "/usr/local/share/orthanc/plugins"], + "ConcurrentJobs": 2, + "HttpServerEnabled": true, + "HttpPort": 8042, + "HttpDescribeErrors": true, + "HttpCompressionEnabled": true, + "DicomServerEnabled": true, + "DicomAet": "ORTHANC", + "DicomCheckCalledAet": false, + "DicomPort": 4242, + "DefaultEncoding": "Latin1", + "DeflatedTransferSyntaxAccepted": true, + "JpegTransferSyntaxAccepted": true, + "Jpeg2000TransferSyntaxAccepted": true, + "JpegLosslessTransferSyntaxAccepted": true, + "JpipTransferSyntaxAccepted": true, + "Mpeg2TransferSyntaxAccepted": true, + "RleTransferSyntaxAccepted": true, + "UnknownSopClassAccepted": false, + "DicomScpTimeout": 30, + + "RemoteAccessAllowed": true, + "SslEnabled": false, + "SslCertificate": "certificate.pem", + "AuthenticationEnabled": false, + "RegisteredUsers": { + "test": "test" + }, + "DicomModalities": {}, + "DicomModalitiesInDatabase": false, + "DicomAlwaysAllowEcho": true, + "DicomAlwaysAllowStore": true, + "DicomCheckModalityHost": false, + "DicomScuTimeout": 10, + "OrthancPeers": {}, + "OrthancPeersInDatabase": false, + "HttpProxy": "", + + "HttpVerbose": true, + + "HttpTimeout": 10, + "HttpsVerifyPeers": true, + "HttpsCACertificates": "", + "UserMetadata": {}, + "UserContentType": {}, + "StableAge": 60, + "StrictAetComparison": false, + "StoreMD5ForAttachments": true, + "LimitFindResults": 0, + "LimitFindInstances": 0, + "LimitJobs": 10, + "LogExportedResources": false, + "KeepAlive": true, + "TcpNoDelay": true, + "HttpThreadsCount": 50, + "StoreDicom": true, + "DicomAssociationCloseDelay": 5, + "QueryRetrieveSize": 10, + "CaseSensitivePN": false, + "LoadPrivateDictionary": true, + "Dictionary": {}, + "SynchronousCMove": true, + "JobsHistorySize": 10, + "SaveJobs": true, + "OverwriteInstances": false, + "MediaArchiveSize": 1, + "StorageAccessOnFind": "Always", + "MetricsEnabled": true, + + "DicomWeb": { + "Enable": true, + "Root": "/dicom-web/", + "EnableWado": true, + "WadoRoot": "/wado", + "Host": "127.0.0.1", + "Ssl": false, + "StowMaxInstances": 10, + "StowMaxSize": 10, + "QidoCaseSensitive": false + } +} diff --git a/platform/app/.recipes/Nginx-Orthanc-Keycloak/docker-compose.yml b/platform/app/.recipes/Nginx-Orthanc-Keycloak/docker-compose.yml new file mode 100644 index 0000000..1a44c30 --- /dev/null +++ b/platform/app/.recipes/Nginx-Orthanc-Keycloak/docker-compose.yml @@ -0,0 +1,126 @@ +services: + ohif_viewer: + build: + context: ./../../../../ + dockerfile: ./platform/app/.recipes/Nginx-Orthanc-Keycloak/dockerfile + image: webapp:latest + container_name: webapp + ports: + - '443:443' # SSL + - '80:80' # Web + depends_on: + keycloak: + condition: service_healthy + restart: on-failure + networks: + - default + extra_hosts: + - 'host.docker.internal:host-gateway' + environment: + - OAUTH2_PROXY_SKIP_PROVIDER_BUTTON=true + volumes: + # - ../../app/dist /var/www/html + - ./config/nginx.conf:/etc/nginx/nginx.conf + - ./config/oauth2-proxy.cfg:/etc/oauth2-proxy/oauth2-proxy.cfg + + - ./config/letsencrypt:/etc/letsencrypt + - ./config/certbot:/var/www/certbot + + orthanc: + image: jodogne/orthanc-plugins + hostname: orthanc + container_name: orthanc + volumes: + - ./config/orthanc.json:/etc/orthanc/orthanc.json:ro + - ./volumes/orthanc-db/:/var/lib/orthanc/db/ + restart: unless-stopped + networks: + - default + + keycloak: + image: quay.io/keycloak/keycloak:24.0.5 + command: 'start-dev --import-realm' + hostname: keycloak + container_name: keycloak + volumes: + - ./config/ohif-keycloak-realm.json:/opt/keycloak/data/import/ohif-keycloak-realm.json + environment: + # Database + KC_DB_URL_HOST: postgres + KC_DB: postgres + KC_DB_URL: 'jdbc:postgresql://postgres:5432/keycloak' + KC_DB_SCHEMA: public + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: password + KC_HOSTNAME_ADMIN_URL: http://YOUR_DOMAIN/keycloak/ + KC_HOSTNAME_URL: http://YOUR_DOMAIN/keycloak/ + KC_HOSTNAME_STRICT_BACKCHANNEL: true + KC_HOSTNAME_STRICT_HTTPS: false + KC_HTTP_ENABLED: true + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + KC_HEALTH_ENABLED: true + KC_METRICS_ENABLED: true + KC_PROXY: edge + KC_PROXY_HEADERS: xforwarded + KEYCLOAK_JDBC_PARAMS: connectTimeout=40000 + KC_LOG_LEVEL: INFO + KC_HOSTNAME_DEBUG: true + # added later + PROXY_ADDRESS_FORWARDING: true + ports: + - 8080:8080 + depends_on: + - postgres + restart: unless-stopped + networks: + - default + extra_hosts: + - 'host.docker.internal:host-gateway' + healthcheck: + test: [ + 'CMD-SHELL', + "exec 3<>/dev/tcp/YOUR_DOMAIN/8080;echo -e \"GET /health/ready HTTP/1.1\r + + host: http://localhost\r + + Connection: close\r + + \r + + \" >&3;grep \"HTTP/1.1 200 OK\" <&3", + ] + interval: 1s + timeout: 5s + retries: 10 + start_period: 60s + + postgres: + image: postgres:15 + hostname: postgres + container_name: postgres + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: keycloak + POSTGRES_PASSWORD: password + restart: unless-stopped + networks: + - default + + certbot: + image: certbot/certbot + container_name: certbot + volumes: + - ./config/letsencrypt:/etc/letsencrypt + - ./config/certbot:/var/www/certbot + entrypoint: + /bin/sh -c "trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;" +volumes: + postgres_data: + driver: local + +networks: + default: + driver: bridge diff --git a/platform/app/.recipes/Nginx-Orthanc-Keycloak/dockerfile b/platform/app/.recipes/Nginx-Orthanc-Keycloak/dockerfile new file mode 100644 index 0000000..7e86135 --- /dev/null +++ b/platform/app/.recipes/Nginx-Orthanc-Keycloak/dockerfile @@ -0,0 +1,57 @@ +# Stage 1: Build the application +FROM node:20.18.1-slim as builder + +# Setup the working directory +RUN mkdir /usr/src/app +WORKDIR /usr/src/app + +# Install dependencies +# apt-get update is combined with apt-get install to avoid using outdated packages +RUN apt-get update && apt-get install -y build-essential python3 + +# Copy package.json and other dependency-related files first +# Assuming your package.json and yarn.lock or similar are located in the project root +# Todo: this probably can get improved by copying +# only the package json files and running yarn install before +# copying the rest of the files but having a monorepo setup +# makes this a bit more complicated, i wasn't able to get it working +COPY ./ /usr/src/app/ + +# Install node dependencies +RUN yarn config set workspaces-experimental true +RUN yarn install + +# Copy the rest of the application code + +# set QUICK_BUILD to true to make the build faster for dev +ENV APP_CONFIG=config/docker-nginx-orthanc-keycloak.js + +# Build the application +RUN yarn run build + +# Use nginx as the base image +FROM nginx:alpine + +# Install dependencies for oauth2-proxy +RUN apk add --no-cache curl + +# Create necessary directories +RUN mkdir -p /var/logs/nginx /var/www/html /etc/oauth2-proxy + +# Download and install oauth2-proxy +RUN curl -L https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v7.4.0/oauth2-proxy-v7.4.0.linux-amd64.tar.gz -o oauth2-proxy.tar.gz && \ + tar -xvzf oauth2-proxy.tar.gz && \ + mv oauth2-proxy-v7.4.0.linux-amd64/oauth2-proxy /usr/local/bin/ && \ + rm -rf oauth2-proxy-v7.4.0.linux-amd64 oauth2-proxy.tar.gz + + +COPY --from=builder /usr/src/app/platform/app/dist /var/www/html + +# Copy the entrypoint script +COPY ./platform/app/.recipes/Nginx-Orthanc-Keycloak/config/entrypoint.sh /entrypoint.sh + +# Expose necessary ports +EXPOSE 80 443 4180 +# Set the entrypoint script as the entrypoint +RUN chmod +x entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] diff --git a/platform/app/.recipes/Nginx-Orthanc/.gitignore b/platform/app/.recipes/Nginx-Orthanc/.gitignore new file mode 100644 index 0000000..12e4308 --- /dev/null +++ b/platform/app/.recipes/Nginx-Orthanc/.gitignore @@ -0,0 +1,2 @@ +logs/* +volumes/* diff --git a/platform/app/.recipes/Nginx-Orthanc/README.md b/platform/app/.recipes/Nginx-Orthanc/README.md new file mode 100644 index 0000000..14e1a70 --- /dev/null +++ b/platform/app/.recipes/Nginx-Orthanc/README.md @@ -0,0 +1,26 @@ +# Docker compose files + +# Build + +Using docker compose you can build the image with the following command: + +```bash +docker-compose build +``` + +# Run + +To run the container use the following command: + +```bash +docker-compose up +``` + + +# Routes + +http://localhost/ -> OHIF +localhost/pacs -> Orthanc + + +See [here](../../../docs/docs/deployment/nginx--image-archive.md) for more information about this recipe. diff --git a/platform/app/.recipes/Nginx-Orthanc/config/nginx.conf b/platform/app/.recipes/Nginx-Orthanc/config/nginx.conf new file mode 100644 index 0000000..a81f9c9 --- /dev/null +++ b/platform/app/.recipes/Nginx-Orthanc/config/nginx.conf @@ -0,0 +1,85 @@ +worker_processes 2; +error_log /var/logs/nginx/mydomain.error.log; +pid /var/run/nginx.pid; +include /usr/share/nginx/modules/*.conf; # See /usr/share/doc/nginx/README.dynamic. + +events { + worker_connections 1024; ## Default: 1024 + use epoll; # http://nginx.org/en/docs/events.html + multi_accept on; # http://nginx.org/en/docs/ngx_core_module.html#multi_accept +} + +# Core Modules Docs: +# http://nginx.org/en/docs/http/ngx_http_core_module.html +http { + include '/etc/nginx/mime.types'; + default_type application/octet-stream; + + keepalive_timeout 65; + keepalive_requests 100000; + tcp_nopush on; + tcp_nodelay on; + + + + # Nginx `listener` block + server { + listen [::]:80 default_server; + listen 80; + + gzip on; + gzip_types text/css application/javascript application/json image/svg+xml; + gzip_comp_level 9; + etag on; + + + # Reverse Proxy for `orthanc` APIs (including DICOMWeb) + # + location /pacs/ { + + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + expires 0; + add_header Cache-Control private; + + # Add CORS headers + # Note: uncomment the following line to allow all domains to access the Orthanc APIs + # You should actually only allow the domains you trust to access the APIs + # add_header 'Access-Control-Allow-Origin' '*' always; + + proxy_pass http://orthanc:8042/; + + # By default, this endpoint is protected by CORS (cross-origin-resource-sharing) + # You can add headers to allow other domains to request this resource. + # See the "Updating CORS Settings" example below + add_header 'Access-Control-Allow-Origin' '*' always; + } + + + # Do not cache sw.js, required for offline-first updates. + location /sw.js { + add_header Cache-Control "no-cache"; + proxy_cache_bypass $http_pragma; + proxy_cache_revalidate on; + expires off; + access_log off; + } + + + # Single Page App + # Try files, fallback to index.html + # + location / { + root /var/www/html; + index index.html; + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } + + } +} diff --git a/platform/app/.recipes/Nginx-Orthanc/config/orthanc.json b/platform/app/.recipes/Nginx-Orthanc/config/orthanc.json new file mode 100644 index 0000000..2e10723 --- /dev/null +++ b/platform/app/.recipes/Nginx-Orthanc/config/orthanc.json @@ -0,0 +1,89 @@ +{ + "Name": "Orthanc inside Docker", + "StorageDirectory": "/var/lib/orthanc/db", + "IndexDirectory": "/var/lib/orthanc/db", + "StorageCompression": false, + "MaximumStorageSize": 0, + "MaximumPatientCount": 0, + "LuaScripts": [], + "Plugins": ["/usr/share/orthanc/plugins", "/usr/local/share/orthanc/plugins"], + "ConcurrentJobs": 2, + "HttpServerEnabled": true, + "HttpPort": 8042, + "HttpDescribeErrors": true, + "HttpCompressionEnabled": true, + "DicomServerEnabled": true, + "DicomAet": "ORTHANC", + "DicomCheckCalledAet": false, + "DicomPort": 4242, + "DefaultEncoding": "Latin1", + "DeflatedTransferSyntaxAccepted": true, + "JpegTransferSyntaxAccepted": true, + "Jpeg2000TransferSyntaxAccepted": true, + "JpegLosslessTransferSyntaxAccepted": true, + "JpipTransferSyntaxAccepted": true, + "Mpeg2TransferSyntaxAccepted": true, + "RleTransferSyntaxAccepted": true, + "UnknownSopClassAccepted": false, + "DicomScpTimeout": 30, + + "RemoteAccessAllowed": true, + "SslEnabled": false, + "SslCertificate": "certificate.pem", + "AuthenticationEnabled": false, + "RegisteredUsers": { + "test": "test" + }, + "DicomModalities": {}, + "DicomModalitiesInDatabase": false, + "DicomAlwaysAllowEcho": true, + "DicomAlwaysAllowStore": true, + "DicomCheckModalityHost": false, + "DicomScuTimeout": 10, + "OrthancPeers": {}, + "OrthancPeersInDatabase": false, + "HttpProxy": "", + + "HttpVerbose": true, + + "HttpTimeout": 10, + "HttpsVerifyPeers": true, + "HttpsCACertificates": "", + "UserMetadata": {}, + "UserContentType": {}, + "StableAge": 60, + "StrictAetComparison": false, + "StoreMD5ForAttachments": true, + "LimitFindResults": 0, + "LimitFindInstances": 0, + "LimitJobs": 10, + "LogExportedResources": false, + "KeepAlive": true, + "TcpNoDelay": true, + "HttpThreadsCount": 50, + "StoreDicom": true, + "DicomAssociationCloseDelay": 5, + "QueryRetrieveSize": 10, + "CaseSensitivePN": false, + "LoadPrivateDictionary": true, + "Dictionary": {}, + "SynchronousCMove": true, + "JobsHistorySize": 10, + "SaveJobs": true, + "OverwriteInstances": false, + "MediaArchiveSize": 1, + "StorageAccessOnFind": "Always", + "MetricsEnabled": true, + + "DicomWeb": { + "Enable": true, + "Root": "/dicom-web/", + "EnableWado": true, + "WadoRoot": "/wado", + "Host": "127.0.0.1", + "Ssl": false, + "StowMaxInstances": 10, + "StowMaxSize": 10, + "QidoCaseSensitive": false + } +} diff --git a/platform/app/.recipes/Nginx-Orthanc/docker-compose.yml b/platform/app/.recipes/Nginx-Orthanc/docker-compose.yml new file mode 100644 index 0000000..c80c160 --- /dev/null +++ b/platform/app/.recipes/Nginx-Orthanc/docker-compose.yml @@ -0,0 +1,46 @@ +# Reference: +# - https://docs.docker.com/compose/compose-file +# - https://eclipsesource.com/blogs/2018/01/11/authenticating-reverse-proxy-with-keycloak/ + +services: + # Exposed server that's handling incoming web requests + ohif_viewer: + build: + # Project root + context: ./../../../../ + # Relative to context + dockerfile: ./platform/app/.recipes/Nginx-Orthanc/dockerfile + image: webapp:latest + container_name: ohif_orthanc + volumes: + # Nginx config + - ./config/nginx.conf:/etc/nginx/nginx.conf + # Logs + - ./logs/nginx:/var/logs/nginx + # Let's Encrypt + # - letsencrypt_certificates:/etc/letsencrypt + # - letsencrypt_challenges:/var/www/letsencrypt + ports: + - '443:443' # SSL + - '80:80' # Web + depends_on: + # - keycloak + - orthanc + restart: on-failure + + # LINK: https://hub.docker.com/r/jodogne/orthanc-plugins/ + # TODO: Update to use Postgres + # https://github.com/mrts/docker-postgresql-multiple-databases + orthanc: + image: jodogne/orthanc-plugins + hostname: orthanc + container_name: orthancPACS + volumes: + # Config + - ./config/orthanc.json:/etc/orthanc/orthanc.json:ro + # Persist data + - ./volumes/orthanc-db/:/var/lib/orthanc/db/ + restart: unless-stopped + ports: + - '4242:4242' # Orthanc REST API + - '8042:8042' # Orthanc HTTP diff --git a/platform/app/.recipes/Nginx-Orthanc/dockerfile b/platform/app/.recipes/Nginx-Orthanc/dockerfile new file mode 100644 index 0000000..846c74c --- /dev/null +++ b/platform/app/.recipes/Nginx-Orthanc/dockerfile @@ -0,0 +1,43 @@ +# Stage 1: Build the application +FROM node:20.18.1-slim as builder + +# Setup the working directory +RUN mkdir /usr/src/app +WORKDIR /usr/src/app + +# Install dependencies +# apt-get update is combined with apt-get install to avoid using outdated packages +RUN apt-get update && apt-get install -y build-essential python3 + +# Copy package.json and other dependency-related files first +# Assuming your package.json and yarn.lock or similar are located in the project root + +COPY ./ /usr/src/app/ + +# Install node dependencies +RUN yarn config set workspaces-experimental true +RUN yarn install + +# Copy the rest of the application code + +# set QUICK_BUILD to true to make the build faster for dev +ENV APP_CONFIG=config/docker-nginx-orthanc.js + +# Build the application +RUN yarn run build + +# # Stage 2: Bundle the built application into a Docker container which runs NGINX using Alpine Linux +FROM nginx:alpine + +# # Create directories for logs and html content if they don't already exist +RUN mkdir -p /var/log/nginx /var/www/html + + +# # Copy build output to serve static files +COPY --from=builder /usr/src/app/platform/app/dist /var/www/html + +# # Expose HTTP and HTTPS ports +EXPOSE 80 443 + +# # Start NGINX +CMD ["nginx", "-g", "daemon off;"] diff --git a/platform/app/.webpack/rules/extractStyleChunks.js b/platform/app/.webpack/rules/extractStyleChunks.js new file mode 100644 index 0000000..f7c467f --- /dev/null +++ b/platform/app/.webpack/rules/extractStyleChunks.js @@ -0,0 +1,35 @@ +const ExtractCssChunksPlugin = require('extract-css-chunks-webpack-plugin'); + +function extractStyleChunks(isProdBuild) { + return [ + // If you are using the old stylus, you should uncomment this + // { + // test: /\.styl$/, + // use: [ + // { + // loader: ExtractCssChunksPlugin.loader, + // options: { + // hot: !isProdBuild, + // }, + // }, + // { loader: 'css-loader' }, + // { loader: 'stylus-loader' }, + // ], + // }, + { + test: /\.(sa|sc|c)ss$/, + use: [ + { + loader: ExtractCssChunksPlugin.loader, + options: { + hot: !isProdBuild, + }, + }, + 'css-loader', + 'postcss-loader', + ], + }, + ]; +} + +module.exports = extractStyleChunks; diff --git a/platform/app/.webpack/rules/fontsToJavaScript.js b/platform/app/.webpack/rules/fontsToJavaScript.js new file mode 100644 index 0000000..b95e5b2 --- /dev/null +++ b/platform/app/.webpack/rules/fontsToJavaScript.js @@ -0,0 +1,20 @@ +/** + * For CommonJS, we want to bundle whatever font we've landed on. This allows + * us to reduce the number of script-tags we need to specify for simple use. + * + * PWA will grab these externally to reduce bundle size (think code split), + * and cache the grab using service-worker. + */ +const fontsToJavaScript = { + test: /\.(ttf|eot|woff|woff2)$/i, + use: [ + { + loader: 'file-loader', + options: { + name: '[name].[ext]', + }, + }, + ], +}; + +module.exports = fontsToJavaScript; diff --git a/platform/app/.webpack/webpack.pwa.js b/platform/app/.webpack/webpack.pwa.js new file mode 100644 index 0000000..1d11c0d --- /dev/null +++ b/platform/app/.webpack/webpack.pwa.js @@ -0,0 +1,208 @@ +// https://developers.google.com/web/tools/workbox/guides/codelabs/webpack +// ~~ WebPack +const path = require('path'); +const { merge } = require('webpack-merge'); +const webpack = require('webpack'); +const webpackBase = require('./../../../.webpack/webpack.base.js'); +// ~~ Plugins +const { CleanWebpackPlugin } = require('clean-webpack-plugin'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const { InjectManifest } = require('workbox-webpack-plugin'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +// ~~ Directories +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); +const PUBLIC_DIR = path.join(__dirname, '../public'); +// ~~ Env Vars +const HTML_TEMPLATE = process.env.HTML_TEMPLATE || 'index.html'; +const PUBLIC_URL = process.env.PUBLIC_URL || '/'; +const APP_CONFIG = process.env.APP_CONFIG || 'config/default.js'; + +// proxy settings +const PROXY_TARGET = process.env.PROXY_TARGET; +const PROXY_DOMAIN = process.env.PROXY_DOMAIN; +const PROXY_PATH_REWRITE_FROM = process.env.PROXY_PATH_REWRITE_FROM; +const PROXY_PATH_REWRITE_TO = process.env.PROXY_PATH_REWRITE_TO; + +const OHIF_PORT = Number(process.env.OHIF_PORT || 3000); +const ENTRY_TARGET = process.env.ENTRY_TARGET || `${SRC_DIR}/index.js`; +const Dotenv = require('dotenv-webpack'); +const writePluginImportFile = require('./writePluginImportsFile.js'); +// const MillionLint = require('@million/lint'); + +const copyPluginFromExtensions = writePluginImportFile(SRC_DIR, DIST_DIR); + +const setHeaders = (res, path) => { + if (path.indexOf('.gz') !== -1) { + res.setHeader('Content-Encoding', 'gzip'); + } else if (path.indexOf('.br') !== -1) { + res.setHeader('Content-Encoding', 'br'); + } + if (path.indexOf('.pdf') !== -1) { + res.setHeader('Content-Type', 'application/pdf'); + } else if (path.indexOf('mp4') !== -1) { + res.setHeader('Content-Type', 'video/mp4'); + } else if (path.indexOf('frames') !== -1) { + res.setHeader('Content-Type', 'multipart/related'); + } else { + res.setHeader('Content-Type', 'application/json'); + } +}; + +module.exports = (env, argv) => { + const baseConfig = webpackBase(env, argv, { SRC_DIR, DIST_DIR }); + const isProdBuild = process.env.NODE_ENV === 'production'; + const hasProxy = PROXY_TARGET && PROXY_DOMAIN; + + const mergedConfig = merge(baseConfig, { + entry: { + app: ENTRY_TARGET, + }, + output: { + path: DIST_DIR, + filename: isProdBuild ? '[name].bundle.[chunkhash].js' : '[name].js', + publicPath: PUBLIC_URL, // Used by HtmlWebPackPlugin for asset prefix + devtoolModuleFilenameTemplate: function (info) { + if (isProdBuild) { + return `webpack:///${info.resourcePath}`; + } else { + return 'file:///' + encodeURI(info.absoluteResourcePath); + } + }, + }, + resolve: { + modules: [ + // Modules specific to this package + path.resolve(__dirname, '../node_modules'), + // Hoisted Yarn Workspace Modules + path.resolve(__dirname, '../../../node_modules'), + SRC_DIR, + ], + }, + plugins: [ + // For debugging re-renders + // MillionLint.webpack(), + new Dotenv(), + // Clean output.path + new CleanWebpackPlugin(), + // Copy "Public" Folder to Dist + new CopyWebpackPlugin({ + patterns: [ + ...copyPluginFromExtensions, + { + from: PUBLIC_DIR, + to: DIST_DIR, + toType: 'dir', + globOptions: { + // Ignore our HtmlWebpackPlugin template file + // Ignore our configuration files + ignore: ['**/config/**', '**/html-templates/**', '.DS_Store'], + }, + }, + // Short term solution to make sure GCloud config is available in output + // for our docker implementation + { + from: `${PUBLIC_DIR}/config/google.js`, + to: `${DIST_DIR}/google.js`, + }, + // Copy over and rename our target app config file + { + from: `${PUBLIC_DIR}/${APP_CONFIG}`, + to: `${DIST_DIR}/app-config.js`, + }, + // Copy Dicom Microscopy Viewer build files + { + from: '../../../node_modules/dicom-microscopy-viewer/dist/dynamic-import', + to: DIST_DIR, + globOptions: { + ignore: ['**/*.min.js.map'], + }, + }, + ], + }), + // Generate "index.html" w/ correct includes/imports + new HtmlWebpackPlugin({ + template: `${PUBLIC_DIR}/html-templates/${HTML_TEMPLATE}`, + filename: 'index.html', + templateParameters: { + PUBLIC_URL: PUBLIC_URL, + }, + }), + // Generate a service worker for fast local loads + new InjectManifest({ + swDest: 'sw.js', + swSrc: path.join(SRC_DIR, 'service-worker.js'), + // Need to exclude the theme as it is updated independently + exclude: [/theme/], + // Cache large files for the manifests to avoid warning messages + maximumFileSizeToCacheInBytes: 1024 * 1024 * 50, + }), + ], + // https://webpack.js.org/configuration/dev-server/ + devServer: { + // gzip compression of everything served + // Causes Cypress: `wait-on` issue in CI + // compress: true, + // http2: true, + // https: true, + open: true, + port: OHIF_PORT, + client: { + overlay: { errors: true, warnings: false }, + }, + proxy: { + '/dicomweb': 'http://localhost:5000', + }, + static: [ + { + directory: '../../testdata', + staticOptions: { + extensions: ['gz', 'br', 'mht'], + index: ['index.json.gz', 'index.mht.gz'], + redirect: true, + setHeaders, + }, + publicPath: '/viewer-testdata', + }, + ], + //public: 'http://localhost:' + 3000, + //writeToDisk: true, + historyApiFallback: { + disableDotRule: true, + index: PUBLIC_URL + 'index.html', + }, + devMiddleware: { + writeToDisk: true, + }, + }, + }); + + if (hasProxy) { + mergedConfig.devServer.proxy = mergedConfig.devServer.proxy || {}; + mergedConfig.devServer.proxy = { + [PROXY_TARGET]: { + target: PROXY_DOMAIN, + changeOrigin: true, + pathRewrite: { + [`^${PROXY_PATH_REWRITE_FROM}`]: PROXY_PATH_REWRITE_TO, + }, + }, + }; + } + + if (isProdBuild) { + mergedConfig.plugins.push( + new MiniCssExtractPlugin({ + filename: '[name].bundle.css', + chunkFilename: '[id].css', + }) + ); + } + + mergedConfig.watchOptions = { + ignored: /node_modules\/@cornerstonejs/, + }; + + return mergedConfig; +}; diff --git a/platform/app/.webpack/writePluginImportsFile.js b/platform/app/.webpack/writePluginImportsFile.js new file mode 100644 index 0000000..29accd9 --- /dev/null +++ b/platform/app/.webpack/writePluginImportsFile.js @@ -0,0 +1,215 @@ +const pluginConfig = require('../pluginConfig.json'); +const fs = require('fs'); +const os = require('os'); +const glob = require('glob'); + +const autogenerationDisclaimer = ` +// THIS FILE IS AUTOGENERATED AS PART OF THE EXTENSION AND MODE PLUGIN PROCESS. +// IT SHOULD NOT BE MODIFIED MANUALLY \n`; + +const extractName = val => (typeof val === 'string' ? val : val.packageName); + +function constructLines(input, categoryName) { + let pluginCount = 0; + + const lines = { + importLines: [], + addToWindowLines: [], + }; + + if (!input) return lines; + + input.forEach(entry => { + if (entry.default === false) return; + + const packageName = extractName(entry); + + lines.addToWindowLines.push(`${categoryName}.push("${packageName}");\n`); + + pluginCount++; + }); + + return lines; +} + +function getFormattedImportBlock(importLines) { + let content = ''; + // Imports + importLines.forEach(importLine => { + content += importLine; + }); + + return content; +} + +function getFormattedWindowBlock(addToWindowLines) { + let content = + 'const extensions = [];\n' + + 'const modes = [];\n' + + '\n// Not required any longer\n' + + 'window.extensions = extensions;\n' + + 'window.modes = modes;\n\n'; + + addToWindowLines.forEach(addToWindowLine => { + content += addToWindowLine; + }); + + return content; +} + +function getRuntimeLoadModesExtensions(modules) { + const dynamicLoad = []; + dynamicLoad.push( + '\n\n// Add a dynamic runtime loader', + 'async function loadModule(module) {', + " if (typeof module !== 'string') return module;" + ); + modules.forEach(module => { + const packageName = extractName(module); + if (!packageName) { + return; + } + if (module.importPath) { + dynamicLoad.push( + ` if( module==="${packageName}") {`, + ` const imported = await window.browserImportFunction('${module.importPath}');`, + ' return ' + + (module.globalName + ? `window["${module.globalName}"];` + : `imported["${module.importName || 'default'}"];`), + ' }' + ); + return; + } + dynamicLoad.push( + ` if( module==="${packageName}") {`, + ` const imported = await import("${packageName}");`, + ' return imported.default;', + ' }' + ); + }); + // TODO - handle more cases for import than just default + dynamicLoad.push( + ' return (await window.browserImportFunction(module)).default;', + '}\n', + '// Import a list of items (modules or string names)', + '// @return a Promise evaluating to a list of modules', + 'export default function importItems(modules) {', + ' return Promise.all(modules.map(loadModule));', + '}\n', + 'export { loadModule, modes, extensions, importItems };\n\n' + ); + return dynamicLoad.join('\n'); +} + +const fromDirectory = (srcDir, path) => { + if (!path) return; + if (path[0] === '.') return srcDir + '/../../..' + path.substring(1); + if (path[0] === '~') return os.homedir() + path.substring(1); + return path; +}; + +const createCopyPluginToDistForLink = (srcDir, distDir, plugins, folderName) => { + return plugins + .map(plugin => { + const fromDir = fromDirectory(srcDir, plugin.directory); + const from = fromDir || `${srcDir}/../node_modules/${plugin.packageName}/${folderName}/`; + const exists = fs.existsSync(from); + return exists + ? { + from, + to: distDir, + toType: 'dir', + } + : undefined; + }) + .filter(x => !!x); +}; + +const createCopyPluginToDistForBuild = (SRC_DIR, DIST_DIR, plugins, folderName) => { + return plugins + .map(plugin => { + const from = `${SRC_DIR}/../../../node_modules/${plugin.packageName}/${folderName}/`; + const exists = fs.existsSync(from); + return exists + ? { + from, + to: DIST_DIR, + toType: 'dir', + } + : undefined; + }) + .filter(x => !!x); +}; + +function writePluginImportsFile(SRC_DIR, DIST_DIR) { + let pluginImportsJsContent = autogenerationDisclaimer; + + const extensionLines = constructLines(pluginConfig.extensions, 'extensions'); + const modeLines = constructLines(pluginConfig.modes, 'modes'); + + pluginImportsJsContent += getFormattedImportBlock([ + ...extensionLines.importLines, + ...modeLines.importLines, + ]); + pluginImportsJsContent += getFormattedWindowBlock([ + ...extensionLines.addToWindowLines, + ...modeLines.addToWindowLines, + ]); + + pluginImportsJsContent += getRuntimeLoadModesExtensions([ + ...pluginConfig.extensions, + ...pluginConfig.modes, + ...pluginConfig.public, + ]); + + fs.writeFileSync(`${SRC_DIR}/pluginImports.js`, pluginImportsJsContent, { flag: 'w+' }, err => { + if (err) { + console.error(err); + return; + } + }); + + // Build packages using cli add-mode and add-extension + // will get added to the root node_modules, but the linked packages + // will be hosted at the viewer node_modules. + + const copyPluginPublicToDistBuild = createCopyPluginToDistForBuild( + SRC_DIR, + DIST_DIR, + [...pluginConfig.modes, ...pluginConfig.extensions], + 'public' + ); + + const copyPluginPublicToDistLink = createCopyPluginToDistForLink( + SRC_DIR, + DIST_DIR, + [...pluginConfig.modes, ...pluginConfig.extensions, ...pluginConfig.public], + 'public' + ); + + // Temporary way to copy chunks from the dist folder so that the become + // available + const copyPluginDistToDistBuild = createCopyPluginToDistForBuild( + SRC_DIR, + DIST_DIR, + [...pluginConfig.modes, ...pluginConfig.extensions], + 'dist' + ); + + const copyPluginDistToDistLink = createCopyPluginToDistForLink( + SRC_DIR, + DIST_DIR, + [...pluginConfig.modes, ...pluginConfig.extensions], + 'dist' + ); + + return [ + ...copyPluginPublicToDistBuild, + ...copyPluginPublicToDistLink, + ...copyPluginDistToDistBuild, + ...copyPluginDistToDistLink, + ]; +} + +module.exports = writePluginImportsFile; diff --git a/platform/app/CHANGELOG.md b/platform/app/CHANGELOG.md new file mode 100644 index 0000000..02f6786 --- /dev/null +++ b/platform/app/CHANGELOG.md @@ -0,0 +1,5330 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + + +### Bug Fixes + +* broken activateViewportBeforeInteraction behavior ([#4810](https://github.com/OHIF/Viewers/issues/4810)) ([fdb073c](https://github.com/OHIF/Viewers/commit/fdb073c216013477c8545db34d254a9ad328fe48)) + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + + +### Features + +* **hotkeys:** Migrate hotkeys to customization service and fix issues with overrides ([#4777](https://github.com/OHIF/Viewers/issues/4777)) ([3e6913b](https://github.com/OHIF/Viewers/commit/3e6913b097569280a5cc2fa5bbe4add52f149305)) + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + + +### Bug Fixes + +* lodash dependencies ([#4791](https://github.com/OHIF/Viewers/issues/4791)) ([4e16099](https://github.com/OHIF/Viewers/commit/4e16099ad3ab777b09f6ac8f181025cfd656ab6b)) + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + + +### Bug Fixes + +* **core:** Address 3D reconstruction and Android compatibility issues and clean up 4D data mode ([#4762](https://github.com/OHIF/Viewers/issues/4762)) ([149d6d0](https://github.com/OHIF/Viewers/commit/149d6d049cd333b9e5846576b403ff387558a66f)) + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + + +### Features + +* **ui:** Add support for Custom Modal component in Modal Service ([#4752](https://github.com/OHIF/Viewers/issues/4752)) ([2c183aa](https://github.com/OHIF/Viewers/commit/2c183aa4a777d7b5a0417ebcc8576a0fc2631ad2)) + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + + +### Features + +* Add customization support for more UI components ([#4634](https://github.com/OHIF/Viewers/issues/4634)) ([f15eb44](https://github.com/OHIF/Viewers/commit/f15eb44b4cf49de1b73a22512571cec02effaef3)) + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + + +### Features + +* **customization:** enable custom onDropHandler for viewportGrid ([#4641](https://github.com/OHIF/Viewers/issues/4641)) ([054b262](https://github.com/OHIF/Viewers/commit/054b262e9cbeb0f44de65d05641efe1e8944a4f5)) + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + + +### Features + +* **static-wado:** add support for case-insensitive searching ([#4603](https://github.com/OHIF/Viewers/issues/4603)) ([ac6e674](https://github.com/OHIF/Viewers/commit/ac6e674b4d094f942556d045178011bbf3f81796)) + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + + +### Features + +* **customization:** new customization service api ([#4688](https://github.com/OHIF/Viewers/issues/4688)) ([55ad8ef](https://github.com/OHIF/Viewers/commit/55ad8efbabc3fabd8031fc08927b2f92ae5aec69)) + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + + +### Features + +* **panels:** responsive thumbnails based on panel size ([#4723](https://github.com/OHIF/Viewers/issues/4723)) ([d9abc3d](https://github.com/OHIF/Viewers/commit/d9abc3da8d94d6c5ab0cc5af25a5f61849905a35)) + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + + +### Bug Fixes + +* **seg:** sphere scissor on stack and cpu rendering reset properties was broken ([#4721](https://github.com/OHIF/Viewers/issues/4721)) ([f00d182](https://github.com/OHIF/Viewers/commit/f00d18292f02e8910215d913edfc994850a68d88)) + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + + +### Features + +* **resizable-side-panels:** Make the left and right side panels (optionally) resizable. ([#4672](https://github.com/OHIF/Viewers/issues/4672)) ([d90a4cf](https://github.com/OHIF/Viewers/commit/d90a4cfb16cc0daed9b905de9780f44cca1323f9)) + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + + +### Bug Fixes + +* **multiframe:** handling proxies properly ([#4693](https://github.com/OHIF/Viewers/issues/4693)) ([ec4b5a6](https://github.com/OHIF/Viewers/commit/ec4b5a6876cea77278e5cffaf4108eeeefdc57dc)) + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + + +### Bug Fixes + +* Having sop instance in a per-frame or shared attribute breaks load ([#4560](https://github.com/OHIF/Viewers/issues/4560)) ([cded082](https://github.com/OHIF/Viewers/commit/cded08261788143e0d5be57a55c927fd96aafb22)) + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + + +### Features + +* **multimonitor:** Add simple multi-monitor support to open another study([#4178](https://github.com/OHIF/Viewers/issues/4178)) ([07c628e](https://github.com/OHIF/Viewers/commit/07c628e689b28f831317a7c28d712509b69c6b13)) + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + + +### Features + +* **dev:** move to rsbuild for dev - faster ([#4674](https://github.com/OHIF/Viewers/issues/4674)) ([d4a4267](https://github.com/OHIF/Viewers/commit/d4a4267429c02916dd51f6aefb290d96dd1c3b04)) + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + + +### Bug Fixes + +* orthanc datasource dev ([#4663](https://github.com/OHIF/Viewers/issues/4663)) ([ebbc37d](https://github.com/OHIF/Viewers/commit/ebbc37d291ba9bfa11baf164bf673c6f0994014c)) + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + + +### Bug Fixes + +* **docker:** publish manifest for multiarch and update cs3d ([#4650](https://github.com/OHIF/Viewers/issues/4650)) ([836e67a](https://github.com/OHIF/Viewers/commit/836e67a6ab8de66d8908c75856774318729544f4)) + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + + +### Bug Fixes + +* Docker build time was very slow on a tiny change ([#4559](https://github.com/OHIF/Viewers/issues/4559)) ([7e43b2f](https://github.com/OHIF/Viewers/commit/7e43b2f768cfc3e08ecde9dfdae275194daece2b)) + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + + +### Bug Fixes + +* **tools:** enable additional tools in volume viewport ([#4620](https://github.com/OHIF/Viewers/issues/4620)) ([1992002](https://github.com/OHIF/Viewers/commit/1992002d2dced171c17b9a0163baf707fc551e3d)) + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + + +### Features + +* migrate icons to ui-next ([#4606](https://github.com/OHIF/Viewers/issues/4606)) ([4e2ae32](https://github.com/OHIF/Viewers/commit/4e2ae328744ed95589c2cdf7a531454a25bf88b5)) + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + + +### Bug Fixes + +* **seg:** jump to the first slice in SEG and RT that has data ([#4605](https://github.com/OHIF/Viewers/issues/4605)) ([9bf24d6](https://github.com/OHIF/Viewers/commit/9bf24d6dc58ed8f65c90899a17c11044b792cf40)) + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + + +### Features + +* **node:** move to node 20 ([#4594](https://github.com/OHIF/Viewers/issues/4594)) ([1f04d6c](https://github.com/OHIF/Viewers/commit/1f04d6c1be729a26fe7bcda923770a1cd461053c)) + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + + +### Bug Fixes + +* **worklist:** selected patient changes randomly when clicked. ([#4592](https://github.com/OHIF/Viewers/issues/4592)) ([684267b](https://github.com/OHIF/Viewers/commit/684267b19b553817590b8760a188a56e17e5d2ec)) + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + + +### Bug Fixes + +* **touch:** For viewport interactions use onPointerDown. ([#4572](https://github.com/OHIF/Viewers/issues/4572)) ([6160718](https://github.com/OHIF/Viewers/commit/6160718fd20db6bac6dd511183a30359d9420140)) + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + + +### Bug Fixes + +* **defaultRouteInit:** fixes 'madeInClient' parameter when 'makeDisplaySets' is called ([#4571](https://github.com/OHIF/Viewers/issues/4571)) ([7cc6c14](https://github.com/OHIF/Viewers/commit/7cc6c1484a551026af5f641254431c23b729c2c2)) + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + + +### Bug Fixes + +* **CinePlayer:** always show cine player for dynamic data ([#4575](https://github.com/OHIF/Viewers/issues/4575)) ([b8e8bbe](https://github.com/OHIF/Viewers/commit/b8e8bbe482b66e8cbe9167d03e9d8dedd2d3b6c5)) + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + + +### Bug Fixes + +* **cli:** publish 4D preclincial mode on NPM so it can be used in the OHIF cli commands ([#4557](https://github.com/OHIF/Viewers/issues/4557)) ([085590a](https://github.com/OHIF/Viewers/commit/085590a4ca64bebad9ef60503411e1a6dd4d93f9)) + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + + +### Bug Fixes + +* **multiframe:** metadata handling of NM studies and loading order ([#4554](https://github.com/OHIF/Viewers/issues/4554)) ([7624ccb](https://github.com/OHIF/Viewers/commit/7624ccb5e495c0a151227a458d8d5bfb8babb22c)) + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + + +### Bug Fixes + +* **colorlut:** use the correct colorlut index and update vtk ([#4544](https://github.com/OHIF/Viewers/issues/4544)) ([b9c26e7](https://github.com/OHIF/Viewers/commit/b9c26e775a49044673473418dd5bdee2e5562ab9)) + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + + +### Bug Fixes + +* **modes:** don't attempt to retrieve a stage index if HPs are an array ([#4542](https://github.com/OHIF/Viewers/issues/4542)) ([44648ee](https://github.com/OHIF/Viewers/commit/44648eef92265f0a80c0c72ca1729d6eca6c4178)) + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + + +### Bug Fixes + +* **error-boundray:** prevent stack trace from overflowing and make it scrollable ([#4541](https://github.com/OHIF/Viewers/issues/4541)) ([27ae385](https://github.com/OHIF/Viewers/commit/27ae385fd7787bf34af00366c5d490ac33abeff9)) + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + + +### Bug Fixes + +* **viewport:** set a minimum width of 5px on viewports to prevent them from turning black/ going into an unrecoverable state. ([#4517](https://github.com/OHIF/Viewers/issues/4517)) ([32fe262](https://github.com/OHIF/Viewers/commit/32fe2623cfb5129a19ee07031dd50e79b530c7e0)) + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + + +### Bug Fixes + +* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed)) + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + + +### Bug Fixes + +* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30)) + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + + +### Bug Fixes + +* **style:** worklist shifting ([#4477](https://github.com/OHIF/Viewers/issues/4477)) ([8fb8b3b](https://github.com/OHIF/Viewers/commit/8fb8b3bfd1c887cd67fc058629d7aba598c76f9e)) + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + + +### Bug Fixes + +* **tmtv:** fix toggle one up weird behaviours ([#4473](https://github.com/OHIF/Viewers/issues/4473)) ([aa2b649](https://github.com/OHIF/Viewers/commit/aa2b649444eb4fe5422e72ea7830a709c4d24a90)) + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + + +### Bug Fixes + +* build ([#4471](https://github.com/OHIF/Viewers/issues/4471)) ([3d11ef2](https://github.com/OHIF/Viewers/commit/3d11ef28f213361ec7586809317bd219fa70e742)) + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + + +### Features + +* **new-study-panel:** default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser ([#4417](https://github.com/OHIF/Viewers/issues/4417)) ([a7fd9fa](https://github.com/OHIF/Viewers/commit/a7fd9fa5bfff7a1b533d99cb96f7147a35fd528f)) + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + + +### Features + +* **hangingProtocols:** added selection of the HangingProtocol stage from the url ([#4310](https://github.com/OHIF/Viewers/issues/4310)) ([fa2435d](https://github.com/OHIF/Viewers/commit/fa2435d5e94e5f903404ca94687b086f90f8d1f8)) +* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3)) + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + + +### Bug Fixes + +* **auth:** oidc-react-issue ([#4410](https://github.com/OHIF/Viewers/issues/4410)) ([e849199](https://github.com/OHIF/Viewers/commit/e849199eb0a9ecba4f9845aa1e07df775d5ded9b)) + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + + +### Features + +* **tours:** freeze versions and add licensings doc ([#4407](https://github.com/OHIF/Viewers/issues/4407)) ([60a8d51](https://github.com/OHIF/Viewers/commit/60a8d5154a5d6d2b121bd93aeacf12d97ef9f8cb)) + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + + +### Features + +* **studies-panel:** New OHIF study panel - under experimental flag ([#4254](https://github.com/OHIF/Viewers/issues/4254)) ([7a96406](https://github.com/OHIF/Viewers/commit/7a96406a116e46e62c396855fa64f434e2984b58)) + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + + +### Bug Fixes + +* Add kheops integration into OHIF v3 again ([#4345](https://github.com/OHIF/Viewers/issues/4345)) ([e1feffa](https://github.com/OHIF/Viewers/commit/e1feffa42553d6c8650a4aceb09f72c637126660)) + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + + +### Bug Fixes + +* ๐Ÿ› SeriesInstanceUID fallback + update retrieve metadata filtered to check for lazy ([#4346](https://github.com/OHIF/Viewers/issues/4346)) ([14498d4](https://github.com/OHIF/Viewers/commit/14498d4e9a6a57324b8be9f0b314f2901459dc4a)) + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + + +### Features + +* Add CS3D WSI and Video Viewports and add annotation navigation for MPR ([#4182](https://github.com/OHIF/Viewers/issues/4182)) ([7599ec9](https://github.com/OHIF/Viewers/commit/7599ec9421129dcade94e6fa6ec7908424ab3134)) + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + + +### Features + +* **ui:** Created design and added core components for ui-next ([#4324](https://github.com/OHIF/Viewers/issues/4324)) ([9036418](https://github.com/OHIF/Viewers/commit/90364189b865514cc471786d2f91c270517e98fc)) + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + + +### Features + +* **pmap:** added support for parametric map ([#4284](https://github.com/OHIF/Viewers/issues/4284)) ([fc0064f](https://github.com/OHIF/Viewers/commit/fc0064fd9d8cdc8fde81b81f0e71fd5d077ca22b)) + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + + +### Features + +* **auth:** Add Authorization Code Flow and new Keycloak recipes with new video tutorials ([#4234](https://github.com/OHIF/Viewers/issues/4234)) ([aefa6d9](https://github.com/OHIF/Viewers/commit/aefa6d94dff82d34fa8358933fb1d5dec3f8246d)) + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + + +### Bug Fixes + +* Cobb angle not working in basic-test mode and open contour ([#4280](https://github.com/OHIF/Viewers/issues/4280)) ([6fd3c7e](https://github.com/OHIF/Viewers/commit/6fd3c7e293fec851dd30e650c1347cc0bc7a99ee)) +* **image-orientation:** Prevent incorrect orientation marker display for single-slice US images ([#4275](https://github.com/OHIF/Viewers/issues/4275)) ([6d11048](https://github.com/OHIF/Viewers/commit/6d11048ca5ea66284948602613a63277083ec6a5)) +* webpack import bugs showing warnings on import ([#4265](https://github.com/OHIF/Viewers/issues/4265)) ([24c511f](https://github.com/OHIF/Viewers/commit/24c511f4bc04c4143bbd3d0d48029f41f7f36014)) + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + + +### Features + +* **studyPrefetcher:** Study Prefetcher ([#4206](https://github.com/OHIF/Viewers/issues/4206)) ([2048b19](https://github.com/OHIF/Viewers/commit/2048b19484c0b1fae73f993cfaa814f861bbd230)) + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + + +### Bug Fixes + +* **orthanc:** Correct bulkdata URL handling and add configuration example PDF ([#4262](https://github.com/OHIF/Viewers/issues/4262)) ([fdf883a](https://github.com/OHIF/Viewers/commit/fdf883ada880c0979acba8fdff9b542dc05b7706)) + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) +* **code:** remove console log ([#4248](https://github.com/OHIF/Viewers/issues/4248)) ([f3bbfff](https://github.com/OHIF/Viewers/commit/f3bbfff09b66ee020daf503656a2b58e763634a3)) +* **CustomViewportOverlay:** pass accurate data to Custom Viewport Functions ([#4224](https://github.com/OHIF/Viewers/issues/4224)) ([aef00e9](https://github.com/OHIF/Viewers/commit/aef00e91d63e9bc2de289cc6f35975e36547fb20)) + + +### Features + +* customization service append and customize functionality should run once ([#4238](https://github.com/OHIF/Viewers/issues/4238)) ([e462fd3](https://github.com/OHIF/Viewers/commit/e462fd31f7944acfee34f08cfbc28cfd9de16169)) +* **sort:** custom series sort in study panel ([#4214](https://github.com/OHIF/Viewers/issues/4214)) ([a433d40](https://github.com/OHIF/Viewers/commit/a433d406e2cac13f644203996c682260b54e8865)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + + +### Bug Fixes + +* Use correct external URL for rendered responses with relative URI ([#4236](https://github.com/OHIF/Viewers/issues/4236)) ([d8f6991](https://github.com/OHIF/Viewers/commit/d8f6991dbe72465080cfc5de39c7ea225702f2e0)) + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + + +### Features + +* Add customization merge, append or replace functionality ([#3871](https://github.com/OHIF/Viewers/issues/3871)) ([55dcfa1](https://github.com/OHIF/Viewers/commit/55dcfa1f6994a7036e7e594efb23673382a41915)) + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + + +### Bug Fixes + +* **seg:** maintain algorithm name and algorithm type when DICOM seg is exported or downloaded ([#4203](https://github.com/OHIF/Viewers/issues/4203)) ([a29e94d](https://github.com/OHIF/Viewers/commit/a29e94de803f79bbb3372d00ad8eb14b4224edc2)) + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + + +### Bug Fixes + +* **tmtv:** crosshairs should not have viewport indicators ([#4197](https://github.com/OHIF/Viewers/issues/4197)) ([f85da32](https://github.com/OHIF/Viewers/commit/f85da32f34389ef7cecae03c07e0af26468b52a6)) + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + + +### Bug Fixes + +* **queryparam:** set all query params to lowercase by default ([#4190](https://github.com/OHIF/Viewers/issues/4190)) ([e073d19](https://github.com/OHIF/Viewers/commit/e073d195fdec7f8bdb67e5e3dae522a0fd121ad2)) + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + + +### Features + +* **hp:** Add displayArea option for Hanging protocols and example with Mamo([#3808](https://github.com/OHIF/Viewers/issues/3808)) ([18ac08e](https://github.com/OHIF/Viewers/commit/18ac08ed860d119721c52e4ffc270332259100b6)) + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + + +### Bug Fixes + +* **ultrasound:** Upgrade cornerstone3D version to resolve coloring issues ([#4181](https://github.com/OHIF/Viewers/issues/4181)) ([75a71db](https://github.com/OHIF/Viewers/commit/75a71db7f89840250ad1c2b35df5a35aceb8be7d)) + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + + +### Features + +* **measurements:** show untracked measurements in measurement panel under additional findings ([#4160](https://github.com/OHIF/Viewers/issues/4160)) ([18686c2](https://github.com/OHIF/Viewers/commit/18686c2caf13ede3e881303100bd4cc34b8b135f)) + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + + +### Features + +* **types:** typed app config ([#4171](https://github.com/OHIF/Viewers/issues/4171)) ([8960b89](https://github.com/OHIF/Viewers/commit/8960b89911a9342d93bf1a62bec97a696f101fd4)) + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + + +### Bug Fixes + +* **crosshairs:** reset angle, position, and slabthickness for crosshairs when reset viewport tool is used ([#4113](https://github.com/OHIF/Viewers/issues/4113)) ([73d9e99](https://github.com/OHIF/Viewers/commit/73d9e99d5d6f38ab6c36f4471d54f18798feacb4)) + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + + +### Bug Fixes + +* dicom json for orthanc by Update package versions for [@cornerstonejs](https://github.com/cornerstonejs) dependencies ([#4165](https://github.com/OHIF/Viewers/issues/4165)) ([34c7d72](https://github.com/OHIF/Viewers/commit/34c7d72142847486b98c9c52469940083eeaf87e)) + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + + +### Bug Fixes + +* **segmentation:** Address issue where segmentation creation failed on layout change ([#4153](https://github.com/OHIF/Viewers/issues/4153)) ([29944c8](https://github.com/OHIF/Viewers/commit/29944c8512c35718af03c03ef82bc43675ee1872)) + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + + +### Features + +* **test:** Playwright testing integration ([#4146](https://github.com/OHIF/Viewers/issues/4146)) ([fe1a706](https://github.com/OHIF/Viewers/commit/fe1a706446cc33670bf5fab8451e8281b487fcd6)) + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + + +### Bug Fixes + +* **auth:** bind handleUnauthenticated to correct context ([#4120](https://github.com/OHIF/Viewers/issues/4120)) ([8fa339f](https://github.com/OHIF/Viewers/commit/8fa339f296fd7e844f3879cfd81e47dbff315e66)) +* **DicomJSONDataSource:** Fix series filtering ([#4092](https://github.com/OHIF/Viewers/issues/4092)) ([2de102c](https://github.com/OHIF/Viewers/commit/2de102c73c795cfb48b49005b10aa788444a45b7)) + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + + +### Bug Fixes + +* **rt:** enhanced RT support, utilize SVGs for rendering. ([#4074](https://github.com/OHIF/Viewers/issues/4074)) ([0156bc4](https://github.com/OHIF/Viewers/commit/0156bc426f1840ae0d090223e94a643726e856cb)) + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + + +### Bug Fixes + +* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f)) + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + + +### Bug Fixes + +* **bugs:** fix patient header for doc, track ball rotate resize observer and add segmentation button not being enabled on viewport data change ([#4068](https://github.com/OHIF/Viewers/issues/4068)) ([c09311d](https://github.com/OHIF/Viewers/commit/c09311d3b7df05fcd00a9f36a7233e9d7e5589d0)) + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + + +### Bug Fixes + +* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951)) + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + + +### Features + +* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb)) + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + + +### Bug Fixes + +* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af)) + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + + +### Bug Fixes + +* **bugs:** and replace seriesInstanceUID and seriesInstanceUIDs URL with seriesInstanceUIDs ([#4049](https://github.com/OHIF/Viewers/issues/4049)) ([da7c1a5](https://github.com/OHIF/Viewers/commit/da7c1a5d8c54bfa1d3f97bbc500386bf76e7fd9d)) + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes - final ([#4048](https://github.com/OHIF/Viewers/issues/4048)) ([170bb96](https://github.com/OHIF/Viewers/commit/170bb96983082c39b22b7352e0c54aacf3e73b02)) + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + + +### Bug Fixes + +* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025)) + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637)) + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + + +### Features + +* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13)) + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + + +### Bug Fixes + +* **general:** enhancements and bug fixes ([#4018](https://github.com/OHIF/Viewers/issues/4018)) ([2b83393](https://github.com/OHIF/Viewers/commit/2b83393f91cb16ea06821d79d14ff60f80c29c90)) + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + + +### Bug Fixes + +* Microscopy bulkdata and image retrieve ([#3894](https://github.com/OHIF/Viewers/issues/3894)) ([7fac49b](https://github.com/OHIF/Viewers/commit/7fac49b4492b4bd5e9ece8e2e2b0fa2faa840d7f)) + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + + +### Features + +* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2)) + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + + +### Bug Fixes + +* **cornerstone-dicom-sr:** Freehand SR hydration support ([#3996](https://github.com/OHIF/Viewers/issues/3996)) ([5645ac1](https://github.com/OHIF/Viewers/commit/5645ac1b271e1ed8c57f5d71100809362447267e)) + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + + +### Features + +* **segmentation:** Enhanced segmentation panel design for TMTV ([#3988](https://github.com/OHIF/Viewers/issues/3988)) ([9f3235f](https://github.com/OHIF/Viewers/commit/9f3235ff096636aafa88d8a42859e8dc85d9036d)) + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + + +### Features + +* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1)) + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + + +### Bug Fixes + +* **new layout:** address black screen bugs ([#4008](https://github.com/OHIF/Viewers/issues/4008)) ([158a181](https://github.com/OHIF/Viewers/commit/158a1816703e0ad66cae08cb9bd1ffb93bbd8d43)) + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + + +### Features + +* **layout:** new layout selector with 3D volume rendering ([#3923](https://github.com/OHIF/Viewers/issues/3923)) ([617043f](https://github.com/OHIF/Viewers/commit/617043fe0da5de91fbea4ac33a27f1df16ae1ca6)) + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + + +### Features + +* **worklist:** new investigational use text ([#3999](https://github.com/OHIF/Viewers/issues/3999)) ([45b68e8](https://github.com/OHIF/Viewers/commit/45b68e841dcb9e28a2ea991c37ee7ac4a8c5b71e)) + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + + +### Features + +* **worklist:** New worklist buttons and tooltips ([#3989](https://github.com/OHIF/Viewers/issues/3989)) ([9bcd1ae](https://github.com/OHIF/Viewers/commit/9bcd1ae6f51d61786cc1e99624f396b56a47cd69)) + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + + +### Bug Fixes + +* **SR display:** and the token based navigation ([#3995](https://github.com/OHIF/Viewers/issues/3995)) ([feed230](https://github.com/OHIF/Viewers/commit/feed2304c124dc2facc7a7371ed9851548c223c5)) + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + + +### Features + +* **delete measurement:** icon for measurement table ([#3775](https://github.com/OHIF/Viewers/issues/3775)) ([f7fe91c](https://github.com/OHIF/Viewers/commit/f7fe91c5f6c4f05f3f3f5f640d3a119bd40a5870)) + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + + +### Bug Fixes + +* **demo:** Deploy issue ([#3951](https://github.com/OHIF/Viewers/issues/3951)) ([21e8a2b](https://github.com/OHIF/Viewers/commit/21e8a2bd0b7cc72f90a31e472d285d761be15d30)) + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + + +### Bug Fixes + +* **viewport-sync:** remember synced viewports bw stack and volume and RENAME StackImageSync to ImageSliceSync ([#3849](https://github.com/OHIF/Viewers/issues/3849)) ([e4a116b](https://github.com/OHIF/Viewers/commit/e4a116b074fcb85c8cbcc9db44fdec565f3386db)) + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + + +### Bug Fixes + +* Update CS3D to fix second render ([#3892](https://github.com/OHIF/Viewers/issues/3892)) ([d00a86b](https://github.com/OHIF/Viewers/commit/d00a86b022742ea089d246d06cfd691f43b64412)) + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + + +### Features + +* **hp:** enable OHIF to run with partial metadata for large studies at the cost of less effective hanging protocol ([#3804](https://github.com/OHIF/Viewers/issues/3804)) ([0049f4c](https://github.com/OHIF/Viewers/commit/0049f4c0303f0b6ea995972326fc8784259f5a47)) + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + + +### Features + +* **transferSyntax:** prefer server transcoded transfer syntax for all images ([#3883](https://github.com/OHIF/Viewers/issues/3883)) ([1456a49](https://github.com/OHIF/Viewers/commit/1456a493d66c90c787b022256c9f2846afb115fc)) + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + + +### Bug Fixes + +* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6)) + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + + +### Features + +* Add on mode init hook ([#3882](https://github.com/OHIF/Viewers/issues/3882)) ([f58725c](https://github.com/OHIF/Viewers/commit/f58725ce40685f7297181ef98d81bc28420c8291)) + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + + +### Bug Fixes + +* PDF display request in v3 ([#3878](https://github.com/OHIF/Viewers/issues/3878)) ([9865030](https://github.com/OHIF/Viewers/commit/98650302c7575f0aea386e32cfc4112c378035e6)) + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + + +### Features + +* **config:** Add activateViewportBeforeInteraction parameter for viewport interaction customization ([#3847](https://github.com/OHIF/Viewers/issues/3847)) ([f707b4e](https://github.com/OHIF/Viewers/commit/f707b4ebc996f379cd30337badc06b07e6e35ac5)) +* **i18n:** enhanced i18n support ([#3761](https://github.com/OHIF/Viewers/issues/3761)) ([d14a8f0](https://github.com/OHIF/Viewers/commit/d14a8f0199db95cd9e85866a011b64d6bf830d57)) + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + + +### Bug Fixes + +* **auth:** fix the issue with oauth at a non root path ([#3840](https://github.com/OHIF/Viewers/issues/3840)) ([6651008](https://github.com/OHIF/Viewers/commit/6651008fbb35dabd5991c7f61128e6ef324012df)) + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + + +### Bug Fixes + +* Update the CS3D packages to add the most recent HTJ2K TSUIDS ([#3806](https://github.com/OHIF/Viewers/issues/3806)) ([9d1884d](https://github.com/OHIF/Viewers/commit/9d1884d7d8b6b2a1cdc26965a96995838aa72682)) + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + + +### Bug Fixes + +* **DICOM Overlay:** The overlay data wasn't being refreshed on change ([#3793](https://github.com/OHIF/Viewers/issues/3793)) ([00e7519](https://github.com/OHIF/Viewers/commit/00e751933ac6d611a34773fa69594243f1b99082)) + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + + +### Bug Fixes + +* **overlay:** Overlays aren't shown on undefined origin ([#3781](https://github.com/OHIF/Viewers/issues/3781)) ([fd1251f](https://github.com/OHIF/Viewers/commit/fd1251f751d8147b8a78c7f4d81c67ba69769afa)) + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + + +### Bug Fixes + +* **thumbnail:** Avoid multiple promise creations for thumbnails ([#3756](https://github.com/OHIF/Viewers/issues/3756)) ([b23eeff](https://github.com/OHIF/Viewers/commit/b23eeff93745769e67e60c33d75293d6242c5ec9)) + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + + +### Features + +* **i18n:** enhanced i18n support ([#3730](https://github.com/OHIF/Viewers/issues/3730)) ([330e11c](https://github.com/OHIF/Viewers/commit/330e11c7ff0151e1096e19b8ffdae7d64cae280e)) + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + + +### Features + +* **filters:** save worklist query filters to session storage so that they persist between navigation to the viewer and back ([#3749](https://github.com/OHIF/Viewers/issues/3749)) ([2a15ef0](https://github.com/OHIF/Viewers/commit/2a15ef0e44b7b4d8bbf5cb9363db6e523201c681)) + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + + +### Bug Fixes + +* **toolbar:** allow customizable toolbar for active viewport and allow active tool to be deactivated via a click ([#3608](https://github.com/OHIF/Viewers/issues/3608)) ([dd6d976](https://github.com/OHIF/Viewers/commit/dd6d9768bbca1d3cc472e8c1e6d85822500b96ef)) + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + + +### Bug Fixes + +* **sr:** dcm4chee requires the patient name for an SR to match what is in the original study ([#3739](https://github.com/OHIF/Viewers/issues/3739)) ([d98439f](https://github.com/OHIF/Viewers/commit/d98439fe7f3825076dbc87b664a1d1480ff414d3)) + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + + +### Bug Fixes + +* **modules:** add stylus loader as an option to be uncommented ([#3710](https://github.com/OHIF/Viewers/issues/3710)) ([7c57f67](https://github.com/OHIF/Viewers/commit/7c57f67844b790fc6e47ac3f9708bf9d576389c8)) + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + + +### Bug Fixes + +* **voi:** should publish voi change event on reset ([#3707](https://github.com/OHIF/Viewers/issues/3707)) ([52f34c6](https://github.com/OHIF/Viewers/commit/52f34c64d014f433ec1661a39b47e7fb27f15332)) + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + + +### Bug Fixes + +* **modality unit:** fix the modality unit per target via upgrade of cs3d ([#3706](https://github.com/OHIF/Viewers/issues/3706)) ([0a42d57](https://github.com/OHIF/Viewers/commit/0a42d573bbca7f2551a831a46d3aa6b56674a580)) + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + + +### Features + +* **Segmentation:** download RTSS from Labelmap([#3692](https://github.com/OHIF/Viewers/issues/3692)) ([40673f6](https://github.com/OHIF/Viewers/commit/40673f64b36b1150149c55632aa1825178a39e65)) + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + + +### Bug Fixes + +* **bugs:** fixing lots of bugs regarding release candidate ([#3700](https://github.com/OHIF/Viewers/issues/3700)) ([8bc12a3](https://github.com/OHIF/Viewers/commit/8bc12a37d0353160ae5ea4624dc0b244b7d59c07)) + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + + +### Bug Fixes + +* **measurement and microscopy:** various small fixes for measurement and microscopy side panel ([#3696](https://github.com/OHIF/Viewers/issues/3696)) ([c1d5ee7](https://github.com/OHIF/Viewers/commit/c1d5ee7e3f7f4c0c6bed9ae81eba5519741c5155)) + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + + +### Features + +* **debug:** Add timing information about time to first image/all images, and query time ([#3681](https://github.com/OHIF/Viewers/issues/3681)) ([108383b](https://github.com/OHIF/Viewers/commit/108383b9ef51e4bef82d9c932b9bc7aa5354e799)) + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + + +### Bug Fixes + +* **editing:** regression bug in disable editing ([#3687](https://github.com/OHIF/Viewers/issues/3687)) ([4dc2acd](https://github.com/OHIF/Viewers/commit/4dc2acdefa872dd1d8df47f465e9e9656f95f67f)) + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + + +### Bug Fixes + +* **StackSync:** Miscellaneous fixes for stack image sync ([#3663](https://github.com/OHIF/Viewers/issues/3663)) ([8a335bd](https://github.com/OHIF/Viewers/commit/8a335bd03d14ba87d65d7468d93f74040aa828d9)) + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + + +### Bug Fixes + +* **config:** support more values for the useSharedArrayBuffer ([#3688](https://github.com/OHIF/Viewers/issues/3688)) ([1129c15](https://github.com/OHIF/Viewers/commit/1129c155d2c7d46c98a5df7c09879aa3d459fa7e)) + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Bug Fixes + +* **react-select:** update react select package ([#3622](https://github.com/OHIF/Viewers/issues/3622)) ([04ca10d](https://github.com/OHIF/Viewers/commit/04ca10d8779dd15454920002f3d48afa8830de8a)) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) +* **SidePanel:** new side panel tab look-and-feel ([#3657](https://github.com/OHIF/Viewers/issues/3657)) ([85c899b](https://github.com/OHIF/Viewers/commit/85c899b399e2521480724be145538993721b9378)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + + +### Performance Improvements + +* **memory:** add 16 bit texture via configuration - reduces memory by half ([#3662](https://github.com/OHIF/Viewers/issues/3662)) ([2bd3b26](https://github.com/OHIF/Viewers/commit/2bd3b26a6aa54b211ef988f3ad64ef1fe5648bab)) + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + + +### Bug Fixes + +* **keyCloak:** fix openresty keycloak deployment recipe ([#3655](https://github.com/OHIF/Viewers/issues/3655)) ([2d7721c](https://github.com/OHIF/Viewers/commit/2d7721cb581f55dc49e3baeca2411b18dd78ad74)) + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + + +### Bug Fixes + +* **health imaging:** studies not loading from healthimaging if imagepositionpatient is missing ([#3646](https://github.com/OHIF/Viewers/issues/3646)) ([74e62a1](https://github.com/OHIF/Viewers/commit/74e62a176374f720080d4e777972f70e7f2d8b2b)) + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + + +### Bug Fixes + +* **hotkeys:** preserve hotkeys if changed, and reduce re-rendering ([#3635](https://github.com/OHIF/Viewers/issues/3635)) ([94f7cfb](https://github.com/OHIF/Viewers/commit/94f7cfb08e3490488394efc42ef089ebe55e86be)) + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + + +### Features + +* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f)) + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + + +### Bug Fixes + +* **nginx archive recipe:** Fixes to various configuration files. ([#3624](https://github.com/OHIF/Viewers/issues/3624)) ([3ce7225](https://github.com/OHIF/Viewers/commit/3ce72254b390f32c9aa207a0589e688805e2659d)) + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + + +### Features + +* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd)) + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + + +### Features + +* **data source UI config:** Popup the configuration dialogue whenever a data source is not fully configured ([#3620](https://github.com/OHIF/Viewers/issues/3620)) ([adedc8c](https://github.com/OHIF/Viewers/commit/adedc8c382e18a2e86a569e3d023cc55a157363f)) + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + + +### Bug Fixes + +* **OpenIdConnectRoutes:** fix handleUnauthenticated ([#3617](https://github.com/OHIF/Viewers/issues/3617)) ([35fc30c](https://github.com/OHIF/Viewers/commit/35fc30c5359d8199cc38ffa670c08687d2672f11)) + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/app + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + + +### Features + +* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1)) + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + + +### Bug Fixes + +* **memory leak:** array buffer was sticking around in volume viewports ([#3611](https://github.com/OHIF/Viewers/issues/3611)) ([65b49ae](https://github.com/OHIF/Viewers/commit/65b49aeb1b5f38224e4892bdf32453500ee351f8)) + + + + + +# [4.0.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.11...@ohif/viewer@4.0.0) (2020-05-14) + + +### Bug Fixes + +* ๐Ÿ› Fix race condition when loading derived display sets ([#1718](https://github.com/OHIF/Viewers/issues/1718)) ([b1678ce](https://github.com/OHIF/Viewers/commit/b1678ce6399dde37a9878f45ccc7c63286d93fab)), closes [#1715](https://github.com/OHIF/Viewers/issues/1715) + + +### BREAKING CHANGES + +* ๐Ÿงจ However we start to load once the first set of metadata arrives. We need +to wait until all series metadata is fetched. + + + + + +## [3.11.11](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.10...@ohif/viewer@3.11.11) (2020-05-14) + + +### Bug Fixes + +* ๐Ÿ› Load default display set when no time metadata ([#1684](https://github.com/OHIF/Viewers/issues/1684)) ([f7b8b6a](https://github.com/OHIF/Viewers/commit/f7b8b6a41c4626084ef56b0fdf7363e914b143c4)), closes [#1683](https://github.com/OHIF/Viewers/issues/1683) + + + + + +## [3.11.10](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.9...@ohif/viewer@3.11.10) (2020-05-13) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.11.9](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.8...@ohif/viewer@3.11.9) (2020-05-12) + + +### Bug Fixes + +* ๐Ÿ› Fix seg color load ([#1724](https://github.com/OHIF/Viewers/issues/1724)) ([c4f84b1](https://github.com/OHIF/Viewers/commit/c4f84b1174d04ba84d37ed89b6d7ab541be28181)) + + + + + +## [3.11.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.7...@ohif/viewer@3.11.8) (2020-05-06) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.11.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.6...@ohif/viewer@3.11.7) (2020-05-04) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.11.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.5...@ohif/viewer@3.11.6) (2020-05-04) + + +### Bug Fixes + +* ๐Ÿ› Proper error handling for derived display sets ([#1708](https://github.com/OHIF/Viewers/issues/1708)) ([5b20d8f](https://github.com/OHIF/Viewers/commit/5b20d8f323e4b3ef9988f2f2ab672d697b6da409)) + + + + + +## [3.11.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.4...@ohif/viewer@3.11.5) (2020-05-04) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.11.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.3...@ohif/viewer@3.11.4) (2020-05-04) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.11.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.2...@ohif/viewer@3.11.3) (2020-04-29) + + +### Bug Fixes + +* Add IHEInvokeImageDisplay routes back into viewer ([#1695](https://github.com/OHIF/Viewers/issues/1695)) ([f7162ce](https://github.com/OHIF/Viewers/commit/f7162ce61708776a6c192732b0904a022bcc6b3a)) + + + + + +## [3.11.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.1...@ohif/viewer@3.11.2) (2020-04-28) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.11.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.11.0...@ohif/viewer@3.11.1) (2020-04-27) + +**Note:** Version bump only for package @ohif/viewer + + + + + +# [3.11.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.10.2...@ohif/viewer@3.11.0) (2020-04-24) + + +### Features + +* ๐ŸŽธ Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f)) + + + + + +## [3.10.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.10.1...@ohif/viewer@3.10.2) (2020-04-23) + + +### Bug Fixes + +* undefined `errorHandler` in cornerstoneWadoImageLoader configuration ([#1664](https://github.com/OHIF/Viewers/issues/1664)) ([709f147](https://github.com/OHIF/Viewers/commit/709f14708e2b0f912b5ea509114acd87af3149cb)) + + + + + +## [3.10.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.10.0...@ohif/viewer@3.10.1) (2020-04-23) + +**Note:** Version bump only for package @ohif/viewer + + + + + +# [3.10.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.9.2...@ohif/viewer@3.10.0) (2020-04-23) + + +### Features + +* configuration to hook into XHR Error handling ([e96205d](https://github.com/OHIF/Viewers/commit/e96205de35e5bec14dc8a9a8509db3dd4e6ecdb6)) + + + + + +## [3.9.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.9.1...@ohif/viewer@3.9.2) (2020-04-22) + + +### Bug Fixes + +* whiteLabeling should support component creation by passing React to defined fn ([#1659](https://github.com/OHIF/Viewers/issues/1659)) ([2093a00](https://github.com/OHIF/Viewers/commit/2093a0036584b2cc698c8f06fe62b334523b1029)) + + + + + +## [3.9.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.9.0...@ohif/viewer@3.9.1) (2020-04-17) + + +### Bug Fixes + +* `showStudyList` config ([#1647](https://github.com/OHIF/Viewers/issues/1647)) ([d9fc7bb](https://github.com/OHIF/Viewers/commit/d9fc7bbb0e6d868f507c515f031aaf88a2353e2f)) + + + + + +# [3.9.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.21...@ohif/viewer@3.9.0) (2020-04-17) + + +### Features + +* set the authorization header for DICOMWeb requests if provided in query string ([#1646](https://github.com/OHIF/Viewers/issues/1646)) ([450c80b](https://github.com/OHIF/Viewers/commit/450c80b9d5f172be8b5713b422370360325a0afc)) + + + + + +## [3.8.21](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.20...@ohif/viewer@3.8.21) (2020-04-15) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.8.20](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.19...@ohif/viewer@3.8.20) (2020-04-09) + + +### Bug Fixes + +* Revert "refactor: Reduce bundle size ([#1575](https://github.com/OHIF/Viewers/issues/1575))" ([#1622](https://github.com/OHIF/Viewers/issues/1622)) ([d21af3f](https://github.com/OHIF/Viewers/commit/d21af3f133492fa31492413b8782936c9ff18b44)) + + + + + +## [3.8.19](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.18...@ohif/viewer@3.8.19) (2020-04-09) + +**Note:** Version bump only for package @ohif/viewer + + + + + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [3.8.18](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.17...@ohif/viewer@3.8.18) (2020-04-07) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.8.17](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.16...@ohif/viewer@3.8.17) (2020-04-06) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.8.16](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.15...@ohif/viewer@3.8.16) (2020-04-02) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.8.15](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.14...@ohif/viewer@3.8.15) (2020-04-02) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.8.14](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.13...@ohif/viewer@3.8.14) (2020-04-02) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.8.13](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.12...@ohif/viewer@3.8.13) (2020-04-01) + + +### Bug Fixes + +* segmentation not loading ([#1566](https://github.com/OHIF/Viewers/issues/1566)) ([4a7ce1c](https://github.com/OHIF/Viewers/commit/4a7ce1c09324d74c61048393e3a2427757e4001a)) + + + + + +## [3.8.12](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.11...@ohif/viewer@3.8.12) (2020-03-31) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.8.11](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.10...@ohif/viewer@3.8.11) (2020-03-26) + + +### Bug Fixes + +* [#1312](https://github.com/OHIF/Viewers/issues/1312) Cine dialog remains on screen ([#1540](https://github.com/OHIF/Viewers/issues/1540)) ([7d22bb7](https://github.com/OHIF/Viewers/commit/7d22bb7d5a8590cffc169725c93942f758fe13a0)) + + + + + +## [3.8.10](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.9...@ohif/viewer@3.8.10) (2020-03-26) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.8.9](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.8...@ohif/viewer@3.8.9) (2020-03-25) + + +### Bug Fixes + +* Load measurement in active viewport. ([#1558](https://github.com/OHIF/Viewers/issues/1558)) ([99022f2](https://github.com/OHIF/Viewers/commit/99022f2bac752f3cd1cedb61e222b8d411e158c8)) + + + + + +## [3.8.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.7...@ohif/viewer@3.8.8) (2020-03-25) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.8.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.6...@ohif/viewer@3.8.7) (2020-03-24) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.8.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.5...@ohif/viewer@3.8.6) (2020-03-24) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.8.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.4...@ohif/viewer@3.8.5) (2020-03-23) + + +### Bug Fixes + +* avoid-wasteful-renders ([#1544](https://github.com/OHIF/Viewers/issues/1544)) ([e41d339](https://github.com/OHIF/Viewers/commit/e41d339f5faef6b93700bc860f37f29f32ad5ed6)) + + + + + +## [3.8.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.3...@ohif/viewer@3.8.4) (2020-03-19) + + +### Bug Fixes + +* Only permit web workers to be initialized once. ([#1535](https://github.com/OHIF/Viewers/issues/1535)) ([9feadd3](https://github.com/OHIF/Viewers/commit/9feadd3c6d71c1c48f7825d024ccf95d5d82606d)) + + + + + +## [3.8.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.2...@ohif/viewer@3.8.3) (2020-03-17) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.8.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.1...@ohif/viewer@3.8.2) (2020-03-17) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.8.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.0...@ohif/viewer@3.8.1) (2020-03-17) + + +### Bug Fixes + +* resolves [#1483](https://github.com/OHIF/Viewers/issues/1483) ([#1527](https://github.com/OHIF/Viewers/issues/1527)) ([2747eff](https://github.com/OHIF/Viewers/commit/2747effd9e893bd78b80ee7d0444f44676e9d632)) + + + + + +# [3.8.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.7.8...@ohif/viewer@3.8.0) (2020-03-13) + + +### Features + +* Segmentations Settings UI - Phase 1 [#1391](https://github.com/OHIF/Viewers/issues/1391) ([#1392](https://github.com/OHIF/Viewers/issues/1392)) ([e8842cf](https://github.com/OHIF/Viewers/commit/e8842cf8aebde98db7fc123e4867c8288552331f)), closes [#1423](https://github.com/OHIF/Viewers/issues/1423) + + + + + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [3.7.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.7.7...@ohif/viewer@3.7.8) (2020-03-09) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.7.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.7.6...@ohif/viewer@3.7.7) (2020-03-09) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.7.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.7.5...@ohif/viewer@3.7.6) (2020-03-06) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.7.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.7.4...@ohif/viewer@3.7.5) (2020-03-05) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.7.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.7.3...@ohif/viewer@3.7.4) (2020-03-03) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.7.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.7.2...@ohif/viewer@3.7.3) (2020-03-02) + + +### Bug Fixes + +* GCloud dataset picker dialog broken ([#1453](https://github.com/OHIF/Viewers/issues/1453)) ([64dfbea](https://github.com/OHIF/Viewers/commit/64dfbeab7af98277efefadd334df14db79e32a4f)) + + + + + +## [3.7.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.7.1...@ohif/viewer@3.7.2) (2020-02-29) + + +### Bug Fixes + +* prevent the native context menu from appearing when right-clicking on a measurement or angle (https://github.com/OHIF/Viewers/issues/1406) ([#1469](https://github.com/OHIF/Viewers/issues/1469)) ([9b3be9b](https://github.com/OHIF/Viewers/commit/9b3be9b0c082c9a5b62f2a40f42e59381860fe73)) + + + + + +## [3.7.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.7.0...@ohif/viewer@3.7.1) (2020-02-21) + +**Note:** Version bump only for package @ohif/viewer + + + + + +# [3.7.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.6.3...@ohif/viewer@3.7.0) (2020-02-20) + + +### Features + +* [#1342](https://github.com/OHIF/Viewers/issues/1342) - Window level tab ([#1429](https://github.com/OHIF/Viewers/issues/1429)) ([ebc01a8](https://github.com/OHIF/Viewers/commit/ebc01a8ca238d5a3437b44d81f75aa8a5e8d0574)) + + + + + +## [3.6.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.6.2...@ohif/viewer@3.6.3) (2020-02-14) + + +### Bug Fixes + +* Creating 2 commands to activate zoom tool and also to move between displaySets ([#1446](https://github.com/OHIF/Viewers/issues/1446)) ([06a4af0](https://github.com/OHIF/Viewers/commit/06a4af06faaecf6fa06ccd90cdfa879ee8d53053)) + + + + + +## [3.6.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.6.1...@ohif/viewer@3.6.2) (2020-02-12) + + +### Bug Fixes + +* Combined Hotkeys for special characters ([#1233](https://github.com/OHIF/Viewers/issues/1233)) ([2f30e7a](https://github.com/OHIF/Viewers/commit/2f30e7a821a238144c49c56f37d8e5565540b4bd)) + + + + + +## [3.6.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.6.0...@ohif/viewer@3.6.1) (2020-02-10) + +**Note:** Version bump only for package @ohif/viewer + + + + + +# [3.6.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.5.1...@ohif/viewer@3.6.0) (2020-02-10) + + +### Features + +* ๐ŸŽธ MeasurementService ([#1314](https://github.com/OHIF/Viewers/issues/1314)) ([0c37a40](https://github.com/OHIF/Viewers/commit/0c37a406d963569af8c3be24c697dafd42712dfc)) + + + + + +## [3.5.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.5.0...@ohif/viewer@3.5.1) (2020-02-07) + +**Note:** Version bump only for package @ohif/viewer + + + + + +# [3.5.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.14...@ohif/viewer@3.5.0) (2020-02-06) + + +### Features + +* lesion-tracker extension ([#1420](https://github.com/OHIF/Viewers/issues/1420)) ([73e4409](https://github.com/OHIF/Viewers/commit/73e440968ce4699d081a9c9f2d21dd68095b3056)) + + + + + +## [3.4.14](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.13...@ohif/viewer@3.4.14) (2020-02-06) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.4.13](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.12...@ohif/viewer@3.4.13) (2020-01-30) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.4.12](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.11...@ohif/viewer@3.4.12) (2020-01-30) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.4.11](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.10...@ohif/viewer@3.4.11) (2020-01-30) + + +### Bug Fixes + +* download tool fixes & improvements ([#1235](https://github.com/OHIF/Viewers/issues/1235)) ([b9574b6](https://github.com/OHIF/Viewers/commit/b9574b6efcfeb85cde35b5cae63282f8e1b35be6)) + + + + + +## [3.4.10](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.9...@ohif/viewer@3.4.10) (2020-01-28) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.4.9](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.8...@ohif/viewer@3.4.9) (2020-01-28) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.4.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.7...@ohif/viewer@3.4.8) (2020-01-28) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.4.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.6...@ohif/viewer@3.4.7) (2020-01-28) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.4.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.5...@ohif/viewer@3.4.6) (2020-01-28) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.4.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.4...@ohif/viewer@3.4.5) (2020-01-27) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.4.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.3...@ohif/viewer@3.4.4) (2020-01-27) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.4.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.2...@ohif/viewer@3.4.3) (2020-01-24) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.4.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.1...@ohif/viewer@3.4.2) (2020-01-17) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.4.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.0...@ohif/viewer@3.4.1) (2020-01-15) + + +### Bug Fixes + +* ๐Ÿ› Metadata is being mistakenly purged ([#1360](https://github.com/OHIF/Viewers/issues/1360)) ([b9a66d4](https://github.com/OHIF/Viewers/commit/b9a66d44241f2896ef184511287fb4984671e16d)), closes [#1326](https://github.com/OHIF/Viewers/issues/1326) + + + + + +# [3.4.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.8...@ohif/viewer@3.4.0) (2020-01-14) + + +### Features + +* Custom Healthcare API endpoint ([#1367](https://github.com/OHIF/Viewers/issues/1367)) ([a5d6bc6](https://github.com/OHIF/Viewers/commit/a5d6bc6a51784ed3a8a40d4ae773de9099f116b9)) + + + + + +## [3.3.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.7...@ohif/viewer@3.3.8) (2020-01-10) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.3.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.6...@ohif/viewer@3.3.7) (2020-01-08) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.3.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.5...@ohif/viewer@3.3.6) (2020-01-07) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.3.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.4...@ohif/viewer@3.3.5) (2020-01-06) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.3.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.3...@ohif/viewer@3.3.4) (2019-12-30) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.3.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.2...@ohif/viewer@3.3.3) (2019-12-20) + + +### Bug Fixes + +* ๐Ÿ› 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241) + + + + + +## [3.3.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.1...@ohif/viewer@3.3.2) (2019-12-20) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.3.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.0...@ohif/viewer@3.3.1) (2019-12-20) + +**Note:** Version bump only for package @ohif/viewer + + + + + +# [3.3.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.2.2...@ohif/viewer@3.3.0) (2019-12-20) + + +### Features + +* ๐ŸŽธ Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223) + + + + + +## [3.2.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.2.1...@ohif/viewer@3.2.2) (2019-12-19) + + +### Bug Fixes + +* ๐Ÿ› Fix drag-n-drop of local files into OHIF ([#1319](https://github.com/OHIF/Viewers/issues/1319)) ([23305ce](https://github.com/OHIF/Viewers/commit/23305cec9c0f514e73a8dd17f984ffc87ad8d131)), closes [#1307](https://github.com/OHIF/Viewers/issues/1307) + + + + + +## [3.2.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.2.0...@ohif/viewer@3.2.1) (2019-12-18) + +**Note:** Version bump only for package @ohif/viewer + + + + + +# [3.2.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.12...@ohif/viewer@3.2.0) (2019-12-16) + + +### Features + +* ๐ŸŽธ Expose extension config to modules ([#1279](https://github.com/OHIF/Viewers/issues/1279)) ([4ea239a](https://github.com/OHIF/Viewers/commit/4ea239a9535ef297e23387c186e537ab273744ea)), closes [#1268](https://github.com/OHIF/Viewers/issues/1268) + + + + + +## [3.1.12](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.11...@ohif/viewer@3.1.12) (2019-12-16) + + +### Bug Fixes + +* ๐Ÿ› Dismiss all dialogs if leaving viewer route [#1242](https://github.com/OHIF/Viewers/issues/1242) ([#1301](https://github.com/OHIF/Viewers/issues/1301)) ([5c3d8b3](https://github.com/OHIF/Viewers/commit/5c3d8b37b6f723fbd8edcc447c37984e7eee8d40)) + + + + + +## [3.1.11](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.10...@ohif/viewer@3.1.11) (2019-12-16) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.1.10](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.9...@ohif/viewer@3.1.10) (2019-12-16) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.1.9](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.8...@ohif/viewer@3.1.9) (2019-12-16) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.1.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.7...@ohif/viewer@3.1.8) (2019-12-16) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.1.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.6...@ohif/viewer@3.1.7) (2019-12-13) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.1.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.5...@ohif/viewer@3.1.6) (2019-12-13) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.1.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.4...@ohif/viewer@3.1.5) (2019-12-13) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.1.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.3...@ohif/viewer@3.1.4) (2019-12-13) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.1.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.2...@ohif/viewer@3.1.3) (2019-12-12) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.1.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.1...@ohif/viewer@3.1.2) (2019-12-12) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.1.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.0...@ohif/viewer@3.1.1) (2019-12-11) + +**Note:** Version bump only for package @ohif/viewer + + + + + +# [3.1.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.3...@ohif/viewer@3.1.0) (2019-12-11) + + +### Features + +* ๐ŸŽธ DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758) + + + + + +## [3.0.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.2...@ohif/viewer@3.0.3) (2019-12-11) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.0.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.1...@ohif/viewer@3.0.2) (2019-12-11) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [3.0.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.0...@ohif/viewer@3.0.1) (2019-12-09) + +**Note:** Version bump only for package @ohif/viewer + + + + + +# [3.0.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.8...@ohif/viewer@3.0.0) (2019-12-09) + + +* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229) + + +### BREAKING CHANGES + +* modifies the exposed react components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance. + + + + + +## [2.11.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.7...@ohif/viewer@2.11.8) (2019-12-07) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [2.11.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.6...@ohif/viewer@2.11.7) (2019-12-07) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [2.11.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.5...@ohif/viewer@2.11.6) (2019-12-07) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [2.11.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.4...@ohif/viewer@2.11.5) (2019-12-06) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [2.11.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.3...@ohif/viewer@2.11.4) (2019-12-02) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [2.11.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.2...@ohif/viewer@2.11.3) (2019-12-02) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [2.11.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.1...@ohif/viewer@2.11.2) (2019-11-28) + + +### Bug Fixes + +* User Preferences Issues ([#1207](https://github.com/OHIF/Viewers/issues/1207)) ([1df21a9](https://github.com/OHIF/Viewers/commit/1df21a9e075b5e6dfc10a429ae825826f46c71b8)), closes [#1161](https://github.com/OHIF/Viewers/issues/1161) [#1164](https://github.com/OHIF/Viewers/issues/1164) [#1177](https://github.com/OHIF/Viewers/issues/1177) [#1179](https://github.com/OHIF/Viewers/issues/1179) [#1180](https://github.com/OHIF/Viewers/issues/1180) [#1181](https://github.com/OHIF/Viewers/issues/1181) [#1182](https://github.com/OHIF/Viewers/issues/1182) [#1183](https://github.com/OHIF/Viewers/issues/1183) [#1184](https://github.com/OHIF/Viewers/issues/1184) [#1185](https://github.com/OHIF/Viewers/issues/1185) + + + + + +## [2.11.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.0...@ohif/viewer@2.11.1) (2019-11-27) + + +### Bug Fixes + +* of undefined name of project ([#1231](https://github.com/OHIF/Viewers/issues/1231)) ([e34a057](https://github.com/OHIF/Viewers/commit/e34a05726319e3e70279c43d5bf976d33cdf71f7)) + + + + + +# [2.11.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.10.2...@ohif/viewer@2.11.0) (2019-11-25) + + +### Features + +* Add new annotate tool using new dialog service ([#1211](https://github.com/OHIF/Viewers/issues/1211)) ([8fd3af1](https://github.com/OHIF/Viewers/commit/8fd3af1e137e793f1b482760a22591c64a072047)) + + + + + +## [2.10.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.10.1...@ohif/viewer@2.10.2) (2019-11-25) + + +### Bug Fixes + +* Issue branch from danny experimental changes pr 1128 ([#1150](https://github.com/OHIF/Viewers/issues/1150)) ([a870b3c](https://github.com/OHIF/Viewers/commit/a870b3cc6056cf824af422e46f1ad674910b534e)), closes [#1161](https://github.com/OHIF/Viewers/issues/1161) [#1164](https://github.com/OHIF/Viewers/issues/1164) [#1177](https://github.com/OHIF/Viewers/issues/1177) [#1179](https://github.com/OHIF/Viewers/issues/1179) [#1180](https://github.com/OHIF/Viewers/issues/1180) [#1181](https://github.com/OHIF/Viewers/issues/1181) [#1182](https://github.com/OHIF/Viewers/issues/1182) [#1183](https://github.com/OHIF/Viewers/issues/1183) [#1184](https://github.com/OHIF/Viewers/issues/1184) [#1185](https://github.com/OHIF/Viewers/issues/1185) + + + + + +## [2.10.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.10.0...@ohif/viewer@2.10.1) (2019-11-20) + +**Note:** Version bump only for package @ohif/viewer + + + + + +# [2.10.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.9.0...@ohif/viewer@2.10.0) (2019-11-19) + + +### Features + +* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37)) + + + + + +# [2.9.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.5...@ohif/viewer@2.9.0) (2019-11-19) + + +### Features + +* Issue 879 viewer route query param not filtering but promoting ([#1141](https://github.com/OHIF/Viewers/issues/1141)) ([b17f753](https://github.com/OHIF/Viewers/commit/b17f753e6222045252ef885e40233681541a32e1)), closes [#1118](https://github.com/OHIF/Viewers/issues/1118) + + + + + +## [2.8.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.4...@ohif/viewer@2.8.5) (2019-11-18) + + +### Bug Fixes + +* minor date picker UX improvements ([813ee5e](https://github.com/OHIF/Viewers/commit/813ee5ed4d78b7bda234922d5f3389efe346451c)) + + + + + +## [2.8.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.3...@ohif/viewer@2.8.4) (2019-11-15) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [2.8.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.2...@ohif/viewer@2.8.3) (2019-11-15) + +**Note:** Version bump only for package @ohif/viewer + + + + + +## [2.8.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.1...@ohif/viewer@2.8.2) (2019-11-14) + +**Note:** Version bump only for package @ohif/viewer + +## [2.8.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.0...@ohif/viewer@2.8.1) (2019-11-14) + +**Note:** Version bump only for package @ohif/viewer + +# [2.8.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.7.1...@ohif/viewer@2.8.0) (2019-11-13) + +### Features + +- expose UiNotifications service + ([#1172](https://github.com/OHIF/Viewers/issues/1172)) + ([5c04e34](https://github.com/OHIF/Viewers/commit/5c04e34c8fb2394ab7acd9eb4f2ab12afeb2f255)) +- filter field for google api windows + ([#1170](https://github.com/OHIF/Viewers/issues/1170)) + ([c59c5b3](https://github.com/OHIF/Viewers/commit/c59c5b3f14d44f1c06aa396125a1f4caaa431c25)) + +## [2.7.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.7.0...@ohif/viewer@2.7.1) (2019-11-12) + +### Bug Fixes + +- ๐Ÿ› Fix for JS breaking on header + ([#1164](https://github.com/OHIF/Viewers/issues/1164)) + ([0fbaf95](https://github.com/OHIF/Viewers/commit/0fbaf95971dc0b3a671e1f586a876d9019e860ed)) + +# [2.7.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.6.4...@ohif/viewer@2.7.0) (2019-11-12) + +### Features + +- ๐ŸŽธ Update hotkeys and user preferences modal + ([#1135](https://github.com/OHIF/Viewers/issues/1135)) + ([e62f5f8](https://github.com/OHIF/Viewers/commit/e62f5f8dd28ab363f23671cd21cee115abb870ff)), + closes [#923](https://github.com/OHIF/Viewers/issues/923) + +## [2.6.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.6.3...@ohif/viewer@2.6.4) (2019-11-11) + +**Note:** Version bump only for package @ohif/viewer + +## [2.6.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.6.2...@ohif/viewer@2.6.3) (2019-11-08) + +**Note:** Version bump only for package @ohif/viewer + +## [2.6.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.6.1...@ohif/viewer@2.6.2) (2019-11-08) + +**Note:** Version bump only for package @ohif/viewer + +## [2.6.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.6.0...@ohif/viewer@2.6.1) (2019-11-06) + +**Note:** Version bump only for package @ohif/viewer + +# [2.6.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.5.0...@ohif/viewer@2.6.0) (2019-11-06) + +### Features + +- modal provider ([#1151](https://github.com/OHIF/Viewers/issues/1151)) + ([75d88bc](https://github.com/OHIF/Viewers/commit/75d88bc454710d2dcdbc7d68c4d9df041159c840)), + closes [#1086](https://github.com/OHIF/Viewers/issues/1086) + [#1116](https://github.com/OHIF/Viewers/issues/1116) + [#1116](https://github.com/OHIF/Viewers/issues/1116) + [#1146](https://github.com/OHIF/Viewers/issues/1146) + [#1142](https://github.com/OHIF/Viewers/issues/1142) + [#1143](https://github.com/OHIF/Viewers/issues/1143) + [#1110](https://github.com/OHIF/Viewers/issues/1110) + [#1086](https://github.com/OHIF/Viewers/issues/1086) + [#1116](https://github.com/OHIF/Viewers/issues/1116) + [#1119](https://github.com/OHIF/Viewers/issues/1119) + +# [2.5.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.4.1...@ohif/viewer@2.5.0) (2019-11-05) + +### Features + +- ๐ŸŽธ Filter by url query param for seriesInstnaceUID + ([#1117](https://github.com/OHIF/Viewers/issues/1117)) + ([e208f2e](https://github.com/OHIF/Viewers/commit/e208f2e6a9c49b16dadead0a917f657cf023929a)), + closes [#1118](https://github.com/OHIF/Viewers/issues/1118) + +## [2.4.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.4.0...@ohif/viewer@2.4.1) (2019-11-05) + +### Bug Fixes + +- [#1075](https://github.com/OHIF/Viewers/issues/1075) Returning to the Study + List before all series have finisheโ€ฆ + ([#1090](https://github.com/OHIF/Viewers/issues/1090)) + ([ecaf578](https://github.com/OHIF/Viewers/commit/ecaf578f92dc40294cec7ff9b272fb432dec4125)) + +# [2.4.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.3.8...@ohif/viewer@2.4.0) (2019-11-04) + +### Features + +- ๐ŸŽธ New modal provider ([#1110](https://github.com/OHIF/Viewers/issues/1110)) + ([5ee832b](https://github.com/OHIF/Viewers/commit/5ee832b19505a4e8e5756660ce6ed03a7f18dec3)), + closes [#1086](https://github.com/OHIF/Viewers/issues/1086) + [#1116](https://github.com/OHIF/Viewers/issues/1116) + +## [2.3.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.3.7...@ohif/viewer@2.3.8) (2019-11-04) + +**Note:** Version bump only for package @ohif/viewer + +## [2.3.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.3.6...@ohif/viewer@2.3.7) (2019-11-04) + +### Bug Fixes + +- ๐Ÿ› Minor issues measurement panel related to description + ([#1142](https://github.com/OHIF/Viewers/issues/1142)) + ([681384b](https://github.com/OHIF/Viewers/commit/681384b7425c83b02a0ed83371ca92d78ca7838c)) + +## [2.3.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.3.5...@ohif/viewer@2.3.6) (2019-11-02) + +**Note:** Version bump only for package @ohif/viewer + +## [2.3.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.3.4...@ohif/viewer@2.3.5) (2019-10-31) + +### Bug Fixes + +- application crash if patientName is an object + ([#1138](https://github.com/OHIF/Viewers/issues/1138)) + ([64cf3b3](https://github.com/OHIF/Viewers/commit/64cf3b324da2383a927af1df2d46db2fca5318aa)) + +## [2.3.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.3.3...@ohif/viewer@2.3.4) (2019-10-30) + +### Bug Fixes + +- ๐Ÿ› Fix ghost shadow on thumb + ([#1113](https://github.com/OHIF/Viewers/issues/1113)) + ([caaa032](https://github.com/OHIF/Viewers/commit/caaa032c4bc24fd69fdb01a15a8feb2721c321db)) + +## [2.3.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.3.2...@ohif/viewer@2.3.3) (2019-10-30) + +### Bug Fixes + +- get adapter store picker to show + ([#1134](https://github.com/OHIF/Viewers/issues/1134)) + ([50ca2bd](https://github.com/OHIF/Viewers/commit/50ca2bde971e1e67b73ece96369052dd1a35ac68)) + +## [2.3.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.3.1...@ohif/viewer@2.3.2) (2019-10-29) + +### Bug Fixes + +- ๐Ÿ› Limit image download size to avoid browser issues + ([#1112](https://github.com/OHIF/Viewers/issues/1112)) + ([5716b71](https://github.com/OHIF/Viewers/commit/5716b71d409ee1c6f13393c8cb7f50222415e198)), + closes [#1099](https://github.com/OHIF/Viewers/issues/1099) + +## [2.3.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.3.0...@ohif/viewer@2.3.1) (2019-10-29) + +### Bug Fixes + +- rollbar template needs PUBLIC_URL defined + ([#1127](https://github.com/OHIF/Viewers/issues/1127)) + ([352407c](https://github.com/OHIF/Viewers/commit/352407c71ae93946e9ebad41446d6086cfbc237b)) + +# [2.3.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.2.2...@ohif/viewer@2.3.0) (2019-10-29) + +### Features + +- service worker ([#1045](https://github.com/OHIF/Viewers/issues/1045)) + ([cf51368](https://github.com/OHIF/Viewers/commit/cf5136899eac08300ec4f15474a6440129ef7a9a)) + +## [2.2.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.2.1...@ohif/viewer@2.2.2) (2019-10-29) + +### Bug Fixes + +- Set SR viewport as active by interaction + ([#1118](https://github.com/OHIF/Viewers/issues/1118)) + ([5b33417](https://github.com/OHIF/Viewers/commit/5b334175c370afb930b4b6dbd307ddece8f850e3)) + +## [2.2.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.2.0...@ohif/viewer@2.2.1) (2019-10-29) + +**Note:** Version bump only for package @ohif/viewer + +# [2.2.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.1.4...@ohif/viewer@2.2.0) (2019-10-28) + +### Features + +- responsive study list ([#1068](https://github.com/OHIF/Viewers/issues/1068)) + ([2cdef4b](https://github.com/OHIF/Viewers/commit/2cdef4b9844cc2ce61e9ce76b5a942ba7051fe16)) + +## [2.1.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.1.3...@ohif/viewer@2.1.4) (2019-10-28) + +**Note:** Version bump only for package @ohif/viewer + +## [2.1.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.1.2...@ohif/viewer@2.1.3) (2019-10-26) + +**Note:** Version bump only for package @ohif/viewer + +## [2.1.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.1.1...@ohif/viewer@2.1.2) (2019-10-26) + +### Bug Fixes + +- update script-tag output to include config from default.js + ([c522ff3](https://github.com/OHIF/Viewers/commit/c522ff3ddab7ed8e3a128dd6edd2cd6902226e99)) + +## [2.1.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.1.0...@ohif/viewer@2.1.1) (2019-10-26) + +### Bug Fixes + +- ๐Ÿ› JSON launch not working properly + ([#1089](https://github.com/OHIF/Viewers/issues/1089)) + ([#1093](https://github.com/OHIF/Viewers/issues/1093)) + ([2677170](https://github.com/OHIF/Viewers/commit/2677170d67659ee178cf77307414d54cfe9cb563)) + +# [2.1.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.0.0...@ohif/viewer@2.1.0) (2019-10-26) + +### Features + +- Snapshot Download Tool ([#840](https://github.com/OHIF/Viewers/issues/840)) + ([450e098](https://github.com/OHIF/Viewers/commit/450e0981a5ba054fcfcb85eeaeb18371af9088f8)) + +# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.12.2...@ohif/viewer@2.0.0) (2019-10-26) + +### Bug Fixes + +- ๐Ÿ› Desc of meas.table not being updated on properly + ([#1094](https://github.com/OHIF/Viewers/issues/1094)) + ([85f836c](https://github.com/OHIF/Viewers/commit/85f836cd918614be722fce1bff2373460ec4900b)), + closes [#1013](https://github.com/OHIF/Viewers/issues/1013) + +### BREAKING CHANGES + +- 1013 + +## [1.12.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.12.1...@ohif/viewer@1.12.2) (2019-10-25) + +### Bug Fixes + +- set SR in ActiveViewport by clicking thumb + ([#1091](https://github.com/OHIF/Viewers/issues/1091)) + ([986b7ae](https://github.com/OHIF/Viewers/commit/986b7ae2bf4f7d27f326e62f93285ce20eaf0a79)) + +## [1.12.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.12.0...@ohif/viewer@1.12.1) (2019-10-25) + +### Bug Fixes + +- ๐Ÿ› Orthographic MPR fix ([#1092](https://github.com/OHIF/Viewers/issues/1092)) + ([460e375](https://github.com/OHIF/Viewers/commit/460e375f0aa75d35f7a46b4d48e6cc706019956d)) + +# [1.12.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.11.5...@ohif/viewer@1.12.0) (2019-10-25) + +### Features + +- ๐ŸŽธ Allow routes to load Google Cloud DICOM Stores in the Study List + ([#1069](https://github.com/OHIF/Viewers/issues/1069)) + ([21b586b](https://github.com/OHIF/Viewers/commit/21b586b08f3dde6613859712a9e0577dece564db)) + +## [1.11.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.11.4...@ohif/viewer@1.11.5) (2019-10-24) + +**Note:** Version bump only for package @ohif/viewer + +## [1.11.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.11.3...@ohif/viewer@1.11.4) (2019-10-23) + +### Bug Fixes + +- Revert "Revert "fix: MPR initialization"" + ([#1065](https://github.com/OHIF/Viewers/issues/1065)) + ([c680720](https://github.com/OHIF/Viewers/commit/c680720ce5ead58fdb399e3a356edac18093f5c0)), + closes [#1062](https://github.com/OHIF/Viewers/issues/1062) + [#1064](https://github.com/OHIF/Viewers/issues/1064) + +## [1.11.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.11.2...@ohif/viewer@1.11.3) (2019-10-23) + +### Bug Fixes + +- ๐Ÿ› Switch to orhtographic view for 2D MPR + ([#1074](https://github.com/OHIF/Viewers/issues/1074)) + ([13d337a](https://github.com/OHIF/Viewers/commit/13d337aaabb8dadf6366c6262c5e47e7781edd08)) + +## [1.11.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.11.1...@ohif/viewer@1.11.2) (2019-10-23) + +**Note:** Version bump only for package @ohif/viewer + +## [1.11.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.11.0...@ohif/viewer@1.11.1) (2019-10-23) + +**Note:** Version bump only for package @ohif/viewer + +# [1.11.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.10.3...@ohif/viewer@1.11.0) (2019-10-22) + +### Bug Fixes + +- MPR initialization ([#1062](https://github.com/OHIF/Viewers/issues/1062)) + ([b037394](https://github.com/OHIF/Viewers/commit/b03739428f72bb50bdabdd6f83b7af885057da69)) + +### Features + +- ๐ŸŽธ Load spinner when selecting gcloud store. Add key on td + ([#1034](https://github.com/OHIF/Viewers/issues/1034)) + ([e62f403](https://github.com/OHIF/Viewers/commit/e62f403fe9e3df56713128e3d59045824b086d8d)), + closes [#1057](https://github.com/OHIF/Viewers/issues/1057) + +## [1.10.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.10.2...@ohif/viewer@1.10.3) (2019-10-18) + +**Note:** Version bump only for package @ohif/viewer + +## [1.10.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.10.1...@ohif/viewer@1.10.2) (2019-10-18) + +**Note:** Version bump only for package @ohif/viewer + +## [1.10.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.10.0...@ohif/viewer@1.10.1) (2019-10-16) + +**Note:** Version bump only for package @ohif/viewer + +# [1.10.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.9.1...@ohif/viewer@1.10.0) (2019-10-15) + +### Features + +- Add browser info and app version + ([#1046](https://github.com/OHIF/Viewers/issues/1046)) + ([c217b8b](https://github.com/OHIF/Viewers/commit/c217b8b)) + +## [1.9.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.9.0...@ohif/viewer@1.9.1) (2019-10-15) + +### Bug Fixes + +- ๐Ÿ› Remove debugger statement left in from last PR + ([#1052](https://github.com/OHIF/Viewers/issues/1052)) + ([d091cd6](https://github.com/OHIF/Viewers/commit/d091cd6)) + +# [1.9.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.8.0...@ohif/viewer@1.9.0) (2019-10-15) + +### Features + +- ๐ŸŽธ Only allow reconstruction of datasets that make sense + ([#1010](https://github.com/OHIF/Viewers/issues/1010)) + ([2d75e01](https://github.com/OHIF/Viewers/commit/2d75e01)), closes + [#561](https://github.com/OHIF/Viewers/issues/561) + +# [1.8.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.7.0...@ohif/viewer@1.8.0) (2019-10-14) + +### Features + +- Notification Service ([#1011](https://github.com/OHIF/Viewers/issues/1011)) + ([92c8996](https://github.com/OHIF/Viewers/commit/92c8996)) + +# [1.7.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.6.3...@ohif/viewer@1.7.0) (2019-10-14) + +### Features + +- Implement a 'Exit 2D MPR' button in the toolbar + ([c99e0d8](https://github.com/OHIF/Viewers/commit/c99e0d8)) + +## [1.6.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.6.2...@ohif/viewer@1.6.3) (2019-10-14) + +**Note:** Version bump only for package @ohif/viewer + +## [1.6.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.6.1...@ohif/viewer@1.6.2) (2019-10-11) + +**Note:** Version bump only for package @ohif/viewer + +## [1.6.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.6.0...@ohif/viewer@1.6.1) (2019-10-11) + +### Bug Fixes + +- Switch token storage back to localStorage because in-memory was annoying for + end users ([#1030](https://github.com/OHIF/Viewers/issues/1030)) + ([412fe4e](https://github.com/OHIF/Viewers/commit/412fe4e)) + +# [1.6.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.5.4...@ohif/viewer@1.6.0) (2019-10-11) + +### Features + +- ๐ŸŽธ Improve usability of Google Cloud adapter, including direct routes to + studies ([#989](https://github.com/OHIF/Viewers/issues/989)) + ([2bc361c](https://github.com/OHIF/Viewers/commit/2bc361c)) + +## [1.5.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.5.3...@ohif/viewer@1.5.4) (2019-10-10) + +**Note:** Version bump only for package @ohif/viewer + +## [1.5.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.5.2...@ohif/viewer@1.5.3) (2019-10-10) + +**Note:** Version bump only for package @ohif/viewer + +## [1.5.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.5.1...@ohif/viewer@1.5.2) (2019-10-10) + +### Bug Fixes + +- ๐ŸŽธ switch ohif logo from text + font to SVG + ([#1021](https://github.com/OHIF/Viewers/issues/1021)) + ([e7de8be](https://github.com/OHIF/Viewers/commit/e7de8be)) + +## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.5.0...@ohif/viewer@1.5.1) (2019-10-09) + +### Bug Fixes + +- ๐Ÿ› set current viewport as active when switching layouts + ([#1018](https://github.com/OHIF/Viewers/issues/1018)) + ([2a74355](https://github.com/OHIF/Viewers/commit/2a74355)) + +# [1.5.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.4.5...@ohif/viewer@1.5.0) (2019-10-09) + +### Features + +- Multiple fixes and implementation changes to react-cornerstone-viewport + ([1cc94f3](https://github.com/OHIF/Viewers/commit/1cc94f3)) + +## [1.4.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.4.4...@ohif/viewer@1.4.5) (2019-10-09) + +**Note:** Version bump only for package @ohif/viewer + +## [1.4.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.4.3...@ohif/viewer@1.4.4) (2019-10-07) + +**Note:** Version bump only for package @ohif/viewer + +## [1.4.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.4.2...@ohif/viewer@1.4.3) (2019-10-04) + +**Note:** Version bump only for package @ohif/viewer + +## [1.4.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.4.1...@ohif/viewer@1.4.2) (2019-10-04) + +**Note:** Version bump only for package @ohif/viewer + +## [1.4.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.4.0...@ohif/viewer@1.4.1) (2019-10-03) + +**Note:** Version bump only for package @ohif/viewer + +# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.3.3...@ohif/viewer@1.4.0) (2019-10-03) + +### Features + +- Use QIDO + WADO to load series metadata individually rather than the entire + study metadata at once ([#953](https://github.com/OHIF/Viewers/issues/953)) + ([9e10c2b](https://github.com/OHIF/Viewers/commit/9e10c2b)) + +## [1.3.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.3.2...@ohif/viewer@1.3.3) (2019-10-02) + +**Note:** Version bump only for package @ohif/viewer + +## [1.3.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.3.1...@ohif/viewer@1.3.2) (2019-10-01) + +**Note:** Version bump only for package @ohif/viewer + +## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.3.0...@ohif/viewer@1.3.1) (2019-10-01) + +### Bug Fixes + +- Exit MPR mode if Layout is changed + ([#984](https://github.com/OHIF/Viewers/issues/984)) + ([674ca9f](https://github.com/OHIF/Viewers/commit/674ca9f)) + +# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.8...@ohif/viewer@1.3.0) (2019-10-01) + +### Features + +- ๐ŸŽธ MPR UI improvements. Added MinIP, AvgIP, slab thickness slider and mode + toggle ([#947](https://github.com/OHIF/Viewers/issues/947)) + ([c79c0c3](https://github.com/OHIF/Viewers/commit/c79c0c3)) + +## [1.2.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.7...@ohif/viewer@1.2.8) (2019-10-01) + +**Note:** Version bump only for package @ohif/viewer + +## [1.2.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.6...@ohif/viewer@1.2.7) (2019-10-01) + +**Note:** Version bump only for package @ohif/viewer + +## [1.2.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.5...@ohif/viewer@1.2.6) (2019-09-27) + +**Note:** Version bump only for package @ohif/viewer + +## [1.2.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.4...@ohif/viewer@1.2.5) (2019-09-27) + +### Bug Fixes + +- version bump issue ([#963](https://github.com/OHIF/Viewers/issues/963)) + ([e607ed2](https://github.com/OHIF/Viewers/commit/e607ed2)), closes + [#962](https://github.com/OHIF/Viewers/issues/962) + +# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.3...@ohif/viewer@2.0.0) (2019-09-27) + +### Bug Fixes + +- ๐Ÿ› Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support + in local file and WADO-RS-only use cases + ([#862](https://github.com/OHIF/Viewers/issues/862)) + ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes + [#838](https://github.com/OHIF/Viewers/issues/838) +- version bump issue ([#962](https://github.com/OHIF/Viewers/issues/962)) + ([c80ea17](https://github.com/OHIF/Viewers/commit/c80ea17)) + +### BREAKING CHANGES + +- DICOM Seg + +# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.3...@ohif/viewer@2.0.0) (2019-09-27) + +### Bug Fixes + +- ๐Ÿ› Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support + in local file and WADO-RS-only use cases + ([#862](https://github.com/OHIF/Viewers/issues/862)) + ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes + [#838](https://github.com/OHIF/Viewers/issues/838) + +### BREAKING CHANGES + +- DICOM Seg + +## [1.2.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.2...@ohif/viewer@1.2.3) (2019-09-27) + +**Note:** Version bump only for package @ohif/viewer + +## [1.2.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.1...@ohif/viewer@1.2.2) (2019-09-27) + +**Note:** Version bump only for package @ohif/viewer + +## [1.2.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.0...@ohif/viewer@1.2.1) (2019-09-26) + +### Bug Fixes + +- google cloud support w/ docker (via env var) + ([#958](https://github.com/OHIF/Viewers/issues/958)) + ([e375a4a](https://github.com/OHIF/Viewers/commit/e375a4a)) + +# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.1.14...@ohif/viewer@1.2.0) (2019-09-26) + +### Features + +- ๐ŸŽธ React custom component on toolbar button + ([#935](https://github.com/OHIF/Viewers/issues/935)) + ([a90605c](https://github.com/OHIF/Viewers/commit/a90605c)) + +## [1.1.14](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.1.13...@ohif/viewer@1.1.14) (2019-09-26) + +### Bug Fixes + +- ๐Ÿ› Set series into active viewport by clicking on thumbnail + ([#945](https://github.com/OHIF/Viewers/issues/945)) + ([5551f81](https://github.com/OHIF/Viewers/commit/5551f81)), closes + [#895](https://github.com/OHIF/Viewers/issues/895) + [#895](https://github.com/OHIF/Viewers/issues/895) + +## [1.1.13](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.1.12...@ohif/viewer@1.1.13) (2019-09-26) + +### Bug Fixes + +- Add some code splitting for PWA build + ([#937](https://github.com/OHIF/Viewers/issues/937)) + ([8938035](https://github.com/OHIF/Viewers/commit/8938035)) + +## [1.1.12](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.1.11...@ohif/viewer@1.1.12) (2019-09-26) + +**Note:** Version bump only for package @ohif/viewer + +## [1.1.11](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.1.10...@ohif/viewer@1.1.11) (2019-09-23) + +**Note:** Version bump only for package @ohif/viewer + +## [1.1.10](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.1.9...@ohif/viewer@1.1.10) (2019-09-19) + +**Note:** Version bump only for package @ohif/viewer + +## [1.1.9](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.1.8...@ohif/viewer@1.1.9) (2019-09-19) + +**Note:** Version bump only for package @ohif/viewer + +## [1.1.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.1.7...@ohif/viewer@1.1.8) (2019-09-19) + +**Note:** Version bump only for package @ohif/viewer + +## [1.1.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.1.6...@ohif/viewer@1.1.7) (2019-09-19) + +**Note:** Version bump only for package @ohif/viewer + +## [1.1.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.1.5...@ohif/viewer@1.1.6) (2019-09-19) + +**Note:** Version bump only for package @ohif/viewer + +## [1.1.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.1.4...@ohif/viewer@1.1.5) (2019-09-17) + +### Bug Fixes + +- bump cornerstone-tools to latest version + ([f519f86](https://github.com/OHIF/Viewers/commit/f519f86)) + +## [1.1.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.1.3...@ohif/viewer@1.1.4) (2019-09-17) + +**Note:** Version bump only for package @ohif/viewer + +## [1.1.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.1.2...@ohif/viewer@1.1.3) (2019-09-16) + +### Bug Fixes + +- ๐Ÿ› Fix issue on not loading gcloud + ([#919](https://github.com/OHIF/Viewers/issues/919)) + ([f723546](https://github.com/OHIF/Viewers/commit/f723546)) + +## [1.1.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.1.1...@ohif/viewer@1.1.2) (2019-09-12) + +**Note:** Version bump only for package @ohif/viewer + +## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.1.0...@ohif/viewer@1.1.1) (2019-09-12) + +**Note:** Version bump only for package @ohif/viewer + +# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.0.5...@ohif/viewer@1.1.0) (2019-09-12) + +### Features + +- ๐ŸŽธ Load local file or folder using native dialog + ([#870](https://github.com/OHIF/Viewers/issues/870)) + ([c221dd8](https://github.com/OHIF/Viewers/commit/c221dd8)) + +## [1.0.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.0.4...@ohif/viewer@1.0.5) (2019-09-10) + +**Note:** Version bump only for package @ohif/viewer + +## [1.0.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.0.3...@ohif/viewer@1.0.4) (2019-09-10) + +**Note:** Version bump only for package @ohif/viewer + +## [1.0.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.0.2...@ohif/viewer@1.0.3) (2019-09-10) + +### Bug Fixes + +- on-brand library global name + ([ababe63](https://github.com/OHIF/Viewers/commit/ababe63)) +- remove requestOptions when key is not needed + ([32bc47d](https://github.com/OHIF/Viewers/commit/32bc47d)) + +## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.0.1...@ohif/viewer@1.0.2) (2019-09-09) + +### Bug Fixes + +- import regenerator-runtime for umd build + ([bad987a](https://github.com/OHIF/Viewers/commit/bad987a)) + +## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.0.0...@ohif/viewer@1.0.1) (2019-09-09) + +**Note:** Version bump only for package @ohif/viewer + +# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.21...@ohif/viewer@1.0.0) (2019-09-06) + +### Code Refactoring + +- ๐Ÿ’ก React components to consume appConfig using Context + ([#852](https://github.com/OHIF/Viewers/issues/852)) + ([7c4ee73](https://github.com/OHIF/Viewers/commit/7c4ee73)), closes + [#725](https://github.com/OHIF/Viewers/issues/725) + [#725](https://github.com/OHIF/Viewers/issues/725) + +### BREAKING CHANGES + +- #725 + +## [0.50.21](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.20...@ohif/viewer@0.50.21) (2019-09-06) + +### Bug Fixes + +- viewer project should build output before publish + ([94b625d](https://github.com/OHIF/Viewers/commit/94b625d)) + +## [0.50.20](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.19...@ohif/viewer@0.50.20) (2019-09-06) + +**Note:** Version bump only for package @ohif/viewer + +## [0.50.19](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.18...@ohif/viewer@0.50.19) (2019-09-06) + +### Bug Fixes + +- @ohif/viewer package build + ([4aa7cbd](https://github.com/OHIF/Viewers/commit/4aa7cbd)) + +## [0.50.18](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.17...@ohif/viewer@0.50.18) (2019-09-05) + +**Note:** Version bump only for package @ohif/viewer + +## [0.50.17](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.16...@ohif/viewer@0.50.17) (2019-09-04) + +**Note:** Version bump only for package @ohif/viewer + +## [0.50.16](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.15...@ohif/viewer@0.50.16) (2019-09-04) + +**Note:** Version bump only for package @ohif/viewer + +## [0.50.15](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.14...@ohif/viewer@0.50.15) (2019-09-04) + +### Bug Fixes + +- measurementsAPI issue caused by production build + ([#842](https://github.com/OHIF/Viewers/issues/842)) + ([49d3439](https://github.com/OHIF/Viewers/commit/49d3439)) + +## [0.50.14](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.13...@ohif/viewer@0.50.14) (2019-09-03) + +**Note:** Version bump only for package @ohif/viewer + +## [0.50.13](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.12...@ohif/viewer@0.50.13) (2019-09-03) + +### Bug Fixes + +- ๐Ÿ› Activating Pan and Zoom on right and middle click by def + ([#841](https://github.com/OHIF/Viewers/issues/841)) + ([7a9b477](https://github.com/OHIF/Viewers/commit/7a9b477)) + +## [0.50.12](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.11...@ohif/viewer@0.50.12) (2019-08-29) + +### Bug Fixes + +- asset resolution when at non-root route + ([#828](https://github.com/OHIF/Viewers/issues/828)) + ([d48b617](https://github.com/OHIF/Viewers/commit/d48b617)) + +## [0.50.11](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.10...@ohif/viewer@0.50.11) (2019-08-29) + +**Note:** Version bump only for package @ohif/viewer + +## [0.50.10](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.9...@ohif/viewer@0.50.10) (2019-08-27) + +**Note:** Version bump only for package @ohif/viewer + +## [0.50.9](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.8...@ohif/viewer@0.50.9) (2019-08-27) + +**Note:** Version bump only for package @ohif/viewer + +## [0.50.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.7...@ohif/viewer@0.50.8) (2019-08-26) + +**Note:** Version bump only for package @ohif/viewer + +## [0.50.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.6...@ohif/viewer@0.50.7) (2019-08-22) + +### Bug Fixes + +- ๐Ÿ› Update for changes in ExpandableToolMenu props + ([e09670a](https://github.com/OHIF/Viewers/commit/e09670a)) + +## [0.50.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.5...@ohif/viewer@0.50.6) (2019-08-22) + +**Note:** Version bump only for package @ohif/viewer + +## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.4...@ohif/viewer@0.50.5) (2019-08-21) + +### Bug Fixes + +- **StandaloneRouting:** Promise rejection - added `return` + ([#791](https://github.com/OHIF/Viewers/issues/791)) + ([d09fb4e](https://github.com/OHIF/Viewers/commit/d09fb4e)) + +## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.3...@ohif/viewer@0.50.4) (2019-08-20) + +**Note:** Version bump only for package @ohif/viewer + +## [0.50.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.2...@ohif/viewer@0.50.3) (2019-08-15) + +**Note:** Version bump only for package @ohif/viewer + +## [0.50.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.1...@ohif/viewer@0.50.2) (2019-08-15) + +**Note:** Version bump only for package @ohif/viewer + +## [0.50.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.0-alpha.13...@ohif/viewer@0.50.1) (2019-08-14) + +**Note:** Version bump only for package @ohif/viewer + +# [0.50.0-alpha.13](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.0-alpha.12...@ohif/viewer@0.50.0-alpha.13) (2019-08-14) + +**Note:** Version bump only for package @ohif/viewer + +# [0.50.0-alpha.12](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.50.0-alpha.11...@ohif/viewer@0.50.0-alpha.12) (2019-08-14) + +**Note:** Version bump only for package @ohif/viewer + +# [0.50.0-alpha.11](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.0.22-alpha.10...@ohif/viewer@0.50.0-alpha.11) (2019-08-14) + +**Note:** Version bump only for package @ohif/viewer + +## [0.0.22-alpha.10](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.0.22-alpha.9...@ohif/viewer@0.0.22-alpha.10) (2019-08-14) + +**Note:** Version bump only for package @ohif/viewer + +## 0.0.22-alpha.9 (2019-08-14) + +**Note:** Version bump only for package @ohif/viewer + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.0.22-alpha.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.0.22-alpha.7...@ohif/viewer@0.0.22-alpha.8) (2019-08-08) + +**Note:** Version bump only for package @ohif/viewer + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.0.22-alpha.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.0.22-alpha.6...@ohif/viewer@0.0.22-alpha.7) (2019-08-08) + +**Note:** Version bump only for package @ohif/viewer + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.0.22-alpha.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.0.22-alpha.5...@ohif/viewer@0.0.22-alpha.6) (2019-08-08) + +**Note:** Version bump only for package @ohif/viewer + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.0.22-alpha.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.0.22-alpha.4...@ohif/viewer@0.0.22-alpha.5) (2019-08-08) + +**Note:** Version bump only for package @ohif/viewer + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.0.22-alpha.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.0.22-alpha.3...@ohif/viewer@0.0.22-alpha.4) (2019-08-08) + +**Note:** Version bump only for package @ohif/viewer + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.0.22-alpha.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.0.22-alpha.2...@ohif/viewer@0.0.22-alpha.3) (2019-08-07) + +**Note:** Version bump only for package @ohif/viewer + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.0.22-alpha.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.0.22-alpha.1...@ohif/viewer@0.0.22-alpha.2) (2019-08-07) + +**Note:** Version bump only for package @ohif/viewer + +## [0.0.22-alpha.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@0.0.22-alpha.0...@ohif/viewer@0.0.22-alpha.1) (2019-08-07) + +**Note:** Version bump only for package @ohif/viewer + +## 0.0.22-alpha.0 (2019-08-05) + +**Note:** Version bump only for package @ohif/viewer diff --git a/platform/app/LICENSE b/platform/app/LICENSE new file mode 100644 index 0000000..8b09055 --- /dev/null +++ b/platform/app/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/platform/app/README.md b/platform/app/README.md new file mode 100644 index 0000000..f1e1dd1 --- /dev/null +++ b/platform/app/README.md @@ -0,0 +1,207 @@ + + +
+

@ohif/app

+

@ohif/app is a zero-footprint medical image viewer provided by the Open Health Imaging Foundation (OHIF). It is a configurable and extensible progressive web application with out-of-the-box support for image archives which support DICOMweb.

+
+ + + + + + +
+ +[![NPM version][npm-version-image]][npm-url] +[![NPM downloads][npm-downloads-image]][npm-url] +[![Pulls][docker-pulls-img]][docker-image-url] +[![All Contributors](https://img.shields.io/badge/all_contributors-9-orange.svg?style=flat-square)](#contributors) +[![MIT License][license-image]][license-url] + + + +> ATTENTION: If you are looking for Version 1 (the Meteor Version) of this +> repository, it lives on +> [the `v1.x` branch](https://github.com/OHIF/Viewers/tree/v1.x) + +## Why? + +Building a web based medical imaging viewer from scratch is time intensive, hard +to get right, and expensive. Instead of re-inventing the wheel, you can use the +OHIF Viewer as a rock solid platform to build on top of. The Viewer is a +[React][react-url] [Progressive Web Application][pwa-url] that can be embedded +in existing applications via it's [packaged source +(ohif-viewer)][ohif-viewer-url] or hosted stand-alone. The Viewer exposes +[configuration][configuration-url] and [extensions][extensions-url] to support +workflow customization and advanced functionality at common integration points. + +If you're interested in using the OHIF Viewer, but you're not sure it supports +your use case [check out our docs](https://docs.ohif.org/). Still not sure, or +you would like to propose new features? Don't hesitate to +[create an issue](https://github.com/OHIF/Viewers/issues) or open a pull +request. + +## Getting Started + +This readme is specific to testing and developing locally. If you're more +interested in production deployment strategies, +[you can check out our documentation on publishing](https://docs.ohif.org/). + +Want to play around before you dig in? +[Check out our LIVE Demo](https://viewer.ohif.org/) + +### Setup + +_Requirements:_ + +- [NodeJS & NPM](https://nodejs.org/en/download/) +- [Yarn](https://yarnpkg.com/lang/en/docs/install/) + +_Steps:_ + +1. Fork this repository +2. Clone your forked repository (your `origin`) + +- `git clone git@github.com:YOUR_GITHUB_USERNAME/Viewers.git` + +3. Add `OHIF/Viewers` as a `remote` repository (the `upstream`) + +- `git remote add upstream git@github.com:OHIF/Viewers.git` + +### Developing Locally + +In your cloned repository's root folder, run: + +```js +// Restore dependencies +yarn install + +// Stands up local server to host Viewer. +// Viewer connects to our public cloud PACS by default +yarn start +``` + +For more advanced local development scenarios, like using your own locally +hosted PACS and test data, +[check out our Essential: Getting Started](https://docs.ohif.org/getting-started.html) +guide. + +### E2E Tests + +Using [Cypress](https://www.cypress.io/) to create End-to-End tests and check +whether the application flow is performing correctly, ensuring that the +integrated components are working as expected. + +#### Why Cypress? + +Cypress is a next generation front end testing tool built for the modern web. +With Cypress is easy to set up, write, run and debug tests + +It allow us to write different types of tests: + +- End-to-End tests +- Integration tests +- Unit tets + +All tests must be in `./cypress/integration` folder. + +Commands to run the tests: + +```js +// Open Cypress Dashboard that provides insight into what happened when your tests ran +yarn run cy + +// Run all tests using Electron browser headless +yarn run cy:run + +// Run all tests in CI mode +yarn run cy:run:ci +``` + +### Contributing + +> Large portions of the Viewer's functionality are maintained in other +> repositories. To get a better understanding of the Viewer's architecture and +> "where things live", read +> [our docs on the Viewer's architecture](https://docs.ohif.org/architecture/index.html#overview) + +It is notoriously difficult to setup multiple dependent repositories for +end-to-end testing and development. That's why we recommend writing and running +unit tests when adding and modifying features. This allows us to program in +isolation without a complex setup, and has the added benefit of producing +well-tested business logic. + +1. Clone this repository +2. Navigate to the project directory, and `yarn install` +3. To begin making changes, `yarn run dev` +4. To commit changes, run `yarn run cm` + +When creating tests, place the test file "next to" the file you're testing. +[For example](https://github.com/OHIF/Viewers/blob/master/src/utils/index.test.js): + +```js +// File +index.js; + +// Test for file +index.test.js; +``` + +As you add and modify code, `jest` will watch for uncommitted changes and run +your tests, reporting the results to your terminal. Make a pull request with +your changes to `master`, and a core team member will review your work. If you +have any questions, please don't hesitate to reach out via a GitHub issue. + +## Contributors + +Thanks goes to these wonderful people +([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + +
Erik Ziegler
Erik Ziegler

๐Ÿ’ป ๐Ÿš‡
Evren Ozkan
Evren Ozkan

๐Ÿ’ป
Gustavo Andrรฉ Lelis
Gustavo Andrรฉ Lelis

๐Ÿ’ป
Danny Brown
Danny Brown

๐Ÿ’ป ๐Ÿš‡
allcontributors[bot]
allcontributors[bot]

๐Ÿ“–
Esref Durna
Esref Durna

๐Ÿ’ฌ
diego0020
diego0020

๐Ÿ’ป
David Wire
David Wire

๐Ÿ’ป
Joรฃo Felipe de Medeiros Moreira
Joรฃo Felipe de Medeiros Moreira

โš ๏ธ
+ + + +This project follows the +[all-contributors](https://github.com/all-contributors/all-contributors) +specification. Contributions of any kind welcome! + +## License + +MIT ยฉ [OHIF](https://github.com/OHIF) + + + + +[npm-url]: https://npmjs.org/package/ohif-viewer +[npm-downloads-image]: https://img.shields.io/npm/dm/ohif-viewer.svg?style=flat-square +[npm-version-image]: https://img.shields.io/npm/v/ohif-viewer.svg?style=flat-square +[docker-pulls-img]: https://img.shields.io/docker/pulls/ohif/viewer.svg?style=flat-square +[docker-image-url]: https://hub.docker.com/r/ohif/viewer +[all-contributors-image]: https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square +[license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square +[license-url]: LICENSE + +[react-url]: https://reactjs.org/ +[pwa-url]: https://developers.google.com/web/progressive-web-apps/ +[ohif-viewer-url]: https://www.npmjs.com/package/ohif-viewer +[configuration-url]: https://docs.ohif.org/configuring/ +[extensions-url]: https://docs.ohif.org/extensions + +[react-viewer]: https://github.com/OHIF/Viewers/tree/react + +[bugs]: https://github.com/OHIF/Viewers/labels/bug +[requests-feature]: https://github.com/OHIF/Viewers/labels/enhancement +[good-first-issue]: https://github.com/OHIF/Viewers/labels/good%20first%20issue +[google-group]: https://groups.google.com/forum/#!forum/cornerstone-platform + + diff --git a/platform/app/assets/open-graph.fig b/platform/app/assets/open-graph.fig new file mode 100644 index 0000000..52ebfa4 Binary files /dev/null and b/platform/app/assets/open-graph.fig differ diff --git a/platform/app/babel.config.js b/platform/app/babel.config.js new file mode 100644 index 0000000..325ca2a --- /dev/null +++ b/platform/app/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/platform/app/cypress.config.ts b/platform/app/cypress.config.ts new file mode 100644 index 0000000..eecb13a --- /dev/null +++ b/platform/app/cypress.config.ts @@ -0,0 +1,37 @@ +import { defineConfig } from 'cypress'; + +export default defineConfig({ + chromeWebSecurity: false, + e2e: { + experimentalRunAllSpecs: true, + supportFile: 'cypress/support/index.js', + setupNodeEvents(on, config) { + on('before:browser:launch', (browser, launchOptions) => { + // `args` is an array of all the arguments that will + // be passed to browsers when it launches + + console.log(launchOptions.args); // print all current args + + console.log('***', browser.family, browser.name, '***'); + + // whatever you return here becomes the launchOptions + return launchOptions; + }); + }, + baseUrl: 'http://localhost:3000', + waitForAnimations: true, + chromeWebSecurity: false, + defaultCommandTimeout: 30000, + requestTimeout: 30000, + responseTimeout: 30000, + pageLoadTimeout: 30000, + specPattern: 'cypress/integration/**/*.spec.[jt]s', + projectId: '4oe38f', + video: true, + reporter: 'junit', + reporterOptions: { + mochaFile: 'cypress/results/test-output.xml', + toConsole: true, + }, + }, +}); diff --git a/platform/app/cypress/.eslintrc.js b/platform/app/cypress/.eslintrc.js new file mode 100644 index 0000000..beb89a7 --- /dev/null +++ b/platform/app/cypress/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: ['cypress'], + env: { + 'cypress/globals': true, + }, +}; diff --git a/platform/app/cypress/fixtures/example.json b/platform/app/cypress/fixtures/example.json new file mode 100644 index 0000000..02e4254 --- /dev/null +++ b/platform/app/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/platform/app/cypress/integration/ImageConsistency.spec.js b/platform/app/cypress/integration/ImageConsistency.spec.js new file mode 100644 index 0000000..37a03e5 --- /dev/null +++ b/platform/app/cypress/integration/ImageConsistency.spec.js @@ -0,0 +1,92 @@ +/** + * Add tests to ensure image consistency and quality + */ + +const testPixel = (dx, dy, expectedPixel) => { + cy.get('.cornerstone-canvas').then(v => { + const canvas = v[0]; + cy.log( + 'testPixel canvas', + dx, + dy, + expectedPixel, + canvas.width, + canvas.height, + canvas.style.width, + canvas.style.height + ); + const ctx = canvas.getContext('2d'); + cy.window() + .its('cornerstone') + .then(cornerstone => { + const { viewport } = cornerstone.getEnabledElements()[0]; + const imageData = viewport.getImageData(); + // cy.log("imageData", imageData); + const origin = viewport.worldToCanvas(imageData?.origin); + const orX = origin[0] * devicePixelRatio; + const orY = origin[1] * devicePixelRatio; + const x = Math.round(orX + dx); + const y = Math.round(orY + dy); + cy.log('testPixel origin x,y point x,y', orX, orY, x, y); + // cy.log('world origin', imageData.origin); + // cy.log('focal', viewport.getCamera().focalPoint, + // viewport.worldToCanvas(viewport.getCamera().focalPoint)); + const pixelData = ctx.getImageData(x, y, 1, 1); + + expect(pixelData.data[0]).closeTo(expectedPixel, 1); + }); + }); +}; + +describe('CS3D Image Consistency and Quality', () => { + const setupStudySeries = (studyUID, seriesUID) => { + cy.checkStudyRouteInViewer( + studyUID, + `&seriesInstanceUID=${seriesUID}&hangingProtocolId=@ohif/hpScale` + ); + cy.initCornerstoneToolsAliases(); + + const skipMarkers = true; + cy.initCommonElementsAliases(skipMarkers); + }; + + it('TG18 Resolution Test Displayed 1:1', () => { + setupStudySeries( + '2.16.124.113543.6004.101.103.20021117.061159.1', + '2.16.124.113543.6004.101.103.20021117.061159.1.004' + ); + + cy.wait(2000); + testPixel(1018, 1028, 255); + // Horizontal and vertical delta from this should not be contaminated + // by values from center + testPixel(1019, 1028, 0); + testPixel(1018, 1029, 0); + testPixel(1017, 1028, 0); + testPixel(1018, 1027, 0); + }); + + // Missing test data - todo + it.skip('8 bit image displayable', () => { + setupStudySeries('1.3.46.670589.17.1.7.1.1.7', '1.3.46.670589.17.1.7.2.1.7'); + + cy.wait(1000); + + // Compare with dcm2jpg generated values or by manually computing WL values + testPixel(258, 257, 171); + testPixel(259, 257, 166); + }); + + it.skip('12 bit image displayable and zoom with pixel spacing', () => { + setupStudySeries( + '1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1', + '1.3.6.1.4.1.25403.345050719074.3824.20170125113608.5' + ); + + cy.wait(1000); + + // Compare with dcm2jpg generated values or by manually computing WL values + testPixel(258, 277, 120); + testPixel(259, 277, 122); + }); +}); diff --git a/platform/app/cypress/integration/MultiStudy.spec.js b/platform/app/cypress/integration/MultiStudy.spec.js new file mode 100644 index 0000000..3c7da1b --- /dev/null +++ b/platform/app/cypress/integration/MultiStudy.spec.js @@ -0,0 +1,26 @@ +describe('OHIF Multi Study', () => { + const beforeSetup = () => { + cy.initViewer( + '1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1,1.2.840.113619.2.5.1762583153.215519.978957063.78', + { + params: '&hangingProtocolId=@ohif/hpCompare', + minimumThumbnails: 3, + } + ); + }; + + it('Should display 2 comparison up', () => { + beforeSetup(); + + cy.get('[data-cy="viewport-pane"]').as('viewportPane'); + cy.get('@viewportPane').its('length').should('be.eq', 4); + + cy.get('[data-cy="viewport-overlay-top-left"] [title="Study date"]').as('studyDate'); + + cy.get('@studyDate').should(studyDate => { + expect(studyDate.length).to.be.eq(4); + expect(studyDate.text()).to.contain('2014').contain('2001'); + expect(studyDate.text().indexOf('2014')).to.be.lessThan(studyDate.text().indexOf('2001')); + }); + }); +}); diff --git a/platform/app/cypress/integration/OHIFPdfDisplay.spec.js b/platform/app/cypress/integration/OHIFPdfDisplay.spec.js new file mode 100644 index 0000000..2ef2f02 --- /dev/null +++ b/platform/app/cypress/integration/OHIFPdfDisplay.spec.js @@ -0,0 +1,9 @@ +describe('OHIF PDF Display', function () { + beforeEach(function () { + cy.openStudyInViewer('2.25.317377619501274872606137091638706705333'); + }); + + it('checks if series thumbnails are being displayed', function () { + cy.get('[data-cy="study-browser-thumbnail-no-image"]').its('length').should('be.gt', 0); + }); +}); diff --git a/platform/app/cypress/integration/OHIFVideoDisplay.spec.js b/platform/app/cypress/integration/OHIFVideoDisplay.spec.js new file mode 100644 index 0000000..704db2e --- /dev/null +++ b/platform/app/cypress/integration/OHIFVideoDisplay.spec.js @@ -0,0 +1,17 @@ +describe('OHIF Video Display', function () { + beforeEach(function () { + Cypress.on('uncaught:exception', () => false); + cy.openStudyInViewer('2.25.96975534054447904995905761963464388233'); + }); + + it('checks if series thumbnails are being displayed', function () { + cy.get('[data-cy="study-browser-thumbnail-no-image"]').its('length').should('be.gt', 1); + }); + + it('performs double-click to load thumbnail in active viewport', () => { + cy.get('[data-cy="study-browser-thumbnail-no-image"]:nth-child(2)').dblclick(); + + //const expectedText = 'Ser: 3'; + //cy.get('@viewportInfoBottomLeft').should('contains.text', expectedText); + }); +}); diff --git a/platform/app/cypress/integration/customization/HangingProtocol.spec.js b/platform/app/cypress/integration/customization/HangingProtocol.spec.js new file mode 100644 index 0000000..795db72 --- /dev/null +++ b/platform/app/cypress/integration/customization/HangingProtocol.spec.js @@ -0,0 +1,42 @@ +describe('OHIF HP', () => { + beforeEach(() => { + cy.checkStudyRouteInViewer( + '1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1', + '&hangingProtocolId=@ohif/mnGrid' + ); + cy.expectMinimumThumbnails(3); + cy.initCornerstoneToolsAliases(); + cy.initCommonElementsAliases(); + cy.waitDicomImage(); + }); + + it('Should display 3 up', () => { + cy.get('[data-cy="viewport-pane"]').its('length').should('be.eq', 4); + }); + + it('Should navigate next/previous stage', () => { + cy.get('body').type(','); + cy.wait(250); + cy.get('[data-cy="viewport-pane"]').its('length').should('be.eq', 4); + + cy.get('body').type('..'); + cy.wait(250); + cy.get('[data-cy="viewport-pane"]').its('length').should('be.eq', 2); + }); + + it('Should navigate to display set specified', () => { + Cypress.on('uncaught:exception', () => false); + // This filters by series instance UID, meaning there will only be 1 thumbnail + // It applies the initial SOP instance, navigating to that image + cy.checkStudyRouteInViewer( + '1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1', + '&SeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125113545.4&initialSopInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125113546.1' + ); + cy.expectMinimumThumbnails(1); + cy.initCornerstoneToolsAliases(); + cy.initCommonElementsAliases(); + + // The specified series/sop UID's are index 101, so ensure that image is displayed + cy.get('@viewportInfoBottomRight').should('contains.text', 'I:6'); + }); +}); diff --git a/platform/app/cypress/integration/customization/OHIFDoubleClick.spec.js b/platform/app/cypress/integration/customization/OHIFDoubleClick.spec.js new file mode 100644 index 0000000..eff5d32 --- /dev/null +++ b/platform/app/cypress/integration/customization/OHIFDoubleClick.spec.js @@ -0,0 +1,49 @@ +describe('OHIF Double Click', () => { + beforeEach(() => { + cy.checkStudyRouteInViewer( + '1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1', + '&hangingProtocolId=@ohif/mnGrid' + ); + cy.expectMinimumThumbnails(3); + cy.initCornerstoneToolsAliases(); + cy.initCommonElementsAliases(); + }); + + it('Should double click each viewport to one up and back', () => { + const numExpectedViewports = 4; + cy.get('[data-cy="viewport-pane"]').its('length').should('be.eq', numExpectedViewports); + + for (let i = 0; i < 3; i += 1) { + cy.wait(1000); + + // For whatever reason, with Cypress tests, we have to activate the + // viewport we are double clicking first. + cy.get('[data-cy="viewport-pane"]').eq(i).trigger('click', 'center', { + force: true, + }); + + // Wait for the viewport to be 'active'. + // TODO Is there a better way to do this? + cy.get('[data-cy="viewport-pane"]') + .eq(i) + .parent() + .find('[data-cy="viewport-pane"]') + .not('.pointer-events-none'); + + // The actual double click. + cy.get('[data-cy="viewport-pane"]').eq(i).trigger('dblclick', 'center'); + + cy.get('[data-cy="viewport-pane"]').its('length').should('be.eq', 1); + + cy.get('[data-cy="viewport-pane"]') + .trigger('mousedown', 'center', { + force: true, + }) + .trigger('mouseup', 'center', { + force: true, + }); + + cy.get('[data-cy="viewport-pane"]').eq(0).trigger('dblclick', 'center'); + } + }); +}); diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFContextMenuCustomization.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFContextMenuCustomization.spec.js new file mode 100644 index 0000000..d307b51 --- /dev/null +++ b/platform/app/cypress/integration/measurement-tracking/OHIFContextMenuCustomization.spec.js @@ -0,0 +1,35 @@ +describe('OHIF Context Menu', function () { + beforeEach(function () { + cy.checkStudyRouteInViewer('1.2.840.113619.2.5.1762583153.215519.978957063.78'); + + cy.expectMinimumThumbnails(3); + cy.initCommonElementsAliases(); + cy.initCornerstoneToolsAliases(); + cy.waitDicomImage(); + }); + + it('checks context menu customization', function () { + // Add length measurement + cy.addLengthMeasurement(); + cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').as('yesBtn').click(); + cy.get('[data-cy="data-row"]').as('measurementItem').click(); + + const [x1, y1] = [150, 100]; + cy.get('@viewport') + .trigger('mousedown', x1, y1, { + which: 3, + }) + .trigger('mouseup', x1, y1, { + which: 3, + }); + + // Contextmenu is visible + cy.get('[data-cy="context-menu"]').as('contextMenu').should('be.visible'); + // Click "Finding" subMenu + cy.get('[data-cy="context-menu-item"]').as('item').contains('Finding').click(); + + // Click "Finding" subMenu + cy.get('[data-cy="context-menu-item"]').as('item').contains('Aortic insufficiency').click(); + cy.get('[data-cy="data-row"]').as('measure-item').contains('Aortic insufficiency'); + }); +}); diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneHotkeys.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneHotkeys.spec.js new file mode 100644 index 0000000..b0d2f0b --- /dev/null +++ b/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneHotkeys.spec.js @@ -0,0 +1,154 @@ +describe('OHIF Cornerstone Hotkeys', () => { + beforeEach(() => { + cy.checkStudyRouteInViewer('1.2.840.113619.2.5.1762583153.215519.978957063.78'); + + cy.window() + .its('cornerstone') + .then(cornerstone => { + // For debugging issues where tests pass locally but fail on CI + // - Sometimes Cypress orb seems to use CPU rendering pathway + cy.log(`Cornerstone using CPU Rendering?: ${cornerstone.getShouldUseCPURendering()}`); + }); + + cy.expectMinimumThumbnails(3); + cy.initCornerstoneToolsAliases(); + cy.initCommonElementsAliases(); + cy.waitDicomImage(); + }); + + it('checks if hotkeys "R" and "L" can rotate the image', () => { + cy.get('body').type('R'); + cy.get('@viewportInfoMidLeft').should('contains.text', 'P'); + cy.get('@viewportInfoMidTop').should('contains.text', 'R'); + // Hotkey L + cy.get('body').type('L'); + cy.get('@viewportInfoMidLeft').should('contains.text', 'R'); + cy.get('@viewportInfoMidTop').should('contains.text', 'A'); + }); + + it('checks if hotkeys "ArrowUp" and "ArrowDown" can navigate in the stack', () => { + // Hotkey ArrowDown + cy.get('body').type('{downarrow}'); + cy.get('@viewportInfoBottomRight').should('contains.text', 'I:2 (2/26)'); + // Hotkey ArrowUp + cy.get('body').type('{uparrow}'); + cy.get('@viewportInfoBottomRight').should('contains.text', 'I:1 (1/26)'); + }); + + it('checks if hotkeys "V" and "H" can flip the image', () => { + // Hotkey H + cy.get('body').type('h'); + cy.get('@viewportInfoMidLeft').should('contains.text', 'L'); + cy.get('@viewportInfoMidTop').should('contains.text', 'A'); + // Hotkey V + cy.get('body').type('v'); + cy.get('@viewportInfoMidLeft').should('contains.text', 'L'); + cy.get('@viewportInfoMidTop').should('contains.text', 'P'); + }); + + // it('checks if hotkeys "+", "-" and "=" can zoom in, out and fit to viewport', () => { + // //Click on button and verify if icon is active on toolbar + // cy.get('@zoomBtn') + // .click() + // .then($zoomBtn => { + // cy.wrap($zoomBtn).should('have.class', 'active'); + // }); + + // // Hotkey + + // cy.get('body').type('+++'); // Press hotkey 3 times + // cy.get('@viewportInfoTopLeft').should('contains.text', 'Zoom:2.30x'); + // // Hotkey - + // cy.get('body').type('-'); + // cy.get('@viewportInfoTopLeft').should('contains.text', 'Zoom:2.09x'); + // // Hotkey = + // cy.get('body').type('='); + // cy.get('@viewportInfoTopLeft').should('contains.text', 'Zoom:1.67x'); + // }); + + it('checks if hotkey "SPACEBAR" can reset the image', () => { + // Press multiples hotkeys + cy.get('body').type('v+++i'); + cy.get('@viewportInfoMidLeft').should('contains.text', 'R'); + cy.get('@viewportInfoMidTop').should('contains.text', 'P'); + + // Hotkey SPACEBAR + cy.get('body').type(' '); + cy.get('@viewportInfoMidLeft').should('contains.text', 'R'); + cy.get('@viewportInfoMidTop').should('contains.text', 'A'); + }); + + /* + // TODO: Pretty sure this is not implemented yet + // it('uses hotkeys "RightArrow" and "LeftArrow" to navigate between multiple viewports', () => { + //Select viewport layout (3,1) + cy.setLayout(3, 1); + cy.waitViewportImageLoading(); + + // Press multiples hotkeys on viewport #1 + cy.get('body').type('VL+++I'); + cy.get('@viewportInfoMidLeft').should('contains.text', 'A'); + cy.get('@viewportInfoMidTop').should('contains.text', 'R'); + cy.get('@viewportInfoBottomRight').should('contains.text', 'Zoom: 134%'); + + // Hotkey RightArrow: Move to next viewport + cy.get('body').type('{rightarrow}'); + + // Get overlay information from viewport #2 + cy.get( + ':nth-child(2) > .viewport-wrapper > .viewport-element > .ViewportOrientationMarkers.noselect > .top-mid.orientation-marker' + ).as('viewport2InfoMidTop'); + cy.get( + ':nth-child(2) > .viewport-wrapper > .viewport-element > .ViewportOrientationMarkers.noselect > .left-mid.orientation-marker' + ).as('viewport2InfoMidLeft'); + cy.get( + ':nth-child(2) > .viewport-wrapper > .viewport-element > .ViewportOverlay > div.bottom-right.overlay-element > div' + ).as('viewport2InfoBottomRight'); + + // Press multiples hotkeys on viewport #2 + cy.get('body').type('RR++H+++I'); + cy.get('@viewport2InfoMidLeft').should('contains.text', 'P'); + cy.get('@viewport2InfoMidTop').should('contains.text', 'H'); + cy.get('@viewport2InfoBottomRight').should('contains.text', 'Zoom: 120%'); + + // Hotkey LeftArrow: Move to previous viewport + cy.get('body').type('{leftarrow}'); + + // Hotkey SPACEBAR: Reset viewport #1 + cy.get('body').type(' '); + cy.get('@viewportInfoMidLeft').should('contains.text', 'R'); + cy.get('@viewportInfoMidTop').should('contains.text', 'A'); + cy.get('@viewportInfoBottomRight').should('contains.text', 'Zoom: 89%'); + + // Hotkey RightArrow: Move to next viewport + cy.get('body').type('{rightarrow}'); + + // Hotkey SPACEBAR: Reset viewport #2 + cy.get('body').type(' '); + cy.get('@viewport2InfoMidLeft').should('contains.text', 'A'); + cy.get('@viewport2InfoMidTop').should('contains.text', 'H'); + cy.get('@viewport2InfoBottomRight').should('contains.text', 'Zoom: 45%'); + + //Select viewport layout (1,1) + cy.setLayout(1, 1); + });*/ + + //TO-DO: This test is blocked by issue #1095 (https://github.com/OHIF/Viewers/issues/1095) + //Once issue is fixed, this test can be uncommented + // it('checks if hotkey "Z" activates zoom tool', () => { + // // Hotkey Z + // cy.get('body').type('Z'); + // // Verify if icon is active on toolbar + // cy.get('@zoomBtn').should('have.class', 'active'); + // }); + + //TO-DO: This test is blocked by issue #1095 (https://github.com/OHIF/Viewers/issues/1095) + //Once issue is fixed, this test can be uncommented + // it('checks if hotkeys "PageDown" and "PageUp" can navigate in the series thumbnails', () => { + // // Hotkey PageDown + // cy.get('body').type('{pagedown}{pagedown}'); // press hotkey twice + // cy.get('@viewportInfoBottomLeft').should('contains.text', 'Ser: 3'); + // // Hotkey PageUp + // cy.get('body').type('{pageup}'); + // cy.get('@viewportInfoBottomLeft').should('contains.text', 'Ser: 2'); + // }); +}); diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js new file mode 100644 index 0000000..3af3111 --- /dev/null +++ b/platform/app/cypress/integration/measurement-tracking/OHIFCornerstoneToolbar.spec.js @@ -0,0 +1,462 @@ +describe('OHIF Cornerstone Toolbar', () => { + beforeEach(() => { + cy.checkStudyRouteInViewer('1.2.840.113619.2.5.1762583153.215519.978957063.78'); + cy.expectMinimumThumbnails(3); + cy.initCornerstoneToolsAliases(); + cy.initCommonElementsAliases(); + + cy.get('[data-cy="study-browser-thumbnail"]').eq(1).click(); + + //const expectedText = 'Ser: 1'; + //cy.get('@viewportInfoBottomLeft').should('contains.text', expectedText); + cy.waitDicomImage(); + }); + + it('checks if all primary buttons are being displayed', () => { + cy.get('@zoomBtn').should('be.visible'); + cy.get('@wwwcBtnPrimary').should('be.visible'); + cy.get('@wwwcBtnSecondary').should('be.visible'); + cy.get('@panBtn').should('be.visible'); + cy.get('@measurementToolsBtnPrimary').should('be.visible'); + cy.get('@measurementToolsBtnSecondary').should('be.visible'); + cy.get('@moreBtnPrimary').should('be.visible'); + cy.get('@moreBtnSecondary').should('be.visible'); + cy.get('@layoutBtn').should('be.visible'); + }); + + /*it('checks if Stack Scroll tool will navigate across all series in the viewport', () => { + //Click on button and verify if icon is active on toolbar + cy.get('@stackScrollBtn') + .click() + .then($stackScrollBtn => { + cy.wrap($stackScrollBtn).should('have.class', 'active'); + }); + + //drags the mouse inside the viewport to be able to interact with series + cy.get('@viewport') + .trigger('mousedown', 'center', { buttons: 1 }) + .trigger('mousemove', 'top', { buttons: 1 }) + .trigger('mouseup'); + const expectedText = + 'Ser: 1Img: 1 1/26256 x 256Loc: -30.00 mm Thick: 5.00 mm'; + cy.get('@viewportInfoBottomLeft').should('have.text', expectedText); + });*/ + + // it('checks if Zoom tool will zoom in/out an image in the viewport', () => { + // //Click on button and verify if icon is active on toolbar + // cy.get('@zoomBtn') + // .click() + // .then($zoomBtn => { + // cy.wrap($zoomBtn).should('have.class', 'active'); + // }); + + // // IMPORTANT: Cypress sends out a mouseEvent which doesn't have the buttons + // // property. This is a workaround to simulate a mouseEvent with the buttons property + // // which is consumed by cornerstone + // cy.get('@viewport') + // .trigger('mousedown', 'center', { buttons: 1 }) + // .trigger('mousemove', 'top', { + // buttons: 1, + // }) + // .trigger('mouseup', { + // buttons: 1, + // }); + + // const expectedText = 'Zoom:0.96x'; + // cy.get('@viewportInfoTopLeft').should('have.text', expectedText); + // }); + + it('checks if Levels tool will change the window width and center of an image', () => { + // Wait for the DICOM image to load + + // Assign an alias to the button element + cy.get('@wwwcBtnPrimary').as('wwwcButton'); + cy.get('@wwwcButton').click(); + cy.get('@wwwcButton').should('have.attr', 'data-active', 'true'); + + //drags the mouse inside the viewport to be able to interact with series + cy.get('@viewport') + .trigger('mousedown', 'center', { buttons: 1 }) + // Since we have scrollbar on the right side of the viewport, we need to + // force the mousemove since it goes to another element + .trigger('mousemove', 'right', { buttons: 1, force: true }) + .trigger('mouseup', { buttons: 1 }); + + // The exact text is slightly dependent on the viewport resolution, so leave a range + cy.get('@viewportInfoBottomLeft').should($txt => { + const text = $txt.text(); + expect(text).to.include('L:479'); + }); + }); + + it('checks if Pan tool will move the image inside the viewport', () => { + // Assign an alias to the button element + cy.get('@panBtn').as('panButton'); + + // Click on the button + cy.get('@panButton').click(); + + // Assert that the button has the 'active' class + cy.get('@panButton').should('have.attr', 'data-active', 'true'); + + // Trigger the pan actions on the viewport + cy.get('@viewport') + .trigger('mousedown', 'center', { buttons: 1 }) + .trigger('mousemove', 'bottom', { buttons: 1 }) + .trigger('mouseup', 'bottom'); + }); + + it('checks if Length annotation can be added to viewport and shows up in the measurements panel', () => { + //Click on button and verify if icon is active on toolbar + cy.addLengthMeasurement(); + cy.get('[data-cy="viewport-notification"]').as('notif').should('exist'); + // cy.get('[data-cy="viewport-notification"]').as('notif').should('be.visible'); + + cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').as('yesBtn').click(); + + //Verify the measurement exists in the table + cy.get('@measurementsPanel').should('be.visible'); + + cy.get('[data-cy="data-row"]').as('measure').its('length').should('be.at.least', 1); + }); + + /*it('checks if angle annotation can be added on viewport without causing any errors', () => { + //Click on button and verify if icon is active on toolbar + cy.get('@angleBtn') + .click() + .then($angleBtn => { + cy.wrap($angleBtn).should('have.class', 'active'); // TODO: should we just add the 'active' class back? Or use a data property? + }); + + //Add annotation on the viewport + const initPos = [180, 390]; + const midPos = [300, 410]; + const finalPos = [180, 450]; + cy.addAngle('@viewport', initPos, midPos, finalPos); + });*/ + + it('checks if Reset tool will reset all changes made on the image', () => { + //Make some changes by zooming in and rotating the image + cy.imageZoomIn(); + cy.imageContrast(); + + //Click on reset button + cy.resetViewport(); + + const expectedText = 'W:958L:479'; + cy.get('@viewportInfoBottomLeft').should('have.text', expectedText); + }); + + /*it('checks if CINE tool will prompt a modal with working controls', () => { + cy.server(); + cy.route('GET', '/!**!/studies/!**!/').as('studies'); + + //Click on button + cy.get('@cineBtn').click(); + + // Verify if cine control overlay is being displayed + cy.get('.cine-controls') + .as('cineControls') + .should('be.visible'); + + //Test PLAY button + cy.get('[title="Play / Stop"]').then($btn => { + $btn.click(); + cy.wait(100); + $btn.click(); + }); + + let expectedText = 'Img: 1 1/26'; + cy.get('@viewportInfoBottomLeft', { timeout: 15000 }).should( + 'not.have.text', + expectedText + ); + + //Test SKIP TO FIRST IMAGE button + cy.get('[title="Skip to first Image"]') + .click() + .wait(1000); + cy.get('@viewportInfoBottomLeft', { timeout: 15000 }).should( + 'contain.text', + expectedText + ); + + //Test NEXT IMAGE button + cy.get('[title="Next Image"]') + .click() + .wait(1000); + expectedText = 'Img: 2 2/26'; + cy.get('@viewportInfoBottomLeft', { timeout: 15000 }).should( + 'contain.text', + expectedText + ); + + //Test SKIP TO LAST IMAGE button + cy.get('[title="Skip to last Image"]') + .click() + .wait(2000); + expectedText = 'Img: 27 26/26'; + cy.get('@viewportInfoBottomLeft', { timeout: 15000 }).should( + 'contain.text', + expectedText + ); + + //Test PREVIOUS IMAGE button + cy.get('[title="Previous Image"]') + .click() + .wait(1000); + expectedText = 'Img: 26 25/26'; + cy.get('@viewportInfoBottomLeft', { timeout: 15000 }).should( + 'contain.text', + expectedText + ); + + //Click on Cine button + cy.get('@cineBtn') + .click() + .then(() => { + // Verify that cine control overlay is hidden + cy.get('@cineControls').should('not.exist'); + }); + });*/ + + /** + it('checks if More button will prompt a modal with secondary tools', () => { + //Click on More button + cy.get('@moreBtnSecondary').click(); + + //Verify if overlay is displayed + cy.get('[data-cy="MoreTools-list-menu"]') + .as('toolbarOverlay') + .should('be.visible'); + + // Click on one of the secondary tools from the overlay + cy.get('[data-cy="Magnify"]').click(); + + // Check if More button is active and if it has same icon as the secondary tool selected + cy.get('@moreBtnPrimary').then($moreBtn => { + cy.wrap($moreBtn) + .should('have.class', 'active') + .should('have.attr', 'data-tool', 'Magnify'); + }); + + // Verify if overlay is hidden + cy.get('@toolbarOverlay').should('not.be.visible'); + }); + */ + + /*it('checks if Layout tool will multiply the number of viewports displayed', () => { + //Click on Layout button and verify if overlay is displayed + cy.get('@layoutBtn') + .click() + .then(() => { + cy.get('.layoutChooser') + .as('layoutChooser') + .should('be.visible') + .find('td') + .its('length') + .should('be.eq', 9); + cy.get('@layoutBtn').click(); + }); + + //verify if layout has changed to 2 viewports + cy.setLayout(1, 2); + cy.get('.viewport-container').then($viewport => { + cy.wrap($viewport) + .its('length') + .should('be.eq', 2); + }); + + cy.setLayout(2, 1); + cy.get('.viewport-container').then($viewport => { + cy.wrap($viewport) + .its('length') + .should('be.eq', 2); + }); + + //verify if layout has changed to 3 viewports + cy.setLayout(1, 3); + cy.get('.viewport-container').then($viewport => { + cy.wait(1000); + cy.wrap($viewport) + .its('length') + .should('be.eq', 3); + }); + + cy.setLayout(3, 1); + cy.get('.viewport-container').then($viewport => { + cy.wrap($viewport) + .its('length') + .should('be.eq', 3); + }); + + //verify if layout has changed to 4 viewports + cy.setLayout(2, 2); + cy.get('.viewport-container').then($viewport => { + cy.wrap($viewport) + .its('length') + .should('be.eq', 4); + }); + + //verify if layout has changed to 6 viewports + cy.setLayout(2, 3); + cy.get('.viewport-container').then($viewport => { + cy.wrap($viewport) + .its('length') + .should('be.eq', 6); + }); + + cy.setLayout(3, 2); + cy.get('.viewport-container').then($viewport => { + cy.wrap($viewport) + .its('length') + .should('be.eq', 6); + }); + + //verify if layout has changed to 9 viewports + cy.setLayout(3, 3); + cy.get('.viewport-container').then($viewport => { + cy.wrap($viewport) + .its('length') + .should('be.eq', 9); + }); + + //verify if layout has changed to 1 viewport + cy.setLayout(1, 1); + cy.get('.viewport-container').then($viewport => { + cy.wrap($viewport) + .its('length') + .should('be.eq', 1); + }); + }); + + it('checks if the available viewport was set to active when layout is decreased', () => { + cy.setLayout(3, 3); + + // activate the ninth viewport + cy.get('[data-cy=viewport-container-8]') + .click() + .should('have.class', 'active'); + + cy.setLayout(1, 1); + + // first viewport should be active + cy.get('[data-cy=viewport-container-0]').should('have.class', 'active'); + }); + + it('checks if Clear tool will delete all measurements added in the viewport', () => { + //Add measurements in the viewport + cy.addLengthMeasurement(); + cy.addAngleMeasurement(); + + //Verify if measurement annotation was added into the measurements panel + cy.get('@measurementsBtn').click(); + cy.get('[data-cy="data-row"]') + .its('length') + .should('be.at.least', 2); + + //Click on More button + cy.get('@moreBtn').click(); + //Verify if overlay is displayed + cy.get('.tooltip-toolbar-overlay') + .as('toolbarOverlay') + .should('be.visible'); + //Click on Clear button + cy.get('[data-cy="clear"]').click(); + + //Verify if measurements were removed from the measurements panel + + //cy.get('.measurementItem'); //.should('not.exist'); + + //Close More button overlay + cy.get('@moreBtn').click(); + + //Close the measurements panel + cy.get('@measurementsBtn').then($btn => { + $btn.click(); + cy.get('@measurementsPanel').should('not.be.enabled'); + }); + }); + + it('check if Rotate tool will change the image orientation in the viewport', () => { + //Click on More button + cy.get('@moreBtn').click(); + //Verify if overlay is displayed + cy.get('.tooltip-toolbar-overlay') + .should('be.visible') + .then(() => { + //Click on Rotate button + cy.get('[data-cy="rotate right"]').click({ force: true }); + cy.get('@viewportInfoMidLeft').should('contains.text', 'F'); + cy.get('@viewportInfoMidTop').should('contains.text', 'R'); + }); + + //Click on More button to close it + cy.get('@moreBtn').click(); + }); + + it('check if Flip H tool will flip the image horizontally in the viewport', () => { + //Click on More button + cy.get('@moreBtn').click(); + //Verify if overlay is displayed + cy.get('.tooltip-toolbar-overlay').should('be.visible'); + + //Click on Flip H button + cy.get('[data-cy="flip h"]').click(); + cy.get('@viewportInfoMidLeft').should('contains.text', 'L'); + cy.get('@viewportInfoMidTop').should('contains.text', 'H'); + + //Click on More button to close it + cy.get('@moreBtn').click(); + cy.get('.tooltip-toolbar-overlay').should('not.exist'); + }); +*/ + it('check if Flip tool will flip the image in the viewport', () => { + cy.get('@viewportInfoMidLeft').should('contains.text', 'R'); + cy.get('@viewportInfoMidTop').should('contains.text', 'A'); + + //Click on More button + cy.get('@moreBtnSecondary').click(); + + //Click on Flip button + cy.get('[data-cy="flipHorizontal"]').click(); + cy.waitDicomImage(); + cy.get('@viewportInfoMidLeft').should('contains.text', 'L'); + cy.get('@viewportInfoMidTop').should('contains.text', 'A'); + }); + + // it('checks if stack sync is preserved on new display set and uses FOR', () => { + // // Active stack image sync and reference lines + // cy.get('[data-cy="MoreTools-split-button-secondary"]').click(); + // cy.get('[data-cy="ImageSliceSync"]').click(); + // // Add reference lines as that sometimes throws an exception + // cy.get('[data-cy="MoreTools-split-button-secondary"]').click(); + // cy.get('[data-cy="ReferenceLines"]').click(); + + // cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick(); + // cy.get('body').type('{downarrow}{downarrow}'); + + // // Change the layout and double load the first + // cy.setLayout(2, 1); + // cy.get('body').type('{rightarrow}'); + // cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick(); + // cy.waitDicomImage(); + + // // Now navigate down once and check that the left hand pane navigated + // cy.get('body').focus().type('{downarrow}'); + + // // The following lines assist in troubleshooting when/if this test were to fail. + // cy.get('[data-cy="viewport-pane"]') + // .eq(0) + // .find('[data-cy="viewport-overlay-top-right"]') + // .should('contains.text', 'I:2 (2/20)'); + // cy.get('[data-cy="viewport-pane"]') + // .eq(1) + // .find('[data-cy="viewport-overlay-top-right"]') + // .should('contains.text', 'I:2 (2/20)'); + + // cy.get('body').type('{leftarrow}'); + // cy.setLayout(1, 1); + // cy.get('@viewportInfoTopRight').should('contains.text', 'I:2 (2/20)'); + // }); +}); diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFDownloadSnapshotFile.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFDownloadSnapshotFile.spec.js new file mode 100644 index 0000000..35ef986 --- /dev/null +++ b/platform/app/cypress/integration/measurement-tracking/OHIFDownloadSnapshotFile.spec.js @@ -0,0 +1,108 @@ +describe('OHIF Download Snapshot File', () => { + beforeEach(() => { + cy.checkStudyRouteInViewer('1.2.840.113619.2.5.1762583153.215519.978957063.78'); + cy.expectMinimumThumbnails(3); + cy.openDownloadImageModal(); + }); + + it('checks displayed information for Desktop experience', function () { + // Set Desktop resolution + // cy.viewport(1750, 720); + // Visual comparison + // cy.screenshot('Download Image Modal - Desktop experience'); + //Check if all elements are displayed + + // TODO: need to add this attribute to the modal + cy.get('[data-cy=modal-header]') + .as('downloadImageModal') + .should('contain.text', 'Download High Quality Image'); + + // Check input fields + // TODO: select2 + // cy.get('[data-cy="file-type"]') + // .select('png') + // .should('have.value', 'png') + // .select('jpg') + // .should('have.value', 'jpg'); + + // Check image preview + cy.get('[data-cy="image-preview"]').should('contain.text', 'Image preview'); + + //TODO: This is a canvas now, not an img with src + // cy.get('[data-cy="viewport-preview-img"]') + // .should('have.attr', 'src') + // .and('include', 'data:image'); + + // Check buttons + cy.get('[data-cy="cancel-btn"]').scrollIntoView().should('be.visible'); + cy.get('[data-cy="download-btn"]').scrollIntoView().should('be.visible'); + + cy.get('[data-cy="cancel-btn"]').click(); + }); + + /*it('cancel changes on download modal', function() { + //Change Image Width, Filename and File Type + cy.get('[data-cy="image-width"]') + .clear() + .type('300'); + cy.get('[data-cy="image-height"]') //Image Height should be the same as width + .should('have.value', '300'); + cy.get('[data-cy="file-name"]') + .clear() + .type('new-filename'); + cy.get('[data-cy="file-type"]').select('png'); + //Click on Cancel button + cy.get('[data-cy="cancel-btn"]') + .scrollIntoView() + .click(); + //Check modal is closed + cy.get('[data-cy="modal"]').should('not.exist'); + //Open Modal + cy.openDownloadImageModal(); + //Verify default values was restored + cy.get('[data-cy="image-width"]').should('have.value', '512'); + cy.get('[data-cy="file-name"]').should('have.value', 'image'); + cy.get('[data-cy=file-type]').should('have.value', 'jpg'); + });*/ + + // TO-DO once issue is fixed: https://github.com/OHIF/Viewers/issues/1217 + // it('checks error messages for empty fields', function() { + // //Clear fields Image Width and Filename + // cy.get('[data-cy="image-width"]').clear(); + // cy.get('[data-cy="file-name"]').clear(); + + // //Click on Download button + // cy.get('[data-cy="download-btn"]') + // .scrollIntoView() + // .click(); + // //Check error message + // }); + + /*it('checks if "Show Annotations" checkbox will display annotations', function() { + // Close modal that is initially opened + cy.get('[data-cy="close-button"]').click(); + + // Add measurements in the viewport + cy.addLengthMeasurement(); + cy.addAngleMeasurement(); + + // Open Modal + cy.openDownloadImageModal(); + // Select "Show Annotations" option + cy.get('[data-cy="show-annotations"]').check(); + // Check image preview + cy.get('[data-cy="image-preview"]').scrollIntoView(); + //Compare classes that exists on Image Preview with Annotations and Without Annotation + cy.get('[data-cy="modal-content"]') + .find('canvas') + .should('have.class', 'magnifyTool'); //Class "MagnifyTool" exists with annotations displayed on Image preview + // Uncheck "Show Annotations" option + cy.get('[data-cy="show-annotations"]') + .uncheck() + .wait(300); + // Check that class "MagnifyTool" should not exist + cy.get('[data-cy="modal-content"]') + .find('canvas') + .should('not.have.class', 'magnifyTool'); + });*/ +}); diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFGeneralViewer.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFGeneralViewer.spec.js new file mode 100644 index 0000000..272e9e4 --- /dev/null +++ b/platform/app/cypress/integration/measurement-tracking/OHIFGeneralViewer.spec.js @@ -0,0 +1,92 @@ +describe('OHIF General Viewer', function () { + beforeEach(() => + cy.initViewer('1.2.840.113619.2.5.1762583153.215519.978957063.78', { + minimumThumbnails: 3, + }) + ); + + it('scrolls series stack using scrollbar', function () { + cy.scrollToIndex(13); + + cy.get('@viewportInfoBottomRight').should('contains.text', '14'); + }); + + it('performs right click to zoom', function () { + // This is not used to activate the tool, it is used to ensure the + // top left viewport info shows the zoom values (it only shows up + // when the zoom tool is active) + cy.get('@zoomBtn') + .click() + .then($zoomBtn => { + cy.wrap($zoomBtn).should('have.attr', 'data-active', 'true'); + }); + + const zoomLevelInitial = cy.get('@viewportInfoTopLeft').then($viewportInfo => { + return $viewportInfo.text().substring(6, 9); + }); + + //Right click on viewport + cy.get('@viewport') + .trigger('mousedown', 'top', { buttons: 2 }) + .trigger('mousemove', 'center', { buttons: 2 }) + .trigger('mouseup'); + + // make sure the new zoom level is less than the initial + cy.get('@viewportInfoBottomLeft').then($viewportInfo => { + const zoomLevelFinal = $viewportInfo.text().substring(6, 9); + expect(zoomLevelFinal < zoomLevelInitial).to.eq(true); + }); + }); + + /*it('performs middle click to pan', function() { + //Get image position from cornerstone and check if y axis was modified + let cornerstone; + let currentPan; + + // TO DO: Replace the cornerstone pan check by Percy snapshot comparison + cy.window() + .its('cornerstone') + .then(c => { + cornerstone = c; + currentPan = () => + cornerstone.getEnabledElements()[0].viewport.translation; + }); + + //pan image with middle click + cy.get('@viewport') + .trigger('mousedown', 'center', { buttons: 3 }) + .trigger('mousemove', 'bottom', { buttons: 3 }) + .trigger('mouseup', 'bottom') + .then(() => { + expect(currentPan().y > 0).to.eq(true); + }); + });*/ + + /*it('opens About modal and verify the displayed information', function() { + cy.get('[data-cy="options-dropdown"]') + .first() + .click(); + cy.get('[data-cy="about-modal"]') + .as('aboutOverlay') + .should('be.visible'); + + //check buttons and links + cy.get('[data-cy="about-modal"]') + .should('contains.text', 'Visit the forum') + .and('contains.text', 'Report an issue') + .and('contains.text', 'https://github.com/OHIF/Viewers/'); + + //check version number + cy.get('[data-cy="about-modal"]').then($modal => { + cy.get('[data-cy="header-version-info"]').should($headerVersionNumber => { + $headerVersionNumber = $headerVersionNumber.text().substring(1); + expect($modal).to.contain($headerVersionNumber); + }); + }); + + //close modal + cy.get('[data-cy="close-button"]').click(); + cy.get('@aboutOverlay').should('not.exist'); + }); + */ +}); diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js new file mode 100644 index 0000000..febbaf4 --- /dev/null +++ b/platform/app/cypress/integration/measurement-tracking/OHIFMeasurementPanel.spec.js @@ -0,0 +1,218 @@ +describe('OHIF Measurement Panel', function () { + beforeEach(function () { + cy.checkStudyRouteInViewer('1.2.840.113619.2.5.1762583153.215519.978957063.78'); + + cy.expectMinimumThumbnails(3); + cy.initCommonElementsAliases(); + cy.initCornerstoneToolsAliases(); + cy.waitDicomImage(); + }); + + it('checks if Measurements right panel can be hidden/displayed', function () { + cy.get('@measurementsPanel').should('exist'); + cy.get('@measurementsPanel').should('be.visible'); + + cy.get('@RightCollapseBtn').click(); + cy.get('@measurementsPanel').should('not.exist'); + + cy.get('@RightCollapseBtn').click({ force: true }); + + // segmentation panel should be visible + cy.get('@segmentationPanel').should('be.visible'); + + // measurements panel should be clickable + cy.get('@measurementsBtn').click(); + cy.get('@measurementsPanel').should('be.visible'); + }); + + it('checks if measurement item can be Relabeled under Measurements panel', function () { + // Add length measurement + cy.addLengthMeasurement(); + + cy.get('[data-cy="viewport-notification"]').as('viewportNotification').should('exist'); + cy.get('[data-cy="viewport-notification"]').as('viewportNotification').should('be.visible'); + + cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').as('yesBtn').click(); + + cy.get('[data-cy="data-row"]').as('measurementItem').click(); + + cy.get('[data-cy="data-row"]').find('svg').eq(0).as('measurementItemSvg').click(); + + // enter Bone label + // Todo: move it to the new annotation input with drop down + // cy.get('[data-cy="input-annotation"]').should('exist'); + // cy.get('[data-cy="input-annotation"]').should('be.visible'); + // cy.get('[data-cy="input-annotation"]').type('Bone{enter}'); + + // cy.get('[data-cy="data-row"]').as('measurementItem').should('contain.text', 'Bone'); + }); + + it('checks if image would jump when clicked on a measurement item', function () { + cy.get('[data-cy="study-browser-thumbnail"][data-series="1"]').dblclick(); + cy.wait(250); + cy.scrollToIndex(0); + + // Add length measurement + cy.addLengthMeasurement().wait(250); + cy.get('[data-cy="prompt-begin-tracking-yes-btn"]').as('yesBtn').click(); + + cy.scrollToIndex(13); + + // Reset to default tool so that the new add length works + cy.addLengthMeasurement([100, 100], [200, 200]); //Adding measurement in the viewport + + cy.get('@viewportInfoBottomRight').should('contains.text', '(14/'); + + // Click on first measurement item + cy.get('[data-cy="data-row"]').eq(0).click(); + + cy.get('@viewportInfoBottomRight').should('contains.text', '(1/'); + cy.get('@viewportInfoBottomRight').should('not.contains.text', '(14/'); + }); + + /* + TODO: Not sure why this is failing + it('checks if Description can be added to measurement item under Measurements panel', () => { + cy.addLengthMeasurement(); //Adding measurement in the viewport + cy.get('@measurementsBtn').click(); + cy.get('.measurementItem').click(); + + // Click "Description" + cy.get('.btnAction') + .contains('Description') + .click(); + + // Enter description text + const descriptionText = 'Adding text for description test'; + cy.get('#description').type(descriptionText); + + // Confirm + cy.get('.btn-confirm').click(); + + //Verify if descriptionText was added + cy.get('.measurementLocation').should('contain.text', descriptionText); + + // Remove the measurement we just added + cy.get('.btnAction') + .last() + .contains('Delete') + .click() + + // Close panel + cy.get('@measurementsBtn').click(); + cy.get('@measurementsPanel').should('not.be.enabled'); + }); + */ + + /*it('checks if measurement item can be deleted through the context menu on the viewport', function() { + cy.addLengthMeasurement([100, 100], [200, 100]); //Adding measurement in the viewport + + //Right click on measurement annotation + const [x1, y1] = [150, 100]; + cy.get('@viewport') + .trigger('mousedown', x1, y1, { + which: 3, + }) + .trigger('mouseup', x1, y1, { + which: 3, + }) + .wait(300) + .then(() => { + //Contextmenu is visible + cy.get('.ToolContextMenu').should('be.visible'); + }); + + //Click "Delete measurement" + cy.get('.form-action') + .contains('Delete measurement') + .click(); + + //Open measurements menu + cy.get('@measurementsBtn').click(); + + //Verify measurements was removed from panel + cy.get('.measurementItem') + .should('not.exist') + .log('Annotation successfully removed'); + + //Close panel + cy.get('@measurementsBtn').click(); + cy.get('@measurementsPanel').should('not.exist'); + });*/ + + /*it('adds relabel and description to measurement item through the context menu on the viewport', function() { + cy.addLengthMeasurement([100, 100], [200, 100]); //Adding measurement in the viewport + + // Relabel + // Right click on measurement annotation + const [x1, y1] = [150, 100]; + cy.get('@viewport') + .trigger('mousedown', x1, y1, { + which: 3, + }) + .trigger('mouseup', x1, y1, { + which: 3, + }); + + // Contextmenu is visible + cy.get('.ToolContextMenu').should('be.visible'); + + // Click "Relabel" + cy.get('.form-action') + .contains('Relabel') + .click(); + + // Search for "Brain" + cy.get('.searchInput').type('Brain'); + + // Select "Brain" Result + cy.get('.treeInputs > .wrapperLabel') + .contains('Brain') + .click(); + + // Confirm Selection + cy.get('.checkIconWrapper').click(); + + // Description + // Right click on measurement annotation + cy.get('@viewport') + .trigger('mousedown', x1, y1, { + which: 3, + }) + .trigger('mouseup', x1, y1, { + which: 3, + }); + + // Contextmenu is visible + cy.get('.ToolContextMenu').should('be.visible'); + + // Click "Description" + cy.get('.form-action') + .contains('Add Description') + .click(); + + // Enter description text + const descriptionText = 'Adding text for description test'; + cy.get('#description').type(descriptionText); + + // Confirm + cy.get('.btn-confirm').click(); + + //Open measurements menu + cy.get('@measurementsBtn').click(); + + // Verify if label was added + cy.get('.measurementLocation') + .should('contain.text', 'Brain') + .log('Relabel added with success'); + + //Verify if descriptionText was added + cy.get('.measurementLocation') + .should('contain.text', descriptionText) + .log('Description added with success'); + + // Close panel + cy.get('@measurementsBtn').click(); + cy.get('@measurementsPanel').should('not.exist'); + });*/ +}); diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFSaveMeasurements.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFSaveMeasurements.spec.js new file mode 100644 index 0000000..3a5ddd1 --- /dev/null +++ b/platform/app/cypress/integration/measurement-tracking/OHIFSaveMeasurements.spec.js @@ -0,0 +1,150 @@ +/*describe('OHIF Save Measurements', function() { + before(() => { + cy.checkStudyRouteInViewer( + '1.2.840.113619.2.5.1762583153.215519.978957063.78' + ); + cy.expectMinimumThumbnails(3); + }); + + beforeEach(() => { + // Wait image to load on viewport + cy.wait(2000); + cy.resetViewport(); + cy.initCommonElementsAliases(); + }); + + it('saves new measurement annotation', function() { + // Add measurement in the viewport + cy.addLengthMeasurement(); + + // Verify if measurement annotation was added into the measurements panel + cy.get('@measurementsBtn').click(); + cy.get('.measurementItem') + .its('length') + .should('be.at.least', 1); + + // TODO: Don't save until we're using in-memory data store + // Save new measurement + // cy.get('[data-cy="save-measurements-btn"]').click(); + + // Verify that success message overlay is displayed + // cy.get('.sb-success') + // .should('be.visible') + // .and('contains.text', 'Measurements saved successfully'); + + // Visual test comparison + cy.screenshot('Save Measurements - new measurement added'); + cy.percyCanvasSnapshot('Save Measurements - new measurement added'); + }); + + // it('retrieves saved measurements', function() { + // // Add measurement in the viewport + // cy.addLengthMeasurement(); + + // // Verify if measurement annotation was added into the measurements panel + // cy.get('@measurementsBtn').click(); + // cy.get('.measurementDisplayText') // Get label size of the recently added measurement + // .last() + // .then($measurementSizeLabel => { + // // Save new measurement + // // TODO: Do not save + // cy.get('[data-cy="save-measurements-btn"]') + // .click() + // .then(() => { + // // Verify that success message overlay is displayed + // cy.get('.sb-success').should('be.visible'); + // }); + // // Reload the page + // cy.reload() + // .wait(1000) //Wait page to load + // .expectMinimumThumbnails(2); //wait all thumbnails to load + // // Verify that recently added measurement was retrieved + // cy.get('@measurementsBtn').click(); + // cy.get('.measurementDisplayText') // Get label size of the recently added measurement + // .last() + // .then($retrivedMeasurementSizeLabel => { + // expect($retrivedMeasurementSizeLabel.textContent).to.eq( + // $measurementSizeLabel.textContent + // ); + // }); + // }); + // }); + + // it('checks error message when saving without any measurement', function() { + // // Checks that measurement list is empty + // cy.get('.numberOfItems').should('have.text', '0'); + + // // Click on Save Measurement button + // cy.get('[data-cy="save-measurements-btn"]').click(); + + // // Verify that error message overlay is displayed + // cy.get('.sb-error') + // .should('be.visible') + // .and('contains.text', 'Error while saving the measurements'); + // // Close message overlay + // cy.get('.sb-closeIcon').click(); + // }); + + it('checks if warning message is displayed on measurements of unsupported tools', function() { + // Add measurement for unsupported tool in the viewport + cy.addAngleMeasurement(); + + // Verify if measurement annotation was added into the measurements panel + cy.get('@measurementsBtn').click(); + cy.get('.measurementItem') + .its('length') + .should('be.at.least', 1); + + // Check that warning is displayed for unsupported tool + cy.get('.hasWarnings').should('be.visible'); + + // // Save new measurement + // cy.get('[data-cy="save-measurements-btn"]').click(); + + // // Verify that error message overlay is displayed + // cy.get('.sb-error') + // .should('be.visible') + // .and('contains.text', 'Error while saving the measurements'); + + // Close Measurements panel + cy.get('@measurementsBtn').click(); + }); + + /*it('checks if measurements of unsupported tools were not saved', function() { + // Add measurement for supported tool in the viewport + cy.addLengthMeasurement(); + // Add measurement for unsupported tool in the viewport + cy.addAngleMeasurement(); + + // Verify if measurement annotation was added into the measurements panel + cy.get('@measurementsBtn').click(); + cy.get('.measurementItem') + .its('length') + .should('be.eq', 2); + + // Check that warning is displayed for unsupported tool + cy.get('.hasWarnings').should('be.visible'); + + // Save new measurement + cy.get('[data-cy="save-measurements-btn"]').click(); + + // Verify that success message overlay is displayed + cy.get('.sb-success') + .should('be.visible') + .and('contains.text', 'Measurements saved successfully'); + + // Reload the page + cy.reload() + .wait(1000) //Wait page to load + .expectMinimumThumbnails(2); //wait all thumbnails to load + + //Verify that measurement for unsupported tool was not saved + cy.get('@measurementsBtn').click(); + cy.get('.measurementItem') + .its('length') + .should('be.eq', 1); + + // Close Measurements panel + cy.get('@measurementsBtn').click(); + }); +});*/ diff --git a/platform/app/cypress/integration/measurement-tracking/OHIFStudyBrowser.spec.js b/platform/app/cypress/integration/measurement-tracking/OHIFStudyBrowser.spec.js new file mode 100644 index 0000000..4a52b00 --- /dev/null +++ b/platform/app/cypress/integration/measurement-tracking/OHIFStudyBrowser.spec.js @@ -0,0 +1,60 @@ +describe('OHIF Study Browser', function () { + beforeEach(function () { + cy.checkStudyRouteInViewer('1.2.840.113619.2.5.1762583153.215519.978957063.78'); + + cy.expectMinimumThumbnails(3); + cy.initCommonElementsAliases(); + cy.initCornerstoneToolsAliases(); + }); + + it('checks if series thumbnails are being displayed', function () { + cy.get('[data-cy="study-browser-thumbnail"]').its('length').should('be.gt', 1); + }); + + it('drags and drop a series thumbnail into viewport', function () { + // Can't use the native drag version as the element should be rerendered + // cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)') //element to be dragged + // .drag('.cornerstone-canvas'); //dropzone element + + const dataTransfer = new DataTransfer(); + + cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').as('seriesThumbnail'); + + cy.get('@seriesThumbnail') + .first() + .trigger('mousedown', { which: 1, button: 0 }) + .trigger('dragstart', { dataTransfer }) + .trigger('drag', {}); + + cy.get('.cornerstone-canvas').as('viewport'); + + cy.get('@viewport') + .trigger('mousemove', 'center') + .trigger('dragover', { dataTransfer, force: true }) + .trigger('drop', { dataTransfer, force: true }); + + //const expectedText = + // 'Ser: 2Img: 1 1/13512 x 512Loc: -17.60 mm Thick: 3.00 mm'; + //cy.get('@viewportInfoBottomLeft').should('contain.text', expectedText); + }); + + it('checks if Series left panel can be hidden/displayed', function () { + cy.get('@seriesPanel').should('exist'); + cy.get('@seriesPanel').should('be.visible'); + + cy.get('@seriesBtn').click(); + cy.get('@seriesPanel').should('not.exist'); + + cy.get('@seriesBtn').click(); + cy.get('@seriesPanel').should('exist'); + cy.get('@seriesPanel').should('be.visible'); + }); + + it('performs double-click to load thumbnail in active viewport', () => { + // Have to finish rendering the image before this works + cy.wait(350); + cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick(); + + //cy.get('@viewportInfoBottomLeft').should('contains.text', expectedText); + }); +}); diff --git a/platform/app/cypress/integration/study-list/OHIFStudyList.spec.js b/platform/app/cypress/integration/study-list/OHIFStudyList.spec.js new file mode 100644 index 0000000..c92216d --- /dev/null +++ b/platform/app/cypress/integration/study-list/OHIFStudyList.spec.js @@ -0,0 +1,184 @@ +//We are keeping the hardcoded results values for the study list tests +//this is intended to be running in a controlled docker environment with test data. +describe('OHIF Study List', function () { + context('Desktop resolution', function () { + beforeEach(function () { + Cypress.on('uncaught:exception', () => false); + cy.window().then(win => win.sessionStorage.clear()); + cy.openStudyList(); + + cy.viewport(1750, 720); + cy.initStudyListAliasesOnDesktop(); + //Clear all text fields + cy.get('@PatientName').clear(); + cy.get('@MRN').clear(); + cy.get('@AccessionNumber').clear(); + cy.get('@StudyDescription').clear(); + }); + + afterEach(function () { + cy.window().then(win => win.sessionStorage.clear()); + }); + + it('Displays several studies initially', function () { + cy.waitStudyList(); + cy.get('@searchResult2').should($list => { + expect($list.length).to.be.greaterThan(1); + expect($list).to.contain('Juno'); + expect($list).to.contain('832040'); + }); + }); + + it('searches Patient Name with exact string', function () { + cy.get('@PatientName').type('Juno'); + //Wait result list to be displayed + cy.waitStudyList(); + cy.get('@searchResult2').should($list => { + expect($list.length).to.be.eq(1); + expect($list).to.contain('Juno'); + }); + }); + + it('maintains Patient Name filter upon return from viewer', function () { + cy.get('@PatientName').type('Juno'); + //Wait result list to be displayed + cy.waitStudyList(); + cy.get('[data-cy="studyRow-1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1"]').click(); + cy.get( + '[data-cy="mode-basic-test-1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1"]' + ).click(); + cy.get('[data-cy="return-to-work-list"]').click(); + cy.wait(2000); + + cy.get('@searchResult2').should($list => { + expect($list.length).to.be.eq(1); + expect($list).to.contain('Juno'); + }); + }); + + it('searches MRN with exact string', function () { + cy.get('@MRN').type('0000003'); + //Wait result list to be displayed + cy.waitStudyList(); + cy.get('@searchResult2').should($list => { + expect($list.length).to.be.eq(1); + expect($list).to.contain('0000003'); + }); + }); + + it('maintains MRN filter upon return from viewer', function () { + cy.get('@MRN').type('0000003'); + //Wait result list to be displayed + cy.waitStudyList(); + cy.get('[data-cy="studyRow-1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1"]').click(); + cy.get( + '[data-cy="mode-basic-test-1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1"]' + ).click(); + cy.get('[data-cy="return-to-work-list"]').click(); + cy.wait(2000); + + cy.get('@searchResult2').should($list => { + expect($list.length).to.be.eq(1); + expect($list).to.contain('0000003'); + }); + }); + + it('searches Accession with exact string', function () { + cy.get('@AccessionNumber').type('321'); + //Wait result list to be displayed + cy.waitStudyList(); + cy.wait(2000); + cy.get('@searchResult2').should($list => { + expect($list.length).to.be.eq(1); + expect($list).to.contain('321'); + }); + }); + + it('maintains Accession filter upon return from viewer', function () { + cy.get('@AccessionNumber').type('0000155811'); + //Wait result list to be displayed + cy.waitStudyList(); + cy.wait(2000); + + cy.get('[data-cy="studyRow-1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1"]').click(); + cy.get( + '[data-cy="mode-basic-test-1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1"]' + ).click(); + cy.get('[data-cy="return-to-work-list"]').click(); + cy.wait(2000); + + cy.get('@searchResult2').should($list => { + expect($list.length).to.be.eq(1); + expect($list).to.contain('0000155811'); + }); + }); + + it('searches Description with exact string', function () { + cy.get('@StudyDescription').type('PETCT'); + //Wait result list to be displayed + cy.waitStudyList(); + cy.wait(2000); + + cy.get('@searchResult2').should($list => { + expect($list.length).to.be.eq(1); + expect($list).to.contain('PETCT'); + }); + }); + + it('maintains Description filter upon return from viewer', function () { + cy.get('@StudyDescription').type('PETCT'); + //Wait result list to be displayed + cy.waitStudyList(); + cy.wait(2000); + + cy.get('[data-cy="studyRow-1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1"]').click(); + cy.get( + '[data-cy="mode-basic-test-1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1"]' + ).click(); + cy.get('[data-cy="return-to-work-list"]').click(); + cy.wait(2000); + + cy.get('@searchResult2').should($list => { + expect($list.length).to.be.eq(1); + expect($list).to.contain('PETCT'); + }); + }); + + /* Todo: fix react select + it('searches Modality with camel case', function() { + cy.get('@modalities').type('Ct'); + // Wait result list to be displayed + cy.waitStudyList(); + cy.get('@searchResult2').should($list => { + expect($list.length).to.be.greaterThan(1); + expect($list).to.contain('CT'); + }); + }); + + it('changes Rows per page and checks the study count', function() { + //Show Rows per page options + const pageRows = [25, 50, 100]; + + //Check all options of Rows + pageRows.forEach(numRows => { + cy.get('select').select(numRows.toString()); //Select Rows per page option + //Wait result list to be displayed + cy.waitStudyList().then(() => { + //Compare the search result with the Study Count on the table header + cy.get('@numStudies') + .should(numStudies => { + expect(parseInt(numStudies.text())).to.be.at.most(numRows); //less than or equals to + }) + .then(numStudies => { + //Compare to the number of Rows in the search result + cy.get('@searchResult2').then($searchResult => { + let countResults = $searchResult.length; + expect(numStudies.text()).to.be.eq(countResults.toString()); + }); + }); + }); + }); + }); + */ + }); +}); diff --git a/platform/app/cypress/integration/study-list/OHIFUserPreferences.spec.js b/platform/app/cypress/integration/study-list/OHIFUserPreferences.spec.js new file mode 100644 index 0000000..3959a64 --- /dev/null +++ b/platform/app/cypress/integration/study-list/OHIFUserPreferences.spec.js @@ -0,0 +1,757 @@ +/*describe('OHIF User Preferences', () => { + context('Study List Page', function() { + before(() => { + cy.visit('/'); + }); + + beforeEach(() => { + // Open User Preferences modal + cy.openPreferences(); + }); + + it('checks displayed information on User Preferences modal', function() { + cy.initPreferencesModalAliases(); + //Check Title + cy.get('@preferencesModal').should('contain.text', 'User Preferences'); + //Check tabs + cy.get('@userPreferencesHotkeysTab') + .should('have.text', 'Hotkeys') + .and('have.class', 'active'); + cy.get('@userPreferencesGeneralTab').should('have.text', 'General'); + cy.get('@userPreferencesWindowLevelTab').should( + 'have.text', + 'Window Level' + ); + //Check buttons + cy.get('@restoreBtn') + .scrollIntoView() + .should('have.text', 'Reset to Defaults'); + cy.get('@cancelBtn').should('have.text', 'Cancel'); + cy.get('@saveBtn').should('have.text', 'Save'); + + cy.get('[data-cy="close-button"]').click(); + }); + + it('checks translation by selecting Spanish language', function() { + cy.selectPreferencesTab('@userPreferencesGeneralTab'); + + // Language dropdown should be displayed + cy.get('#language-select').should('be.visible'); + + // Set language to Spanish and save + cy.setLanguage('Spanish'); + + // Header should be translated to Spanish + cy.get('.research-use') + .scrollIntoView() + .should('have.text', 'SOLO USO PARA INVESTIGACIร“N'); + + // Options menu should be translated + cy.get('[data-cy="options-menu"]') + .should('have.text', 'Opciones') + .click(); + + cy.get('[data-cy="dd-item-menu"]') + .first() + .should('contain.text', 'Acerca de'); + cy.get('[data-cy="dd-item-menu"]') + .last() + .should('contain.text', 'Preferencias'); + + // Close Options menu + cy.get('[data-cy="options-menu"]').click(); + }); + + it('checks if user can cancel the language selection and application will be in "English (USA)"', function() { + // Set language to English and save + cy.setLanguage('English (USA)'); + + // Set language to Spanish and cancel + cy.setLanguage('Spanish', false); + + // Header should be kept in "English (USA)" + cy.get('.research-use') + .scrollIntoView() + .should('have.text', 'INVESTIGATIONAL USE ONLY'); + + // Options menu should be translated + cy.get('[data-cy="options-menu"]') + .should('have.text', 'Options') + .click(); + + cy.get('[data-cy="dd-item-menu"]') + .first() + .should('contain.text', 'About'); + cy.get('[data-cy="dd-item-menu"]') + .last() + .should('contain.text', 'Preferences'); + + // Close Options menu + cy.get('[data-cy="options-menu"]').click(); + }); + + it('checks if user can restore to default the language selection and application will be in "English (USA)"', function() { + // Set language to Spanish + cy.setLanguage('Spanish'); + + //Open Preferences again + cy.openPreferences(); + + // Go to general tab + cy.selectPreferencesTab('@userPreferencesGeneralTab'); + + cy.get('@restoreBtn') + .scrollIntoView() + .click(); + + // Close Success Message overlay (if displayed) + cy.get('body').then(body => { + if (body.find('.sb-closeIcon').length > 0) { + cy.get('.sb-closeIcon').click({ force: true }); + } + // click on save button + cy.get('@saveBtn') + .scrollIntoView() + .click(); + }); + + // Header should be in "English (USA)" + cy.get('.research-use') + .scrollIntoView() + .should('have.text', 'INVESTIGATIONAL USE ONLY'); + + // Options menu should be in "English (USA)" + cy.get('[data-cy="options-menu"]') + .should('have.text', 'Options') + .click(); + + cy.get('[data-cy="dd-item-menu"]') + .first() + .should('contain.text', 'About'); + cy.get('[data-cy="dd-item-menu"]') + .last() + .should('contain.text', 'Preferences'); + + // Close options Menu + cy.get('[data-cy="options-menu"]').click(); + }); + + it('checks if W/L Preferences table is being displayed in the Window Level tab', function() { + //Navigate to Window Level tab + cy.selectPreferencesTab('@userPreferencesWindowLevelTab'); + + //Check table header + cy.get('.wlRow.header') + .should('contains.text', 'Preset') + .and('contains.text', 'Description') + .and('contains.text', 'Window') + .and('contains.text', 'Level'); + + //Check table has more than 1 row (more than header) + cy.get('.wlRow') + .its('length') + .should('be.greaterThan', 1); + }); + + it('checks if Preferences set in Study List Page will be consistent on Viewer Page', function() { + // Go go hotkeys tab + cy.selectPreferencesTab('@userPreferencesHotkeysTab'); + + // Set new hotkey for 'Rotate Right' function + cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{shift}Q'); + + // Close Success Message overlay (if displayed) + cy.get('body').then(body => { + if (body.find('.sb-closeIcon').length > 0) { + cy.get('.sb-closeIcon').click({ force: true }); + } + // click on save button + cy.get('@saveBtn') + .scrollIntoView() + .click(); + }); + + // Open User Preferences modal again + cy.openPreferences(); + + // Go to General tab + cy.selectPreferencesTab('@userPreferencesGeneralTab'); + + // Set language to Spanish + cy.setLanguage('Spanish'); + + // Go to Study Viewer page + cy.checkStudyRouteInViewer( + '1.2.840.113619.2.5.1762583153.215519.978957063.78' + ); + cy.expectMinimumThumbnails(3); + cy.initCommonElementsAliases(); + + // Check if application is in Spanish + // Header should be translated to Spanish + cy.get('.research-use') + .scrollIntoView() + .should('have.text', 'SOLO USO PARA INVESTIGACIร“N'); + + // Options menu should be translated + cy.get('[data-cy="options-menu"]') + .should('have.text', 'Opciones') + .click(); + cy.get('[data-cy="dd-item-menu"]') + .first() + .should('contain.text', 'Acerca de'); + cy.get('[data-cy="dd-item-menu"]') + .last() + .should('contain.text', 'Preferencias'); + + // Check if new hotkey is working on viewport + cy.get('body').type('{shift}Q', { + release: false, + }); + cy.get('@viewportInfoMidTop').should('contains.text', 'R'); + }); + }); + + context('Study Viewer Page', function() { + before(() => { + cy.checkStudyRouteInViewer( + '1.2.840.113619.2.5.1762583153.215519.978957063.78' + ); + cy.expectMinimumThumbnails(3); + }); + + beforeEach(() => { + cy.initCommonElementsAliases(); + cy.resetViewport(); + + cy.resetUserHotkeyPreferences(); + cy.resetUserGeneralPreferences(); + // Open User Preferences modal + cy.openPreferences(); + }); + + afterEach(() => { + // Close User Preferences Modal (if displayed) + cy.get('body').then(body => { + if (body.find('.OHIFModal__header').length > 0) { + cy.get('[data-cy="close-button"]').click({ force: true }); + } + }); + }); + + it('checks displayed information on User Preferences modal', function() { + cy.get('@preferencesModal').should('contain.text', 'User Preferences'); + cy.get('@userPreferencesHotkeysTab') + .should('have.text', 'Hotkeys') + .and('have.class', 'active'); + cy.get('@userPreferencesGeneralTab').should('have.text', 'General'); + cy.get('@userPreferencesWindowLevelTab').should( + 'have.text', + 'Window Level' + ); + cy.get('@restoreBtn') + .scrollIntoView() + .should('have.text', 'Reset to Defaults'); + cy.get('@cancelBtn').should('have.text', 'Cancel'); + cy.get('@saveBtn').should('have.text', 'Save'); + }); + + it('checks translation by selecting Spanish language', function() { + cy.selectPreferencesTab('@userPreferencesGeneralTab'); + + // Language dropdown should be displayed + cy.get('#language-select').should('be.visible'); + + // Set language to Spanish + cy.setLanguage('Spanish'); + + // Header should be translated to Spanish + cy.get('.research-use') + .scrollIntoView() + .should('have.text', 'SOLO USO PARA INVESTIGACIร“N'); + + // Options menu should be translated + cy.get('[data-cy="options-menu"]') + .should('have.text', 'Opciones') + .click(); + + cy.get('[data-cy="dd-item-menu"]') + .first() + .should('contain.text', 'Acerca de'); + cy.get('[data-cy="dd-item-menu"]') + .last() + .should('contain.text', 'Preferencias'); + }); + + it('checks if user can cancel the language selection and application will be in "English (USA)"', function() { + // Set language to English and save + cy.setLanguage('English (USA)'); + + // Set language to Spanish and cancel + cy.setLanguage('Spanish', false); + + // Header should be kept in "English (USA)" + cy.get('.research-use') + .scrollIntoView() + .should('have.text', 'INVESTIGATIONAL USE ONLY'); + + // Options menu should be translated + cy.get('[data-cy="options-menu"]') + .should('have.text', 'Options') + .click(); + + cy.get('[data-cy="dd-item-menu"]') + .first() + .should('contain.text', 'About'); + cy.get('[data-cy="dd-item-menu"]') + .last() + .should('contain.text', 'Preferences'); + }); + + it('checks if user can restore to default the language selection and application will be in "English (USA)', function() { + cy.selectPreferencesTab('@userPreferencesGeneralTab'); + + // Language dropdown should be displayed + cy.get('#language-select').should('be.visible'); + + // Set language to Spanish + cy.setLanguage('Spanish'); + + // Open User Preferences modal + cy.openPreferences(); + + // Go to general tab + cy.selectPreferencesTab('@userPreferencesGeneralTab'); + + // click on restore button + cy.get('@restoreBtn') + .scrollIntoView() + .click(); + + // click on save button + cy.get('@saveBtn') + .scrollIntoView() + .click(); + + // Header should be in "English (USA)"" + cy.get('.research-use') + .scrollIntoView() + .should('have.text', 'INVESTIGATIONAL USE ONLY'); + + // Options menu should be in "English (USA)" + cy.get('[data-cy="options-menu"]') + .should('have.text', 'Options') + .click(); + cy.get('[data-cy="dd-item-menu"]') + .first() + .should('contain.text', 'About'); + cy.get('[data-cy="dd-item-menu"]') + .last() + .should('contain.text', 'Preferences'); + }); + + it('checks new hotkeys for "Rotate Right" and "Rotate Left"', function() { + // Go go hotkeys tab + cy.selectPreferencesTab('@userPreferencesHotkeysTab'); + + // Set new hotkey for 'Rotate Right' function + cy.setNewHotkeyShortcutOnUserPreferencesModal( + 'Rotate Right', + '{shift}{rightarrow}' + ); + // Set new hotkey for 'Rotate Left' function + cy.setNewHotkeyShortcutOnUserPreferencesModal( + 'Rotate Left', + '{shift}{leftarrow}' + ); + + // Close Success Message overlay (if displayed) + cy.get('body').then(body => { + if (body.find('.sb-closeIcon').length > 0) { + cy.get('.sb-closeIcon').click({ force: true }); + } + // click on save button + cy.get('@saveBtn') + .scrollIntoView() + .click(); + }); + + //Rotate Right with new Hotkey + cy.get('body').type('{shift}{rightarrow}'); + cy.get('@viewportInfoMidTop').should('contains.text', 'R'); + + //Rotate Left with new Hotkey + cy.get('body').type('{shift}{leftarrow}'); + cy.get('@viewportInfoMidTop').should('contains.text', 'A'); + }); + + it('checks new hotkeys for "Next" and "Previous" Image on Viewport', function() { + // Update hotkeys for 'Next/Previous Viewport' + cy.selectPreferencesTab('@userPreferencesHotkeysTab'); + + cy.setNewHotkeyShortcutOnUserPreferencesModal( + 'Next Viewport', + '{shift}{rightarrow}' + ); + cy.setNewHotkeyShortcutOnUserPreferencesModal( + 'Previous Viewport', + '{shift}{leftarrow}' + ); + // Close Success Message overlay (if displayed) + cy.get('body').then(body => { + if (body.find('.sb-closeIcon').length > 0) { + cy.get('.sb-closeIcon').click({ force: true }); + } + // click on save button + cy.get('@saveBtn') + .scrollIntoView() + .click(); + }); + + // Set 3 viewports layout + cy.setLayout(3, 1); + cy.waitViewportImageLoading(); + + // Reset, Rotate Right and Invert colors on Viewport #1 + cy.get('body').type(' '); + cy.get('body').type('r'); + cy.get('body').type('i'); + + // Shift active viewport to next + // Reset, Rotate Left and Invert colors on Viewport #2 + cy.get('body').type('{shift}{rightarrow}'); + cy.get('body').type(' '); + cy.get('body').type('l'); + cy.get('body').type('i'); + + // Verify 1st viewport was rotated + cy.get('@viewportInfoMidTop').should('contains.text', 'R'); + + // Verify 2nd viewport was rotated + cy.get( + ':nth-child(2) > .viewport-wrapper > .viewport-element > .ViewportOrientationMarkers.noselect > .top-mid.orientation-marker' + ).as('viewport2InfoMidTop'); + cy.get('@viewport2InfoMidTop').should('contains.text', 'P'); + + //Move to Previous Viewport + cy.get('body').type('{shift}{leftarrow}'); + // Reset viewport #1 with spacebar hotkey + cy.get('body').type(' '); + cy.get('@viewportInfoMidTop').should('contains.text', 'A'); + + // Set 1 viewport layout + cy.setLayout(1, 1); + }); + + it('checks error message when duplicated hotkeys are inserted', function() { + // Go go hotkeys tab + cy.selectPreferencesTab('@userPreferencesHotkeysTab'); + + // Set duplicated hotkey for 'Rotate Right' function + cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{i}'); + + // Check error message + cy.get('.HotkeysPreferences').within(() => { + cy.contains('Rotate Right') // label we're looking for + .parent() + .find('.preferencesInputErrorMessage') + .as('errorMsg') + .should('have.text', '"Invert" is already using the "i" shortcut.'); + }); + }); + + it('checks error message when invalid hotkey is inserted', function() { + // Go go hotkeys tab + cy.selectPreferencesTab('@userPreferencesHotkeysTab'); + + // Set invalid hotkey for 'Rotate Right' function + cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{ctrl}Z'); + + // Check error message + cy.get('.HotkeysPreferences').within(() => { + cy.contains('Rotate Right') // label we're looking for + .parent() + .find('.preferencesInputErrorMessage') + .as('errorMsg') + .should('have.text', '"ctrl+z" shortcut combination is not allowed'); + }); + }); + + it('checks error message when only modifier keys are inserted', function() { + // Go go hotkeys tab + cy.selectPreferencesTab('@userPreferencesHotkeysTab'); + + // Set invalid modifier key: ctrl + cy.setNewHotkeyShortcutOnUserPreferencesModal('Zoom Out', '{ctrl}'); + // Check error message + cy.get('.HotkeysPreferences').within(() => { + cy.contains('Zoom Out') // label we're looking for + .parent() + .find('.preferencesInputErrorMessage') + .as('errorMsg') + .should( + 'have.text', + "It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut" + ); + }); + + // Set invalid modifier key: shift + cy.setNewHotkeyShortcutOnUserPreferencesModal('Zoom Out', '{shift}'); + // Check error message + cy.get('@errorMsg').should( + 'have.text', + "It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut" + ); + + // Set invalid modifier key: alt + cy.setNewHotkeyShortcutOnUserPreferencesModal('Zoom Out', '{alt}'); + // Check error message + cy.get('@errorMsg').should( + 'have.text', + "It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut" + ); + }); + + it('checks if user can cancel changes made on User Preferences Hotkeys tab', function() { + // Go go hotkeys tab + cy.selectPreferencesTab('@userPreferencesHotkeysTab'); + + // Set new hotkey for 'Rotate Right' function + cy.setNewHotkeyShortcutOnUserPreferencesModal( + 'Rotate Right', + '{ctrl}{shift}S' + ); + + // Close Success Message overlay (if displayed) + cy.get('body').then(body => { + if (body.find('.sb-closeIcon').length > 0) { + cy.get('.sb-closeIcon').click({ force: true }); + } + //Cancel hotkeys + cy.get('@cancelBtn') + .scrollIntoView() + .click(); + }); + + // Open User Preferences modal again + cy.openPreferences(); + + //Check that hotkey for 'Rotate Right' function was not changed + cy.get('.HotkeysPreferences').within(() => { + cy.contains('Rotate Right') // label we're looking for + .parent() + .find('input') + .should('have.value', 'r'); + }); + }); + + it('checks if user can reset to default values on User Preferences Hotkeys tab', function() { + // Go go hotkeys tab + cy.selectPreferencesTab('@userPreferencesHotkeysTab'); + + // Set new hotkey for 'Rotate Right' function + cy.setNewHotkeyShortcutOnUserPreferencesModal( + 'Rotate Right', + '{ctrl}{shift}S' + ); + + // click on save button + cy.get('@saveBtn') + .scrollIntoView() + .click(); + + // Open User Preferences modal again + cy.openPreferences(); + + //Restore Default hotkeys + cy.get('@restoreBtn') + .scrollIntoView() + .click(); + + //Check that hotkey for 'Rotate Right' function was not changed + cy.get('.HotkeysPreferences').within(() => { + cy.contains('Rotate Right') // label we're looking for + .parent() + .find('input') + .should('have.value', 'r'); + }); + }); + }); + + context('W/L Preset Preferences', function() { + before(() => { + cy.checkStudyRouteInViewer( + '1.2.840.113619.2.5.1762583153.215519.978957063.78' + ); + cy.expectMinimumThumbnails(3); + }); + + beforeEach(() => { + cy.initCommonElementsAliases(); + + // Open User Preferences modal + cy.openPreferences(); + // Navigate to Window Level tab + cy.selectPreferencesTab('@userPreferencesWindowLevelTab'); + }); + + it('checks if W/L Preferences table is being displayed in the Window Level tab', function() { + //Check table header + cy.get('.wlRow.header') + .should('contains.text', 'Preset') + .and('contains.text', 'Description') + .and('contains.text', 'Window') + .and('contains.text', 'Level'); + + //Check table has more than 1 row (more than header) + cy.get('.wlRow') + .its('length') + .should('be.greaterThan', 1); + }); + + // //TODO: Test blocked by issue #1551: https://github.com/OHIF/Viewers/issues/1551 + // it('checks if user can add a new W/L preset', function() { + // let description = ':nth-child(8) > .description > .preferencesInput'; + // let window = ':nth-child(8) > .window > .preferencesInput'; + // let level = ':nth-child(8) > .level > .preferencesInput'; + // let new_window_value = 150; + // let new_level_value = -600; + // // Check existing preset values + // cy.get(description).should('have.value', ''); + // cy.get(window).should('have.value', ''); + // cy.get(level).should('have.value', ''); + + // // Set new preset value + // cy.setWindowLevelPreset( + // 7, + // 'New Description', + // new_window_value, + // new_level_value + // ); + // cy.get('@saveBtn').click(); + + // // Open User Preferences modal + // cy.openPreferences(); + // // Navigate to Window Level tab + // cy.selectPreferencesTab('@userPreferencesWindowLevelTab'); + + // // Check recently added preset values + // cy.get(description).should('have.value', 'New Description'); + // cy.get(window).should('have.value', new_window_value); + // cy.get(level).should('have.value', new_level_value); + + // // Close User Preferences modal + // cy.get('[data-cy="close-button"]').click(); + + // // Check if new hotkey preset is working on viewport + // cy.get('body').type('8'); + // cy.get('@viewportInfoBottomRight').should( + // 'contains.text', + // 'W: ' + new_window_value + ' L: ' + new_level_value + // ); + // }); + + it('checks if user can remove an existing W/L preset', function() { + let description = ':nth-child(3) > .description > .preferencesInput'; + let window = ':nth-child(3) > .window > .preferencesInput'; + let level = ':nth-child(3) > .level > .preferencesInput'; + // Check existing preset values + cy.get(description) + .should('not.have.value', '') + .clear(); + cy.get(window) + .should('not.have.value', '') + .clear(); + cy.get(level) + .should('not.have.value', '') + .clear(); + + // Save changes + cy.get('@saveBtn').click(); + // Open User Preferences modal + cy.openPreferences(); + // Navigate to Window Level tab + cy.selectPreferencesTab('@userPreferencesWindowLevelTab'); + + // Check recently added preset values + cy.get(description).should('have.value', ''); + cy.get(window).should('have.value', ''); + cy.get(level).should('have.value', ''); + // Close User Preferences modal + cy.get('[data-cy="close-button"]').click(); + }); + + // //TODO: Test blocked by issue #1551: https://github.com/OHIF/Viewers/issues/1551 + // it('checks if user can edit an existing W/L preset', function() { + // let description = ':nth-child(2) > .description > .preferencesInput'; + // let window = ':nth-child(2) > .window > .preferencesInput'; + // let level = ':nth-child(2) > .level > .preferencesInput'; + // // Check existing preset values + // cy.get(description).should('have.value', 'Soft tissue'); + // cy.get(window).should('have.value', '550'); + // cy.get(level).should('have.value', '40'); + + // // Set new preset value + // cy.setWindowLevelPreset(1, 'Soft tissue New Description', 1220, 333); + // cy.get('@saveBtn').click(); + + // // Open User Preferences modal + // cy.openPreferences(); + // // Navigate to Window Level tab + // cy.selectPreferencesTab('@userPreferencesWindowLevelTab'); + + // // Check recently added preset values + // cy.get(description).should('have.value', 'Soft tissue New Description'); + // cy.get(window).should('have.value', '1220'); + // cy.get(level).should('have.value', '333'); + // }); + + it('checks if user can change the W/L by triggering different hotkeys with W/L presets', function() { + // Close User Preferences modal + cy.get('[data-cy="close-button"]').click(); + // Check if hotkey preset is working on viewport + cy.get('body').type('3'); + cy.get('@viewportInfoBottomRight').should( + 'contains.text', + 'W: 150 L: 90' + ); + + // Check if hotkey preset is working on viewport + cy.get('body').type('4'); + cy.get('@viewportInfoBottomRight').should( + 'contains.text', + 'W: 2500 L: 480' + ); + }); + + it('checks if user can change the W/L by triggering different hotkeys with W/L presets on multiple viewports', function() { + // Close User Preferences modal + cy.get('[data-cy="close-button"]').click(); + + // Set 3 viewports layout + cy.setLayout(3, 1); + cy.waitViewportImageLoading(); + + // Check if hotkey preset is working on viewport + cy.get('body').type('3'); + cy.get('@viewportInfoBottomRight').should( + 'contains.text', + 'W: 150 L: 90' + ); + + // Overlay information from 2nd viewport + let second_viewport_overlay = + 'div:nth-child(2) > div > div.viewport-element > div.ViewportOverlay > div.bottom-right.overlay-element > div'; + + // Shift active viewport to Viewport #2 + cy.get('body').type('{rightarrow}'); + + // Check if hotkey preset is working on viewport #2 + cy.get('body').type('4'); + cy.get(second_viewport_overlay).should('contains.text', 'W: 2500 L: 480'); + + // Set 1 viewport layout + cy.setLayout(1, 1); + }); + }); +});*/ diff --git a/platform/app/cypress/integration/volume/MPR.spec.js b/platform/app/cypress/integration/volume/MPR.spec.js new file mode 100644 index 0000000..f9de4e0 --- /dev/null +++ b/platform/app/cypress/integration/volume/MPR.spec.js @@ -0,0 +1,102 @@ +describe('OHIF MPR', () => { + beforeEach(() => { + cy.checkStudyRouteInViewer('1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1'); + cy.expectMinimumThumbnails(3); + cy.initCornerstoneToolsAliases(); + cy.initCommonElementsAliases(); + }); + + it('should not go MPR for non reconstructible displaySets', () => { + cy.get('[data-cy="MPR"]').should('have.class', 'cursor-not-allowed'); + }); + + it('should go MPR for reconstructible displaySets and come back', () => { + cy.wait(250); + cy.get('[data-cy="study-browser-thumbnail"][data-series="4"]').dblclick(); + cy.wait(250); + + cy.get('[data-cy="MPR"]').click(); + + cy.get('.cornerstone-canvas').should('have.length', 3); + + cy.get('[data-cy="MPR"]').click(); + + cy.get('.cornerstone-canvas').should('have.length', 1); + }); + + it('should render correctly the MPR', () => { + cy.wait(250); + + cy.get('[data-cy="study-browser-thumbnail"][data-series="4"]').dblclick(); + cy.wait(250); + cy.get('[data-cy="MPR"]').click(); + + cy.get('.cornerstone-canvas').should('have.length', 3); + + // check cornerstone to see if each has images + // we can later do visual testing to match the images with a baseline + cy.window() + .its('cornerstone') + .then(cornerstone => { + const viewports = cornerstone.getRenderingEngines()[0].getViewports(); + // The stack viewport still exists after the changes to viewportId and inde + const imageData1 = viewports[0].getImageData(); + const imageData2 = viewports[1].getImageData(); + const imageData3 = viewports[2].getImageData(); + + // for some reason map doesn't work here + cy.wrap(imageData1).should('not.be', undefined); + cy.wrap(imageData2).should('not.be', undefined); + cy.wrap(imageData3).should('not.be', undefined); + + cy.wrap(imageData1.dimensions).should('deep.equal', imageData2.dimensions); + + cy.wrap(imageData1.origin).should('deep.equal', imageData2.origin); + }); + + cy.get('[data-cy="MPR"]').click(); + + cy.get('.cornerstone-canvas').should('have.length', 1); + }); + + it('should correctly render Crosshairs for MPR', () => { + cy.get('[data-cy="study-browser-thumbnail"][data-series="4"]').dblclick(); + cy.get('[data-cy="MPR"]').click(); + cy.get('[data-cy="Crosshairs"]').click(); + + cy.wait(250); + + // check cornerstone to see if each has crosshairs + // we can later do visual testing to match the images with a baseline + cy.window() + .its('cornerstoneTools') + .then(cornerstoneTools => { + const state = cornerstoneTools.annotation.state.getAnnotationManager(); + + const fORMap = state.annotations; + const fOR = Object.keys(fORMap)[0]; + const fORAnnotation = fORMap[fOR]; + + // it should have crosshairs as the only key (references lines make this 2) + expect(Object.keys(fORAnnotation)).to.have.length(2); + + const crosshairs = fORAnnotation.Crosshairs; + + // it should have three + expect(crosshairs).to.have.length(3); + + expect(crosshairs[0].data.handles.toolCenter).to.deep.equal( + crosshairs[1].data.handles.toolCenter + ); + }); + }); + + it('should activate window level when the active Crosshairs tool for MPR is clicked', () => { + cy.get('[data-cy="study-browser-thumbnail"][data-series="4"]').dblclick(); + cy.get('[data-cy="MPR"]').click(); + cy.get('[data-cy="Crosshairs"]').click(); + + // Click the crosshairs button to deactivate it. + cy.get('[data-cy="Crosshairs"]').click(); + }); +}); diff --git a/platform/app/cypress/plugins/index.js b/platform/app/cypress/plugins/index.js new file mode 100644 index 0000000..8dd144a --- /dev/null +++ b/platform/app/cypress/plugins/index.js @@ -0,0 +1,21 @@ +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +}; diff --git a/platform/app/cypress/results/.gitignore b/platform/app/cypress/results/.gitignore new file mode 100644 index 0000000..e3343cd --- /dev/null +++ b/platform/app/cypress/results/.gitignore @@ -0,0 +1 @@ +test-output.xml diff --git a/platform/app/cypress/support/DragSimulator.js b/platform/app/cypress/support/DragSimulator.js new file mode 100644 index 0000000..c6a8e5f --- /dev/null +++ b/platform/app/cypress/support/DragSimulator.js @@ -0,0 +1,70 @@ +const dataTransfer = new DataTransfer(); + +export const DragSimulator = { + MAX_TRIES: 1, + DELAY_INTERVAL_MS: 10, + counter: 0, + rectsEqual(r1, r2) { + return ( + r1.top === r2.top && r1.right === r2.right && r1.bottom === r2.bottom && r1.left === r2.left + ); + }, + get dropped() { + const currentSourcePosition = this.source.getBoundingClientRect(); + return !this.rectsEqual(this.initialSourcePosition, currentSourcePosition); + }, + get hasTriesLeft() { + return this.counter < this.MAX_TRIES; + }, + dragstart() { + cy.log('**DRAG START**'); + cy.wrap(this.source) + .trigger('mousedown', { which: 1, button: 0 }) + .trigger('dragstart', { dataTransfer }) + .trigger('drag', {}); + }, + drop() { + cy.log('**DROP**'); + return cy + .wrap(this.target) + .trigger('mousemove', 'center') + .trigger('dragover', { dataTransfer, force: true }) + .trigger('drop', { dataTransfer, force: true }) + .trigger('dragend', { dataTransfer }) + .trigger('mouseup', { which: 1, button: 0 }); + }, + dragover() { + cy.log('**DRAGOVER**'); + if (!this.dropped && this.hasTriesLeft) { + this.counter += 1; + return cy + .wrap(this.target) + .trigger('mousemove', 'center') + .trigger('dragover', { + dataTransfer, + position: this.position, + }) + .wait(this.DELAY_INTERVAL_MS) + .then(() => this.dragover()); + } + return this.drop().then(() => true); + }, + init(source, target, position) { + this.source = source; + this.target = target; + this.position = position; + this.counter = 0; + + this.dragstart(); + + return cy.wait(this.DELAY_INTERVAL_MS).then(() => { + this.initialSourcePosition = this.source.getBoundingClientRect(); + return this.dragover(); + }); + }, + simulate(sourceWrapper, targetSelector, position = 'center') { + return cy + .get(targetSelector) + .then(targetWrapper => this.init(sourceWrapper.get(0), targetWrapper.get(0), position)); + }, +}; diff --git a/platform/app/cypress/support/aliases.js b/platform/app/cypress/support/aliases.js new file mode 100644 index 0000000..7fdb271 --- /dev/null +++ b/platform/app/cypress/support/aliases.js @@ -0,0 +1,94 @@ +//Creating aliases for Cornerstone tools buttons +export function initCornerstoneToolsAliases() { + // Note: stack scroll is not in the DOM when the study is loaded + // cy.get('[data-cy="StackScroll"]').as('stackScrollBtn'); + + cy.get('[data-cy="Zoom"]').as('zoomBtn'); + cy.get('[data-cy="WindowLevel-split-button-primary"]').as('wwwcBtnPrimary'); + cy.get('[data-cy="WindowLevel-split-button-secondary"]').as('wwwcBtnSecondary'); + cy.get('[data-cy="Pan"]').as('panBtn'); + cy.get('[data-cy="MeasurementTools-split-button-primary"]').as('measurementToolsBtnPrimary'); + cy.get('[data-cy="MeasurementTools-split-button-secondary"]').as('measurementToolsBtnSecondary'); + // cy.get('[data-cy="Angle"]').as('angleBtn'); + cy.get('[data-cy="MoreTools-split-button-primary"]').as('moreBtnPrimary'); + cy.get('[data-cy="MoreTools-split-button-secondary"]').as('moreBtnSecondary'); + cy.get('[data-cy="Layout"]').as('layoutBtn'); + cy.get('.cornerstone-viewport-element').as('viewport'); +} + +//Creating aliases for Common page elements +export function initCommonElementsAliases(skipMarkers) { + cy.get('[data-cy="trackedMeasurements-btn"]').as('measurementsBtn'); + cy.get('.cornerstone-viewport-element').as('viewport'); + cy.get('[data-cy="seriesList-btn"]').as('seriesBtn'); + cy.get('[data-cy="side-panel-header-right"]').as('RightCollapseBtn'); + cy.get('[data-cy="side-panel-header-left"]').as('LeftCollapseBtn'); + + // click on the measurements button + cy.get('[data-cy="trackedMeasurements-btn"]').click(); + + // TODO: Panels are not in DOM when closed, move this somewhere else + cy.get('[data-cy="trackedMeasurements-panel"]').as('measurementsPanel'); + cy.get('[data-cy="panelSegmentation-btn"]').as('segmentationPanel'); + cy.get('[data-cy="studyBrowser-panel"]').as('seriesPanel'); + cy.get('[data-cy="viewport-overlay-top-right"]').as('viewportInfoTopRight'); + cy.get('[data-cy="viewport-overlay-top-left"]').as('viewportInfoTopLeft'); + cy.get('[data-cy="viewport-overlay-bottom-right"]').as('viewportInfoBottomRight'); + cy.get('[data-cy="viewport-overlay-bottom-left"]').as('viewportInfoBottomLeft'); + + if (skipMarkers) { + return; + } + + try { + cy.get('.left-mid.orientation-marker')?.as('viewportInfoMidLeft'); + cy.get('.top-mid.orientation-marker')?.as('viewportInfoMidTop'); + } catch (error) { + console.log('Error: ', error); + } +} + +//Creating aliases for Routes +export function initRouteAliases() { + cy.intercept('GET', '**/series**', { statusCode: 200, body: [] }).as('getStudySeries'); + + // Todo: for some reason cypress does not redirect to the correct url + // so we intercept the request and redirect it to the correct url + cy.intercept('/studies?limit*', req => { + const url = req.url.replace(/\/studies\?/, '/studies/?limit'); + req.url = url; + }); +} + +//Creating aliases for Study List page elements on Desktop experience +export function initStudyListAliasesOnDesktop() { + cy.get('[data-cy="num-studies"]').as('numStudies'); + cy.get('[data-cy="input-patientName"]').as('PatientName'); + cy.get('[data-cy="input-mrn"]').as('MRN'); + cy.get('[data-cy="input-accession"]').as('AccessionNumber'); + cy.get('[data-cy="input-description"]').as('StudyDescription'); + cy.get('[data-cy="study-list-results"]').as('searchResult'); + cy.get('[data-cy="study-list-results"] > tr').as('searchResult2'); + + // We can't use data attributes (e.g. data--cy) for these since + // they are using third party libraries (i.e. react-dates, react-select) + cy.get('[data-cy="input-date-range-start"').as('studyListStartDate'); + cy.get('[data-cy="input-date-range-end"').as('studyListEndDate'); + cy.get('#input-modalities').as('modalities'); +} + +//Creating aliases for User Preferences modal +export function initPreferencesModalAliases() { + cy.get('.OHIFModal').as('preferencesModal'); + cy.get('[data-cy="hotkeys"]').as('userPreferencesHotkeysTab'); + cy.get('[data-cy="general"]').as('userPreferencesGeneralTab'); + cy.get('[data-cy="window-level"]').as('userPreferencesWindowLevelTab'); + initPreferencesModalFooterBtnAliases(); +} + +//Creating aliases for User Preferences modal +export function initPreferencesModalFooterBtnAliases() { + cy.get('.active [data-cy="reset-default-btn"]').as('restoreBtn'); + cy.get('.active [data-cy="cancel-btn"]').as('cancelBtn'); + cy.get('.active [data-cy="save-btn"]').as('saveBtn'); +} diff --git a/platform/app/cypress/support/commands.js b/platform/app/cypress/support/commands.js new file mode 100644 index 0000000..93f0864 --- /dev/null +++ b/platform/app/cypress/support/commands.js @@ -0,0 +1,639 @@ +import '@percy/cypress'; +import 'cypress-file-upload'; +import { DragSimulator } from './DragSimulator.js'; +import { + initCornerstoneToolsAliases, + initCommonElementsAliases, + initRouteAliases, + initStudyListAliasesOnDesktop, + initPreferencesModalAliases, + initPreferencesModalFooterBtnAliases, +} from './aliases.js'; + +/** + * Command to select a layout preset. + * The layout preset is selected by clicking on the Layout button and then clicking on the desired preset. + * The preset name is the text that is displayed on the button. + * @param {string} presetName - The name of the layout preset that we would like to select + * @param {boolean} screenshot - If true, a screenshot will be taken when the layout tool is opened + */ +Cypress.Commands.add('selectLayoutPreset', (presetName, screenshot) => { + cy.get('[data-cy="Layout"]').click(); + if (screenshot) { + cy.percyCanvasSnapshot('Layout tool opened'); + } + cy.get('div').contains(presetName).should('be.visible').click(); + // fixed wait time for layout changes and rendering + cy.wait(3000); +}); + +/** + * Command to search for a patient name and open his/her study. + * + * @param {string} PatientName - Patient name that we would like to search for + */ +Cypress.Commands.add('openStudy', PatientName => { + cy.openStudyList(); + cy.get('#filter-patientNameOrId').type(PatientName); + // cy.get('@getStudies').then(() => { + // cy.waitQueryList(); + + cy.get('[data-cy="study-list-results"]', { timeout: 15000 }) + .contains(PatientName) + .first() + .click({ force: true }); +}); + +Cypress.Commands.add( + 'checkStudyRouteInViewer', + (StudyInstanceUID, otherParams = '', mode = '/basic-test') => { + Cypress.on('uncaught:exception', () => false); + cy.location('pathname').then($url => { + cy.log($url); + if ($url === 'blank' || !$url.includes(`${mode}/${StudyInstanceUID}${otherParams}`)) { + cy.openStudyInViewer(StudyInstanceUID, otherParams, mode); + cy.waitDicomImage(); + // Very short wait to ensure pending updates are handled + cy.wait(25); + } + }); + } +); + +Cypress.Commands.add('initViewer', (StudyInstanceUID, other = {}) => { + const { mode = '/basic-test', minimumThumbnails = 1, params = '' } = other; + cy.openStudyInViewer(StudyInstanceUID, params, mode); + cy.waitDicomImage(); + // Very short wait to ensure pending updates are handled + cy.wait(25); + + cy.expectMinimumThumbnails(minimumThumbnails); + cy.initCommonElementsAliases(); + cy.initCornerstoneToolsAliases(); +}); + +Cypress.Commands.add( + 'openStudyInViewer', + (StudyInstanceUID, otherParams = '', mode = '/basic-test') => { + cy.visit(`${mode}?StudyInstanceUIDs=${StudyInstanceUID}${otherParams}`); + } +); + +Cypress.Commands.add('waitQueryList', () => { + cy.get('[data-querying="false"]', { timeout: 15000 }); +}); + +/** + * Command to search for a Modality and open the study. + * + * @param {string} Modality - Modality type that we would like to search for + */ +Cypress.Commands.add('openStudyModality', Modality => { + cy.initRouteAliases(); + cy.visit('/'); + + cy.get('#filter-accessionOrModalityOrDescription').type(Modality).waitQueryList(); + + cy.get('[data-cy="study-list-results"]').contains(Modality).first().click(); +}); + +/** + * Command to wait and check if a new page was loaded + * + * @param {string} url - part of the expected url. Default value is /basic-test + */ +Cypress.Commands.add('isPageLoaded', (url = '/basic-test') => { + return cy.location('pathname', { timeout: 60000 }).should('include', url); +}); + +Cypress.Commands.add('openStudyList', () => { + cy.initRouteAliases(); + cy.visit('/', { timeout: 30000 }); + + // For some reason cypress 12.x does not like to stub the network request + // so we just wait here for querying to be done. + // cy.wait('@getStudies'); + cy.waitQueryList(); +}); + +Cypress.Commands.add('waitStudyList', () => { + // wait 1 second for the studies to get updated + cy.wait(1000); + cy.get('@searchResult').should($list => { + expect($list).to.not.have.class('no-hover'); + }); +}); + +Cypress.Commands.add('waitViewportImageLoading', () => { + // Wait for finish loading + cy.get('[data-cy="viewport-grid"]', { timeout: 30000 }).should($grid => { + expect($grid).not.to.contain.text('Load'); + }); +}); + +/** + * Command to perform a drag and drop action. Before using this command, we must get the element that should be dragged first. + * Example of usage: cy.get(element-to-be-dragged).drag(dropzone-element) + * + * @param {*} element - Selector for element that we want to use as dropzone + */ +Cypress.Commands.add('drag', { prevSubject: 'element' }, (...args) => + DragSimulator.simulate(...args) +); + +/** + * Command to perform three clicks into three different positions. Each position must be [x, y]. + * The positions are considering the element as reference, therefore, top-left of the element will be (0, 0). + * + * @param {*} viewport - Selector for viewport we would like to interact with + * @param {number[]} firstClick - Click position [x, y] + * @param {number[]} secondClick - Click position [x, y] + * @param {number[]} thirdClick - Click position [x, y] + */ +Cypress.Commands.add('addAngle', (viewport, firstClick, secondClick, thirdClick) => { + cy.get(viewport).then($viewport => { + const [x1, y1] = firstClick; + const [x2, y2] = secondClick; + const [x3, y3] = thirdClick; + + cy.wrap($viewport) + .click(x1, y1, { force: true }) + .trigger('mousemove', { clientX: x2, clientY: y2 }) + .click(x2, y2, { force: true }) + .trigger('mousemove', { clientX: x3, clientY: y3 }) + .click(x3, y3, { force: true }); + }); +}); + +Cypress.Commands.add('expectMinimumThumbnails', (seriesToWait = 1) => { + cy.get('[data-cy="study-browser-thumbnail"]', { timeout: 50000 }).should( + 'have.length.gte', + seriesToWait + ); +}); + +//Command to wait DICOM image to load into the viewport +Cypress.Commands.add('waitDicomImage', (mode = '/basic-test', timeout = 50000) => { + cy.window() + .its('cornerstone', { timeout: 30000 }) + .should($cornerstone => { + const enabled = $cornerstone.getEnabledElements(); + if (enabled?.length) { + enabled.forEach((item, i) => { + if (item.viewport.viewportStatus !== $cornerstone.Enums.ViewportStatus.RENDERED) { + throw new Error(`Viewport ${i} in state ${item.viewport.viewportStatus}`); + } + }); + } else { + throw new Error('No enabled elements'); + } + }); + // This shouldn't be necessary, but seems to be. + cy.wait(250); + cy.log('DICOM image loaded'); +}); + +//Command to reset and clear all the changes made to the viewport +Cypress.Commands.add('resetViewport', () => { + // Assign an alias to the More button + cy.get('[data-cy="MoreTools-split-button-primary"]') + .should('have.attr', 'data-tool', 'Reset') + .as('moreBtn'); + + // Use the alias to click on the More button + cy.get('@moreBtn').click(); +}); + +Cypress.Commands.add('imageZoomIn', () => { + cy.initCornerstoneToolsAliases(); + cy.get('@zoomBtn').click(); + cy.wait(25); + + //drags the mouse inside the viewport to be able to interact with series + cy.get('@viewport') + .trigger('mousedown', 'top', { buttons: 1 }) + .trigger('mousemove', 'center', { buttons: 1 }) + .trigger('mouseup'); +}); + +Cypress.Commands.add('imageContrast', () => { + cy.initCornerstoneToolsAliases(); + cy.get('@wwwcBtnPrimary').click(); + cy.wait(25); + + //drags the mouse inside the viewport to be able to interact with series + cy.get('@viewport') + .trigger('mousedown', 'center', { buttons: 1 }) + .trigger('mousemove', 'top', { buttons: 1 }) + .trigger('mouseup'); +}); + +//Initialize aliases for Cornerstone tools buttons +Cypress.Commands.add('initCornerstoneToolsAliases', () => { + initCornerstoneToolsAliases(); +}); + +//Initialize aliases for Common page elements +Cypress.Commands.add('initCommonElementsAliases', skipMarkers => { + initCommonElementsAliases(skipMarkers); +}); + +//Initialize aliases for Routes +Cypress.Commands.add('initRouteAliases', () => { + initRouteAliases(); +}); + +//Initialize aliases for Study List page elements +Cypress.Commands.add('initStudyListAliasesOnDesktop', () => { + initStudyListAliasesOnDesktop(); +}); + +//Add measurements in the viewport +Cypress.Commands.add( + 'addLengthMeasurement', + (firstClick = [150, 100], secondClick = [130, 170]) => { + // Assign an alias to the button element + cy.get('@measurementToolsBtnPrimary').as('lengthButton'); + + cy.get('@lengthButton').should('have.attr', 'data-tool', 'Length'); + + cy.get('@lengthButton').then(button => { + // Only click the length tool if it is not active, in case the length tool is set up to + // toggle to inactive. + if (!button.is('.active')) { + cy.wrap(button).click(); + } + }); + + cy.get('@lengthButton').should('have.attr', 'data-active', 'true'); + + cy.get('@viewport').then($viewport => { + const [x1, y1] = firstClick; + const [x2, y2] = secondClick; + + cy.wrap($viewport) + .click(x1, y1, { force: true }) + .wait(1000) + .click(x2, y2, { force: true }) + .wait(1000); + }); + } +); + +// Add brush stroke in the viewport +Cypress.Commands.add('addBrush', (viewport, firstClick = [85, 100], secondClick = [85, 300]) => { + cy.get(viewport) + .first() + .then(viewportElement => { + const [x1, y1] = firstClick; + const [x2, y2] = secondClick; + + const steps = 10; + const xStep = (x2 - x1) / steps; + const yStep = (y2 - y1) / steps; + + cy.wrap(viewportElement) + .trigger('mousedown', x1, y1, { buttons: 1 }) + .then(() => { + for (let i = 1; i <= steps; i++) { + let x = x1 + xStep * i; + let y = y1 + yStep * i; + cy.wrap(viewportElement).trigger('mousemove', x, y, { buttons: 1 }); + } + }) + .trigger('mouseup'); + }); +}); + +// Add erase stroke in the viewport +Cypress.Commands.add('addEraser', (viewport, firstClick = [85, 100], secondClick = [85, 300]) => { + cy.get(viewport) + .first() + .then(viewportElement => { + const [x1, y1] = firstClick; + const [x2, y2] = secondClick; + + const steps = 10; + const xStep = (x2 - x1) / steps; + const yStep = (y2 - y1) / steps; + + cy.wrap(viewportElement) + .trigger('mousedown', x1, y1, { buttons: 1 }) + .then(() => { + for (let i = 1; i <= steps; i++) { + let x = x1 + xStep * i; + let y = y1 + yStep * i; + cy.wrap(viewportElement).trigger('mousemove', x, y, { buttons: 1 }); + } + }) + .trigger('mouseup'); + }); +}); + +//Add measurements in the viewport +Cypress.Commands.add( + 'addAngleMeasurement', + (initPos = [180, 390], midPos = [300, 410], finalPos = [180, 450]) => { + cy.get('[data-cy="MeasurementTools-split-button-secondary"]').click(); + cy.get('[data-cy="Angle"]').click(); + + cy.addAngle('.cornerstone-canvas', initPos, midPos, finalPos); + } +); + +/** + * Tests if element is NOT in viewport, or does not exist in DOM + * + * @param {string} element - element selector string or alias + * @returns + */ +Cypress.Commands.add('isNotInViewport', element => { + cy.get(element, { timeout: 3000 }).should($el => { + const bottom = Cypress.$(cy.state('window')).height() - 50; + const right = Cypress.$(cy.state('window')).width() - 50; + + // If it's not visible, it's not in the viewport + if ($el) { + const rect = $el[0].getBoundingClientRect(); + + // TODO: support leftOf, above + const isBeneath = rect.top >= bottom && rect.bottom >= bottom; + const isRightOf = rect.left >= right && rect.right >= right; + const isNotInViewport = isBeneath && isRightOf; + + expect(isNotInViewport).to.be.true; + } + }); +}); + +/** + * Tests if element is in viewport, or it does exist in DOM + * + * @param {string} element - element selector string or alias + * @returns + */ +Cypress.Commands.add('isInViewport', element => { + cy.get(element, { timeout: 3000 }).should($el => { + const bottom = Cypress.$(cy.state('window')).height(); + const right = Cypress.$(cy.state('window')).width(); + + // If it's not visible, it's not in the viewport + if ($el) { + const rect = $el[0].getBoundingClientRect(); + + // TODO: support leftOf, above + const isBeneath = rect.top < bottom && rect.bottom < bottom; + const isRightOf = rect.left < right && rect.right < right; + const isInViewport = isBeneath && isRightOf; + + expect(isInViewport).to.be.true; + } + }); +}); + +/** + * Percy.io Canvas screenshot workaround + * + */ +Cypress.Commands.add('percyCanvasSnapshot', (name, options = {}) => { + cy.document().then(doc => { + convertCanvas(doc); + }); + + // `domTransformation` does not appear to be working + // But modifying our immediate DOM does. + cy.percySnapshot(name, { ...options }); //, domTransformation: convertCanvas }); + + cy.document().then(doc => { + unconvertCanvas(doc); + }); +}); + +Cypress.Commands.add('setLayout', (columns = 1, rows = 1) => { + cy.get('[data-cy="Layout"]').click(); + + cy.get(`[data-cy="Layout-${columns - 1}-${rows - 1}"]`).click(); + + cy.wait(10); + cy.waitDicomImage(); +}); + +function convertCanvas(documentClone) { + documentClone.querySelectorAll('canvas').forEach(selector => canvasToImage(selector)); + + return documentClone; +} + +function unconvertCanvas(documentClone) { + // Remove previously generated images + documentClone.querySelectorAll('[data-percy-image]').forEach(selector => selector.remove()); + // Restore canvas visibility + documentClone.querySelectorAll('[data-percy-canvas]').forEach(selector => { + selector.removeAttribute('data-percy-canvas'); + selector.style = ''; + }); +} + +function canvasToImage(selectorOrEl) { + let canvas = + typeof selectorOrEl === 'object' ? selectorOrEl : document.querySelector(selectorOrEl); + let image = document.createElement('img'); + let canvasImageBase64 = canvas.toDataURL('image/png'); + + // Show Image + image.src = canvasImageBase64; + image.style = 'width: 100%'; + image.setAttribute('data-percy-image', true); + // Hide Canvas + canvas.setAttribute('data-percy-canvas', true); + canvas.parentElement.appendChild(image); + canvas.style = 'display: none'; +} + +//Initialize aliases for User Preferences modal +Cypress.Commands.add('initPreferencesModalAliases', () => { + initPreferencesModalAliases(); +}); + +Cypress.Commands.add('openPreferences', () => { + cy.log('Open User Preferences Modal'); + // Open User Preferences modal + cy.get('body').then(body => { + if (body.find('.OHIFModal').length === 0) { + cy.get('[data-cy="options-chevron-down-icon"]') + .scrollIntoView() + .click() + .then(() => { + cy.get('[data-cy="options-dropdown"]').last().click().wait(200); + }); + } + }); +}); + +Cypress.Commands.add('scrollToIndex', index => { + // Workaround implemented based on Cypress issue: + // https://github.com/cypress-io/cypress/issues/1570#issuecomment-450966053 + const nativeInputValueSetter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, + 'value' + ).set; + + cy.get('input.imageSlider[type=range]').then($range => { + // get the DOM node + const range = $range[0]; + // set the value manually + nativeInputValueSetter.call(range, index); + // now dispatch the event + range.dispatchEvent( + new Event('change', { + value: index, + bubbles: true, + }) + ); + }); +}); + +Cypress.Commands.add('closePreferences', () => { + cy.log('Close User Preferences Modal'); + + cy.get('body').then(body => { + // Close notification if displayed + if (body.find('.sb-closeIcon').length > 0) { + cy.get('.sb-closeIcon').first().click({ force: true }); + } + + // Close User Preferences Modal (if displayed) + if (body.find('.OHIFModal__header').length > 0) { + cy.get('[data-cy="close-button"]').click({ force: true }); + } + }); +}); + +Cypress.Commands.add('selectPreferencesTab', tabAlias => { + cy.initPreferencesModalAliases(); + + cy.get(tabAlias).as('selectedTab'); + cy.get('@selectedTab').click(); + cy.get('@selectedTab').should('have.class', 'active'); + + initPreferencesModalFooterBtnAliases(); +}); + +Cypress.Commands.add('resetUserHotkeyPreferences', () => { + // Open User Preferences modal + cy.openPreferences(); + + cy.selectPreferencesTab('@userPreferencesHotkeysTab').then(() => { + cy.log('Reset Hotkeys to Default Preferences'); + cy.get('@restoreBtn').click(); + }); + + // Close Success Message overlay (if displayed) + cy.get('body').then(body => { + if (body.find('.sb-closeIcon').length > 0) { + cy.get('.sb-closeIcon').first().click({ force: true }); + } + // Click on Save Button + cy.get('@saveBtn').click(); + }); +}); + +Cypress.Commands.add('resetUserGeneralPreferences', () => { + // Open User Preferences modal + cy.openPreferences(); + + cy.selectPreferencesTab('@userPreferencesGeneralTab').then(() => { + cy.log('Reset Language to Default Preferences'); + cy.get('@restoreBtn').click(); + }); + + // Close Success Message overlay (if displayed) + cy.get('body').then(body => { + if (body.find('.sb-closeIcon').length > 0) { + cy.get('.sb-closeIcon').first().click({ force: true }); + } + // Click on Save Button + cy.get('@saveBtn').click(); + }); +}); + +Cypress.Commands.add('setNewHotkeyShortcutOnUserPreferencesModal', (function_label, shortcut) => { + // Within scopes all `.get` and `.contains` to within the matched elements + // dom instead of checking from document + cy.get('.HotkeysPreferences').within(() => { + cy.contains(function_label) // label we're looking for + .parent() + .find('input') // closest input to that label + .type(shortcut, { force: true }); // Set new shortcut for that function + }); +}); + +Cypress.Commands.add( + 'setWindowLevelPreset', + (preset_index, description_value, window_value, level_value) => { + let index = parseInt(preset_index) + 1; + + // Set new Description value + cy.get(':nth-child(' + index + ') > .description > .preferencesInput') + .clear() + .type(description_value, { + force: true, + }) + .blur(); + + // Set new Window value + cy.get(':nth-child(' + index + ') > .window > .preferencesInput') + .clear() + .type(window_value, { + force: true, + }) + .blur(); + + // Set new Level value + cy.get(':nth-child(' + index + ') > .level > .preferencesInput') + .clear() + .type(level_value, { + force: true, + }) + .blur(); + } +); + +Cypress.Commands.add('openDownloadImageModal', () => { + // Click on More button + cy.get('[data-cy="Capture"]').as('captureBtn').click(); +}); + +Cypress.Commands.add('setLanguage', (language, save = true) => { + cy.openPreferences(); + cy.initPreferencesModalAliases(); + cy.selectPreferencesTab('@userPreferencesGeneralTab'); + + // Language dropdown should be displayed + cy.get('#language-select').should('be.visible'); + + // Select Language and Save/Cancel + cy.get('#language-select').select(language); + + // Close Success Message overlay (if displayed) + cy.get('body').then(body => { + if (body.find('.sb-closeIcon').length > 0) { + cy.get('.sb-closeIcon').first().click({ force: true }); + } + + //Click on Save/Cancel button + const toClick = save ? '@saveBtn' : '@cancelBtn'; + cy.get(toClick).scrollIntoView().click(); + }); +}); + +// hide noisy logs +// https://github.com/cypress-io/cypress/issues/7362 +// uncomment this if you really need the network logs +const origLog = Cypress.log; +Cypress.log = function (opts, ...other) { + if (opts.displayName === 'script' || opts.name === 'request') { + return; + } + return origLog(opts, ...other); +}; diff --git a/platform/app/cypress/support/index.js b/platform/app/cypress/support/index.js new file mode 100644 index 0000000..2263d33 --- /dev/null +++ b/platform/app/cypress/support/index.js @@ -0,0 +1,2 @@ +import './aliases.js'; +import './commands.js'; diff --git a/platform/app/jest.config.js b/platform/app/jest.config.js new file mode 100644 index 0000000..e7bd742 --- /dev/null +++ b/platform/app/jest.config.js @@ -0,0 +1,13 @@ +const base = require('../../jest.config.base.js'); +const pkg = require('./package'); + +module.exports = { + ...base, + displayName: pkg.name, + setupFilesAfterEnv: ['/src/__tests__/globalSetup.js'], + // rootDir: "../.." + // testMatch: [ + // //`/platform/${pack.name}/**/*.spec.js` + // "/platform/app/**/*.test.js" + // ] +}; diff --git a/platform/app/jestBabelTransform.js b/platform/app/jestBabelTransform.js new file mode 100644 index 0000000..81c0a5c --- /dev/null +++ b/platform/app/jestBabelTransform.js @@ -0,0 +1,5 @@ +const babelJest = require('babel-jest'); + +module.exports = babelJest.createTransformer({ + rootMode: 'upward', +}); diff --git a/platform/app/netlify.toml b/platform/app/netlify.toml new file mode 100644 index 0000000..b2bd548 --- /dev/null +++ b/platform/app/netlify.toml @@ -0,0 +1,41 @@ +# Netlify Config +# +# TOML Reference: +# https://www.netlify.com/docs/netlify-toml-reference/ +# +# We use Netlify for deploy previews and for publishing docs (gh-pages branch). +# https://viewer.ohif.org is created using a different process that is +# managed by CircleCI and deployed to our Google Hosting +# + +[build] + base = "" + build = "yarn run build:viewer:ci" + publish = "dist" + + +# NODE_VERSION in root `.nvmrc` takes priority +# YARN_FLAGS: https://www.netlify.com/docs/build-gotchas/#yarn +[build.environment] + # If 'production', `yarn install` does not install devDependencies + NODE_ENV = "development" + NODE_VERSION = "20.18.1" + YARN_VERSION = "1.22.5" + RUBY_VERSION = "2.6.2" + YARN_FLAGS = "--no-ignore-optional --pure-lockfile" + NETLIFY_USE_YARN = "true" + +[[headers]] + # Define which paths this specific [[headers]] block will cover. + for = "/*" + + [headers.values] + X-Frame-Options = "DENY" + X-XSS-Protection = "1; mode=block" + + # Multi-key header rules are expressed with multi-line strings. + cache-control = ''' + max-age=0, + no-cache, + no-store, + must-revalidate''' diff --git a/platform/app/package.json b/platform/app/package.json new file mode 100644 index 0000000..f45dda8 --- /dev/null +++ b/platform/app/package.json @@ -0,0 +1,113 @@ +{ + "name": "@ohif/app", + "version": "3.10.0-beta.111", + "productVersion": "3.4.0", + "description": "OHIF Viewer", + "author": "OHIF Contributors", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/index.umd.js", + "module": "src/index.js", + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "proxy": "http://localhost:8042", + "scripts": { + "build:viewer": "cross-env NODE_ENV=production yarn run build", + "build:dev": "cross-env NODE_ENV=development yarn run build", + "build:aws": "cross-env NODE_ENV=development APP_CONFIG=config/aws_static.js yarn run build && gzip -9 -r dist", + "build:viewer:ci": "cross-env NODE_ENV=production PUBLIC_URL=/ APP_CONFIG=config/netlify.js QUICK_BUILD=false yarn run build", + "build:viewer:qa": "cross-env NODE_ENV=production APP_CONFIG=config/google.js yarn run build", + "build:viewer:demo": "cross-env NODE_ENV=production APP_CONFIG=config/demo.js HTML_TEMPLATE=rollbar.html QUICK_BUILD=false yarn run build", + "build": "node --max_old_space_size=8096 ./../../node_modules/webpack/bin/webpack.js --progress --config .webpack/webpack.pwa.js", + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev:fast": "rsbuild dev --config ../../rsbuild.config.ts", + "dev": "cross-env NODE_ENV=development webpack serve --config .webpack/webpack.pwa.js", + "dev:no:cache": "cross-env NODE_ENV=development webpack serve --no-cache --config .webpack/webpack.pwa.js", + "dev:orthanc": "cross-env NODE_ENV=development PROXY_TARGET=http://localhost:3000/pacs/dicom-web PROXY_DOMAIN=http://localhost:8042 PROXY_PATH_REWRITE_FROM=/pacs/dicom-web PROXY_PATH_REWRITE_TO=/dicom-web APP_CONFIG=config/docker-nginx-orthanc.js webpack serve --config .webpack/webpack.pwa.js", + "dev:orthanc:no:cache": "cross-env NODE_ENV=development PROXY_TARGET=http://localhost:3000/pacs/dicom-web PROXY_DOMAIN=http://localhost:8042 PROXY_PATH_REWRITE_FROM=/pacs/dicom-web PROXY_PATH_REWRITE_TO=/dicom-web APP_CONFIG=config/docker-nginx-orthanc.js webpack serve --no-cache --config .webpack/webpack.pwa.js", + "dev:dcm4chee": "cross-env NODE_ENV=development APP_CONFIG=config/local_dcm4chee.js webpack serve --config .webpack/webpack.pwa.js", + "dev:static": "cross-env NODE_ENV=development APP_CONFIG=config/local_static.js webpack serve --config .webpack/webpack.pwa.js", + "dev:viewer": "yarn run dev", + "start": "yarn run dev", + "test:e2e": "cypress open", + "test:e2e:local": "cypress run --config video=false --browser chrome --spec 'cypress/integration/common/**/*,cypress/integration/pwa/**/*'", + "test:e2e:dist": "start-server-and-test test:e2e:serve http://localhost:3000 test:e2e:ci", + "test:e2e:serve": "cross-env APP_CONFIG=config/e2e.js yarn start", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage", + "ci:generateSuccessVersion": "node -p -e \"require('./package.json').version\" > success_version.txt" + }, + "files": [ + "dist", + "README.md" + ], + "dependencies": { + "@babel/runtime": "^7.20.13", + "@cornerstonejs/codec-charls": "^1.2.3", + "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", + "@cornerstonejs/codec-openjpeg": "^1.2.4", + "@cornerstonejs/codec-openjph": "^2.4.5", + "@cornerstonejs/dicom-image-loader": "^2.19.14", + "@emotion/serialize": "^1.1.3", + "@ohif/core": "3.10.0-beta.111", + "@ohif/extension-cornerstone": "3.10.0-beta.111", + "@ohif/extension-cornerstone-dicom-rt": "3.10.0-beta.111", + "@ohif/extension-cornerstone-dicom-seg": "3.10.0-beta.111", + "@ohif/extension-cornerstone-dicom-sr": "3.10.0-beta.111", + "@ohif/extension-default": "3.10.0-beta.111", + "@ohif/extension-dicom-microscopy": "3.10.0-beta.111", + "@ohif/extension-dicom-pdf": "3.10.0-beta.111", + "@ohif/extension-dicom-video": "3.10.0-beta.111", + "@ohif/extension-test": "3.10.0-beta.111", + "@ohif/i18n": "3.10.0-beta.111", + "@ohif/mode-basic-dev-mode": "3.10.0-beta.111", + "@ohif/mode-longitudinal": "3.10.0-beta.111", + "@ohif/mode-microscopy": "3.10.0-beta.111", + "@ohif/mode-test": "3.10.0-beta.111", + "@ohif/ui": "3.10.0-beta.111", + "@ohif/ui-next": "3.10.0-beta.111", + "@svgr/webpack": "^8.1.0", + "@types/react": "^18.3.3", + "classnames": "^2.3.2", + "core-js": "*", + "cornerstone-math": "^0.1.9", + "dcmjs": "*", + "detect-gpu": "^4.0.16", + "dicom-parser": "^1.8.9", + "dotenv-webpack": "^1.7.0", + "file-loader": "^6.2.0", + "hammerjs": "^2.0.8", + "history": "^5.3.0", + "i18next": "^17.0.3", + "i18next-browser-languagedetector": "^3.0.1", + "lodash.isequal": "4.5.0", + "oidc-client": "1.11.5", + "oidc-client-ts": "^3.0.1", + "prop-types": "^15.7.2", + "query-string": "^6.12.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-dropzone": "^10.1.7", + "react-i18next": "^12.2.2", + "react-resize-detector": "^10.0.1", + "react-router": "^6.23.1", + "react-router-dom": "^6.8.1", + "react-shepherd": "6.1.1", + "shepherd.js": "13.0.3", + "url-loader": "^4.1.1", + "zustand": "4.5.5" + }, + "devDependencies": { + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@types/node": "^20.12.12", + "identity-obj-proxy": "3.0.x", + "tailwindcss": "3.2.4" + } +} diff --git a/platform/app/pluginConfig.json b/platform/app/pluginConfig.json new file mode 100644 index 0000000..06ae62e --- /dev/null +++ b/platform/app/pluginConfig.json @@ -0,0 +1,104 @@ +{ + "extensions": [ + { + "packageName": "@ohif/extension-default" + }, + { + "packageName": "@ohif/extension-cornerstone", + "version": "3.0.0" + }, + { + "packageName": "@ohif/extension-measurement-tracking", + "default": false, + "version": "3.0.0" + }, + { + "packageName": "@ohif/extension-cornerstone-dicom-sr", + "default": false, + "version": "3.0.0" + }, + { + "packageName": "@ohif/extension-cornerstone-dicom-seg", + "default": false, + "version": "3.0.0" + }, + { + "packageName": "@ohif/extension-cornerstone-dicom-pmap", + "default": false, + "version": "3.0.0" + }, + { + "packageName": "@ohif/extension-cornerstone-dynamic-volume", + "default": false, + "version": "3.0.0" + }, + { + "packageName": "@ohif/extension-dicom-microscopy", + "default": false, + "version": "3.0.0" + }, + { + "packageName": "@ohif/extension-dicom-pdf", + "default": false, + "version": "3.0.0" + }, + { + "packageName": "@ohif/extension-dicom-video", + "default": false, + "version": "3.0.0" + }, + { + "packageName": "@ohif/extension-tmtv", + "default": false, + "version": "3.0.0" + }, + { + "packageName": "@ohif/extension-test", + "default": false, + "version": "3.0.0" + }, + { + "packageName": "@ohif/extension-cornerstone-dicom-rt", + "default": false, + "version": "3.0.0" + } + ], + "modes": [ + { + "packageName": "@ohif/mode-longitudinal" + }, + { + "packageName": "@ohif/mode-segmentation" + }, + { + "packageName": "@ohif/mode-tmtv" + }, + { + "packageName": "@ohif/mode-microscopy" + }, + { + "packageName": "@ohif/mode-preclinical-4d" + }, + { + "packageName": "@ohif/mode-test", + "default": false, + "version": "3.0.0" + }, + { + "packageName": "@ohif/mode-basic-dev-mode", + "default": false, + "version": "3.0.0" + } + ], + "public": [ + { + "directory": "./platform/public" + }, + { + "packageName": "dicom-microscopy-viewer", + "importPath": "/dicom-microscopy-viewer/dicomMicroscopyViewer.min.js", + "globalName": "dicomMicroscopyViewer", + "directory": "./node_modules/dicom-microscopy-viewer/dist/dynamic-import" + } + ] +} diff --git a/platform/app/postcss.config.js b/platform/app/postcss.config.js new file mode 100644 index 0000000..a0daaed --- /dev/null +++ b/platform/app/postcss.config.js @@ -0,0 +1 @@ +module.exports = require('../../postcss.config.js'); diff --git a/platform/app/preinstall.js b/platform/app/preinstall.js new file mode 100644 index 0000000..458e41f --- /dev/null +++ b/platform/app/preinstall.js @@ -0,0 +1,13 @@ +console.log('preinstall.js'); + +const { exec } = require('child_process'); +const log = (err, stdout, stderr) => console.log(stdout); + +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; +if (GITHUB_TOKEN) { + const command = `git config --global url."https://${GITHUB_TOKEN}:x-oauth-basic@github.com/".insteadOf ssh://git@github.com/`; + console.log(command); + exec(command, log); +} else { + console.log('No GITHUB_TOKEN found, skipping private repo customization'); +} diff --git a/platform/app/public/_headers b/platform/app/public/_headers new file mode 100644 index 0000000..e69de29 diff --git a/platform/app/public/_redirects b/platform/app/public/_redirects new file mode 100644 index 0000000..c3f7726 --- /dev/null +++ b/platform/app/public/_redirects @@ -0,0 +1,6 @@ +# Specific to our deploy-preview +# Our docs are published using CircleCI + GitBook +# Configure redirects using netlify.toml + +# Spa +/* /index.html 200 diff --git a/platform/app/public/assets/android-chrome-144x144.png b/platform/app/public/assets/android-chrome-144x144.png new file mode 100644 index 0000000..feb771f Binary files /dev/null and b/platform/app/public/assets/android-chrome-144x144.png differ diff --git a/platform/app/public/assets/android-chrome-192x192.png b/platform/app/public/assets/android-chrome-192x192.png new file mode 100644 index 0000000..946be33 Binary files /dev/null and b/platform/app/public/assets/android-chrome-192x192.png differ diff --git a/platform/app/public/assets/android-chrome-256x256.png b/platform/app/public/assets/android-chrome-256x256.png new file mode 100644 index 0000000..7dceb3d Binary files /dev/null and b/platform/app/public/assets/android-chrome-256x256.png differ diff --git a/platform/app/public/assets/android-chrome-36x36.png b/platform/app/public/assets/android-chrome-36x36.png new file mode 100644 index 0000000..1d7392c Binary files /dev/null and b/platform/app/public/assets/android-chrome-36x36.png differ diff --git a/platform/app/public/assets/android-chrome-384x384.png b/platform/app/public/assets/android-chrome-384x384.png new file mode 100644 index 0000000..36744a6 Binary files /dev/null and b/platform/app/public/assets/android-chrome-384x384.png differ diff --git a/platform/app/public/assets/android-chrome-48x48.png b/platform/app/public/assets/android-chrome-48x48.png new file mode 100644 index 0000000..46910fb Binary files /dev/null and b/platform/app/public/assets/android-chrome-48x48.png differ diff --git a/platform/app/public/assets/android-chrome-512x512.png b/platform/app/public/assets/android-chrome-512x512.png new file mode 100644 index 0000000..c45400a Binary files /dev/null and b/platform/app/public/assets/android-chrome-512x512.png differ diff --git a/platform/app/public/assets/android-chrome-72x72.png b/platform/app/public/assets/android-chrome-72x72.png new file mode 100644 index 0000000..3b78d37 Binary files /dev/null and b/platform/app/public/assets/android-chrome-72x72.png differ diff --git a/platform/app/public/assets/android-chrome-96x96.png b/platform/app/public/assets/android-chrome-96x96.png new file mode 100644 index 0000000..9082f43 Binary files /dev/null and b/platform/app/public/assets/android-chrome-96x96.png differ diff --git a/platform/app/public/assets/apple-touch-icon-1024x1024.png b/platform/app/public/assets/apple-touch-icon-1024x1024.png new file mode 100644 index 0000000..9aec17f Binary files /dev/null and b/platform/app/public/assets/apple-touch-icon-1024x1024.png differ diff --git a/platform/app/public/assets/apple-touch-icon-114x114.png b/platform/app/public/assets/apple-touch-icon-114x114.png new file mode 100644 index 0000000..b2b6d05 Binary files /dev/null and b/platform/app/public/assets/apple-touch-icon-114x114.png differ diff --git a/platform/app/public/assets/apple-touch-icon-120x120.png b/platform/app/public/assets/apple-touch-icon-120x120.png new file mode 100644 index 0000000..ce08329 Binary files /dev/null and b/platform/app/public/assets/apple-touch-icon-120x120.png differ diff --git a/platform/app/public/assets/apple-touch-icon-144x144.png b/platform/app/public/assets/apple-touch-icon-144x144.png new file mode 100644 index 0000000..feb771f Binary files /dev/null and b/platform/app/public/assets/apple-touch-icon-144x144.png differ diff --git a/platform/app/public/assets/apple-touch-icon-152x152.png b/platform/app/public/assets/apple-touch-icon-152x152.png new file mode 100644 index 0000000..82b0791 Binary files /dev/null and b/platform/app/public/assets/apple-touch-icon-152x152.png differ diff --git a/platform/app/public/assets/apple-touch-icon-167x167.png b/platform/app/public/assets/apple-touch-icon-167x167.png new file mode 100644 index 0000000..1465c75 Binary files /dev/null and b/platform/app/public/assets/apple-touch-icon-167x167.png differ diff --git a/platform/app/public/assets/apple-touch-icon-180x180.png b/platform/app/public/assets/apple-touch-icon-180x180.png new file mode 100644 index 0000000..c7eb09a Binary files /dev/null and b/platform/app/public/assets/apple-touch-icon-180x180.png differ diff --git a/platform/app/public/assets/apple-touch-icon-57x57.png b/platform/app/public/assets/apple-touch-icon-57x57.png new file mode 100644 index 0000000..d868d4d Binary files /dev/null and b/platform/app/public/assets/apple-touch-icon-57x57.png differ diff --git a/platform/app/public/assets/apple-touch-icon-60x60.png b/platform/app/public/assets/apple-touch-icon-60x60.png new file mode 100644 index 0000000..0bf6954 Binary files /dev/null and b/platform/app/public/assets/apple-touch-icon-60x60.png differ diff --git a/platform/app/public/assets/apple-touch-icon-72x72.png b/platform/app/public/assets/apple-touch-icon-72x72.png new file mode 100644 index 0000000..3b78d37 Binary files /dev/null and b/platform/app/public/assets/apple-touch-icon-72x72.png differ diff --git a/platform/app/public/assets/apple-touch-icon-76x76.png b/platform/app/public/assets/apple-touch-icon-76x76.png new file mode 100644 index 0000000..d607781 Binary files /dev/null and b/platform/app/public/assets/apple-touch-icon-76x76.png differ diff --git a/platform/app/public/assets/apple-touch-icon-precomposed.png b/platform/app/public/assets/apple-touch-icon-precomposed.png new file mode 100644 index 0000000..c7eb09a Binary files /dev/null and b/platform/app/public/assets/apple-touch-icon-precomposed.png differ diff --git a/platform/app/public/assets/apple-touch-icon.png b/platform/app/public/assets/apple-touch-icon.png new file mode 100644 index 0000000..c7eb09a Binary files /dev/null and b/platform/app/public/assets/apple-touch-icon.png differ diff --git a/platform/app/public/assets/apple-touch-startup-image-1182x2208.png b/platform/app/public/assets/apple-touch-startup-image-1182x2208.png new file mode 100644 index 0000000..7c47f55 Binary files /dev/null and b/platform/app/public/assets/apple-touch-startup-image-1182x2208.png differ diff --git a/platform/app/public/assets/apple-touch-startup-image-1242x2148.png b/platform/app/public/assets/apple-touch-startup-image-1242x2148.png new file mode 100644 index 0000000..a03effe Binary files /dev/null and b/platform/app/public/assets/apple-touch-startup-image-1242x2148.png differ diff --git a/platform/app/public/assets/apple-touch-startup-image-1496x2048.png b/platform/app/public/assets/apple-touch-startup-image-1496x2048.png new file mode 100644 index 0000000..69f0d12 Binary files /dev/null and b/platform/app/public/assets/apple-touch-startup-image-1496x2048.png differ diff --git a/platform/app/public/assets/apple-touch-startup-image-1536x2008.png b/platform/app/public/assets/apple-touch-startup-image-1536x2008.png new file mode 100644 index 0000000..dc57695 Binary files /dev/null and b/platform/app/public/assets/apple-touch-startup-image-1536x2008.png differ diff --git a/platform/app/public/assets/apple-touch-startup-image-320x460.png b/platform/app/public/assets/apple-touch-startup-image-320x460.png new file mode 100644 index 0000000..2773c6a Binary files /dev/null and b/platform/app/public/assets/apple-touch-startup-image-320x460.png differ diff --git a/platform/app/public/assets/apple-touch-startup-image-640x1096.png b/platform/app/public/assets/apple-touch-startup-image-640x1096.png new file mode 100644 index 0000000..cfddf0d Binary files /dev/null and b/platform/app/public/assets/apple-touch-startup-image-640x1096.png differ diff --git a/platform/app/public/assets/apple-touch-startup-image-640x920.png b/platform/app/public/assets/apple-touch-startup-image-640x920.png new file mode 100644 index 0000000..4de040c Binary files /dev/null and b/platform/app/public/assets/apple-touch-startup-image-640x920.png differ diff --git a/platform/app/public/assets/apple-touch-startup-image-748x1024.png b/platform/app/public/assets/apple-touch-startup-image-748x1024.png new file mode 100644 index 0000000..c653ff5 Binary files /dev/null and b/platform/app/public/assets/apple-touch-startup-image-748x1024.png differ diff --git a/platform/app/public/assets/apple-touch-startup-image-750x1294.png b/platform/app/public/assets/apple-touch-startup-image-750x1294.png new file mode 100644 index 0000000..0cf9942 Binary files /dev/null and b/platform/app/public/assets/apple-touch-startup-image-750x1294.png differ diff --git a/platform/app/public/assets/apple-touch-startup-image-768x1004.png b/platform/app/public/assets/apple-touch-startup-image-768x1004.png new file mode 100644 index 0000000..a83affc Binary files /dev/null and b/platform/app/public/assets/apple-touch-startup-image-768x1004.png differ diff --git a/platform/app/public/assets/browserconfig.xml b/platform/app/public/assets/browserconfig.xml new file mode 100644 index 0000000..53dc86f --- /dev/null +++ b/platform/app/public/assets/browserconfig.xml @@ -0,0 +1,12 @@ + + + + + + + + + #fff + + + diff --git a/platform/app/public/assets/coast-228x228.png b/platform/app/public/assets/coast-228x228.png new file mode 100644 index 0000000..841bc04 Binary files /dev/null and b/platform/app/public/assets/coast-228x228.png differ diff --git a/platform/app/public/assets/favicon-16x16.png b/platform/app/public/assets/favicon-16x16.png new file mode 100644 index 0000000..0dbb01b Binary files /dev/null and b/platform/app/public/assets/favicon-16x16.png differ diff --git a/platform/app/public/assets/favicon-32x32.png b/platform/app/public/assets/favicon-32x32.png new file mode 100644 index 0000000..2e71dee Binary files /dev/null and b/platform/app/public/assets/favicon-32x32.png differ diff --git a/platform/app/public/assets/favicon.ico b/platform/app/public/assets/favicon.ico new file mode 100644 index 0000000..faaa2cf Binary files /dev/null and b/platform/app/public/assets/favicon.ico differ diff --git a/platform/app/public/assets/firefox_app_128x128.png b/platform/app/public/assets/firefox_app_128x128.png new file mode 100644 index 0000000..1fa92bb Binary files /dev/null and b/platform/app/public/assets/firefox_app_128x128.png differ diff --git a/platform/app/public/assets/firefox_app_512x512.png b/platform/app/public/assets/firefox_app_512x512.png new file mode 100644 index 0000000..49fdba6 Binary files /dev/null and b/platform/app/public/assets/firefox_app_512x512.png differ diff --git a/platform/app/public/assets/firefox_app_60x60.png b/platform/app/public/assets/firefox_app_60x60.png new file mode 100644 index 0000000..c3e8c6d Binary files /dev/null and b/platform/app/public/assets/firefox_app_60x60.png differ diff --git a/platform/app/public/assets/manifest.webapp b/platform/app/public/assets/manifest.webapp new file mode 100644 index 0000000..4f4b540 --- /dev/null +++ b/platform/app/public/assets/manifest.webapp @@ -0,0 +1,14 @@ +{ + "version": "2.2.1", + "name": "OHIF Viewer", + "description": "OHIF Viewer", + "icons": { + "60": "/assets/firefox_app_60x60.png", + "128": "/assets/firefox_app_128x128.png", + "512": "/assets/firefox_app_512x512.png" + }, + "developer": { + "name": "OHIF Contributors", + "url": "https://github.com/ohif/viewers" + } +} diff --git a/platform/app/public/assets/mstile-144x144.png b/platform/app/public/assets/mstile-144x144.png new file mode 100644 index 0000000..feb771f Binary files /dev/null and b/platform/app/public/assets/mstile-144x144.png differ diff --git a/platform/app/public/assets/mstile-150x150.png b/platform/app/public/assets/mstile-150x150.png new file mode 100644 index 0000000..1c7d5bb Binary files /dev/null and b/platform/app/public/assets/mstile-150x150.png differ diff --git a/platform/app/public/assets/mstile-310x150.png b/platform/app/public/assets/mstile-310x150.png new file mode 100644 index 0000000..3f5b912 Binary files /dev/null and b/platform/app/public/assets/mstile-310x150.png differ diff --git a/platform/app/public/assets/mstile-310x310.png b/platform/app/public/assets/mstile-310x310.png new file mode 100644 index 0000000..2e53c0b Binary files /dev/null and b/platform/app/public/assets/mstile-310x310.png differ diff --git a/platform/app/public/assets/mstile-70x70.png b/platform/app/public/assets/mstile-70x70.png new file mode 100644 index 0000000..973d503 Binary files /dev/null and b/platform/app/public/assets/mstile-70x70.png differ diff --git a/platform/app/public/assets/yandex-browser-50x50.png b/platform/app/public/assets/yandex-browser-50x50.png new file mode 100644 index 0000000..b934bc2 Binary files /dev/null and b/platform/app/public/assets/yandex-browser-50x50.png differ diff --git a/platform/app/public/assets/yandex-browser-manifest.json b/platform/app/public/assets/yandex-browser-manifest.json new file mode 100644 index 0000000..846829c --- /dev/null +++ b/platform/app/public/assets/yandex-browser-manifest.json @@ -0,0 +1,9 @@ +{ + "version": "2.2.1", + "api_version": 1, + "layout": { + "logo": "/assets/yandex-browser-50x50.png", + "color": "#fff", + "show_title": true + } +} diff --git a/platform/app/public/config/aws.js b/platform/app/public/config/aws.js new file mode 100644 index 0000000..be11fa8 --- /dev/null +++ b/platform/app/public/config/aws.js @@ -0,0 +1,60 @@ +/** @type {AppTypes.Config} */ + +window.config = { + routerBasename: '/', + extensions: [], + modes: [], + showStudyList: true, + // below flag is for performance reasons, but it might not work for all servers + + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + strictZSpacingForVolumeViewport: true, + // filterQueryParam: false, + defaultDataSourceName: 'dicomweb', + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'dcmjs DICOMWeb Server', + name: 'DCM4CHEE', + // Something here to check build + wadoUriRoot: 'https://myserver.com/dicomweb', + qidoRoot: 'https://myserver.com/dicomweb', + wadoRoot: 'https://myserver.com/dicomweb', + qidoSupportsIncludeField: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: false, + staticWado: true, + omitQuotationForMultipartRequest: true, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + friendlyName: 'dicom json', + name: 'json', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: { + friendlyName: 'dicom local', + }, + }, + ], + httpErrorHandler: error => { + // This is 429 when rejected from the public idc sandbox too often. + console.warn(error.status); + + // Could use services manager here to bring up a dialog/modal if needed. + console.warn('test, navigate to https://ohif.org/'); + }, +}; diff --git a/platform/app/public/config/customization.js b/platform/app/public/config/customization.js new file mode 100644 index 0000000..aee6a23 --- /dev/null +++ b/platform/app/public/config/customization.js @@ -0,0 +1,226 @@ +/** @type {AppTypes.Config} */ +window.config = { + routerBasename: '/', + extensions: [], + modes: ['@ohif/mode-test'], + showStudyList: true, + // below flag is for performance reasons, but it might not work for all servers + maxNumberOfWebWorkers: 3, + showWarningMessageForCrossOrigin: false, + showCPUFallbackMessage: false, + strictZSpacingForVolumeViewport: true, + // filterQueryParam: false, + + // Add some customizations to the default e2e datasource + customizationService: [ + '@ohif/extension-default.customizationModule.datasources', + '@ohif/extension-default.customizationModule.helloPage', + ], + + defaultDataSourceName: 'e2e', + investigationalUseDialog: { + option: 'never', + }, + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'e2e', + configuration: { + friendlyName: 'StaticWado test data', + // The most important field to set for static WADO + staticWado: true, + name: 'StaticWADO', + wadoUriRoot: '/viewer-testdata', + qidoRoot: '/viewer-testdata', + wadoRoot: '/viewer-testdata', + qidoSupportsIncludeField: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + singlepart: 'video,thumbnail,pdf', + omitQuotationForMultipartRequest: true, + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'local5000', + configuration: { + friendlyName: 'Static WADO Local Data', + name: 'DCM4CHEE', + qidoRoot: 'http://localhost:5000/dicomweb', + wadoRoot: 'http://localhost:5000/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: true, + supportsStow: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'video', + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'docker', + configuration: { + friendlyName: 'Static WADO Docker Data', + name: 'DCM4CHEE', + qidoRoot: 'http://localhost:25080/dicomweb', + wadoRoot: 'http://localhost:25080/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: true, + supportsStow: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video,pdf', + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'ohif', + configuration: { + friendlyName: 'AWS S3 Static wado server', + name: 'aws', + wadoUriRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + wadoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'video,pdf', + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + }, + }, + + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'ohif2', + configuration: { + friendlyName: 'AWS S3 Static wado secondary server', + name: 'aws', + wadoUriRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb', + wadoRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video', + // whether the data source should use retrieveBulkData to grab metadata, + // and in case of relative path, what would it be relative to, options + // are in the series level or study level (some servers like series some study) + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + omitQuotationForMultipartRequest: true, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'ohif3', + configuration: { + friendlyName: 'AWS S3 Static wado secondary server', + name: 'aws', + wadoUriRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + qidoRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + wadoRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video', + // whether the data source should use retrieveBulkData to grab metadata, + // and in case of relative path, what would it be relative to, options + // are in the series level or study level (some servers like series some study) + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + omitQuotationForMultipartRequest: true, + }, + }, + + { + friendlyName: 'StaticWado default data', + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + name: 'DCM4CHEE', + wadoUriRoot: '/dicomweb', + qidoRoot: '/dicomweb', + wadoRoot: '/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + friendlyName: 'dicom json', + name: 'json', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: { + friendlyName: 'dicom local', + }, + }, + ], + httpErrorHandler: error => { + // This is 429 when rejected from the public idc sandbox too often. + console.warn(error.status); + + // Could use services manager here to bring up a dialog/modal if needed. + console.warn('test, navigate to https://ohif.org/'); + }, + hotkeys: [], +}; diff --git a/platform/app/public/config/default.js b/platform/app/public/config/default.js new file mode 100644 index 0000000..147c324 --- /dev/null +++ b/platform/app/public/config/default.js @@ -0,0 +1,327 @@ +/** @type {AppTypes.Config} */ + +window.config = { + name: 'config/default.js', + routerBasename: '/', + // whiteLabeling: {}, + extensions: [ + { + name: "pdf", + sopClassUIDs: ["1.2.840.10008.5.1.4.1.1.104.1"], // Encapsulated PDF + } + ], + modes: [], + customizationService: {}, + showStudyList: true, + // some windows systems have issues with more than 3 web workers + maxNumberOfWebWorkers: 3, + // below flag is for performance reasons, but it might not work for all servers + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + experimentalStudyBrowserSort: false, + strictZSpacingForVolumeViewport: true, + groupEnabledModesFirst: true, + maxNumRequests: { + interaction: 100, + thumbnail: 75, + // Prefetch number is dependent on the http protocol. For http 2 or + // above, the number of requests can be go a lot higher. + prefetch: 25, + }, + // filterQueryParam: false, + // Defines multi-monitor layouts + multimonitor: [ + { + id: 'split', + test: ({ multimonitor }) => multimonitor === 'split', + screens: [ + { + id: 'ohif0', + screen: null, + location: { + screen: 0, + width: 0.5, + height: 1, + left: 0, + top: 0, + }, + options: 'location=no,menubar=no,scrollbars=no,status=no,titlebar=no', + }, + { + id: 'ohif1', + screen: null, + location: { + width: 0.5, + height: 1, + left: 0.5, + top: 0, + }, + options: 'location=no,menubar=no,scrollbars=no,status=no,titlebar=no', + }, + ], + }, + + { + id: '2', + test: ({ multimonitor }) => multimonitor === '2', + screens: [ + { + id: 'ohif0', + screen: 0, + location: { + width: 1, + height: 1, + left: 0, + top: 0, + }, + options: 'fullscreen=yes,location=no,menubar=no,scrollbars=no,status=no,titlebar=no', + }, + { + id: 'ohif1', + screen: 1, + location: { + width: 1, + height: 1, + left: 0, + top: 0, + }, + options: 'fullscreen=yes,location=no,menubar=no,scrollbars=no,status=no,titlebar=no', + }, + ], + }, + ], + // defaultDataSourceName: 'dicomweb', // Default + defaultDataSourceName: 'local-proxy', //* Mengganti dengan dicomweb-proxy + + /* Dynamic config allows user to pass "configUrl" query string this allows to load config without recompiling application. The regex will ensure valid configuration source */ + // dangerouslyUseDynamicConfig: { + // enabled: true, + // // regex will ensure valid configuration source and default is /.*/ which matches any character. To use this, setup your own regex to choose a specific source of configuration only. + // // Example 1, to allow numbers and letters in an absolute or sub-path only. + // // regex: /(0-9A-Za-z.]+)(\/[0-9A-Za-z.]+)*/ + // // Example 2, to restricts to either hosptial.com or othersite.com. + // // regex: /(https:\/\/hospital.com(\/[0-9A-Za-z.]+)*)|(https:\/\/othersite.com(\/[0-9A-Za-z.]+)*)/ + // regex: /.*/, + // }, + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'local-proxy', + configuration: { + friendlyName: 'Static WADO Local Data', + name: 'DCM4CHEE', + qidoRoot: 'http://128.199.154.150:5000/rs', + wadoRoot: 'http://128.199.154.150:5000/rs', + qidoSupportsIncludeField: false, + supportsReject: true, + supportsStow: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'video', + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'AWS S3 Static wado server', + name: 'aws', + wadoUriRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + wadoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: false, + staticWado: true, + singlepart: 'bulkdata,video', + // whether the data source should use retrieveBulkData to grab metadata, + // and in case of relative path, what would it be relative to, options + // are in the series level or study level (some servers like series some study) + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + transform: url => url.replace('/pixeldata.mp4', '/rendered'), + }, + omitQuotationForMultipartRequest: true, + }, + }, + + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'ohif2', + configuration: { + friendlyName: 'AWS S3 Static wado secondary server', + name: 'aws', + wadoUriRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb', + wadoRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video', + // whether the data source should use retrieveBulkData to grab metadata, + // and in case of relative path, what would it be relative to, options + // are in the series level or study level (some servers like series some study) + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + omitQuotationForMultipartRequest: true, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'ohif3', + configuration: { + friendlyName: 'AWS S3 Static wado secondary server', + name: 'aws', + wadoUriRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + qidoRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + wadoRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video', + // whether the data source should use retrieveBulkData to grab metadata, + // and in case of relative path, what would it be relative to, options + // are in the series level or study level (some servers like series some study) + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + omitQuotationForMultipartRequest: true, + }, + }, + + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'local5000', + configuration: { + friendlyName: 'Static WADO Local Data', + name: 'DCM4CHEE', + qidoRoot: 'http://localhost:5000/dicomweb', + wadoRoot: 'http://localhost:5000/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: true, + supportsStow: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'video', + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'orthanc', + configuration: { + friendlyName: 'local Orthanc DICOMWeb Server', + name: 'DCM4CHEE', + wadoUriRoot: 'http://localhost/pacs/dicom-web', + qidoRoot: 'http://localhost/pacs/dicom-web', + wadoRoot: 'http://localhost/pacs/dicom-web', + qidoSupportsIncludeField: true, + supportsReject: true, + dicomUploadEnabled: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: true, + omitQuotationForMultipartRequest: true, + bulkDataURI: { + enabled: true, + // This is an example config that can be used to fix the retrieve URL + // where it has the wrong prefix (eg a canned prefix). It is better to + // just use the correct prefix out of the box, but that is sometimes hard + // when URLs go through several systems. + // Example URLS are: + // "BulkDataURI" : "http://localhost/dicom-web/studies/1.2.276.0.7230010.3.1.2.2344313775.14992.1458058363.6979/series/1.2.276.0.7230010.3.1.3.1901948703.36080.1484835349.617/instances/1.2.276.0.7230010.3.1.4.1901948703.36080.1484835349.618/bulk/00420011", + // when running on http://localhost:3003 with no server running on localhost. This can be corrected to: + // /orthanc/dicom-web/studies/1.2.276.0.7230010.3.1.2.2344313775.14992.1458058363.6979/series/1.2.276.0.7230010.3.1.3.1901948703.36080.1484835349.617/instances/1.2.276.0.7230010.3.1.4.1901948703.36080.1484835349.618/bulk/00420011 + // which is a valid relative URL, and will result in using the http://localhost:3003/orthanc/.... path + // startsWith: 'http://localhost/', + // prefixWith: '/orthanc/', + }, + }, + }, + + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomwebproxy', + sourceName: 'dicomwebproxy', + configuration: { + friendlyName: 'dicomweb delegating proxy', + name: 'dicomwebproxy', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + friendlyName: 'dicom json', + name: 'json', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: { + friendlyName: 'dicom local', + }, + }, + ], + httpErrorHandler: error => { + // This is 429 when rejected from the public idc sandbox too often. + console.warn(error.status); + + // Could use services manager here to bring up a dialog/modal if needed. + console.warn('test, navigate to https://ohif.org/'); + }, + // whiteLabeling: { + // /* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */ + // createLogoComponentFn: function (React) { + // return React.createElement( + // 'a', + // { + // target: '_self', + // rel: 'noopener noreferrer', + // className: 'text-purple-600 line-through', + // href: '/', + // }, + // React.createElement('img', + // { + // src: './assets/customLogo.svg', + // className: 'w-8 h-8', + // } + // )) + // }, + // }, +}; diff --git a/platform/app/public/config/default_16bit.js b/platform/app/public/config/default_16bit.js new file mode 100644 index 0000000..6c08bd2 --- /dev/null +++ b/platform/app/public/config/default_16bit.js @@ -0,0 +1,96 @@ +/** @type {AppTypes.Config} */ +window.config = { + routerBasename: '/', + // whiteLabeling: {}, + extensions: [], + modes: [], + customizationService: { + // Shows a custom route -access via http://localhost:3000/custom + // helloPage: '@ohif/extension-default.customizationModule.helloPage', + }, + showStudyList: true, + // some windows systems have issues with more than 3 web workers + maxNumberOfWebWorkers: 3, + // below flag is for performance reasons, but it might not work for all servers + omitQuotationForMultipartRequest: true, + showWarningMessageForCrossOrigin: false, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + useNorm16Texture: true, + maxNumRequests: { + interaction: 100, + thumbnail: 75, + // Prefetch number is dependent on the http protocol. For http 2 or + // above, the number of requests can be go a lot higher. + prefetch: 25, + }, + // filterQueryParam: false, + dataSources: [ + { + friendlyName: 'dcmjs DICOMWeb Server', + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + name: 'aws', + // old server + // wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado', + // qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + // wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + // new server + wadoUriRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb', + qidoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb', + wadoRoot: 'https://domvja9iplmyu.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video,pdf', + }, + }, + { + friendlyName: 'dicom json', + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + name: 'json', + }, + }, + { + friendlyName: 'dicom local', + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: {}, + }, + ], + httpErrorHandler: error => { + // This is 429 when rejected from the public idc sandbox too often. + console.warn(error.status); + + // Could use services manager here to bring up a dialog/modal if needed. + console.warn('test, navigate to https://ohif.org/'); + }, + // whiteLabeling: { + // /* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */ + // createLogoComponentFn: function (React) { + // return React.createElement( + // 'a', + // { + // target: '_self', + // rel: 'noopener noreferrer', + // className: 'text-purple-600 line-through', + // href: '/', + // }, + // React.createElement('img', + // { + // src: './customLogo.svg', + // className: 'w-8 h-8', + // } + // )) + // }, + // }, + defaultDataSourceName: 'dicomweb', +}; diff --git a/platform/app/public/config/demo.js b/platform/app/public/config/demo.js new file mode 100644 index 0000000..791a5c3 --- /dev/null +++ b/platform/app/public/config/demo.js @@ -0,0 +1,37 @@ +/** @type {AppTypes.Config} */ +window.config = { + routerBasename: '/', + modes: [], + extensions: [], + showStudyList: true, + // below flag is for performance reasons, but it might not work for all servers + showWarningMessageForCrossOrigin: true, + strictZSpacingForVolumeViewport: true, + showCPUFallbackMessage: true, + defaultDataSourceName: 'dicomweb', + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'DCM4CHEE Server', + name: 'DCM4CHEE', + wadoUriRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + wadoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoSupportsIncludeField: true, + imageRendering: 'wadors', + enableStudyLazyLoad: true, + bulkDataURI: { + enabled: false, + }, + omitQuotationForMultipartRequest: true, + }, + }, + ], + i18n: { + LOCIZE_PROJECTID: 'a8da3f9a-e467-4dd6-af33-474d582a0294', + LOCIZE_API_KEY: null, // Developers can use this to do in-context editing. DO NOT COMMIT THIS KEY! + USE_LOCIZE: true, + }, +}; diff --git a/platform/app/public/config/deprecated/docker_openresty-orthanc-keycloak.js b/platform/app/public/config/deprecated/docker_openresty-orthanc-keycloak.js new file mode 100644 index 0000000..d93a853 --- /dev/null +++ b/platform/app/public/config/deprecated/docker_openresty-orthanc-keycloak.js @@ -0,0 +1,39 @@ +/** @type {AppTypes.Config} */ +window.config = { + routerBasename: '/', + showStudyList: true, + extensions: [], + modes: [], + // below flag is for performance reasons, but it might not work for all servers + + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + strictZSpacingForVolumeViewport: true, + defaultDataSourceName: 'dicomweb', + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'Orthanc Server', + name: 'Orthanc', + wadoUriRoot: 'http://127.0.0.1/pacs/dicom-web', + qidoRoot: 'http://127.0.0.1/pacs/dicom-web', + wadoRoot: 'http://127.0.0.1/pacs/dicom-web', + qidoSupportsIncludeField: true, + supportsReject: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: true, + dicomUploadEnabled: true, + bulkDataURI: { + enabled: true, + }, + }, + }, + ], + // This is an array, but we'll only use the first entry for now +}; diff --git a/platform/app/public/config/deprecated/docker_openresty-orthanc.js b/platform/app/public/config/deprecated/docker_openresty-orthanc.js new file mode 100644 index 0000000..1acdff5 --- /dev/null +++ b/platform/app/public/config/deprecated/docker_openresty-orthanc.js @@ -0,0 +1,53 @@ +/** @type {AppTypes.Config} */ +window.config = { + routerBasename: '/', + showStudyList: true, + extensions: [], + modes: [], + // below flag is for performance reasons, but it might not work for all servers + + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + strictZSpacingForVolumeViewport: true, + defaultDataSourceName: 'dicomweb', + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'Orthanc Server', + name: 'Orthanc', + wadoUriRoot: 'http://127.0.0.1/pacs/dicom-web', + qidoRoot: 'http://127.0.0.1/pacs/dicom-web', + wadoRoot: 'http://127.0.0.1/pacs/dicom-web', + qidoSupportsIncludeField: true, + supportsReject: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: true, + dicomUploadEnabled: true, + bulkDataURI: { + enabled: true, + }, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + friendlyName: 'dicom json', + name: 'json', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: { + friendlyName: 'dicom local', + }, + }, + ], +}; diff --git a/platform/app/public/config/dicomweb-server.js b/platform/app/public/config/dicomweb-server.js new file mode 100644 index 0000000..874e750 --- /dev/null +++ b/platform/app/public/config/dicomweb-server.js @@ -0,0 +1,50 @@ +/** @type {AppTypes.Config} */ +window.config = { + routerBasename: '/', + extensions: [], + modes: [], + showStudyList: true, + // below flag is for performance reasons, but it might not work for all servers + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + strictZSpacingForVolumeViewport: true, + // filterQueryParam: false, + defaultDataSourceName: 'dicomweb', + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'dcmjs DICOMWeb Server', + name: 'DCM4CHEE', + wadoUriRoot: 'http://localhost:5985', + qidoRoot: 'http://localhost:5985', + wadoRoot: 'http://localhost:5985', + qidoSupportsIncludeField: true, + supportsReject: true, + imageRendering: 'wadouri', + thumbnailRendering: 'wadouri', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: false, + omitQuotationForMultipartRequest: true, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + friendlyName: 'dicom json', + name: 'json', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: { + friendlyName: 'dicom local', + }, + }, + ], +}; diff --git a/platform/app/public/config/dicomweb_relative.js b/platform/app/public/config/dicomweb_relative.js new file mode 100644 index 0000000..a814ac9 --- /dev/null +++ b/platform/app/public/config/dicomweb_relative.js @@ -0,0 +1,59 @@ +/** @type {AppTypes.Config} */ +window.config = { + routerBasename: '/', + extensions: [], + modes: [], + showStudyList: true, + maxNumberOfWebWorkers: 3, + // below flag is for performance reasons, but it might not work for all servers + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + strictZSpacingForVolumeViewport: true, + // filterQueryParam: false, + defaultDataSourceName: 'dicomweb', + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'Static WADO Local Data', + name: 'DCM4CHEE', + wadoUriRoot: '/dicomweb', + qidoRoot: '/dicomweb', + wadoRoot: '/dicomweb', + qidoSupportsIncludeField: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video,pdf', + omitQuotationForMultipartRequest: true, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + friendlyName: 'dicom json', + name: 'json', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: { + friendlyName: 'dicom local', + }, + }, + ], + httpErrorHandler: error => { + // This is 429 when rejected from the public idc sandbox too often. + console.warn(error.status); + + // Could use services manager here to bring up a dialog/modal if needed. + console.warn('test, navigate to https://ohif.org/'); + }, +}; diff --git a/platform/app/public/config/docker-nginx-dcm4chee-keycloak.js b/platform/app/public/config/docker-nginx-dcm4chee-keycloak.js new file mode 100644 index 0000000..b5ea6c4 --- /dev/null +++ b/platform/app/public/config/docker-nginx-dcm4chee-keycloak.js @@ -0,0 +1,31 @@ +/** @type {AppTypes.Config} */ +window.config = { + routerBasename: '/ohif-viewer/', + showStudyList: true, + extensions: [], + modes: [], + // below flag is for performance reasons, but it might not work for all servers + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + strictZSpacingForVolumeViewport: true, + defaultDataSourceName: 'dicomweb', + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'Dcm4chee Server', + name: 'Dcm4chee', + wadoUriRoot: 'http://127.0.0.1/pacs', + qidoRoot: 'http://127.0.0.1/pacs', + wadoRoot: 'http://127.0.0.1/pacs', + qidoSupportsIncludeField: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + dicomUploadEnabled: true, + omitQuotationForMultipartRequest: true, + }, + }, + ], +}; diff --git a/platform/app/public/config/docker-nginx-dcm4chee.js b/platform/app/public/config/docker-nginx-dcm4chee.js new file mode 100644 index 0000000..b44f357 --- /dev/null +++ b/platform/app/public/config/docker-nginx-dcm4chee.js @@ -0,0 +1,31 @@ +/** @type {AppTypes.Config} */ +window.config = { + routerBasename: '/', + showStudyList: true, + extensions: [], + modes: [], + // below flag is for performance reasons, but it might not work for all servers + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + strictZSpacingForVolumeViewport: true, + defaultDataSourceName: 'dicomweb', + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'Dcm4chee Server', + name: 'Dcm4chee', + wadoUriRoot: '/dcm4chee-arc/aets/DCM4CHEE/wado', + qidoRoot: '/dcm4chee-arc/aets/DCM4CHEE/rs', + wadoRoot: '/dcm4chee-arc/aets/DCM4CHEE/rs', + qidoSupportsIncludeField: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + dicomUploadEnabled: true, + omitQuotationForMultipartRequest: true, + }, + }, + ], +}; diff --git a/platform/app/public/config/docker-nginx-orthanc-keycloak.js b/platform/app/public/config/docker-nginx-orthanc-keycloak.js new file mode 100644 index 0000000..3a8b60a --- /dev/null +++ b/platform/app/public/config/docker-nginx-orthanc-keycloak.js @@ -0,0 +1,52 @@ +/** @type {AppTypes.Config} */ +window.config = { + routerBasename: '/ohif-viewer', + extensions: [], + modes: [], + customizationService: {}, + showStudyList: true, + maxNumberOfWebWorkers: 3, + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + strictZSpacingForVolumeViewport: true, + groupEnabledModesFirst: true, + maxNumRequests: { + interaction: 100, + thumbnail: 75, + prefetch: 25, + }, + defaultDataSourceName: 'dicomweb', + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'Local Orthanc', + name: 'Orthanc', + wadoUriRoot: 'http://127.0.0.1/pacs', + qidoRoot: 'http://127.0.0.1/pacs', + wadoRoot: 'http://127.0.0.1/pacs', + qidoSupportsIncludeField: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video', + // whether the data source should use retrieveBulkData to grab metadata, + // and in case of relative path, what would it be relative to, options + // are in the series level or study level (some servers like series some study) + bulkDataURI: { + enabled: true, + }, + omitQuotationForMultipartRequest: true, + }, + }, + ], + httpErrorHandler: error => { + console.warn(error.status); + console.warn('test, navigate to https://ohif.org/'); + }, +}; diff --git a/platform/app/public/config/docker-nginx-orthanc.js b/platform/app/public/config/docker-nginx-orthanc.js new file mode 100644 index 0000000..f4be9f9 --- /dev/null +++ b/platform/app/public/config/docker-nginx-orthanc.js @@ -0,0 +1,56 @@ +/** @type {AppTypes.Config} */ +window.config = { + routerBasename: '/', + showStudyList: true, + extensions: [], + modes: [], + // below flag is for performance reasons, but it might not work for all servers + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + experimentalStudyBrowserSort: false, + strictZSpacingForVolumeViewport: true, + studyPrefetcher: { + enabled: true, + displaySetsCount: 2, + maxNumPrefetchRequests: 10, + order: 'closest', + }, + defaultDataSourceName: 'dicomweb', + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'Orthanc Server', + name: 'Orthanc', + wadoUriRoot: '/wado', + qidoRoot: '/pacs/dicom-web', + wadoRoot: '/pacs/dicom-web', + qidoSupportsIncludeField: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + dicomUploadEnabled: true, + omitQuotationForMultipartRequest: true, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + friendlyName: 'dicom json', + name: 'json', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: { + friendlyName: 'dicom local', + }, + }, + ], + httpErrorHandler: error => { + console.warn(`HTTP Error Handler (status: ${error.status})`, error); + }, +}; diff --git a/platform/app/public/config/e2e.js b/platform/app/public/config/e2e.js new file mode 100644 index 0000000..0173175 --- /dev/null +++ b/platform/app/public/config/e2e.js @@ -0,0 +1,341 @@ +// This is example code that dynamically sets the initial search conditions to +// search for today +if (window.location.search === '?today') { + const now = new Date(); + const month = now.getMonth() + 1; + const day = now.getDate(); + window.sessionStorage.setItem( + 'queryFilterValues', + JSON.stringify({ + studyDate: { + startDate: `${now.getFullYear()}${month < 10 ? '0' + month : month}${day < 10 ? '0' + day : day}`, + endDate: null, + }, + }) + ); +} + +/** @type {AppTypes.Config} */ +window.config = { + routerBasename: '/', + extensions: [], + modes: ['@ohif/mode-test'], + showStudyList: true, + // below flag is for performance reasons, but it might not work for all servers + maxNumberOfWebWorkers: 3, + showWarningMessageForCrossOrigin: false, + showCPUFallbackMessage: false, + strictZSpacingForVolumeViewport: true, + // filterQueryParam: false, + + // Add some customizations to the default e2e datasource + customizationService: [ + '@ohif/extension-default.customizationModule.datasources', + '@ohif/extension-default.customizationModule.helloPage', + ], + + defaultDataSourceName: 'e2e', + investigationalUseDialog: { + option: 'never', + }, + // Defines multi-monitor layouts + multimonitor: [ + { + id: 'split', + test: ({ multimonitor }) => multimonitor === 'split', + screens: [ + { + id: 'ohif0', + screen: null, + location: { + screen: 0, + width: 0.5, + height: 1, + left: 0, + top: 0, + }, + options: 'location=no,menubar=no,scrollbars=no,status=no,titlebar=no', + }, + { + id: 'ohif1', + screen: null, + location: { + width: 0.5, + height: 1, + left: 0.5, + top: 0, + }, + options: 'location=no,menubar=no,scrollbars=no,status=no,titlebar=no', + }, + ], + }, + + { + id: '2', + test: ({ multimonitor }) => multimonitor === '2', + screens: [ + { + id: 'ohif0', + screen: 0, + location: { + width: 1, + height: 1, + left: 0, + top: 0, + }, + options: 'fullscreen=yes,location=no,menubar=no,scrollbars=no,status=no,titlebar=no', + }, + { + id: 'ohif1', + screen: 1, + location: { + width: 1, + height: 1, + left: 0, + top: 0, + }, + options: 'fullscreen=yes,location=no,menubar=no,scrollbars=no,status=no,titlebar=no', + }, + ], + }, + ], + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'e2e', + configuration: { + friendlyName: 'StaticWado test data', + // The most important field to set for static WADO + staticWado: true, + name: 'StaticWADO', + wadoUriRoot: '/viewer-testdata', + qidoRoot: '/viewer-testdata', + wadoRoot: '/viewer-testdata', + qidoSupportsIncludeField: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + singlepart: 'video,thumbnail,pdf', + omitQuotationForMultipartRequest: true, + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + transform: url => url.replace('/pixeldata.mp4', '/index.mp4'), + }, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'local5000', + configuration: { + friendlyName: 'Static WADO Local Data', + name: 'DCM4CHEE', + qidoRoot: 'http://localhost:5000/dicomweb', + wadoRoot: 'http://localhost:5000/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: true, + supportsStow: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'video', + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'docker', + configuration: { + friendlyName: 'Static WADO Docker Data', + name: 'DCM4CHEE', + qidoRoot: 'http://localhost:25080/dicomweb', + wadoRoot: 'http://localhost:25080/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: true, + supportsStow: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video,pdf', + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'ohif', + configuration: { + friendlyName: 'AWS S3 Static wado server', + name: 'aws', + wadoUriRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + wadoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'video,pdf', + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + transform: url => url.replace('/pixeldata.mp4', '/rendered'), + }, + }, + }, + + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'ohif2', + configuration: { + friendlyName: 'AWS S3 Static wado secondary server', + name: 'aws', + wadoUriRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb', + wadoRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video', + // whether the data source should use retrieveBulkData to grab metadata, + // and in case of relative path, what would it be relative to, options + // are in the series level or study level (some servers like series some study) + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + omitQuotationForMultipartRequest: true, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'ohif3', + configuration: { + friendlyName: 'AWS S3 Static wado secondary server', + name: 'aws', + wadoUriRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + qidoRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + wadoRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video', + // whether the data source should use retrieveBulkData to grab metadata, + // and in case of relative path, what would it be relative to, options + // are in the series level or study level (some servers like series some study) + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + omitQuotationForMultipartRequest: true, + }, + }, + + { + friendlyName: 'StaticWado default data', + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + name: 'DCM4CHEE', + wadoUriRoot: '/dicomweb', + qidoRoot: '/dicomweb', + wadoRoot: '/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'orthanc', + configuration: { + friendlyName: 'local Orthanc DICOMWeb Server', + name: 'DCM4CHEE', + wadoUriRoot: 'http://localhost/pacs/dicom-web', + qidoRoot: 'http://localhost/pacs/dicom-web', + wadoRoot: 'http://localhost/pacs/dicom-web', + qidoSupportsIncludeField: true, + supportsReject: true, + dicomUploadEnabled: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: true, + omitQuotationForMultipartRequest: true, + bulkDataURI: { + enabled: true, + // This is an example config that can be used to fix the retrieve URL + // where it has the wrong prefix (eg a canned prefix). It is better to + // just use the correct prefix out of the box, but that is sometimes hard + // when URLs go through several systems. + // Example URLS are: + // "BulkDataURI" : "http://localhost/dicom-web/studies/1.2.276.0.7230010.3.1.2.2344313775.14992.1458058363.6979/series/1.2.276.0.7230010.3.1.3.1901948703.36080.1484835349.617/instances/1.2.276.0.7230010.3.1.4.1901948703.36080.1484835349.618/bulk/00420011", + // when running on http://localhost:3003 with no server running on localhost. This can be corrected to: + // /orthanc/dicom-web/studies/1.2.276.0.7230010.3.1.2.2344313775.14992.1458058363.6979/series/1.2.276.0.7230010.3.1.3.1901948703.36080.1484835349.617/instances/1.2.276.0.7230010.3.1.4.1901948703.36080.1484835349.618/bulk/00420011 + // which is a valid relative URL, and will result in using the http://localhost:3003/orthanc/.... path + // startsWith: 'http://localhost/', + // prefixWith: '/orthanc/', + }, + }, + }, + + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + friendlyName: 'dicom json', + name: 'json', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: { + friendlyName: 'dicom local', + }, + }, + ], + httpErrorHandler: error => { + // This is 429 when rejected from the public idc sandbox too often. + console.warn(error.status); + + // Could use services manager here to bring up a dialog/modal if needed. + console.warn('test, navigate to https://ohif.org/'); + }, + hotkeys: [], +}; diff --git a/platform/app/public/config/google.js b/platform/app/public/config/google.js new file mode 100644 index 0000000..08799ff --- /dev/null +++ b/platform/app/public/config/google.js @@ -0,0 +1,73 @@ +/** @type {AppTypes.Config} */ +window.config = { + routerBasename: '/', + enableGoogleCloudAdapter: false, + // below flag is for performance reasons, but it might not work for all servers + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + strictZSpacingForVolumeViewport: true, + // This is an array, but we'll only use the first entry for now + oidc: [ + { + // ~ REQUIRED + // Authorization Server URL + authority: 'https://accounts.google.com', + client_id: '723928408739-k9k9r3i44j32rhu69vlnibipmmk9i57p.apps.googleusercontent.com', + redirect_uri: '/callback', + response_type: 'id_token token', + scope: + 'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid + // ~ OPTIONAL + post_logout_redirect_uri: '/logout-redirect.html', + revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=', + automaticSilentRenew: true, + revokeAccessTokenOnSignout: true, + }, + ], + extensions: [], + modes: [], + showStudyList: true, + // filterQueryParam: false, + defaultDataSourceName: 'dicomweb', + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'dcmjs DICOMWeb Server', + name: 'GCP', + wadoUriRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + qidoRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + wadoRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + qidoSupportsIncludeField: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: false, + dicomUploadEnabled: true, + omitQuotationForMultipartRequest: true, + configurationAPI: 'ohif.dataSourceConfigurationAPI.google', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + friendlyName: 'dicom json', + name: 'json', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: { + friendlyName: 'dicom local', + }, + }, + ], +}; diff --git a/platform/app/public/config/idc.js b/platform/app/public/config/idc.js new file mode 100644 index 0000000..3a4e550 --- /dev/null +++ b/platform/app/public/config/idc.js @@ -0,0 +1,33 @@ +/** @type {AppTypes.Config} */ +window.config = { + routerBasename: '/', + enableGoogleCloudAdapter: true, + // below flag is for performance reasons, but it might not work for all servers + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + strictZSpacingForVolumeViewport: true, + servers: { + // This is an array, but we'll only use the first entry for now + dicomWeb: [], + }, + // This is an array, but we'll only use the first entry for now + oidc: [ + { + // ~ REQUIRED + // Authorization Server URL + authority: 'https://accounts.google.com', + client_id: '723928408739-k9k9r3i44j32rhu69vlnibipmmk9i57p.apps.googleusercontent.com', + redirect_uri: '/callback', // `OHIFStandaloneViewer.js` + response_type: 'id_token token', + scope: + 'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid + // ~ OPTIONAL + post_logout_redirect_uri: '/logout-redirect.html', + revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=', + automaticSilentRenew: true, + revokeAccessTokenOnSignout: true, + }, + ], + studyListFunctionsEnabled: true, +}; diff --git a/platform/app/public/config/kheops.js b/platform/app/public/config/kheops.js new file mode 100644 index 0000000..e1ee3aa --- /dev/null +++ b/platform/app/public/config/kheops.js @@ -0,0 +1,202 @@ +/** @type {AppTypes.Config} */ + +window.config = { + name: 'config/kheops.js', + routerBasename: '/', + extensions: [], + modes: [], + customizationService: {}, + showStudyList: true, + // some windows systems have issues with more than 3 web workers + maxNumberOfWebWorkers: 3, + // below flag is for performance reasons, but it might not work for all servers + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + experimentalStudyBrowserSort: false, + strictZSpacingForVolumeViewport: true, + groupEnabledModesFirst: true, + maxNumRequests: { + interaction: 100, + thumbnail: 75, + // Prefetch number is dependent on the http protocol. For http 2 or + // above, the number of requests can be go a lot higher. + prefetch: 25, + }, + // filterQueryParam: false, + // Uses the ohif datasource as the default - this requires that KHEOPS be + // configured with an OHIF path to .../viewer/dicomwebproxy + defaultDataSourceName: 'ohif3', + /* Dynamic config allows user to pass "configUrl" query string this allows to load config without recompiling application. The regex will ensure valid configuration source */ + // dangerouslyUseDynamicConfig: { + // enabled: true, + // // regex will ensure valid configuration source and default is /.*/ which matches any character. To use this, setup your own regex to choose a specific source of configuration only. + // // Example 1, to allow numbers and letters in an absolute or sub-path only. + // // regex: /(0-9A-Za-z.]+)(\/[0-9A-Za-z.]+)*/ + // // Example 2, to restricts to either hosptial.com or othersite.com. + // // regex: /(https:\/\/hospital.com(\/[0-9A-Za-z.]+)*)|(https:\/\/othersite.com(\/[0-9A-Za-z.]+)*)/ + // regex: /.*/, + // }, + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'AWS S3 Static wado server', + name: 'aws', + wadoUriRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + wadoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video', + // whether the data source should use retrieveBulkData to grab metadata, + // and in case of relative path, what would it be relative to, options + // are in the series level or study level (some servers like series some study) + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + transform: url => url.replace('/pixeldata.mp4', '/rendered'), + }, + omitQuotationForMultipartRequest: true, + }, + }, + + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'ohif2', + configuration: { + friendlyName: 'AWS S3 Static wado secondary server', + name: 'aws', + wadoUriRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb', + wadoRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video', + // whether the data source should use retrieveBulkData to grab metadata, + // and in case of relative path, what would it be relative to, options + // are in the series level or study level (some servers like series some study) + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + omitQuotationForMultipartRequest: true, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'ohif3', + configuration: { + friendlyName: 'AWS S3 Static wado secondary server', + name: 'aws', + wadoUriRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + qidoRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + wadoRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video', + // whether the data source should use retrieveBulkData to grab metadata, + // and in case of relative path, what would it be relative to, options + // are in the series level or study level (some servers like series some study) + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + omitQuotationForMultipartRequest: true, + }, + }, + + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'local5000', + configuration: { + friendlyName: 'Static WADO Local Data', + name: 'DCM4CHEE', + qidoRoot: 'http://localhost:5000/dicomweb', + wadoRoot: 'http://localhost:5000/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: true, + supportsStow: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'video', + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + }, + }, + + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomwebproxy', + sourceName: 'dicomwebproxy', + configuration: { + friendlyName: 'dicomweb delegating proxy', + name: 'dicomwebproxy', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + friendlyName: 'dicom json', + name: 'json', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: { + friendlyName: 'dicom local', + }, + }, + ], + httpErrorHandler: error => { + // This is 429 when rejected from the public idc sandbox too often. + console.warn(error.status); + + // Could use services manager here to bring up a dialog/modal if needed. + console.warn('test, navigate to https://ohif.org/'); + }, + // whiteLabeling: { + // /* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */ + // createLogoComponentFn: function (React) { + // return React.createElement( + // 'a', + // { + // target: '_self', + // rel: 'noopener noreferrer', + // className: 'text-purple-600 line-through', + // href: '/', + // }, + // React.createElement('img', + // { + // src: './assets/customLogo.svg', + // className: 'w-8 h-8', + // } + // )) + // }, + // }, +}; diff --git a/platform/app/public/config/local_dcm4chee.js b/platform/app/public/config/local_dcm4chee.js new file mode 100644 index 0000000..1e223a4 --- /dev/null +++ b/platform/app/public/config/local_dcm4chee.js @@ -0,0 +1,58 @@ +/** @type {AppTypes.Config} */ +window.config = { + routerBasename: '/', + showStudyList: true, + extensions: [], + modes: [], + // below flag is for performance reasons, but it might not work for all servers + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + strictZSpacingForVolumeViewport: true, + defaultDataSourceName: 'dicomweb', + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'DCM4CHEE Server', + name: 'DCM4CHEE', + wadoUriRoot: 'http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/wado', + qidoRoot: 'http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/rs', + wadoRoot: 'http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/rs', + qidoSupportsIncludeField: true, + imageRendering: 'wadors', + enableStudyLazyLoad: true, + thumbnailRendering: 'wadors', + requestOptions: { + auth: 'admin:admin', + }, + dicomUploadEnabled: true, + singlepart: 'pdf,video', + // whether the data source should use retrieveBulkData to grab metadata, + // and in case of relative path, what would it be relative to, options + // are in the series level or study level (some servers like series some study) + bulkDataURI: { + enabled: true, + }, + omitQuotationForMultipartRequest: true, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + friendlyName: 'dicom json', + name: 'json', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: { + friendlyName: 'dicom local', + }, + }, + ], + studyListFunctionsEnabled: true, +}; diff --git a/platform/app/public/config/local_orthanc.js b/platform/app/public/config/local_orthanc.js new file mode 100644 index 0000000..68d3b9b --- /dev/null +++ b/platform/app/public/config/local_orthanc.js @@ -0,0 +1,72 @@ +/** @type {AppTypes.Config} */ +window.config = { + routerBasename: '/', + extensions: [], + modes: [], + showStudyList: true, + maxNumberOfWebWorkers: 3, + showLoadingIndicator: true, + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + strictZSpacingForVolumeViewport: true, + // filterQueryParam: false, + defaultDataSourceName: 'orthanc', + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'orthanc', + configuration: { + friendlyName: 'local Orthanc DICOMWeb Server', + name: 'DCM4CHEE', + wadoUriRoot: 'http://localhost/dicom-web', + qidoRoot: 'http://localhost/dicom-web', + wadoRoot: 'http://localhost/dicom-web', + qidoSupportsIncludeField: true, + supportsReject: true, + dicomUploadEnabled: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: true, + omitQuotationForMultipartRequest: true, + bulkDataURI: { + enabled: true, + // This is an example config that can be used to fix the retrieve URL + // where it has the wrong prefix (eg a canned prefix). It is better to + // just use the correct prefix out of the box, but that is sometimes hard + // when URLs go through several systems. + // Example URLS are: + // "BulkDataURI" : "http://localhost/dicom-web/studies/1.2.276.0.7230010.3.1.2.2344313775.14992.1458058363.6979/series/1.2.276.0.7230010.3.1.3.1901948703.36080.1484835349.617/instances/1.2.276.0.7230010.3.1.4.1901948703.36080.1484835349.618/bulk/00420011", + // when running on http://localhost:3003 with no server running on localhost. This can be corrected to: + // /orthanc/dicom-web/studies/1.2.276.0.7230010.3.1.2.2344313775.14992.1458058363.6979/series/1.2.276.0.7230010.3.1.3.1901948703.36080.1484835349.617/instances/1.2.276.0.7230010.3.1.4.1901948703.36080.1484835349.618/bulk/00420011 + // which is a valid relative URL, and will result in using the http://localhost:3003/orthanc/.... path + // startsWith: 'http://localhost/', + // prefixWith: '/orthanc/', + }, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + friendlyName: 'dicom json', + name: 'json', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: { + friendlyName: 'dicom local', + }, + }, + ], + httpErrorHandler: error => { + // This is 429 when rejected from the public idc sandbox too often. + console.warn(error.status); + + // Could use services manager here to bring up a dialog/modal if needed. + console.warn('test, navigate to https://ohif.org/'); + }, +}; diff --git a/platform/app/public/config/local_static.js b/platform/app/public/config/local_static.js new file mode 100644 index 0000000..696632d --- /dev/null +++ b/platform/app/public/config/local_static.js @@ -0,0 +1,134 @@ +/** @type {AppTypes.Config} */ +window.config = { + routerBasename: '/', + customizationService: ['@ohif/extension-default.customizationModule.helloPage'], + extensions: [], + modes: [], + showStudyList: true, + maxNumberOfWebWorkers: 4, + // below flag is for performance reasons, but it might not work for all servers + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + strictZSpacingForVolumeViewport: true, + // filterQueryParam: false, + defaultDataSourceName: 'local', + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'local', + configuration: { + friendlyName: 'Static WADO Local Data', + name: 'DCM4CHEE', + qidoRoot: 'http://localhost:3001/dicomweb', + wadoRoot: 'http://localhost:3001/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: true, + supportsStow: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'video', + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'docker', + configuration: { + friendlyName: 'Static WADO Docker Data', + name: 'DCM4CHEE', + qidoRoot: 'http://localhost:25080/dicomweb', + wadoRoot: 'http://localhost:25080/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: true, + supportsStow: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video,pdf', + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'ohif', + configuration: { + friendlyName: 'AWS S3 Static wado server', + name: 'aws', + wadoUriRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + wadoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video,pdf', + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + }, + }, + { + friendlyName: 'StaticWado default data', + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + name: 'DCM4CHEE', + wadoUriRoot: '/dicomweb', + qidoRoot: '/dicomweb', + wadoRoot: '/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + friendlyName: 'dicom json', + name: 'json', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: { + friendlyName: 'dicom local', + }, + }, + ], + httpErrorHandler: error => { + // This is 429 when rejected from the public idc sandbox too often. + console.warn(error.status); + + // Could use services manager here to bring up a dialog/modal if needed. + console.warn('test, navigate to https://ohif.org/'); + }, +}; diff --git a/platform/app/public/config/multiple.js b/platform/app/public/config/multiple.js new file mode 100644 index 0000000..762c14c --- /dev/null +++ b/platform/app/public/config/multiple.js @@ -0,0 +1,137 @@ +/** @type {AppTypes.Config} */ +window.config = { + // Activate the new HP mode.... + isNewHP: true, + + routerBasename: '/', + customizationService: [ + '@ohif/extension-default.customizationModule.datasources', + { + id: 'class:StudyBrowser', + true: 'black', + false: 'default', + }, + ], + extensions: [], + modes: ['@ohif/mode-test', '@ohif/mode-basic-dev-mode'], + showStudyList: true, + maxNumberOfWebWorkers: 4, + // below flag is for performance reasons, but it might not work for all servers + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + strictZSpacingForVolumeViewport: true, + // filterQueryParam: false, + defaultDataSourceName: 'default', + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'default', + configuration: { + friendlyName: 'Static WADO Local Data', + name: 'DCM4CHEE', + qidoRoot: '/dicomweb', + wadoRoot: '/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: true, + supportsStow: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video,pdf', + omitQuotationForMultipartRequest: true, + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'ohif', + configuration: { + friendlyName: 'dcmjs DICOMWeb Server', + name: 'aws', + // old server + // wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado', + // qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + // wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + // new server + wadoUriRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + wadoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video,pdf', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'aws', + configuration: { + friendlyName: 'AWS S3 OHIF', + name: 'aws', + qidoRoot: 'https://dd32w2rfebxel.cloudfront.net/dicomweb', + wadoRoot: 'https://dd32w2rfebxel.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + supportsStow: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video,pdf', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'e2e', + configuration: { + friendlyName: 'E2E Test Data', + name: 'DCM4CHEE', + wadoUriRoot: '/viewer-testdata', + qidoRoot: '/viewer-testdata', + wadoRoot: '/viewer-testdata', + qidoSupportsIncludeField: false, + supportsStow: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'video,thumbnail,pdf', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + friendlyName: 'dicom json', + name: 'json', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: { + friendlyName: 'dicom local', + }, + }, + ], + httpErrorHandler: error => { + // This is 429 when rejected from the public idc sandbox too often. + console.warn(error.status); + + // Could use services manager here to bring up a dialog/modal if needed. + console.warn('test, navigate to https://ohif.org/'); + }, + + // Only list the unique hotkeys + hotkeys: [], +}; diff --git a/platform/app/public/config/netlify.js b/platform/app/public/config/netlify.js new file mode 100644 index 0000000..6b4e3b9 --- /dev/null +++ b/platform/app/public/config/netlify.js @@ -0,0 +1,178 @@ +/** @type {AppTypes.Config} */ + +window.config = { + routerBasename: '/', + extensions: [], + modes: [], + showStudyList: true, + // below flag is for performance reasons, but it might not work for all servers + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + experimentalStudyBrowserSort: false, + strictZSpacingForVolumeViewport: true, + groupEnabledModesFirst: true, + // filterQueryParam: false, + defaultDataSourceName: 'ohif', + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'ohif', + configuration: { + friendlyName: 'AWS S3 Static wado server', + name: 'aws', + wadoUriRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + wadoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video', + // whether the data source should use retrieveBulkData to grab metadata, + // and in case of relative path, what would it be relative to, options + // are in the series level or study level (some servers like series some study) + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + transform: url => url.replace('/pixeldata.mp4', '/rendered'), + }, + omitQuotationForMultipartRequest: true, + }, + }, + + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'ohif2', + configuration: { + friendlyName: 'AWS S3 Static wado secondary server', + name: 'aws', + wadoUriRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb', + wadoRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video', + // whether the data source should use retrieveBulkData to grab metadata, + // and in case of relative path, what would it be relative to, options + // are in the series level or study level (some servers like series some study) + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + omitQuotationForMultipartRequest: true, + }, + }, + + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'ohif3', + configuration: { + friendlyName: 'AWS S3 Static wado secondary server', + name: 'aws', + wadoUriRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + qidoRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + wadoRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'bulkdata,video', + // whether the data source should use retrieveBulkData to grab metadata, + // and in case of relative path, what would it be relative to, options + // are in the series level or study level (some servers like series some study) + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + omitQuotationForMultipartRequest: true, + }, + }, + + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'local5000', + configuration: { + friendlyName: 'Static WADO Local Data', + name: 'DCM4CHEE', + qidoRoot: 'http://localhost:5000/dicomweb', + wadoRoot: 'http://localhost:5000/dicomweb', + qidoSupportsIncludeField: false, + supportsReject: true, + supportsStow: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: true, + staticWado: true, + singlepart: 'video', + bulkDataURI: { + enabled: true, + relativeResolution: 'studies', + }, + }, + }, + + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomwebproxy', + sourceName: 'dicomwebproxy', + configuration: { + friendlyName: 'dicomweb delegating proxy', + name: 'dicomwebproxy', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomjson', + sourceName: 'dicomjson', + configuration: { + friendlyName: 'dicom json', + name: 'json', + }, + }, + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal', + sourceName: 'dicomlocal', + configuration: { + friendlyName: 'dicom local', + }, + }, + ], + httpErrorHandler: error => { + // This is 429 when rejected from the public idc sandbox too often. + console.warn(error.status); + + // Could use services manager here to bring up a dialog/modal if needed. + console.warn('test, navigate to https://ohif.org/'); + }, +}; + +function waitForElement(selector, maxAttempts = 20, interval = 25) { + return new Promise(resolve => { + let attempts = 0; + + const checkForElement = setInterval(() => { + const element = document.querySelector(selector); + + if (element || attempts >= maxAttempts) { + clearInterval(checkForElement); + resolve(); + } + + attempts++; + }, interval); + }); +} diff --git a/platform/app/public/config/public_dicomweb.js b/platform/app/public/config/public_dicomweb.js new file mode 100644 index 0000000..9f1eb76 --- /dev/null +++ b/platform/app/public/config/public_dicomweb.js @@ -0,0 +1,29 @@ +window.config = { + routerBasename: '/', + showStudyList: true, + // below flag is for performance reasons, but it might not work for all servers + showWarningMessageForCrossOrigin: true, + showCPUFallbackMessage: true, + showLoadingIndicator: true, + strictZSpacingForVolumeViewport: true, + servers: { + dicomWeb: [ + { + name: 'aws', + wadoUriRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + wadoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb', + qidoSupportsIncludeField: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + supportsFuzzyMatching: true, + omitQuotationForMultipartRequest: true, + }, + ], + }, + i18n: { + LOCIZE_PROJECTID: 'a8da3f9a-e467-4dd6-af33-474d582a0294', + LOCIZE_API_KEY: null, // Developers can use this to do in-context editing. DO NOT COMMIT THIS KEY! + USE_LOCIZE: false, + }, +}; diff --git a/platform/app/public/es6-shim.min.js b/platform/app/public/es6-shim.min.js new file mode 100644 index 0000000..c1447d6 --- /dev/null +++ b/platform/app/public/es6-shim.min.js @@ -0,0 +1,3579 @@ +/*! + * https://github.com/paulmillr/es6-shim + * @license es6-shim Copyright 2013-2016 by Paul Miller (http://paulmillr.com) + * and contributors, MIT License + * es6-shim: v0.35.4 + * see https://github.com/paulmillr/es6-shim/blob/0.35.3/LICENSE + * Details and documentation: + * https://github.com/paulmillr/es6-shim/ + */ +(function (e, t) { + if (typeof define === 'function' && define.amd) { + define(t); + } else if (typeof exports === 'object') { + module.exports = t(); + } else { + e.returnExports = t(); + } +})(this, function () { + 'use strict'; + var e = Function.call.bind(Function.apply); + var t = Function.call.bind(Function.call); + var r = Array.isArray; + var n = Object.keys; + var o = function notThunker(t) { + return function notThunk() { + return !e(t, this, arguments); + }; + }; + var i = function (e) { + try { + e(); + return false; + } catch (t) { + return true; + } + }; + var a = function valueOrFalseIfThrows(e) { + try { + return e(); + } catch (t) { + return false; + } + }; + var u = o(i); + var f = function () { + return !i(function () { + return Object.defineProperty({}, 'x', { get: function () {} }); + }); + }; + var s = !!Object.defineProperty && f(); + var c = function foo() {}.name === 'foo'; + var l = Function.call.bind(Array.prototype.forEach); + var p = Function.call.bind(Array.prototype.reduce); + var v = Function.call.bind(Array.prototype.filter); + var y = Function.call.bind(Array.prototype.some); + var h = function (e, t, r, n) { + if (!n && t in e) { + return; + } + if (s) { + Object.defineProperty(e, t, { + configurable: true, + enumerable: false, + writable: true, + value: r, + }); + } else { + e[t] = r; + } + }; + var b = function (e, t, r) { + l(n(t), function (n) { + var o = t[n]; + h(e, n, o, !!r); + }); + }; + var g = Function.call.bind(Object.prototype.toString); + var d = + typeof /abc/ === 'function' + ? function IsCallableSlow(e) { + return typeof e === 'function' && g(e) === '[object Function]'; + } + : function IsCallableFast(e) { + return typeof e === 'function'; + }; + var m = { + getter: function (e, t, r) { + if (!s) { + throw new TypeError('getters require true ES5 support'); + } + Object.defineProperty(e, t, { + configurable: true, + enumerable: false, + get: r, + }); + }, + proxy: function (e, t, r) { + if (!s) { + throw new TypeError('getters require true ES5 support'); + } + var n = Object.getOwnPropertyDescriptor(e, t); + Object.defineProperty(r, t, { + configurable: n.configurable, + enumerable: n.enumerable, + get: function getKey() { + return e[t]; + }, + set: function setKey(r) { + e[t] = r; + }, + }); + }, + redefine: function (e, t, r) { + if (s) { + var n = Object.getOwnPropertyDescriptor(e, t); + n.value = r; + Object.defineProperty(e, t, n); + } else { + e[t] = r; + } + }, + defineByDescriptor: function (e, t, r) { + if (s) { + Object.defineProperty(e, t, r); + } else if ('value' in r) { + e[t] = r.value; + } + }, + preserveToString: function (e, t) { + if (t && d(t.toString)) { + h(e, 'toString', t.toString.bind(t), true); + } + }, + }; + var O = + Object.create || + function (e, t) { + var r = function Prototype() {}; + r.prototype = e; + var o = new r(); + if (typeof t !== 'undefined') { + n(t).forEach(function (e) { + m.defineByDescriptor(o, e, t[e]); + }); + } + return o; + }; + var w = function (e, t) { + if (!Object.setPrototypeOf) { + return false; + } + return a(function () { + var r = function Subclass(t) { + var r = new e(t); + Object.setPrototypeOf(r, Subclass.prototype); + return r; + }; + Object.setPrototypeOf(r, e); + r.prototype = O(e.prototype, { constructor: { value: r } }); + return t(r); + }); + }; + var j = function () { + if (typeof self !== 'undefined') { + return self; + } + if (typeof window !== 'undefined') { + return window; + } + if (typeof global !== 'undefined') { + return global; + } + throw new Error('unable to locate global object'); + }; + var S = j(); + var T = S.isFinite; + var I = Function.call.bind(String.prototype.indexOf); + var E = Function.apply.bind(Array.prototype.indexOf); + var P = Function.call.bind(Array.prototype.concat); + var C = Function.call.bind(String.prototype.slice); + var M = Function.call.bind(Array.prototype.push); + var x = Function.apply.bind(Array.prototype.push); + var N = Function.call.bind(Array.prototype.shift); + var A = Math.max; + var R = Math.min; + var _ = Math.floor; + var k = Math.abs; + var L = Math.exp; + var F = Math.log; + var D = Math.sqrt; + var z = Function.call.bind(Object.prototype.hasOwnProperty); + var q; + var W = function () {}; + var G = S.Map; + var H = G && G.prototype['delete']; + var V = G && G.prototype.get; + var B = G && G.prototype.has; + var U = G && G.prototype.set; + var $ = S.Symbol || {}; + var J = $.species || '@@species'; + var X = + Number.isNaN || + function isNaN(e) { + return e !== e; + }; + var K = + Number.isFinite || + function isFinite(e) { + return typeof e === 'number' && T(e); + }; + var Z = d(Math.sign) + ? Math.sign + : function sign(e) { + var t = Number(e); + if (t === 0) { + return t; + } + if (X(t)) { + return t; + } + return t < 0 ? -1 : 1; + }; + var Y = function log1p(e) { + var t = Number(e); + if (t < -1 || X(t)) { + return NaN; + } + if (t === 0 || t === Infinity) { + return t; + } + if (t === -1) { + return -Infinity; + } + return 1 + t - 1 === 0 ? t : t * (F(1 + t) / (1 + t - 1)); + }; + var Q = function isArguments(e) { + return g(e) === '[object Arguments]'; + }; + var ee = function isArguments(e) { + return ( + e !== null && + typeof e === 'object' && + typeof e.length === 'number' && + e.length >= 0 && + g(e) !== '[object Array]' && + g(e.callee) === '[object Function]' + ); + }; + var te = Q(arguments) ? Q : ee; + var re = { + primitive: function (e) { + return e === null || (typeof e !== 'function' && typeof e !== 'object'); + }, + string: function (e) { + return g(e) === '[object String]'; + }, + regex: function (e) { + return g(e) === '[object RegExp]'; + }, + symbol: function (e) { + return typeof S.Symbol === 'function' && typeof e === 'symbol'; + }, + }; + var ne = function overrideNative(e, t, r) { + var n = e[t]; + h(e, t, r, true); + m.preserveToString(e[t], n); + }; + var oe = typeof $ === 'function' && typeof $['for'] === 'function' && re.symbol($()); + var ie = re.symbol($.iterator) ? $.iterator : '_es6-shim iterator_'; + if (S.Set && typeof new S.Set()['@@iterator'] === 'function') { + ie = '@@iterator'; + } + if (!S.Reflect) { + h(S, 'Reflect', {}, true); + } + var ae = S.Reflect; + var ue = String; + var fe = typeof document === 'undefined' || !document ? null : document.all; + var se = + fe == null + ? function isNullOrUndefined(e) { + return e == null; + } + : function isNullOrUndefinedAndNotDocumentAll(e) { + return e == null && e !== fe; + }; + var ce = { + Call: function Call(t, r) { + var n = arguments.length > 2 ? arguments[2] : []; + if (!ce.IsCallable(t)) { + throw new TypeError(t + ' is not a function'); + } + return e(t, r, n); + }, + RequireObjectCoercible: function (e, t) { + if (se(e)) { + throw new TypeError(t || 'Cannot call method on ' + e); + } + return e; + }, + TypeIsObject: function (e) { + if (e === void 0 || e === null || e === true || e === false) { + return false; + } + return typeof e === 'function' || typeof e === 'object' || e === fe; + }, + ToObject: function (e, t) { + return Object(ce.RequireObjectCoercible(e, t)); + }, + IsCallable: d, + IsConstructor: function (e) { + return ce.IsCallable(e); + }, + ToInt32: function (e) { + return ce.ToNumber(e) >> 0; + }, + ToUint32: function (e) { + return ce.ToNumber(e) >>> 0; + }, + ToNumber: function (e) { + if (g(e) === '[object Symbol]') { + throw new TypeError('Cannot convert a Symbol value to a number'); + } + return +e; + }, + ToInteger: function (e) { + var t = ce.ToNumber(e); + if (X(t)) { + return 0; + } + if (t === 0 || !K(t)) { + return t; + } + return (t > 0 ? 1 : -1) * _(k(t)); + }, + ToLength: function (e) { + var t = ce.ToInteger(e); + if (t <= 0) { + return 0; + } + if (t > Number.MAX_SAFE_INTEGER) { + return Number.MAX_SAFE_INTEGER; + } + return t; + }, + SameValue: function (e, t) { + if (e === t) { + if (e === 0) { + return 1 / e === 1 / t; + } + return true; + } + return X(e) && X(t); + }, + SameValueZero: function (e, t) { + return e === t || (X(e) && X(t)); + }, + IsIterable: function (e) { + return ce.TypeIsObject(e) && (typeof e[ie] !== 'undefined' || te(e)); + }, + GetIterator: function (e) { + if (te(e)) { + return new q(e, 'value'); + } + var t = ce.GetMethod(e, ie); + if (!ce.IsCallable(t)) { + throw new TypeError('value is not an iterable'); + } + var r = ce.Call(t, e); + if (!ce.TypeIsObject(r)) { + throw new TypeError('bad iterator'); + } + return r; + }, + GetMethod: function (e, t) { + var r = ce.ToObject(e)[t]; + if (se(r)) { + return void 0; + } + if (!ce.IsCallable(r)) { + throw new TypeError('Method not callable: ' + t); + } + return r; + }, + IteratorComplete: function (e) { + return !!e.done; + }, + IteratorClose: function (e, t) { + var r = ce.GetMethod(e, 'return'); + if (r === void 0) { + return; + } + var n, o; + try { + n = ce.Call(r, e); + } catch (i) { + o = i; + } + if (t) { + return; + } + if (o) { + throw o; + } + if (!ce.TypeIsObject(n)) { + throw new TypeError("Iterator's return method returned a non-object."); + } + }, + IteratorNext: function (e) { + var t = arguments.length > 1 ? e.next(arguments[1]) : e.next(); + if (!ce.TypeIsObject(t)) { + throw new TypeError('bad iterator'); + } + return t; + }, + IteratorStep: function (e) { + var t = ce.IteratorNext(e); + var r = ce.IteratorComplete(t); + return r ? false : t; + }, + Construct: function (e, t, r, n) { + var o = typeof r === 'undefined' ? e : r; + if (!n && ae.construct) { + return ae.construct(e, t, o); + } + var i = o.prototype; + if (!ce.TypeIsObject(i)) { + i = Object.prototype; + } + var a = O(i); + var u = ce.Call(e, a, t); + return ce.TypeIsObject(u) ? u : a; + }, + SpeciesConstructor: function (e, t) { + var r = e.constructor; + if (r === void 0) { + return t; + } + if (!ce.TypeIsObject(r)) { + throw new TypeError('Bad constructor'); + } + var n = r[J]; + if (se(n)) { + return t; + } + if (!ce.IsConstructor(n)) { + throw new TypeError('Bad @@species'); + } + return n; + }, + CreateHTML: function (e, t, r, n) { + var o = ce.ToString(e); + var i = '<' + t; + if (r !== '') { + var a = ce.ToString(n); + var u = a.replace(/"/g, '"'); + i += ' ' + r + '="' + u + '"'; + } + var f = i + '>'; + var s = f + o; + return s + ''; + }, + IsRegExp: function IsRegExp(e) { + if (!ce.TypeIsObject(e)) { + return false; + } + var t = e[$.match]; + if (typeof t !== 'undefined') { + return !!t; + } + return re.regex(e); + }, + ToString: function ToString(e) { + return ue(e); + }, + }; + if (s && oe) { + var le = function defineWellKnownSymbol(e) { + if (re.symbol($[e])) { + return $[e]; + } + var t = $['for']('Symbol.' + e); + Object.defineProperty($, e, { + configurable: false, + enumerable: false, + writable: false, + value: t, + }); + return t; + }; + if (!re.symbol($.search)) { + var pe = le('search'); + var ve = String.prototype.search; + h(RegExp.prototype, pe, function search(e) { + return ce.Call(ve, e, [this]); + }); + var ye = function search(e) { + var t = ce.RequireObjectCoercible(this); + if (!se(e)) { + var r = ce.GetMethod(e, pe); + if (typeof r !== 'undefined') { + return ce.Call(r, e, [t]); + } + } + return ce.Call(ve, t, [ce.ToString(e)]); + }; + ne(String.prototype, 'search', ye); + } + if (!re.symbol($.replace)) { + var he = le('replace'); + var be = String.prototype.replace; + h(RegExp.prototype, he, function replace(e, t) { + return ce.Call(be, e, [this, t]); + }); + var ge = function replace(e, t) { + var r = ce.RequireObjectCoercible(this); + if (!se(e)) { + var n = ce.GetMethod(e, he); + if (typeof n !== 'undefined') { + return ce.Call(n, e, [r, t]); + } + } + return ce.Call(be, r, [ce.ToString(e), t]); + }; + ne(String.prototype, 'replace', ge); + } + if (!re.symbol($.split)) { + var de = le('split'); + var me = String.prototype.split; + h(RegExp.prototype, de, function split(e, t) { + return ce.Call(me, e, [this, t]); + }); + var Oe = function split(e, t) { + var r = ce.RequireObjectCoercible(this); + if (!se(e)) { + var n = ce.GetMethod(e, de); + if (typeof n !== 'undefined') { + return ce.Call(n, e, [r, t]); + } + } + return ce.Call(me, r, [ce.ToString(e), t]); + }; + ne(String.prototype, 'split', Oe); + } + var we = re.symbol($.match); + var je = + we && + (function () { + var e = {}; + e[$.match] = function () { + return 42; + }; + return 'a'.match(e) !== 42; + })(); + if (!we || je) { + var Se = le('match'); + var Te = String.prototype.match; + h(RegExp.prototype, Se, function match(e) { + return ce.Call(Te, e, [this]); + }); + var Ie = function match(e) { + var t = ce.RequireObjectCoercible(this); + if (!se(e)) { + var r = ce.GetMethod(e, Se); + if (typeof r !== 'undefined') { + return ce.Call(r, e, [t]); + } + } + return ce.Call(Te, t, [ce.ToString(e)]); + }; + ne(String.prototype, 'match', Ie); + } + } + var Ee = function wrapConstructor(e, t, r) { + m.preserveToString(t, e); + if (Object.setPrototypeOf) { + Object.setPrototypeOf(e, t); + } + if (s) { + l(Object.getOwnPropertyNames(e), function (n) { + if (n in W || r[n]) { + return; + } + m.proxy(e, n, t); + }); + } else { + l(Object.keys(e), function (n) { + if (n in W || r[n]) { + return; + } + t[n] = e[n]; + }); + } + t.prototype = e.prototype; + m.redefine(e.prototype, 'constructor', t); + }; + var Pe = function () { + return this; + }; + var Ce = function (e) { + if (s && !z(e, J)) { + m.getter(e, J, Pe); + } + }; + var Me = function (e, t) { + var r = + t || + function iterator() { + return this; + }; + h(e, ie, r); + if (!e[ie] && re.symbol(ie)) { + e[ie] = r; + } + }; + var xe = function createDataProperty(e, t, r) { + if (s) { + Object.defineProperty(e, t, { + configurable: true, + enumerable: true, + writable: true, + value: r, + }); + } else { + e[t] = r; + } + }; + var Ne = function createDataPropertyOrThrow(e, t, r) { + xe(e, t, r); + if (!ce.SameValue(e[t], r)) { + throw new TypeError('property is nonconfigurable'); + } + }; + var Ae = function (e, t, r, n) { + if (!ce.TypeIsObject(e)) { + throw new TypeError('Constructor requires `new`: ' + t.name); + } + var o = t.prototype; + if (!ce.TypeIsObject(o)) { + o = r; + } + var i = O(o); + for (var a in n) { + if (z(n, a)) { + var u = n[a]; + h(i, a, u, true); + } + } + return i; + }; + if (String.fromCodePoint && String.fromCodePoint.length !== 1) { + var Re = String.fromCodePoint; + ne(String, 'fromCodePoint', function fromCodePoint(e) { + return ce.Call(Re, this, arguments); + }); + } + var _e = { + fromCodePoint: function fromCodePoint(e) { + var t = []; + var r; + for (var n = 0, o = arguments.length; n < o; n++) { + r = Number(arguments[n]); + if (!ce.SameValue(r, ce.ToInteger(r)) || r < 0 || r > 1114111) { + throw new RangeError('Invalid code point ' + r); + } + if (r < 65536) { + M(t, String.fromCharCode(r)); + } else { + r -= 65536; + M(t, String.fromCharCode((r >> 10) + 55296)); + M(t, String.fromCharCode((r % 1024) + 56320)); + } + } + return t.join(''); + }, + raw: function raw(e) { + var t = ce.ToObject(e, 'bad callSite'); + var r = ce.ToObject(t.raw, 'bad raw value'); + var n = r.length; + var o = ce.ToLength(n); + if (o <= 0) { + return ''; + } + var i = []; + var a = 0; + var u, f, s, c; + while (a < o) { + u = ce.ToString(a); + s = ce.ToString(r[u]); + M(i, s); + if (a + 1 >= o) { + break; + } + f = a + 1 < arguments.length ? arguments[a + 1] : ''; + c = ce.ToString(f); + M(i, c); + a += 1; + } + return i.join(''); + }, + }; + if (String.raw && String.raw({ raw: { 0: 'x', 1: 'y', length: 2 } }) !== 'xy') { + ne(String, 'raw', _e.raw); + } + b(String, _e); + var ke = function repeat(e, t) { + if (t < 1) { + return ''; + } + if (t % 2) { + return repeat(e, t - 1) + e; + } + var r = repeat(e, t / 2); + return r + r; + }; + var Le = Infinity; + var Fe = { + repeat: function repeat(e) { + var t = ce.ToString(ce.RequireObjectCoercible(this)); + var r = ce.ToInteger(e); + if (r < 0 || r >= Le) { + throw new RangeError( + 'repeat count must be less than infinity and not overflow maximum string size' + ); + } + return ke(t, r); + }, + startsWith: function startsWith(e) { + var t = ce.ToString(ce.RequireObjectCoercible(this)); + if (ce.IsRegExp(e)) { + throw new TypeError('Cannot call method "startsWith" with a regex'); + } + var r = ce.ToString(e); + var n; + if (arguments.length > 1) { + n = arguments[1]; + } + var o = A(ce.ToInteger(n), 0); + return C(t, o, o + r.length) === r; + }, + endsWith: function endsWith(e) { + var t = ce.ToString(ce.RequireObjectCoercible(this)); + if (ce.IsRegExp(e)) { + throw new TypeError('Cannot call method "endsWith" with a regex'); + } + var r = ce.ToString(e); + var n = t.length; + var o; + if (arguments.length > 1) { + o = arguments[1]; + } + var i = typeof o === 'undefined' ? n : ce.ToInteger(o); + var a = R(A(i, 0), n); + return C(t, a - r.length, a) === r; + }, + includes: function includes(e) { + if (ce.IsRegExp(e)) { + throw new TypeError('"includes" does not accept a RegExp'); + } + var t = ce.ToString(e); + var r; + if (arguments.length > 1) { + r = arguments[1]; + } + return I(this, t, r) !== -1; + }, + codePointAt: function codePointAt(e) { + var t = ce.ToString(ce.RequireObjectCoercible(this)); + var r = ce.ToInteger(e); + var n = t.length; + if (r >= 0 && r < n) { + var o = t.charCodeAt(r); + var i = r + 1 === n; + if (o < 55296 || o > 56319 || i) { + return o; + } + var a = t.charCodeAt(r + 1); + if (a < 56320 || a > 57343) { + return o; + } + return (o - 55296) * 1024 + (a - 56320) + 65536; + } + }, + }; + if (String.prototype.includes && 'a'.includes('a', Infinity) !== false) { + ne(String.prototype, 'includes', Fe.includes); + } + if (String.prototype.startsWith && String.prototype.endsWith) { + var De = i(function () { + return '/a/'.startsWith(/a/); + }); + var ze = a(function () { + return 'abc'.startsWith('a', Infinity) === false; + }); + if (!De || !ze) { + ne(String.prototype, 'startsWith', Fe.startsWith); + ne(String.prototype, 'endsWith', Fe.endsWith); + } + } + if (oe) { + var qe = a(function () { + var e = /a/; + e[$.match] = false; + return '/a/'.startsWith(e); + }); + if (!qe) { + ne(String.prototype, 'startsWith', Fe.startsWith); + } + var We = a(function () { + var e = /a/; + e[$.match] = false; + return '/a/'.endsWith(e); + }); + if (!We) { + ne(String.prototype, 'endsWith', Fe.endsWith); + } + var Ge = a(function () { + var e = /a/; + e[$.match] = false; + return '/a/'.includes(e); + }); + if (!Ge) { + ne(String.prototype, 'includes', Fe.includes); + } + } + b(String.prototype, Fe); + var He = [ + '\t\n\x0B\f\r \xa0\u1680\u180e\u2000\u2001\u2002\u2003', + '\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028', + '\u2029\ufeff', + ].join(''); + var Ve = new RegExp('(^[' + He + ']+)|([' + He + ']+$)', 'g'); + var Be = function trim() { + return ce.ToString(ce.RequireObjectCoercible(this)).replace(Ve, ''); + }; + var Ue = ['\x85', '\u200b', '\ufffe'].join(''); + var $e = new RegExp('[' + Ue + ']', 'g'); + var Je = /^[-+]0x[0-9a-f]+$/i; + var Xe = Ue.trim().length !== Ue.length; + h(String.prototype, 'trim', Be, Xe); + var Ke = function (e) { + return { value: e, done: arguments.length === 0 }; + }; + var Ze = function (e) { + ce.RequireObjectCoercible(e); + this._s = ce.ToString(e); + this._i = 0; + }; + Ze.prototype.next = function () { + var e = this._s; + var t = this._i; + if (typeof e === 'undefined' || t >= e.length) { + this._s = void 0; + return Ke(); + } + var r = e.charCodeAt(t); + var n, o; + if (r < 55296 || r > 56319 || t + 1 === e.length) { + o = 1; + } else { + n = e.charCodeAt(t + 1); + o = n < 56320 || n > 57343 ? 1 : 2; + } + this._i = t + o; + return Ke(e.substr(t, o)); + }; + Me(Ze.prototype); + Me(String.prototype, function () { + return new Ze(this); + }); + var Ye = { + from: function from(e) { + var r = this; + var n; + if (arguments.length > 1) { + n = arguments[1]; + } + var o, i; + if (typeof n === 'undefined') { + o = false; + } else { + if (!ce.IsCallable(n)) { + throw new TypeError('Array.from: when provided, the second argument must be a function'); + } + if (arguments.length > 2) { + i = arguments[2]; + } + o = true; + } + var a = typeof (te(e) || ce.GetMethod(e, ie)) !== 'undefined'; + var u, f, s; + if (a) { + f = ce.IsConstructor(r) ? Object(new r()) : []; + var c = ce.GetIterator(e); + var l, p; + s = 0; + while (true) { + l = ce.IteratorStep(c); + if (l === false) { + break; + } + p = l.value; + try { + if (o) { + p = typeof i === 'undefined' ? n(p, s) : t(n, i, p, s); + } + f[s] = p; + } catch (v) { + ce.IteratorClose(c, true); + throw v; + } + s += 1; + } + u = s; + } else { + var y = ce.ToObject(e); + u = ce.ToLength(y.length); + f = ce.IsConstructor(r) ? Object(new r(u)) : new Array(u); + var h; + for (s = 0; s < u; ++s) { + h = y[s]; + if (o) { + h = typeof i === 'undefined' ? n(h, s) : t(n, i, h, s); + } + Ne(f, s, h); + } + } + f.length = u; + return f; + }, + of: function of() { + var e = arguments.length; + var t = this; + var n = r(t) || !ce.IsCallable(t) ? new Array(e) : ce.Construct(t, [e]); + for (var o = 0; o < e; ++o) { + Ne(n, o, arguments[o]); + } + n.length = e; + return n; + }, + }; + b(Array, Ye); + Ce(Array); + q = function (e, t) { + this.i = 0; + this.array = e; + this.kind = t; + }; + b(q.prototype, { + next: function () { + var e = this.i; + var t = this.array; + if (!(this instanceof q)) { + throw new TypeError('Not an ArrayIterator'); + } + if (typeof t !== 'undefined') { + var r = ce.ToLength(t.length); + for (; e < r; e++) { + var n = this.kind; + var o; + if (n === 'key') { + o = e; + } else if (n === 'value') { + o = t[e]; + } else if (n === 'entry') { + o = [e, t[e]]; + } + this.i = e + 1; + return Ke(o); + } + } + this.array = void 0; + return Ke(); + }, + }); + Me(q.prototype); + var Qe = + Array.of === Ye.of || + (function () { + var e = function Foo(e) { + this.length = e; + }; + e.prototype = []; + var t = Array.of.apply(e, [1, 2]); + return t instanceof e && t.length === 2; + })(); + if (!Qe) { + ne(Array, 'of', Ye.of); + } + var et = { + copyWithin: function copyWithin(e, t) { + var r = ce.ToObject(this); + var n = ce.ToLength(r.length); + var o = ce.ToInteger(e); + var i = ce.ToInteger(t); + var a = o < 0 ? A(n + o, 0) : R(o, n); + var u = i < 0 ? A(n + i, 0) : R(i, n); + var f; + if (arguments.length > 2) { + f = arguments[2]; + } + var s = typeof f === 'undefined' ? n : ce.ToInteger(f); + var c = s < 0 ? A(n + s, 0) : R(s, n); + var l = R(c - u, n - a); + var p = 1; + if (u < a && a < u + l) { + p = -1; + u += l - 1; + a += l - 1; + } + while (l > 0) { + if (u in r) { + r[a] = r[u]; + } else { + delete r[a]; + } + u += p; + a += p; + l -= 1; + } + return r; + }, + fill: function fill(e) { + var t; + if (arguments.length > 1) { + t = arguments[1]; + } + var r; + if (arguments.length > 2) { + r = arguments[2]; + } + var n = ce.ToObject(this); + var o = ce.ToLength(n.length); + t = ce.ToInteger(typeof t === 'undefined' ? 0 : t); + r = ce.ToInteger(typeof r === 'undefined' ? o : r); + var i = t < 0 ? A(o + t, 0) : R(t, o); + var a = r < 0 ? o + r : r; + for (var u = i; u < o && u < a; ++u) { + n[u] = e; + } + return n; + }, + find: function find(e) { + var r = ce.ToObject(this); + var n = ce.ToLength(r.length); + if (!ce.IsCallable(e)) { + throw new TypeError('Array#find: predicate must be a function'); + } + var o = arguments.length > 1 ? arguments[1] : null; + for (var i = 0, a; i < n; i++) { + a = r[i]; + if (o) { + if (t(e, o, a, i, r)) { + return a; + } + } else if (e(a, i, r)) { + return a; + } + } + }, + findIndex: function findIndex(e) { + var r = ce.ToObject(this); + var n = ce.ToLength(r.length); + if (!ce.IsCallable(e)) { + throw new TypeError('Array#findIndex: predicate must be a function'); + } + var o = arguments.length > 1 ? arguments[1] : null; + for (var i = 0; i < n; i++) { + if (o) { + if (t(e, o, r[i], i, r)) { + return i; + } + } else if (e(r[i], i, r)) { + return i; + } + } + return -1; + }, + keys: function keys() { + return new q(this, 'key'); + }, + values: function values() { + return new q(this, 'value'); + }, + entries: function entries() { + return new q(this, 'entry'); + }, + }; + if (Array.prototype.keys && !ce.IsCallable([1].keys().next)) { + delete Array.prototype.keys; + } + if (Array.prototype.entries && !ce.IsCallable([1].entries().next)) { + delete Array.prototype.entries; + } + if ( + Array.prototype.keys && + Array.prototype.entries && + !Array.prototype.values && + Array.prototype[ie] + ) { + b(Array.prototype, { values: Array.prototype[ie] }); + if (re.symbol($.unscopables)) { + Array.prototype[$.unscopables].values = true; + } + } + if (c && Array.prototype.values && Array.prototype.values.name !== 'values') { + var tt = Array.prototype.values; + ne(Array.prototype, 'values', function values() { + return ce.Call(tt, this, arguments); + }); + h(Array.prototype, ie, Array.prototype.values, true); + } + b(Array.prototype, et); + if (1 / [true].indexOf(true, -0) < 0) { + h( + Array.prototype, + 'indexOf', + function indexOf(e) { + var t = E(this, arguments); + if (t === 0 && 1 / t < 0) { + return 0; + } + return t; + }, + true + ); + } + Me(Array.prototype, function () { + return this.values(); + }); + if (Object.getPrototypeOf) { + Me(Object.getPrototypeOf([].values())); + } + var rt = (function () { + return a(function () { + return Array.from({ length: -1 }).length === 0; + }); + })(); + var nt = (function () { + var e = Array.from([0].entries()); + return e.length === 1 && r(e[0]) && e[0][0] === 0 && e[0][1] === 0; + })(); + if (!rt || !nt) { + ne(Array, 'from', Ye.from); + } + var ot = (function () { + return a(function () { + return Array.from([0], void 0); + }); + })(); + if (!ot) { + var it = Array.from; + ne(Array, 'from', function from(e) { + if (arguments.length > 1 && typeof arguments[1] !== 'undefined') { + return ce.Call(it, this, arguments); + } else { + return t(it, this, e); + } + }); + } + var at = -(Math.pow(2, 32) - 1); + var ut = function (e, r) { + var n = { length: at }; + n[r ? (n.length >>> 0) - 1 : 0] = true; + return a(function () { + t( + e, + n, + function () { + throw new RangeError('should not reach here'); + }, + [] + ); + return true; + }); + }; + if (!ut(Array.prototype.forEach)) { + var ft = Array.prototype.forEach; + ne( + Array.prototype, + 'forEach', + function forEach(e) { + return ce.Call(ft, this.length >= 0 ? this : [], arguments); + }, + true + ); + } + if (!ut(Array.prototype.map)) { + var st = Array.prototype.map; + ne( + Array.prototype, + 'map', + function map(e) { + return ce.Call(st, this.length >= 0 ? this : [], arguments); + }, + true + ); + } + if (!ut(Array.prototype.filter)) { + var ct = Array.prototype.filter; + ne( + Array.prototype, + 'filter', + function filter(e) { + return ce.Call(ct, this.length >= 0 ? this : [], arguments); + }, + true + ); + } + if (!ut(Array.prototype.some)) { + var lt = Array.prototype.some; + ne( + Array.prototype, + 'some', + function some(e) { + return ce.Call(lt, this.length >= 0 ? this : [], arguments); + }, + true + ); + } + if (!ut(Array.prototype.every)) { + var pt = Array.prototype.every; + ne( + Array.prototype, + 'every', + function every(e) { + return ce.Call(pt, this.length >= 0 ? this : [], arguments); + }, + true + ); + } + if (!ut(Array.prototype.reduce)) { + var vt = Array.prototype.reduce; + ne( + Array.prototype, + 'reduce', + function reduce(e) { + return ce.Call(vt, this.length >= 0 ? this : [], arguments); + }, + true + ); + } + if (!ut(Array.prototype.reduceRight, true)) { + var yt = Array.prototype.reduceRight; + ne( + Array.prototype, + 'reduceRight', + function reduceRight(e) { + return ce.Call(yt, this.length >= 0 ? this : [], arguments); + }, + true + ); + } + var ht = Number('0o10') !== 8; + var bt = Number('0b10') !== 2; + var gt = y(Ue, function (e) { + return Number(e + 0 + e) === 0; + }); + if (ht || bt || gt) { + var dt = Number; + var mt = /^0b[01]+$/i; + var Ot = /^0o[0-7]+$/i; + var wt = mt.test.bind(mt); + var jt = Ot.test.bind(Ot); + var St = function (e) { + var t; + if (typeof e.valueOf === 'function') { + t = e.valueOf(); + if (re.primitive(t)) { + return t; + } + } + if (typeof e.toString === 'function') { + t = e.toString(); + if (re.primitive(t)) { + return t; + } + } + throw new TypeError('No default value'); + }; + var Tt = $e.test.bind($e); + var It = Je.test.bind(Je); + var Et = (function () { + var e = function Number(t) { + var r; + if (arguments.length > 0) { + r = re.primitive(t) ? t : St(t, 'number'); + } else { + r = 0; + } + if (typeof r === 'string') { + r = ce.Call(Be, r); + if (wt(r)) { + r = parseInt(C(r, 2), 2); + } else if (jt(r)) { + r = parseInt(C(r, 2), 8); + } else if (Tt(r) || It(r)) { + r = NaN; + } + } + var n = this; + var o = a(function () { + dt.prototype.valueOf.call(n); + return true; + }); + if (n instanceof e && !o) { + return new dt(r); + } + return dt(r); + }; + return e; + })(); + Ee(dt, Et, {}); + b(Et, { + NaN: dt.NaN, + MAX_VALUE: dt.MAX_VALUE, + MIN_VALUE: dt.MIN_VALUE, + NEGATIVE_INFINITY: dt.NEGATIVE_INFINITY, + POSITIVE_INFINITY: dt.POSITIVE_INFINITY, + }); + Number = Et; + m.redefine(S, 'Number', Et); + } + var Pt = Math.pow(2, 53) - 1; + b(Number, { + MAX_SAFE_INTEGER: Pt, + MIN_SAFE_INTEGER: -Pt, + EPSILON: 2.220446049250313e-16, + parseInt: S.parseInt, + parseFloat: S.parseFloat, + isFinite: K, + isInteger: function isInteger(e) { + return K(e) && ce.ToInteger(e) === e; + }, + isSafeInteger: function isSafeInteger(e) { + return Number.isInteger(e) && k(e) <= Number.MAX_SAFE_INTEGER; + }, + isNaN: X, + }); + h(Number, 'parseInt', S.parseInt, Number.parseInt !== S.parseInt); + if ( + [, 1].find(function () { + return true; + }) === 1 + ) { + ne(Array.prototype, 'find', et.find); + } + if ( + [, 1].findIndex(function () { + return true; + }) !== 0 + ) { + ne(Array.prototype, 'findIndex', et.findIndex); + } + var Ct = Function.bind.call(Function.bind, Object.prototype.propertyIsEnumerable); + var Mt = function ensureEnumerable(e, t) { + if (s && Ct(e, t)) { + Object.defineProperty(e, t, { enumerable: false }); + } + }; + var xt = function sliceArgs() { + var e = Number(this); + var t = arguments.length; + var r = t - e; + var n = new Array(r < 0 ? 0 : r); + for (var o = e; o < t; ++o) { + n[o - e] = arguments[o]; + } + return n; + }; + var Nt = function assignTo(e) { + return function assignToSource(t, r) { + t[r] = e[r]; + return t; + }; + }; + var At = function (e, t) { + var r = n(Object(t)); + var o; + if (ce.IsCallable(Object.getOwnPropertySymbols)) { + o = v(Object.getOwnPropertySymbols(Object(t)), Ct(t)); + } + return p(P(r, o || []), Nt(t), e); + }; + var Rt = { + assign: function (e, t) { + var r = ce.ToObject(e, 'Cannot convert undefined or null to object'); + return p(ce.Call(xt, 1, arguments), At, r); + }, + is: function is(e, t) { + return ce.SameValue(e, t); + }, + }; + var _t = + Object.assign && + Object.preventExtensions && + (function () { + var e = Object.preventExtensions({ 1: 2 }); + try { + Object.assign(e, 'xy'); + } catch (t) { + return e[1] === 'y'; + } + })(); + if (_t) { + ne(Object, 'assign', Rt.assign); + } + b(Object, Rt); + if (s) { + var kt = { + setPrototypeOf: (function (e, r) { + var n; + var o = function (e, t) { + if (!ce.TypeIsObject(e)) { + throw new TypeError('cannot set prototype on a non-object'); + } + if (!(t === null || ce.TypeIsObject(t))) { + throw new TypeError('can only set prototype to an object or null' + t); + } + }; + var i = function (e, r) { + o(e, r); + t(n, e, r); + return e; + }; + try { + n = e.getOwnPropertyDescriptor(e.prototype, r).set; + t(n, {}, null); + } catch (a) { + if (e.prototype !== {}[r]) { + return; + } + n = function (e) { + this[r] = e; + }; + i.polyfill = i(i({}, null), e.prototype) instanceof e; + } + return i; + })(Object, '__proto__'), + }; + b(Object, kt); + } + if ( + Object.setPrototypeOf && + Object.getPrototypeOf && + Object.getPrototypeOf(Object.setPrototypeOf({}, null)) !== null && + Object.getPrototypeOf(Object.create(null)) === null + ) { + (function () { + var e = Object.create(null); + var t = Object.getPrototypeOf; + var r = Object.setPrototypeOf; + Object.getPrototypeOf = function (r) { + var n = t(r); + return n === e ? null : n; + }; + Object.setPrototypeOf = function (t, n) { + var o = n === null ? e : n; + return r(t, o); + }; + Object.setPrototypeOf.polyfill = false; + })(); + } + var Lt = !i(function () { + return Object.keys('foo'); + }); + if (!Lt) { + var Ft = Object.keys; + ne(Object, 'keys', function keys(e) { + return Ft(ce.ToObject(e)); + }); + n = Object.keys; + } + var Dt = i(function () { + return Object.keys(/a/g); + }); + if (Dt) { + var zt = Object.keys; + ne(Object, 'keys', function keys(e) { + if (re.regex(e)) { + var t = []; + for (var r in e) { + if (z(e, r)) { + M(t, r); + } + } + return t; + } + return zt(e); + }); + n = Object.keys; + } + if (Object.getOwnPropertyNames) { + var qt = !i(function () { + return Object.getOwnPropertyNames('foo'); + }); + if (!qt) { + var Wt = typeof window === 'object' ? Object.getOwnPropertyNames(window) : []; + var Gt = Object.getOwnPropertyNames; + ne(Object, 'getOwnPropertyNames', function getOwnPropertyNames(e) { + var t = ce.ToObject(e); + if (g(t) === '[object Window]') { + try { + return Gt(t); + } catch (r) { + return P([], Wt); + } + } + return Gt(t); + }); + } + } + if (Object.getOwnPropertyDescriptor) { + var Ht = !i(function () { + return Object.getOwnPropertyDescriptor('foo', 'bar'); + }); + if (!Ht) { + var Vt = Object.getOwnPropertyDescriptor; + ne(Object, 'getOwnPropertyDescriptor', function getOwnPropertyDescriptor(e, t) { + return Vt(ce.ToObject(e), t); + }); + } + } + if (Object.seal) { + var Bt = !i(function () { + return Object.seal('foo'); + }); + if (!Bt) { + var Ut = Object.seal; + ne(Object, 'seal', function seal(e) { + if (!ce.TypeIsObject(e)) { + return e; + } + return Ut(e); + }); + } + } + if (Object.isSealed) { + var $t = !i(function () { + return Object.isSealed('foo'); + }); + if (!$t) { + var Jt = Object.isSealed; + ne(Object, 'isSealed', function isSealed(e) { + if (!ce.TypeIsObject(e)) { + return true; + } + return Jt(e); + }); + } + } + if (Object.freeze) { + var Xt = !i(function () { + return Object.freeze('foo'); + }); + if (!Xt) { + var Kt = Object.freeze; + ne(Object, 'freeze', function freeze(e) { + if (!ce.TypeIsObject(e)) { + return e; + } + return Kt(e); + }); + } + } + if (Object.isFrozen) { + var Zt = !i(function () { + return Object.isFrozen('foo'); + }); + if (!Zt) { + var Yt = Object.isFrozen; + ne(Object, 'isFrozen', function isFrozen(e) { + if (!ce.TypeIsObject(e)) { + return true; + } + return Yt(e); + }); + } + } + if (Object.preventExtensions) { + var Qt = !i(function () { + return Object.preventExtensions('foo'); + }); + if (!Qt) { + var er = Object.preventExtensions; + ne(Object, 'preventExtensions', function preventExtensions(e) { + if (!ce.TypeIsObject(e)) { + return e; + } + return er(e); + }); + } + } + if (Object.isExtensible) { + var tr = !i(function () { + return Object.isExtensible('foo'); + }); + if (!tr) { + var rr = Object.isExtensible; + ne(Object, 'isExtensible', function isExtensible(e) { + if (!ce.TypeIsObject(e)) { + return false; + } + return rr(e); + }); + } + } + if (Object.getPrototypeOf) { + var nr = !i(function () { + return Object.getPrototypeOf('foo'); + }); + if (!nr) { + var or = Object.getPrototypeOf; + ne(Object, 'getPrototypeOf', function getPrototypeOf(e) { + return or(ce.ToObject(e)); + }); + } + } + var ir = + s && + (function () { + var e = Object.getOwnPropertyDescriptor(RegExp.prototype, 'flags'); + return e && ce.IsCallable(e.get); + })(); + if (s && !ir) { + var ar = function flags() { + if (!ce.TypeIsObject(this)) { + throw new TypeError('Method called on incompatible type: must be an object.'); + } + var e = ''; + if (this.global) { + e += 'g'; + } + if (this.ignoreCase) { + e += 'i'; + } + if (this.multiline) { + e += 'm'; + } + if (this.unicode) { + e += 'u'; + } + if (this.sticky) { + e += 'y'; + } + return e; + }; + m.getter(RegExp.prototype, 'flags', ar); + } + var ur = + s && + a(function () { + return String(new RegExp(/a/g, 'i')) === '/a/i'; + }); + var fr = + oe && + s && + (function () { + var e = /./; + e[$.match] = false; + return RegExp(e) === e; + })(); + var sr = a(function () { + return RegExp.prototype.toString.call({ source: 'abc' }) === '/abc/'; + }); + var cr = + sr && + a(function () { + return RegExp.prototype.toString.call({ source: 'a', flags: 'b' }) === '/a/b'; + }); + if (!sr || !cr) { + var lr = RegExp.prototype.toString; + h( + RegExp.prototype, + 'toString', + function toString() { + var e = ce.RequireObjectCoercible(this); + if (re.regex(e)) { + return t(lr, e); + } + var r = ue(e.source); + var n = ue(e.flags); + return '/' + r + '/' + n; + }, + true + ); + m.preserveToString(RegExp.prototype.toString, lr); + } + if (s && (!ur || fr)) { + var pr = Object.getOwnPropertyDescriptor(RegExp.prototype, 'flags').get; + var vr = Object.getOwnPropertyDescriptor(RegExp.prototype, 'source') || {}; + var yr = function () { + return this.source; + }; + var hr = ce.IsCallable(vr.get) ? vr.get : yr; + var br = RegExp; + var gr = (function () { + return function RegExp(e, t) { + var r = ce.IsRegExp(e); + var n = this instanceof RegExp; + if (!n && r && typeof t === 'undefined' && e.constructor === RegExp) { + return e; + } + var o = e; + var i = t; + if (re.regex(e)) { + o = ce.Call(hr, e); + i = typeof t === 'undefined' ? ce.Call(pr, e) : t; + return new RegExp(o, i); + } else if (r) { + o = e.source; + i = typeof t === 'undefined' ? e.flags : t; + } + return new br(e, t); + }; + })(); + Ee(br, gr, { $input: true }); + RegExp = gr; + m.redefine(S, 'RegExp', gr); + } + if (s) { + var dr = { + input: '$_', + lastMatch: '$&', + lastParen: '$+', + leftContext: '$`', + rightContext: "$'", + }; + l(n(dr), function (e) { + if (e in RegExp && !(dr[e] in RegExp)) { + m.getter(RegExp, dr[e], function get() { + return RegExp[e]; + }); + } + }); + } + Ce(RegExp); + var mr = 1 / Number.EPSILON; + var Or = function roundTiesToEven(e) { + return e + mr - mr; + }; + var wr = Math.pow(2, -23); + var jr = Math.pow(2, 127) * (2 - wr); + var Sr = Math.pow(2, -126); + var Tr = Math.E; + var Ir = Math.LOG2E; + var Er = Math.LOG10E; + var Pr = Number.prototype.clz; + delete Number.prototype.clz; + var Cr = { + acosh: function acosh(e) { + var t = Number(e); + if (X(t) || e < 1) { + return NaN; + } + if (t === 1) { + return 0; + } + if (t === Infinity) { + return t; + } + var r = 1 / (t * t); + if (t < 2) { + return Y(t - 1 + D(1 - r) * t); + } + var n = t / 2; + return Y(n + D(1 - r) * n - 1) + 1 / Ir; + }, + asinh: function asinh(e) { + var t = Number(e); + if (t === 0 || !T(t)) { + return t; + } + var r = k(t); + var n = r * r; + var o = Z(t); + if (r < 1) { + return o * Y(r + n / (D(n + 1) + 1)); + } + return o * (Y(r / 2 + (D(1 + 1 / n) * r) / 2 - 1) + 1 / Ir); + }, + atanh: function atanh(e) { + var t = Number(e); + if (t === 0) { + return t; + } + if (t === -1) { + return -Infinity; + } + if (t === 1) { + return Infinity; + } + if (X(t) || t < -1 || t > 1) { + return NaN; + } + var r = k(t); + return (Z(t) * Y((2 * r) / (1 - r))) / 2; + }, + cbrt: function cbrt(e) { + var t = Number(e); + if (t === 0) { + return t; + } + var r = t < 0; + var n; + if (r) { + t = -t; + } + if (t === Infinity) { + n = Infinity; + } else { + n = L(F(t) / 3); + n = (t / (n * n) + 2 * n) / 3; + } + return r ? -n : n; + }, + clz32: function clz32(e) { + var t = Number(e); + var r = ce.ToUint32(t); + if (r === 0) { + return 32; + } + return Pr ? ce.Call(Pr, r) : 31 - _(F(r + 0.5) * Ir); + }, + cosh: function cosh(e) { + var t = Number(e); + if (t === 0) { + return 1; + } + if (X(t)) { + return NaN; + } + if (!T(t)) { + return Infinity; + } + var r = L(k(t) - 1); + return (r + 1 / (r * Tr * Tr)) * (Tr / 2); + }, + expm1: function expm1(e) { + var t = Number(e); + if (t === -Infinity) { + return -1; + } + if (!T(t) || t === 0) { + return t; + } + if (k(t) > 0.5) { + return L(t) - 1; + } + var r = t; + var n = 0; + var o = 1; + while (n + r !== n) { + n += r; + o += 1; + r *= t / o; + } + return n; + }, + hypot: function hypot(e, t) { + var r = 0; + var n = 0; + for (var o = 0; o < arguments.length; ++o) { + var i = k(Number(arguments[o])); + if (n < i) { + r *= (n / i) * (n / i); + r += 1; + n = i; + } else { + r += i > 0 ? (i / n) * (i / n) : i; + } + } + return n === Infinity ? Infinity : n * D(r); + }, + log2: function log2(e) { + return F(e) * Ir; + }, + log10: function log10(e) { + return F(e) * Er; + }, + log1p: Y, + sign: Z, + sinh: function sinh(e) { + var t = Number(e); + if (!T(t) || t === 0) { + return t; + } + var r = k(t); + if (r < 1) { + var n = Math.expm1(r); + return (Z(t) * n * (1 + 1 / (n + 1))) / 2; + } + var o = L(r - 1); + return Z(t) * (o - 1 / (o * Tr * Tr)) * (Tr / 2); + }, + tanh: function tanh(e) { + var t = Number(e); + if (X(t) || t === 0) { + return t; + } + if (t >= 20) { + return 1; + } + if (t <= -20) { + return -1; + } + return (Math.expm1(t) - Math.expm1(-t)) / (L(t) + L(-t)); + }, + trunc: function trunc(e) { + var t = Number(e); + return t < 0 ? -_(-t) : _(t); + }, + imul: function imul(e, t) { + var r = ce.ToUint32(e); + var n = ce.ToUint32(t); + var o = (r >>> 16) & 65535; + var i = r & 65535; + var a = (n >>> 16) & 65535; + var u = n & 65535; + return (i * u + (((o * u + i * a) << 16) >>> 0)) | 0; + }, + fround: function fround(e) { + var t = Number(e); + if (t === 0 || t === Infinity || t === -Infinity || X(t)) { + return t; + } + var r = Z(t); + var n = k(t); + if (n < Sr) { + return r * Or(n / Sr / wr) * Sr * wr; + } + var o = (1 + wr / Number.EPSILON) * n; + var i = o - (o - n); + if (i > jr || X(i)) { + return r * Infinity; + } + return r * i; + }, + }; + var Mr = function withinULPDistance(e, t, r) { + return k(1 - e / t) / Number.EPSILON < (r || 8); + }; + b(Math, Cr); + h(Math, 'sinh', Cr.sinh, Math.sinh(710) === Infinity); + h(Math, 'cosh', Cr.cosh, Math.cosh(710) === Infinity); + h(Math, 'log1p', Cr.log1p, Math.log1p(-1e-17) !== -1e-17); + h(Math, 'asinh', Cr.asinh, Math.asinh(-1e7) !== -Math.asinh(1e7)); + h(Math, 'asinh', Cr.asinh, Math.asinh(1e300) === Infinity); + h(Math, 'atanh', Cr.atanh, Math.atanh(1e-300) === 0); + h(Math, 'tanh', Cr.tanh, Math.tanh(-2e-17) !== -2e-17); + h(Math, 'acosh', Cr.acosh, Math.acosh(Number.MAX_VALUE) === Infinity); + h(Math, 'acosh', Cr.acosh, !Mr(Math.acosh(1 + Number.EPSILON), Math.sqrt(2 * Number.EPSILON))); + h(Math, 'cbrt', Cr.cbrt, !Mr(Math.cbrt(1e-300), 1e-100)); + h(Math, 'sinh', Cr.sinh, Math.sinh(-2e-17) !== -2e-17); + var xr = Math.expm1(10); + h(Math, 'expm1', Cr.expm1, xr > 22025.465794806718 || xr < 22025.465794806718); + var Nr = Math.round; + var Ar = + Math.round(0.5 - Number.EPSILON / 4) === 0 && Math.round(-0.5 + Number.EPSILON / 3.99) === 1; + var Rr = mr + 1; + var _r = 2 * mr - 1; + var kr = [Rr, _r].every(function (e) { + return Math.round(e) === e; + }); + h( + Math, + 'round', + function round(e) { + var t = _(e); + var r = t === -1 ? -0 : t + 1; + return e - t < 0.5 ? t : r; + }, + !Ar || !kr + ); + m.preserveToString(Math.round, Nr); + var Lr = Math.imul; + if (Math.imul(4294967295, 5) !== -5) { + Math.imul = Cr.imul; + m.preserveToString(Math.imul, Lr); + } + if (Math.imul.length !== 2) { + ne(Math, 'imul', function imul(e, t) { + return ce.Call(Lr, Math, arguments); + }); + } + var Fr = (function () { + var e = S.setTimeout; + if (typeof e !== 'function' && typeof e !== 'object') { + return; + } + ce.IsPromise = function (e) { + if (!ce.TypeIsObject(e)) { + return false; + } + if (typeof e._promise === 'undefined') { + return false; + } + return true; + }; + var r = function (e) { + if (!ce.IsConstructor(e)) { + throw new TypeError('Bad promise constructor'); + } + var t = this; + var r = function (e, r) { + if (t.resolve !== void 0 || t.reject !== void 0) { + throw new TypeError('Bad Promise implementation!'); + } + t.resolve = e; + t.reject = r; + }; + t.resolve = void 0; + t.reject = void 0; + t.promise = new e(r); + if (!(ce.IsCallable(t.resolve) && ce.IsCallable(t.reject))) { + throw new TypeError('Bad promise constructor'); + } + }; + var n; + if (typeof window !== 'undefined' && ce.IsCallable(window.postMessage)) { + n = function () { + var e = []; + var t = 'zero-timeout-message'; + var r = function (r) { + M(e, r); + window.postMessage(t, '*'); + }; + var n = function (r) { + if (r.source === window && r.data === t) { + r.stopPropagation(); + if (e.length === 0) { + return; + } + var n = N(e); + n(); + } + }; + window.addEventListener('message', n, true); + return r; + }; + } + var o = function () { + var e = S.Promise; + var t = e && e.resolve && e.resolve(); + return ( + t && + function (e) { + return t.then(e); + } + ); + }; + var i = ce.IsCallable(S.setImmediate) + ? S.setImmediate + : typeof process === 'object' && process.nextTick + ? process.nextTick + : o() || + (ce.IsCallable(n) + ? n() + : function (t) { + e(t, 0); + }); + var a = function (e) { + return e; + }; + var u = function (e) { + throw e; + }; + var f = 0; + var s = 1; + var c = 2; + var l = 0; + var p = 1; + var v = 2; + var y = {}; + var h = function (e, t, r) { + i(function () { + g(e, t, r); + }); + }; + var g = function (e, t, r) { + var n, o; + if (t === y) { + return e(r); + } + try { + n = e(r); + o = t.resolve; + } catch (i) { + n = i; + o = t.reject; + } + o(n); + }; + var d = function (e, t) { + var r = e._promise; + var n = r.reactionLength; + if (n > 0) { + h(r.fulfillReactionHandler0, r.reactionCapability0, t); + r.fulfillReactionHandler0 = void 0; + r.rejectReactions0 = void 0; + r.reactionCapability0 = void 0; + if (n > 1) { + for (var o = 1, i = 0; o < n; o++, i += 3) { + h(r[i + l], r[i + v], t); + e[i + l] = void 0; + e[i + p] = void 0; + e[i + v] = void 0; + } + } + } + r.result = t; + r.state = s; + r.reactionLength = 0; + }; + var m = function (e, t) { + var r = e._promise; + var n = r.reactionLength; + if (n > 0) { + h(r.rejectReactionHandler0, r.reactionCapability0, t); + r.fulfillReactionHandler0 = void 0; + r.rejectReactions0 = void 0; + r.reactionCapability0 = void 0; + if (n > 1) { + for (var o = 1, i = 0; o < n; o++, i += 3) { + h(r[i + p], r[i + v], t); + e[i + l] = void 0; + e[i + p] = void 0; + e[i + v] = void 0; + } + } + } + r.result = t; + r.state = c; + r.reactionLength = 0; + }; + var O = function (e) { + var t = false; + var r = function (r) { + var n; + if (t) { + return; + } + t = true; + if (r === e) { + return m(e, new TypeError('Self resolution')); + } + if (!ce.TypeIsObject(r)) { + return d(e, r); + } + try { + n = r.then; + } catch (o) { + return m(e, o); + } + if (!ce.IsCallable(n)) { + return d(e, r); + } + i(function () { + j(e, r, n); + }); + }; + var n = function (r) { + if (t) { + return; + } + t = true; + return m(e, r); + }; + return { resolve: r, reject: n }; + }; + var w = function (e, r, n, o) { + if (e === I) { + t(e, r, n, o, y); + } else { + t(e, r, n, o); + } + }; + var j = function (e, t, r) { + var n = O(e); + var o = n.resolve; + var i = n.reject; + try { + w(r, t, o, i); + } catch (a) { + i(a); + } + }; + var T, I; + var E = (function () { + var e = function Promise(t) { + if (!(this instanceof e)) { + throw new TypeError('Constructor Promise requires "new"'); + } + if (this && this._promise) { + throw new TypeError('Bad construction'); + } + if (!ce.IsCallable(t)) { + throw new TypeError('not a valid resolver'); + } + var r = Ae(this, e, T, { + _promise: { + result: void 0, + state: f, + reactionLength: 0, + fulfillReactionHandler0: void 0, + rejectReactionHandler0: void 0, + reactionCapability0: void 0, + }, + }); + var n = O(r); + var o = n.reject; + try { + t(n.resolve, o); + } catch (i) { + o(i); + } + return r; + }; + return e; + })(); + T = E.prototype; + var P = function (e, t, r, n) { + var o = false; + return function (i) { + if (o) { + return; + } + o = true; + t[e] = i; + if (--n.count === 0) { + var a = r.resolve; + a(t); + } + }; + }; + var C = function (e, t, r) { + var n = e.iterator; + var o = []; + var i = { count: 1 }; + var a, u; + var f = 0; + while (true) { + try { + a = ce.IteratorStep(n); + if (a === false) { + e.done = true; + break; + } + u = a.value; + } catch (s) { + e.done = true; + throw s; + } + o[f] = void 0; + var c = t.resolve(u); + var l = P(f, o, r, i); + i.count += 1; + w(c.then, c, l, r.reject); + f += 1; + } + if (--i.count === 0) { + var p = r.resolve; + p(o); + } + return r.promise; + }; + var x = function (e, t, r) { + var n = e.iterator; + var o, i, a; + while (true) { + try { + o = ce.IteratorStep(n); + if (o === false) { + e.done = true; + break; + } + i = o.value; + } catch (u) { + e.done = true; + throw u; + } + a = t.resolve(i); + w(a.then, a, r.resolve, r.reject); + } + return r.promise; + }; + b(E, { + all: function all(e) { + var t = this; + if (!ce.TypeIsObject(t)) { + throw new TypeError('Promise is not object'); + } + var n = new r(t); + var o, i; + try { + o = ce.GetIterator(e); + i = { iterator: o, done: false }; + return C(i, t, n); + } catch (a) { + var u = a; + if (i && !i.done) { + try { + ce.IteratorClose(o, true); + } catch (f) { + u = f; + } + } + var s = n.reject; + s(u); + return n.promise; + } + }, + race: function race(e) { + var t = this; + if (!ce.TypeIsObject(t)) { + throw new TypeError('Promise is not object'); + } + var n = new r(t); + var o, i; + try { + o = ce.GetIterator(e); + i = { iterator: o, done: false }; + return x(i, t, n); + } catch (a) { + var u = a; + if (i && !i.done) { + try { + ce.IteratorClose(o, true); + } catch (f) { + u = f; + } + } + var s = n.reject; + s(u); + return n.promise; + } + }, + reject: function reject(e) { + var t = this; + if (!ce.TypeIsObject(t)) { + throw new TypeError('Bad promise constructor'); + } + var n = new r(t); + var o = n.reject; + o(e); + return n.promise; + }, + resolve: function resolve(e) { + var t = this; + if (!ce.TypeIsObject(t)) { + throw new TypeError('Bad promise constructor'); + } + if (ce.IsPromise(e)) { + var n = e.constructor; + if (n === t) { + return e; + } + } + var o = new r(t); + var i = o.resolve; + i(e); + return o.promise; + }, + }); + b(T, { + catch: function (e) { + return this.then(null, e); + }, + then: function then(e, t) { + var n = this; + if (!ce.IsPromise(n)) { + throw new TypeError('not a promise'); + } + var o = ce.SpeciesConstructor(n, E); + var i; + var b = arguments.length > 2 && arguments[2] === y; + if (b && o === E) { + i = y; + } else { + i = new r(o); + } + var g = ce.IsCallable(e) ? e : a; + var d = ce.IsCallable(t) ? t : u; + var m = n._promise; + var O; + if (m.state === f) { + if (m.reactionLength === 0) { + m.fulfillReactionHandler0 = g; + m.rejectReactionHandler0 = d; + m.reactionCapability0 = i; + } else { + var w = 3 * (m.reactionLength - 1); + m[w + l] = g; + m[w + p] = d; + m[w + v] = i; + } + m.reactionLength += 1; + } else if (m.state === s) { + O = m.result; + h(g, i, O); + } else if (m.state === c) { + O = m.result; + h(d, i, O); + } else { + throw new TypeError('unexpected Promise state'); + } + return i.promise; + }, + }); + y = new r(E); + I = T.then; + return E; + })(); + if (S.Promise) { + delete S.Promise.accept; + delete S.Promise.defer; + delete S.Promise.prototype.chain; + } + if (typeof Fr === 'function') { + b(S, { Promise: Fr }); + var Dr = w(S.Promise, function (e) { + return e.resolve(42).then(function () {}) instanceof e; + }); + var zr = !i(function () { + return S.Promise.reject(42).then(null, 5).then(null, W); + }); + var qr = i(function () { + return S.Promise.call(3, W); + }); + var Wr = (function (e) { + var t = e.resolve(5); + t.constructor = {}; + var r = e.resolve(t); + try { + r.then(null, W).then(null, W); + } catch (n) { + return true; + } + return t === r; + })(S.Promise); + var Gr = + s && + (function () { + var e = 0; + var t = Object.defineProperty({}, 'then', { + get: function () { + e += 1; + }, + }); + Promise.resolve(t); + return e === 1; + })(); + var Hr = function BadResolverPromise(e) { + var t = new Promise(e); + e(3, function () {}); + this.then = t.then; + this.constructor = BadResolverPromise; + }; + Hr.prototype = Promise.prototype; + Hr.all = Promise.all; + var Vr = a(function () { + return !!Hr.all([1, 2]); + }); + if (!Dr || !zr || !qr || Wr || !Gr || Vr) { + Promise = Fr; + ne(S, 'Promise', Fr); + } + if (Promise.all.length !== 1) { + var Br = Promise.all; + ne(Promise, 'all', function all(e) { + return ce.Call(Br, this, arguments); + }); + } + if (Promise.race.length !== 1) { + var Ur = Promise.race; + ne(Promise, 'race', function race(e) { + return ce.Call(Ur, this, arguments); + }); + } + if (Promise.resolve.length !== 1) { + var $r = Promise.resolve; + ne(Promise, 'resolve', function resolve(e) { + return ce.Call($r, this, arguments); + }); + } + if (Promise.reject.length !== 1) { + var Jr = Promise.reject; + ne(Promise, 'reject', function reject(e) { + return ce.Call(Jr, this, arguments); + }); + } + Mt(Promise, 'all'); + Mt(Promise, 'race'); + Mt(Promise, 'resolve'); + Mt(Promise, 'reject'); + Ce(Promise); + } + var Xr = function (e) { + var t = n( + p( + e, + function (e, t) { + e[t] = true; + return e; + }, + {} + ) + ); + return e.join(':') === t.join(':'); + }; + var Kr = Xr(['z', 'a', 'bb']); + var Zr = Xr(['z', 1, 'a', '3', 2]); + if (s) { + var Yr = function fastkey(e, t) { + if (!t && !Kr) { + return null; + } + if (se(e)) { + return '^' + ce.ToString(e); + } else if (typeof e === 'string') { + return '$' + e; + } else if (typeof e === 'number') { + if (!Zr) { + return 'n' + e; + } + return e; + } else if (typeof e === 'boolean') { + return 'b' + e; + } + return null; + }; + var Qr = function emptyObject() { + return Object.create ? Object.create(null) : {}; + }; + var en = function addIterableToMap(e, n, o) { + if (r(o) || re.string(o)) { + l(o, function (e) { + if (!ce.TypeIsObject(e)) { + throw new TypeError('Iterator value ' + e + ' is not an entry object'); + } + n.set(e[0], e[1]); + }); + } else if (o instanceof e) { + t(e.prototype.forEach, o, function (e, t) { + n.set(t, e); + }); + } else { + var i, a; + if (!se(o)) { + a = n.set; + if (!ce.IsCallable(a)) { + throw new TypeError('bad map'); + } + i = ce.GetIterator(o); + } + if (typeof i !== 'undefined') { + while (true) { + var u = ce.IteratorStep(i); + if (u === false) { + break; + } + var f = u.value; + try { + if (!ce.TypeIsObject(f)) { + throw new TypeError('Iterator value ' + f + ' is not an entry object'); + } + t(a, n, f[0], f[1]); + } catch (s) { + ce.IteratorClose(i, true); + throw s; + } + } + } + } + }; + var tn = function addIterableToSet(e, n, o) { + if (r(o) || re.string(o)) { + l(o, function (e) { + n.add(e); + }); + } else if (o instanceof e) { + t(e.prototype.forEach, o, function (e) { + n.add(e); + }); + } else { + var i, a; + if (!se(o)) { + a = n.add; + if (!ce.IsCallable(a)) { + throw new TypeError('bad set'); + } + i = ce.GetIterator(o); + } + if (typeof i !== 'undefined') { + while (true) { + var u = ce.IteratorStep(i); + if (u === false) { + break; + } + var f = u.value; + try { + t(a, n, f); + } catch (s) { + ce.IteratorClose(i, true); + throw s; + } + } + } + } + }; + var rn = { + Map: (function () { + var e = {}; + var r = function MapEntry(e, t) { + this.key = e; + this.value = t; + this.next = null; + this.prev = null; + }; + r.prototype.isRemoved = function isRemoved() { + return this.key === e; + }; + var n = function isMap(e) { + return !!e._es6map; + }; + var o = function requireMapSlot(e, t) { + if (!ce.TypeIsObject(e) || !n(e)) { + throw new TypeError( + 'Method Map.prototype.' + t + ' called on incompatible receiver ' + ce.ToString(e) + ); + } + }; + var i = function MapIterator(e, t) { + o(e, '[[MapIterator]]'); + this.head = e._head; + this.i = this.head; + this.kind = t; + }; + i.prototype = { + isMapIterator: true, + next: function next() { + if (!this.isMapIterator) { + throw new TypeError('Not a MapIterator'); + } + var e = this.i; + var t = this.kind; + var r = this.head; + if (typeof this.i === 'undefined') { + return Ke(); + } + while (e.isRemoved() && e !== r) { + e = e.prev; + } + var n; + while (e.next !== r) { + e = e.next; + if (!e.isRemoved()) { + if (t === 'key') { + n = e.key; + } else if (t === 'value') { + n = e.value; + } else { + n = [e.key, e.value]; + } + this.i = e; + return Ke(n); + } + } + this.i = void 0; + return Ke(); + }, + }; + Me(i.prototype); + var a; + var u = function Map() { + if (!(this instanceof Map)) { + throw new TypeError('Constructor Map requires "new"'); + } + if (this && this._es6map) { + throw new TypeError('Bad construction'); + } + var e = Ae(this, Map, a, { + _es6map: true, + _head: null, + _map: G ? new G() : null, + _size: 0, + _storage: Qr(), + }); + var t = new r(null, null); + t.next = t.prev = t; + e._head = t; + if (arguments.length > 0) { + en(Map, e, arguments[0]); + } + return e; + }; + a = u.prototype; + m.getter(a, 'size', function () { + if (typeof this._size === 'undefined') { + throw new TypeError('size method called on incompatible Map'); + } + return this._size; + }); + b(a, { + get: function get(e) { + o(this, 'get'); + var t; + var r = Yr(e, true); + if (r !== null) { + t = this._storage[r]; + if (t) { + return t.value; + } else { + return; + } + } + if (this._map) { + t = V.call(this._map, e); + if (t) { + return t.value; + } else { + return; + } + } + var n = this._head; + var i = n; + while ((i = i.next) !== n) { + if (ce.SameValueZero(i.key, e)) { + return i.value; + } + } + }, + has: function has(e) { + o(this, 'has'); + var t = Yr(e, true); + if (t !== null) { + return typeof this._storage[t] !== 'undefined'; + } + if (this._map) { + return B.call(this._map, e); + } + var r = this._head; + var n = r; + while ((n = n.next) !== r) { + if (ce.SameValueZero(n.key, e)) { + return true; + } + } + return false; + }, + set: function set(e, t) { + o(this, 'set'); + var n = this._head; + var i = n; + var a; + var u = Yr(e, true); + if (u !== null) { + if (typeof this._storage[u] !== 'undefined') { + this._storage[u].value = t; + return this; + } else { + a = this._storage[u] = new r(e, t); + i = n.prev; + } + } else if (this._map) { + if (B.call(this._map, e)) { + V.call(this._map, e).value = t; + } else { + a = new r(e, t); + U.call(this._map, e, a); + i = n.prev; + } + } + while ((i = i.next) !== n) { + if (ce.SameValueZero(i.key, e)) { + i.value = t; + return this; + } + } + a = a || new r(e, t); + if (ce.SameValue(-0, e)) { + a.key = +0; + } + a.next = this._head; + a.prev = this._head.prev; + a.prev.next = a; + a.next.prev = a; + this._size += 1; + return this; + }, + delete: function (t) { + o(this, 'delete'); + var r = this._head; + var n = r; + var i = Yr(t, true); + if (i !== null) { + if (typeof this._storage[i] === 'undefined') { + return false; + } + n = this._storage[i].prev; + delete this._storage[i]; + } else if (this._map) { + if (!B.call(this._map, t)) { + return false; + } + n = V.call(this._map, t).prev; + H.call(this._map, t); + } + while ((n = n.next) !== r) { + if (ce.SameValueZero(n.key, t)) { + n.key = e; + n.value = e; + n.prev.next = n.next; + n.next.prev = n.prev; + this._size -= 1; + return true; + } + } + return false; + }, + clear: function clear() { + o(this, 'clear'); + this._map = G ? new G() : null; + this._size = 0; + this._storage = Qr(); + var t = this._head; + var r = t; + var n = r.next; + while ((r = n) !== t) { + r.key = e; + r.value = e; + n = r.next; + r.next = r.prev = t; + } + t.next = t.prev = t; + }, + keys: function keys() { + o(this, 'keys'); + return new i(this, 'key'); + }, + values: function values() { + o(this, 'values'); + return new i(this, 'value'); + }, + entries: function entries() { + o(this, 'entries'); + return new i(this, 'key+value'); + }, + forEach: function forEach(e) { + o(this, 'forEach'); + var r = arguments.length > 1 ? arguments[1] : null; + var n = this.entries(); + for (var i = n.next(); !i.done; i = n.next()) { + if (r) { + t(e, r, i.value[1], i.value[0], this); + } else { + e(i.value[1], i.value[0], this); + } + } + }, + }); + Me(a, a.entries); + return u; + })(), + Set: (function () { + var e = function isSet(e) { + return e._es6set && typeof e._storage !== 'undefined'; + }; + var r = function requireSetSlot(t, r) { + if (!ce.TypeIsObject(t) || !e(t)) { + throw new TypeError( + 'Set.prototype.' + r + ' called on incompatible receiver ' + ce.ToString(t) + ); + } + }; + var o; + var i = function Set() { + if (!(this instanceof Set)) { + throw new TypeError('Constructor Set requires "new"'); + } + if (this && this._es6set) { + throw new TypeError('Bad construction'); + } + var e = Ae(this, Set, o, { + _es6set: true, + '[[SetData]]': null, + _storage: Qr(), + }); + if (!e._es6set) { + throw new TypeError('bad set'); + } + if (arguments.length > 0) { + tn(Set, e, arguments[0]); + } + return e; + }; + o = i.prototype; + var a = function (e) { + var t = e; + if (t === '^null') { + return null; + } else if (t === '^undefined') { + return void 0; + } else { + var r = t.charAt(0); + if (r === '$') { + return C(t, 1); + } else if (r === 'n') { + return +C(t, 1); + } else if (r === 'b') { + return t === 'btrue'; + } + } + return +t; + }; + var u = function ensureMap(e) { + if (!e['[[SetData]]']) { + var t = new rn.Map(); + e['[[SetData]]'] = t; + l(n(e._storage), function (e) { + var r = a(e); + t.set(r, r); + }); + e['[[SetData]]'] = t; + } + e._storage = null; + }; + m.getter(i.prototype, 'size', function () { + r(this, 'size'); + if (this._storage) { + return n(this._storage).length; + } + u(this); + return this['[[SetData]]'].size; + }); + b(i.prototype, { + has: function has(e) { + r(this, 'has'); + var t; + if (this._storage && (t = Yr(e)) !== null) { + return !!this._storage[t]; + } + u(this); + return this['[[SetData]]'].has(e); + }, + add: function add(e) { + r(this, 'add'); + var t; + if (this._storage && (t = Yr(e)) !== null) { + this._storage[t] = true; + return this; + } + u(this); + this['[[SetData]]'].set(e, e); + return this; + }, + delete: function (e) { + r(this, 'delete'); + var t; + if (this._storage && (t = Yr(e)) !== null) { + var n = z(this._storage, t); + return delete this._storage[t] && n; + } + u(this); + return this['[[SetData]]']['delete'](e); + }, + clear: function clear() { + r(this, 'clear'); + if (this._storage) { + this._storage = Qr(); + } + if (this['[[SetData]]']) { + this['[[SetData]]'].clear(); + } + }, + values: function values() { + r(this, 'values'); + u(this); + return new f(this['[[SetData]]'].values()); + }, + entries: function entries() { + r(this, 'entries'); + u(this); + return new f(this['[[SetData]]'].entries()); + }, + forEach: function forEach(e) { + r(this, 'forEach'); + var n = arguments.length > 1 ? arguments[1] : null; + var o = this; + u(o); + this['[[SetData]]'].forEach(function (r, i) { + if (n) { + t(e, n, i, i, o); + } else { + e(i, i, o); + } + }); + }, + }); + h(i.prototype, 'keys', i.prototype.values, true); + Me(i.prototype, i.prototype.values); + var f = function SetIterator(e) { + this.it = e; + }; + f.prototype = { + isSetIterator: true, + next: function next() { + if (!this.isSetIterator) { + throw new TypeError('Not a SetIterator'); + } + return this.it.next(); + }, + }; + Me(f.prototype); + return i; + })(), + }; + var nn = + S.Set && + !Set.prototype['delete'] && + Set.prototype.remove && + Set.prototype.items && + Set.prototype.map && + Array.isArray(new Set().keys); + if (nn) { + S.Set = rn.Set; + } + if (S.Map || S.Set) { + var on = a(function () { + return new Map([[1, 2]]).get(1) === 2; + }); + if (!on) { + S.Map = function Map() { + if (!(this instanceof Map)) { + throw new TypeError('Constructor Map requires "new"'); + } + var e = new G(); + if (arguments.length > 0) { + en(Map, e, arguments[0]); + } + delete e.constructor; + Object.setPrototypeOf(e, S.Map.prototype); + return e; + }; + S.Map.prototype = O(G.prototype); + h(S.Map.prototype, 'constructor', S.Map, true); + m.preserveToString(S.Map, G); + } + var an = new Map(); + var un = (function () { + var e = new Map([ + [1, 0], + [2, 0], + [3, 0], + [4, 0], + ]); + e.set(-0, e); + return e.get(0) === e && e.get(-0) === e && e.has(0) && e.has(-0); + })(); + var fn = an.set(1, 2) === an; + if (!un || !fn) { + ne(Map.prototype, 'set', function set(e, r) { + t(U, this, e === 0 ? 0 : e, r); + return this; + }); + } + if (!un) { + b( + Map.prototype, + { + get: function get(e) { + return t(V, this, e === 0 ? 0 : e); + }, + has: function has(e) { + return t(B, this, e === 0 ? 0 : e); + }, + }, + true + ); + m.preserveToString(Map.prototype.get, V); + m.preserveToString(Map.prototype.has, B); + } + var sn = new Set(); + var cn = + Set.prototype['delete'] && + Set.prototype.add && + Set.prototype.has && + (function (e) { + e['delete'](0); + e.add(-0); + return !e.has(0); + })(sn); + var ln = sn.add(1) === sn; + if (!cn || !ln) { + var pn = Set.prototype.add; + Set.prototype.add = function add(e) { + t(pn, this, e === 0 ? 0 : e); + return this; + }; + m.preserveToString(Set.prototype.add, pn); + } + if (!cn) { + var vn = Set.prototype.has; + Set.prototype.has = function has(e) { + return t(vn, this, e === 0 ? 0 : e); + }; + m.preserveToString(Set.prototype.has, vn); + var yn = Set.prototype['delete']; + Set.prototype['delete'] = function SetDelete(e) { + return t(yn, this, e === 0 ? 0 : e); + }; + m.preserveToString(Set.prototype['delete'], yn); + } + var hn = w(S.Map, function (e) { + var t = new e([]); + t.set(42, 42); + return t instanceof e; + }); + var bn = Object.setPrototypeOf && !hn; + var gn = (function () { + try { + return !(S.Map() instanceof S.Map); + } catch (e) { + return e instanceof TypeError; + } + })(); + if (S.Map.length !== 0 || bn || !gn) { + S.Map = function Map() { + if (!(this instanceof Map)) { + throw new TypeError('Constructor Map requires "new"'); + } + var e = new G(); + if (arguments.length > 0) { + en(Map, e, arguments[0]); + } + delete e.constructor; + Object.setPrototypeOf(e, Map.prototype); + return e; + }; + S.Map.prototype = G.prototype; + h(S.Map.prototype, 'constructor', S.Map, true); + m.preserveToString(S.Map, G); + } + var dn = w(S.Set, function (e) { + var t = new e([]); + t.add(42, 42); + return t instanceof e; + }); + var mn = Object.setPrototypeOf && !dn; + var On = (function () { + try { + return !(S.Set() instanceof S.Set); + } catch (e) { + return e instanceof TypeError; + } + })(); + if (S.Set.length !== 0 || mn || !On) { + var wn = S.Set; + S.Set = function Set() { + if (!(this instanceof Set)) { + throw new TypeError('Constructor Set requires "new"'); + } + var e = new wn(); + if (arguments.length > 0) { + tn(Set, e, arguments[0]); + } + delete e.constructor; + Object.setPrototypeOf(e, Set.prototype); + return e; + }; + S.Set.prototype = wn.prototype; + h(S.Set.prototype, 'constructor', S.Set, true); + m.preserveToString(S.Set, wn); + } + var jn = new S.Map(); + var Sn = !a(function () { + return jn.keys().next().done; + }); + if ( + typeof S.Map.prototype.clear !== 'function' || + new S.Set().size !== 0 || + jn.size !== 0 || + typeof S.Map.prototype.keys !== 'function' || + typeof S.Set.prototype.keys !== 'function' || + typeof S.Map.prototype.forEach !== 'function' || + typeof S.Set.prototype.forEach !== 'function' || + u(S.Map) || + u(S.Set) || + typeof jn.keys().next !== 'function' || + Sn || + !hn + ) { + b(S, { Map: rn.Map, Set: rn.Set }, true); + } + if (S.Set.prototype.keys !== S.Set.prototype.values) { + h(S.Set.prototype, 'keys', S.Set.prototype.values, true); + } + Me(Object.getPrototypeOf(new S.Map().keys())); + Me(Object.getPrototypeOf(new S.Set().keys())); + if (c && S.Set.prototype.has.name !== 'has') { + var Tn = S.Set.prototype.has; + ne(S.Set.prototype, 'has', function has(e) { + return t(Tn, this, e); + }); + } + } + b(S, rn); + Ce(S.Map); + Ce(S.Set); + } + var In = function throwUnlessTargetIsObject(e) { + if (!ce.TypeIsObject(e)) { + throw new TypeError('target must be an object'); + } + }; + var En = { + apply: function apply() { + return ce.Call(ce.Call, null, arguments); + }, + construct: function construct(e, t) { + if (!ce.IsConstructor(e)) { + throw new TypeError('First argument must be a constructor.'); + } + var r = arguments.length > 2 ? arguments[2] : e; + if (!ce.IsConstructor(r)) { + throw new TypeError('new.target must be a constructor.'); + } + return ce.Construct(e, t, r, 'internal'); + }, + deleteProperty: function deleteProperty(e, t) { + In(e); + if (s) { + var r = Object.getOwnPropertyDescriptor(e, t); + if (r && !r.configurable) { + return false; + } + } + return delete e[t]; + }, + has: function has(e, t) { + In(e); + return t in e; + }, + }; + if (Object.getOwnPropertyNames) { + Object.assign(En, { + ownKeys: function ownKeys(e) { + In(e); + var t = Object.getOwnPropertyNames(e); + if (ce.IsCallable(Object.getOwnPropertySymbols)) { + x(t, Object.getOwnPropertySymbols(e)); + } + return t; + }, + }); + } + var Pn = function ConvertExceptionToBoolean(e) { + return !i(e); + }; + if (Object.preventExtensions) { + Object.assign(En, { + isExtensible: function isExtensible(e) { + In(e); + return Object.isExtensible(e); + }, + preventExtensions: function preventExtensions(e) { + In(e); + return Pn(function () { + return Object.preventExtensions(e); + }); + }, + }); + } + if (s) { + var Cn = function get(e, t, r) { + var n = Object.getOwnPropertyDescriptor(e, t); + if (!n) { + var o = Object.getPrototypeOf(e); + if (o === null) { + return void 0; + } + return Cn(o, t, r); + } + if ('value' in n) { + return n.value; + } + if (n.get) { + return ce.Call(n.get, r); + } + return void 0; + }; + var Mn = function set(e, r, n, o) { + var i = Object.getOwnPropertyDescriptor(e, r); + if (!i) { + var a = Object.getPrototypeOf(e); + if (a !== null) { + return Mn(a, r, n, o); + } + i = { + value: void 0, + writable: true, + enumerable: true, + configurable: true, + }; + } + if ('value' in i) { + if (!i.writable) { + return false; + } + if (!ce.TypeIsObject(o)) { + return false; + } + var u = Object.getOwnPropertyDescriptor(o, r); + if (u) { + return ae.defineProperty(o, r, { value: n }); + } else { + return ae.defineProperty(o, r, { + value: n, + writable: true, + enumerable: true, + configurable: true, + }); + } + } + if (i.set) { + t(i.set, o, n); + return true; + } + return false; + }; + Object.assign(En, { + defineProperty: function defineProperty(e, t, r) { + In(e); + return Pn(function () { + return Object.defineProperty(e, t, r); + }); + }, + getOwnPropertyDescriptor: function getOwnPropertyDescriptor(e, t) { + In(e); + return Object.getOwnPropertyDescriptor(e, t); + }, + get: function get(e, t) { + In(e); + var r = arguments.length > 2 ? arguments[2] : e; + return Cn(e, t, r); + }, + set: function set(e, t, r) { + In(e); + var n = arguments.length > 3 ? arguments[3] : e; + return Mn(e, t, r, n); + }, + }); + } + if (Object.getPrototypeOf) { + var xn = Object.getPrototypeOf; + En.getPrototypeOf = function getPrototypeOf(e) { + In(e); + return xn(e); + }; + } + if (Object.setPrototypeOf && En.getPrototypeOf) { + var Nn = function (e, t) { + var r = t; + while (r) { + if (e === r) { + return true; + } + r = En.getPrototypeOf(r); + } + return false; + }; + Object.assign(En, { + setPrototypeOf: function setPrototypeOf(e, t) { + In(e); + if (t !== null && !ce.TypeIsObject(t)) { + throw new TypeError('proto must be an object or null'); + } + if (t === ae.getPrototypeOf(e)) { + return true; + } + if (ae.isExtensible && !ae.isExtensible(e)) { + return false; + } + if (Nn(e, t)) { + return false; + } + Object.setPrototypeOf(e, t); + return true; + }, + }); + } + var An = function (e, t) { + if (!ce.IsCallable(S.Reflect[e])) { + h(S.Reflect, e, t); + } else { + var r = a(function () { + S.Reflect[e](1); + S.Reflect[e](NaN); + S.Reflect[e](true); + return true; + }); + if (r) { + ne(S.Reflect, e, t); + } + } + }; + Object.keys(En).forEach(function (e) { + An(e, En[e]); + }); + var Rn = S.Reflect.getPrototypeOf; + if (c && Rn && Rn.name !== 'getPrototypeOf') { + ne(S.Reflect, 'getPrototypeOf', function getPrototypeOf(e) { + return t(Rn, S.Reflect, e); + }); + } + if (S.Reflect.setPrototypeOf) { + if ( + a(function () { + S.Reflect.setPrototypeOf(1, {}); + return true; + }) + ) { + ne(S.Reflect, 'setPrototypeOf', En.setPrototypeOf); + } + } + if (S.Reflect.defineProperty) { + if ( + !a(function () { + var e = !S.Reflect.defineProperty(1, 'test', { value: 1 }); + var t = + typeof Object.preventExtensions !== 'function' || + !S.Reflect.defineProperty(Object.preventExtensions({}), 'test', {}); + return e && t; + }) + ) { + ne(S.Reflect, 'defineProperty', En.defineProperty); + } + } + if (S.Reflect.construct) { + if ( + !a(function () { + var e = function F() {}; + return S.Reflect.construct(function () {}, [], e) instanceof e; + }) + ) { + ne(S.Reflect, 'construct', En.construct); + } + } + if (String(new Date(NaN)) !== 'Invalid Date') { + var _n = Date.prototype.toString; + var kn = function toString() { + var e = +this; + if (e !== e) { + return 'Invalid Date'; + } + return ce.Call(_n, this); + }; + ne(Date.prototype, 'toString', kn); + } + var Ln = { + anchor: function anchor(e) { + return ce.CreateHTML(this, 'a', 'name', e); + }, + big: function big() { + return ce.CreateHTML(this, 'big', '', ''); + }, + blink: function blink() { + return ce.CreateHTML(this, 'blink', '', ''); + }, + bold: function bold() { + return ce.CreateHTML(this, 'b', '', ''); + }, + fixed: function fixed() { + return ce.CreateHTML(this, 'tt', '', ''); + }, + fontcolor: function fontcolor(e) { + return ce.CreateHTML(this, 'font', 'color', e); + }, + fontsize: function fontsize(e) { + return ce.CreateHTML(this, 'font', 'size', e); + }, + italics: function italics() { + return ce.CreateHTML(this, 'i', '', ''); + }, + link: function link(e) { + return ce.CreateHTML(this, 'a', 'href', e); + }, + small: function small() { + return ce.CreateHTML(this, 'small', '', ''); + }, + strike: function strike() { + return ce.CreateHTML(this, 'strike', '', ''); + }, + sub: function sub() { + return ce.CreateHTML(this, 'sub', '', ''); + }, + sup: function sub() { + return ce.CreateHTML(this, 'sup', '', ''); + }, + }; + l(Object.keys(Ln), function (e) { + var r = String.prototype[e]; + var n = false; + if (ce.IsCallable(r)) { + var o = t(r, '', ' " '); + var i = P([], o.match(/"/g)).length; + n = o !== o.toLowerCase() || i > 2; + } else { + n = true; + } + if (n) { + ne(String.prototype, e, Ln[e]); + } + }); + var Fn = (function () { + if (!oe) { + return false; + } + var e = + typeof JSON === 'object' && typeof JSON.stringify === 'function' ? JSON.stringify : null; + if (!e) { + return false; + } + if (typeof e($()) !== 'undefined') { + return true; + } + if (e([$()]) !== '[null]') { + return true; + } + var t = { a: $() }; + t[$()] = true; + if (e(t) !== '{}') { + return true; + } + return false; + })(); + var Dn = a(function () { + if (!oe) { + return true; + } + return JSON.stringify(Object($())) === '{}' && JSON.stringify([Object($())]) === '[{}]'; + }); + if (Fn || !Dn) { + var zn = JSON.stringify; + ne(JSON, 'stringify', function stringify(e) { + if (typeof e === 'symbol') { + return; + } + var n; + if (arguments.length > 1) { + n = arguments[1]; + } + var o = [e]; + if (!r(n)) { + var i = ce.IsCallable(n) ? n : null; + var a = function (e, r) { + var n = i ? t(i, this, e, r) : r; + if (typeof n !== 'symbol') { + if (re.symbol(n)) { + return Nt({})(n); + } else { + return n; + } + } + }; + o.push(a); + } else { + o.push(n); + } + if (arguments.length > 2) { + o.push(arguments[2]); + } + return zn.apply(this, o); + }); + } + return S; +}); +//# sourceMappingURL=es6-shim.map diff --git a/platform/app/public/html-templates/index.html b/platform/app/public/html-templates/index.html new file mode 100644 index 0000000..08acb95 --- /dev/null +++ b/platform/app/public/html-templates/index.html @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OHIF Viewer + + + + + + + + + + + + +
+
+ + diff --git a/platform/app/public/html-templates/rollbar.html b/platform/app/public/html-templates/rollbar.html new file mode 100644 index 0000000..98738fa --- /dev/null +++ b/platform/app/public/html-templates/rollbar.html @@ -0,0 +1,605 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OHIF Viewer + + + + + + + + + + + +
+ + + + diff --git a/platform/app/public/init-service-worker.js b/platform/app/public/init-service-worker.js new file mode 100644 index 0000000..9439162 --- /dev/null +++ b/platform/app/public/init-service-worker.js @@ -0,0 +1,62 @@ +navigator.serviceWorker.getRegistrations().then(function (registrations) { + for (let registration of registrations) { + registration.unregister(); + } +}); + +// https://developers.google.com/web/tools/workbox/modules/workbox-window +// All major browsers that support service worker also support native JavaScript +// modules, so it's perfectly fine to serve this code to any browsers +// (older browsers will just ignore it) +// +//import { Workbox } from './workbox-window.prod.mjs'; +// proper initialization +if ('function' === typeof importScripts) { + importScripts( + 'https://storage.googleapis.com/workbox-cdn/releases/6.5.4/workbox-window.prod.mjs' + ); + + var supportsServiceWorker = 'serviceWorker' in navigator; + var isNotLocalDevelopment = ['localhost', '127'].indexOf(location.hostname) === -1; + + if (supportsServiceWorker && isNotLocalDevelopment) { + const swFileLocation = (window.PUBLIC_URL || '/') + 'sw.js'; + const wb = new Workbox(swFileLocation); + + // Add an event listener to detect when the registered + // service worker has installed but is waiting to activate. + wb.addEventListener('waiting', event => { + // customize the UI prompt accordingly. + const isFirstTimeUpdatedServiceWorkerIsWaiting = event.wasWaitingBeforeRegister === false; + console.log( + 'isFirstTimeUpdatedServiceWorkerIsWaiting', + isFirstTimeUpdatedServiceWorkerIsWaiting + ); + + // Assumes your app has some sort of prompt UI element + // that a user can either accept or reject. + // const prompt = createUIPrompt({ + // onAccept: async () => { + // Assuming the user accepted the update, set up a listener + // that will reload the page as soon as the previously waiting + // service worker has taken control. + wb.addEventListener('controlling', event => { + window.location.reload(); + }); + + // Send a message telling the service worker to skip waiting. + // This will trigger the `controlling` event handler above. + // Note: for this to work, you have to add a message + // listener in your service worker. See below. + wb.messageSW({ type: 'SKIP_WAITING' }); + // }, + + // onReject: () => { + // prompt.dismiss(); + // }, + // }); + }); + + wb.register(); + } +} diff --git a/platform/app/public/manifest.json b/platform/app/public/manifest.json new file mode 100644 index 0000000..c5224ff --- /dev/null +++ b/platform/app/public/manifest.json @@ -0,0 +1,59 @@ +{ + "name": "OHIF Viewer", + "short_name": "Viewer", + "description": "OHIF Viewer", + "dir": "auto", + "lang": "en-US", + "orientation": "any", + "icons": [ + { + "src": "/assets/android-chrome-36x36.png", + "sizes": "36x36", + "type": "image/png" + }, + { + "src": "/assets/android-chrome-48x48.png", + "sizes": "48x48", + "type": "image/png" + }, + { + "src": "/assets/android-chrome-72x72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "/assets/android-chrome-96x96.png", + "sizes": "96x96", + "type": "image/png" + }, + { + "src": "/assets/android-chrome-144x144.png", + "sizes": "144x144", + "type": "image/png" + }, + { + "src": "/assets/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/assets/android-chrome-256x256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "/assets/android-chrome-384x384.png", + "sizes": "384x384", + "type": "image/png" + }, + { + "src": "/assets/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "start_url": "./index.html", + "background_color": "#000000", + "display": "standalone", + "theme_color": "#20a5d6" +} diff --git a/platform/app/public/ohif-logo-light.svg b/platform/app/public/ohif-logo-light.svg new file mode 100644 index 0000000..a0414e2 --- /dev/null +++ b/platform/app/public/ohif-logo-light.svg @@ -0,0 +1,15 @@ + + + ohif-logo + + \ No newline at end of file diff --git a/platform/app/public/ohif-logo.svg b/platform/app/public/ohif-logo.svg new file mode 100644 index 0000000..c0a8760 --- /dev/null +++ b/platform/app/public/ohif-logo.svg @@ -0,0 +1,15 @@ + + + ohif-logo + + \ No newline at end of file diff --git a/platform/app/public/oidc-client.min.js b/platform/app/public/oidc-client.min.js new file mode 100644 index 0000000..1e82a13 --- /dev/null +++ b/platform/app/public/oidc-client.min.js @@ -0,0 +1,10864 @@ +!(function webpackUniversalModuleDefinition(e, t) { + if ('object' == typeof exports && 'object' == typeof module) module.exports = t(); + else if ('function' == typeof define && define.amd) define([], t); + else { + var r = t(); + for (var n in r) ('object' == typeof exports ? exports : e)[n] = r[n]; + } +})(window, function () { + return (function (e) { + var t = {}; + function __webpack_require__(r) { + if (t[r]) return t[r].exports; + var n = (t[r] = { i: r, l: !1, exports: {} }); + return e[r].call(n.exports, n, n.exports, __webpack_require__), (n.l = !0), n.exports; + } + return ( + (__webpack_require__.m = e), + (__webpack_require__.c = t), + (__webpack_require__.d = function (e, t, r) { + __webpack_require__.o(e, t) || Object.defineProperty(e, t, { enumerable: !0, get: r }); + }), + (__webpack_require__.r = function (e) { + 'undefined' != typeof Symbol && + Symbol.toStringTag && + Object.defineProperty(e, Symbol.toStringTag, { value: 'Module' }), + Object.defineProperty(e, '__esModule', { value: !0 }); + }), + (__webpack_require__.t = function (e, t) { + if ((1 & t && (e = __webpack_require__(e)), 8 & t)) return e; + if (4 & t && 'object' == typeof e && e && e.__esModule) return e; + var r = Object.create(null); + if ( + (__webpack_require__.r(r), + Object.defineProperty(r, 'default', { enumerable: !0, value: e }), + 2 & t && 'string' != typeof e) + ) + for (var n in e) + __webpack_require__.d( + r, + n, + function (t) { + return e[t]; + }.bind(null, n) + ); + return r; + }), + (__webpack_require__.n = function (e) { + var t = + e && e.__esModule + ? function getDefault() { + return e.default; + } + : function getModuleExports() { + return e; + }; + return __webpack_require__.d(t, 'a', t), t; + }), + (__webpack_require__.o = function (e, t) { + return Object.prototype.hasOwnProperty.call(e, t); + }), + (__webpack_require__.p = ''), + __webpack_require__((__webpack_require__.s = 45)) + ); + })([ + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }); + var n = (function () { + function defineProperties(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + return function (e, t, r) { + return t && defineProperties(e.prototype, t), r && defineProperties(e, r), e; + }; + })(); + var i = { + debug: function debug() {}, + info: function info() {}, + warn: function warn() {}, + error: function error() {}, + }, + o = void 0, + s = void 0; + (t.Log = (function () { + function Log() { + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, Log); + } + return ( + (Log.reset = function reset() { + (s = 3), (o = i); + }), + (Log.debug = function debug() { + if (s >= 4) { + for (var e = arguments.length, t = Array(e), r = 0; r < e; r++) t[r] = arguments[r]; + o.debug.apply(o, Array.from(t)); + } + }), + (Log.info = function info() { + if (s >= 3) { + for (var e = arguments.length, t = Array(e), r = 0; r < e; r++) t[r] = arguments[r]; + o.info.apply(o, Array.from(t)); + } + }), + (Log.warn = function warn() { + if (s >= 2) { + for (var e = arguments.length, t = Array(e), r = 0; r < e; r++) t[r] = arguments[r]; + o.warn.apply(o, Array.from(t)); + } + }), + (Log.error = function error() { + if (s >= 1) { + for (var e = arguments.length, t = Array(e), r = 0; r < e; r++) t[r] = arguments[r]; + o.error.apply(o, Array.from(t)); + } + }), + n(Log, null, [ + { + key: 'NONE', + get: function get() { + return 0; + }, + }, + { + key: 'ERROR', + get: function get() { + return 1; + }, + }, + { + key: 'WARN', + get: function get() { + return 2; + }, + }, + { + key: 'INFO', + get: function get() { + return 3; + }, + }, + { + key: 'DEBUG', + get: function get() { + return 4; + }, + }, + { + key: 'level', + get: function get() { + return s; + }, + set: function set(e) { + if (!(0 <= e && e <= 4)) throw new Error('Invalid log level'); + s = e; + }, + }, + { + key: 'logger', + get: function get() { + return o; + }, + set: function set(e) { + if ( + (!e.debug && e.info && (e.debug = e.info), + !(e.debug && e.info && e.warn && e.error)) + ) + throw new Error('Invalid logger'); + o = e; + }, + }, + ]), + Log + ); + })()).reset(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }); + var n = (function () { + function defineProperties(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + return function (e, t, r) { + return t && defineProperties(e.prototype, t), r && defineProperties(e, r), e; + }; + })(); + var i = { + setInterval: (function (e) { + function setInterval(t, r) { + return e.apply(this, arguments); + } + return ( + (setInterval.toString = function () { + return e.toString(); + }), + setInterval + ); + })(function (e, t) { + return setInterval(e, t); + }), + clearInterval: (function (e) { + function clearInterval(t) { + return e.apply(this, arguments); + } + return ( + (clearInterval.toString = function () { + return e.toString(); + }), + clearInterval + ); + })(function (e) { + return clearInterval(e); + }), + }, + o = !1, + s = null; + t.Global = (function () { + function Global() { + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, Global); + } + return ( + (Global._testing = function _testing() { + o = !0; + }), + (Global.setXMLHttpRequest = function setXMLHttpRequest(e) { + s = e; + }), + n(Global, null, [ + { + key: 'location', + get: function get() { + if (!o) return location; + }, + }, + { + key: 'localStorage', + get: function get() { + if (!o && 'undefined' != typeof window) return localStorage; + }, + }, + { + key: 'sessionStorage', + get: function get() { + if (!o && 'undefined' != typeof window) return sessionStorage; + }, + }, + { + key: 'XMLHttpRequest', + get: function get() { + if (!o && 'undefined' != typeof window) return s || XMLHttpRequest; + }, + }, + { + key: 'timer', + get: function get() { + if (!o) return i; + }, + }, + ]), + Global + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.UrlUtility = void 0); + var n = r(0), + i = r(1); + t.UrlUtility = (function () { + function UrlUtility() { + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, UrlUtility); + } + return ( + (UrlUtility.addQueryParam = function addQueryParam(e, t, r) { + return ( + e.indexOf('?') < 0 && (e += '?'), + '?' !== e[e.length - 1] && (e += '&'), + (e += encodeURIComponent(t)), + (e += '='), + (e += encodeURIComponent(r)) + ); + }), + (UrlUtility.parseUrlFragment = function parseUrlFragment(e) { + var t = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : '#', + r = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : i.Global; + 'string' != typeof e && (e = r.location.href); + var o = e.lastIndexOf(t); + o >= 0 && (e = e.substr(o + 1)); + for (var s, a = {}, u = /([^&=]+)=([^&]*)/g, c = 0; (s = u.exec(e)); ) + if (((a[decodeURIComponent(s[1])] = decodeURIComponent(s[2])), c++ > 50)) + return ( + n.Log.error( + 'UrlUtility.parseUrlFragment: response exceeded expected number of parameters', + e + ), + { error: 'Response exceeded expected number of parameters' } + ); + for (var h in a) return a; + return {}; + }), + UrlUtility + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.MetadataService = void 0); + var n = (function () { + function defineProperties(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + return function (e, t, r) { + return t && defineProperties(e.prototype, t), r && defineProperties(e, r), e; + }; + })(), + i = r(0), + o = r(17); + t.MetadataService = (function () { + function MetadataService(e) { + var t = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : o.JsonService; + if ( + ((function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, MetadataService), + !e) + ) + throw ( + (i.Log.error('MetadataService: No settings passed to MetadataService'), + new Error('settings')) + ); + (this._settings = e), (this._jsonService = new t(['application/jwk-set+json'])); + } + return ( + (MetadataService.prototype.getMetadata = function getMetadata() { + var e = this; + return this._settings.metadata + ? (i.Log.debug('MetadataService.getMetadata: Returning metadata from settings'), + Promise.resolve(this._settings.metadata)) + : this.metadataUrl + ? (i.Log.debug( + 'MetadataService.getMetadata: getting metadata from', + this.metadataUrl + ), + this._jsonService.getJson(this.metadataUrl).then(function (t) { + return ( + i.Log.debug('MetadataService.getMetadata: json received'), + (e._settings.metadata = t), + t + ); + })) + : (i.Log.error( + 'MetadataService.getMetadata: No authority or metadataUrl configured on settings' + ), + Promise.reject(new Error('No authority or metadataUrl configured on settings'))); + }), + (MetadataService.prototype.getIssuer = function getIssuer() { + return this._getMetadataProperty('issuer'); + }), + (MetadataService.prototype.getAuthorizationEndpoint = + function getAuthorizationEndpoint() { + return this._getMetadataProperty('authorization_endpoint'); + }), + (MetadataService.prototype.getUserInfoEndpoint = function getUserInfoEndpoint() { + return this._getMetadataProperty('userinfo_endpoint'); + }), + (MetadataService.prototype.getTokenEndpoint = function getTokenEndpoint() { + return this._getMetadataProperty('token_endpoint', !0); + }), + (MetadataService.prototype.getCheckSessionIframe = function getCheckSessionIframe() { + return this._getMetadataProperty('check_session_iframe', !0); + }), + (MetadataService.prototype.getEndSessionEndpoint = function getEndSessionEndpoint() { + return this._getMetadataProperty('end_session_endpoint', !0); + }), + (MetadataService.prototype.getRevocationEndpoint = function getRevocationEndpoint() { + return this._getMetadataProperty('revocation_endpoint', !0); + }), + (MetadataService.prototype._getMetadataProperty = function _getMetadataProperty(e) { + var t = arguments.length > 1 && void 0 !== arguments[1] && arguments[1]; + return ( + i.Log.debug('MetadataService.getMetadataProperty for: ' + e), + this.getMetadata().then(function (r) { + if ( + (i.Log.debug('MetadataService.getMetadataProperty: metadata recieved'), + void 0 === r[e]) + ) { + if (!0 === t) + return void i.Log.warn( + 'MetadataService.getMetadataProperty: Metadata does not contain optional property ' + + e + ); + throw ( + (i.Log.error( + 'MetadataService.getMetadataProperty: Metadata does not contain property ' + e + ), + new Error('Metadata does not contain property ' + e)) + ); + } + return r[e]; + }) + ); + }), + (MetadataService.prototype.getSigningKeys = function getSigningKeys() { + var e = this; + return this._settings.signingKeys + ? (i.Log.debug('MetadataService.getSigningKeys: Returning signingKeys from settings'), + Promise.resolve(this._settings.signingKeys)) + : this._getMetadataProperty('jwks_uri').then(function (t) { + return ( + i.Log.debug('MetadataService.getSigningKeys: jwks_uri received', t), + e._jsonService.getJson(t).then(function (t) { + if ( + (i.Log.debug('MetadataService.getSigningKeys: key set received', t), + !t.keys) + ) + throw ( + (i.Log.error('MetadataService.getSigningKeys: Missing keys on keyset'), + new Error('Missing keys on keyset')) + ); + return (e._settings.signingKeys = t.keys), e._settings.signingKeys; + }) + ); + }); + }), + n(MetadataService, [ + { + key: 'metadataUrl', + get: function get() { + return ( + this._metadataUrl || + (this._settings.metadataUrl + ? (this._metadataUrl = this._settings.metadataUrl) + : ((this._metadataUrl = this._settings.authority), + this._metadataUrl && + this._metadataUrl.indexOf('.well-known/openid-configuration') < 0 && + ('/' !== this._metadataUrl[this._metadataUrl.length - 1] && + (this._metadataUrl += '/'), + (this._metadataUrl += '.well-known/openid-configuration')))), + this._metadataUrl + ); + }, + }, + ]), + MetadataService + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.State = void 0); + var n = (function () { + function defineProperties(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + return function (e, t, r) { + return t && defineProperties(e.prototype, t), r && defineProperties(e, r), e; + }; + })(), + i = r(0), + o = (function _interopRequireDefault(e) { + return e && e.__esModule ? e : { default: e }; + })(r(14)); + t.State = (function () { + function State() { + var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + t = e.id, + r = e.data, + n = e.created; + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, State), + (this._id = t || (0, o.default)()), + (this._data = r), + (this._created = 'number' == typeof n && n > 0 ? n : parseInt(Date.now() / 1e3)); + } + return ( + (State.prototype.toStorageString = function toStorageString() { + return ( + i.Log.debug('State.toStorageString'), + JSON.stringify({ + id: this.id, + data: this.data, + created: this.created, + }) + ); + }), + (State.fromStorageString = function fromStorageString(e) { + return i.Log.debug('State.fromStorageString'), new State(JSON.parse(e)); + }), + (State.clearStaleState = function clearStaleState(e, t) { + var r = Date.now() / 1e3 - t; + return e.getAllKeys().then(function (t) { + i.Log.debug('State.clearStaleState: got keys', t); + for ( + var n = [], + o = function _loop(o) { + var s = t[o]; + (a = e.get(s).then(function (t) { + var n = !1; + if (t) + try { + var o = State.fromStorageString(t); + i.Log.debug('State.clearStaleState: got item from key: ', s, o.created), + o.created <= r && (n = !0); + } catch (e) { + i.Log.error( + 'State.clearStaleState: Error parsing state for key', + s, + e.message + ), + (n = !0); + } + else + i.Log.debug('State.clearStaleState: no item in storage for key: ', s), + (n = !0); + if (n) + return ( + i.Log.debug('State.clearStaleState: removed item for key: ', s), + e.remove(s) + ); + })), + n.push(a); + }, + s = 0; + s < t.length; + s++ + ) { + var a; + o(s); + } + return ( + i.Log.debug('State.clearStaleState: waiting on promise count:', n.length), + Promise.all(n) + ); + }); + }), + n(State, [ + { + key: 'id', + get: function get() { + return this._id; + }, + }, + { + key: 'data', + get: function get() { + return this._data; + }, + }, + { + key: 'created', + get: function get() { + return this._created; + }, + }, + ]), + State + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.WebStorageStateStore = void 0); + var n = r(0), + i = r(1); + t.WebStorageStateStore = (function () { + function WebStorageStateStore() { + var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + t = e.prefix, + r = void 0 === t ? 'oidc.' : t, + n = e.store, + o = void 0 === n ? i.Global.localStorage : n; + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, WebStorageStateStore), + (this._store = o), + (this._prefix = r); + } + return ( + (WebStorageStateStore.prototype.set = function set(e, t) { + return ( + n.Log.debug('WebStorageStateStore.set', e), + (e = this._prefix + e), + this._store.setItem(e, t), + Promise.resolve() + ); + }), + (WebStorageStateStore.prototype.get = function get(e) { + n.Log.debug('WebStorageStateStore.get', e), (e = this._prefix + e); + var t = this._store.getItem(e); + return Promise.resolve(t); + }), + (WebStorageStateStore.prototype.remove = function remove(e) { + n.Log.debug('WebStorageStateStore.remove', e), (e = this._prefix + e); + var t = this._store.getItem(e); + return this._store.removeItem(e), Promise.resolve(t); + }), + (WebStorageStateStore.prototype.getAllKeys = function getAllKeys() { + n.Log.debug('WebStorageStateStore.getAllKeys'); + for (var e = [], t = 0; t < this._store.length; t++) { + var r = this._store.key(t); + 0 === r.indexOf(this._prefix) && e.push(r.substr(this._prefix.length)); + } + return Promise.resolve(e); + }), + WebStorageStateStore + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.OidcClientSettings = void 0); + var n = + 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator + ? function (e) { + return typeof e; + } + : function (e) { + return e && + 'function' == typeof Symbol && + e.constructor === Symbol && + e !== Symbol.prototype + ? 'symbol' + : typeof e; + }, + i = (function () { + function defineProperties(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + return function (e, t, r) { + return t && defineProperties(e.prototype, t), r && defineProperties(e, r), e; + }; + })(), + o = r(0), + s = r(5), + a = r(44), + u = r(3); + var c = 'id_token', + h = 'openid', + f = 900, + l = 300; + t.OidcClientSettings = (function () { + function OidcClientSettings() { + var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + t = e.authority, + r = e.metadataUrl, + i = e.metadata, + o = e.signingKeys, + g = e.client_id, + p = e.client_secret, + d = e.response_type, + v = void 0 === d ? c : d, + y = e.scope, + m = void 0 === y ? h : y, + S = e.redirect_uri, + F = e.post_logout_redirect_uri, + b = e.prompt, + _ = e.display, + w = e.max_age, + E = e.ui_locales, + x = e.acr_values, + C = e.resource, + P = e.filterProtocolClaims, + A = void 0 === P || P, + k = e.loadUserInfo, + I = void 0 === k || k, + B = e.staleStateAge, + R = void 0 === B ? f : B, + T = e.clockSkew, + U = void 0 === T ? l : T, + M = e.stateStore, + L = void 0 === M ? new s.WebStorageStateStore() : M, + D = e.ResponseValidatorCtor, + N = void 0 === D ? a.ResponseValidator : D, + O = e.MetadataServiceCtor, + H = void 0 === O ? u.MetadataService : O, + j = e.extraQueryParams, + K = void 0 === j ? {} : j; + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, OidcClientSettings), + (this._authority = t), + (this._metadataUrl = r), + (this._metadata = i), + (this._signingKeys = o), + (this._client_id = g), + (this._client_secret = p), + (this._response_type = v), + (this._scope = m), + (this._redirect_uri = S), + (this._post_logout_redirect_uri = F), + (this._prompt = b), + (this._display = _), + (this._max_age = w), + (this._ui_locales = E), + (this._acr_values = x), + (this._resource = C), + (this._filterProtocolClaims = !!A), + (this._loadUserInfo = !!I), + (this._staleStateAge = R), + (this._clockSkew = U), + (this._stateStore = L), + (this._validator = new N(this)), + (this._metadataService = new H(this)), + (this._extraQueryParams = 'object' === (void 0 === K ? 'undefined' : n(K)) ? K : {}); + } + return ( + i(OidcClientSettings, [ + { + key: 'client_id', + get: function get() { + return this._client_id; + }, + set: function set(e) { + if (this._client_id) + throw ( + (o.Log.error( + 'OidcClientSettings.set_client_id: client_id has already been assigned.' + ), + new Error('client_id has already been assigned.')) + ); + this._client_id = e; + }, + }, + { + key: 'client_secret', + get: function get() { + return this._client_secret; + }, + }, + { + key: 'response_type', + get: function get() { + return this._response_type; + }, + }, + { + key: 'scope', + get: function get() { + return this._scope; + }, + }, + { + key: 'redirect_uri', + get: function get() { + return this._redirect_uri; + }, + }, + { + key: 'post_logout_redirect_uri', + get: function get() { + return this._post_logout_redirect_uri; + }, + }, + { + key: 'prompt', + get: function get() { + return this._prompt; + }, + }, + { + key: 'display', + get: function get() { + return this._display; + }, + }, + { + key: 'max_age', + get: function get() { + return this._max_age; + }, + }, + { + key: 'ui_locales', + get: function get() { + return this._ui_locales; + }, + }, + { + key: 'acr_values', + get: function get() { + return this._acr_values; + }, + }, + { + key: 'resource', + get: function get() { + return this._resource; + }, + }, + { + key: 'authority', + get: function get() { + return this._authority; + }, + set: function set(e) { + if (this._authority) + throw ( + (o.Log.error( + 'OidcClientSettings.set_authority: authority has already been assigned.' + ), + new Error('authority has already been assigned.')) + ); + this._authority = e; + }, + }, + { + key: 'metadataUrl', + get: function get() { + return ( + this._metadataUrl || + ((this._metadataUrl = this.authority), + this._metadataUrl && + this._metadataUrl.indexOf('.well-known/openid-configuration') < 0 && + ('/' !== this._metadataUrl[this._metadataUrl.length - 1] && + (this._metadataUrl += '/'), + (this._metadataUrl += '.well-known/openid-configuration'))), + this._metadataUrl + ); + }, + }, + { + key: 'metadata', + get: function get() { + return this._metadata; + }, + set: function set(e) { + this._metadata = e; + }, + }, + { + key: 'signingKeys', + get: function get() { + return this._signingKeys; + }, + set: function set(e) { + this._signingKeys = e; + }, + }, + { + key: 'filterProtocolClaims', + get: function get() { + return this._filterProtocolClaims; + }, + }, + { + key: 'loadUserInfo', + get: function get() { + return this._loadUserInfo; + }, + }, + { + key: 'staleStateAge', + get: function get() { + return this._staleStateAge; + }, + }, + { + key: 'clockSkew', + get: function get() { + return this._clockSkew; + }, + }, + { + key: 'stateStore', + get: function get() { + return this._stateStore; + }, + }, + { + key: 'validator', + get: function get() { + return this._validator; + }, + }, + { + key: 'metadataService', + get: function get() { + return this._metadataService; + }, + }, + { + key: 'extraQueryParams', + get: function get() { + return this._extraQueryParams; + }, + set: function set(e) { + 'object' === (void 0 === e ? 'undefined' : n(e)) + ? (this._extraQueryParams = e) + : (this._extraQueryParams = {}); + }, + }, + ]), + OidcClientSettings + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.CordovaPopupWindow = void 0); + var n = (function () { + function defineProperties(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + return function (e, t, r) { + return t && defineProperties(e.prototype, t), r && defineProperties(e, r), e; + }; + })(), + i = r(0); + var o = 'location=no,toolbar=no,zoom=no', + s = '_blank'; + t.CordovaPopupWindow = (function () { + function CordovaPopupWindow(e) { + var t = this; + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, CordovaPopupWindow), + (this._promise = new Promise(function (e, r) { + (t._resolve = e), (t._reject = r); + })), + (this.features = e.popupWindowFeatures || o), + (this.target = e.popupWindowTarget || s), + (this.redirect_uri = e.startUrl), + i.Log.debug('CordovaPopupWindow.ctor: redirect_uri: ' + this.redirect_uri); + } + return ( + (CordovaPopupWindow.prototype._isInAppBrowserInstalled = + function _isInAppBrowserInstalled(e) { + return [ + 'cordova-plugin-inappbrowser', + 'cordova-plugin-inappbrowser.inappbrowser', + 'org.apache.cordova.inappbrowser', + ].some(function (t) { + return e.hasOwnProperty(t); + }); + }), + (CordovaPopupWindow.prototype.navigate = function navigate(e) { + if (e && e.url) { + if (!window.cordova) return this._error('cordova is undefined'); + var t = window.cordova.require('cordova/plugin_list').metadata; + if (!1 === this._isInAppBrowserInstalled(t)) + return this._error('InAppBrowser plugin not found'); + (this._popup = cordova.InAppBrowser.open(e.url, this.target, this.features)), + this._popup + ? (i.Log.debug('CordovaPopupWindow.navigate: popup successfully created'), + (this._exitCallbackEvent = this._exitCallback.bind(this)), + (this._loadStartCallbackEvent = this._loadStartCallback.bind(this)), + this._popup.addEventListener('exit', this._exitCallbackEvent, !1), + this._popup.addEventListener('loadstart', this._loadStartCallbackEvent, !1)) + : this._error('Error opening popup window'); + } else this._error('No url provided'); + return this.promise; + }), + (CordovaPopupWindow.prototype._loadStartCallback = function _loadStartCallback(e) { + 0 === e.url.indexOf(this.redirect_uri) && this._success({ url: e.url }); + }), + (CordovaPopupWindow.prototype._exitCallback = function _exitCallback(e) { + this._error(e); + }), + (CordovaPopupWindow.prototype._success = function _success(e) { + this._cleanup(), + i.Log.debug('CordovaPopupWindow: Successful response from cordova popup window'), + this._resolve(e); + }), + (CordovaPopupWindow.prototype._error = function _error(e) { + this._cleanup(), i.Log.error(e), this._reject(new Error(e)); + }), + (CordovaPopupWindow.prototype.close = function close() { + this._cleanup(); + }), + (CordovaPopupWindow.prototype._cleanup = function _cleanup() { + this._popup && + (i.Log.debug('CordovaPopupWindow: cleaning up popup'), + this._popup.removeEventListener('exit', this._exitCallbackEvent, !1), + this._popup.removeEventListener('loadstart', this._loadStartCallbackEvent, !1), + this._popup.close()), + (this._popup = null); + }), + n(CordovaPopupWindow, [ + { + key: 'promise', + get: function get() { + return this._promise; + }, + }, + ]), + CordovaPopupWindow + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.TokenRevocationClient = void 0); + var n = r(0), + i = r(3), + o = r(1); + t.TokenRevocationClient = (function () { + function TokenRevocationClient(e) { + var t = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : o.Global.XMLHttpRequest, + r = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : i.MetadataService; + if ( + ((function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, TokenRevocationClient), + !e) + ) + throw ( + (n.Log.error('TokenRevocationClient.ctor: No settings provided'), + new Error('No settings provided.')) + ); + (this._settings = e), + (this._XMLHttpRequestCtor = t), + (this._metadataService = new r(this._settings)); + } + return ( + (TokenRevocationClient.prototype.revoke = function revoke(e, t) { + var r = this; + if (!e) + throw ( + (n.Log.error('TokenRevocationClient.revoke: No accessToken provided'), + new Error('No accessToken provided.')) + ); + return this._metadataService.getRevocationEndpoint().then(function (i) { + if (i) { + n.Log.error('TokenRevocationClient.revoke: Revoking access token'); + var o = r._settings.client_id, + s = r._settings.client_secret; + return r._revoke(i, o, s, e); + } + if (t) + throw ( + (n.Log.error('TokenRevocationClient.revoke: Revocation not supported'), + new Error('Revocation not supported')) + ); + }); + }), + (TokenRevocationClient.prototype._revoke = function _revoke(e, t, r, i) { + var o = this; + return new Promise(function (s, a) { + var u = new o._XMLHttpRequestCtor(); + u.open('POST', e), + (u.onload = function () { + n.Log.debug( + 'TokenRevocationClient.revoke: HTTP response received, status', + u.status + ), + 200 === u.status ? s() : a(Error(u.statusText + ' (' + u.status + ')')); + }); + var c = 'client_id=' + encodeURIComponent(t); + r && (c += '&client_secret=' + encodeURIComponent(r)), + (c += '&token_type_hint=' + encodeURIComponent('access_token')), + (c += '&token=' + encodeURIComponent(i)), + u.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'), + u.send(c); + }); + }), + TokenRevocationClient + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.CheckSessionIFrame = void 0); + var n = r(0); + var i = 2e3; + t.CheckSessionIFrame = (function () { + function CheckSessionIFrame(e, t, r, n) { + var o = !(arguments.length > 4 && void 0 !== arguments[4]) || arguments[4]; + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, CheckSessionIFrame), + (this._callback = e), + (this._client_id = t), + (this._url = r), + (this._interval = n || i), + (this._stopOnError = o); + var s = r.indexOf('/', r.indexOf('//') + 2); + (this._frame_origin = r.substr(0, s)), + (this._frame = window.document.createElement('iframe')), + (this._frame.style.visibility = 'hidden'), + (this._frame.style.position = 'absolute'), + (this._frame.style.display = 'none'), + (this._frame.style.width = 0), + (this._frame.style.height = 0), + (this._frame.src = r); + } + return ( + (CheckSessionIFrame.prototype.load = function load() { + var e = this; + return new Promise(function (t) { + (e._frame.onload = function () { + t(); + }), + window.document.body.appendChild(e._frame), + (e._boundMessageEvent = e._message.bind(e)), + window.addEventListener('message', e._boundMessageEvent, !1); + }); + }), + (CheckSessionIFrame.prototype._message = function _message(e) { + e.origin === this._frame_origin && + e.source === this._frame.contentWindow && + ('error' === e.data + ? (n.Log.error('CheckSessionIFrame: error message from check session op iframe'), + this._stopOnError && this.stop()) + : 'changed' === e.data + ? (n.Log.debug('CheckSessionIFrame: changed message from check session op iframe'), + this.stop(), + this._callback()) + : n.Log.debug( + 'CheckSessionIFrame: ' + e.data + ' message from check session op iframe' + )); + }), + (CheckSessionIFrame.prototype.start = function start(e) { + var t = this; + if (this._session_state !== e) { + n.Log.debug('CheckSessionIFrame.start'), this.stop(), (this._session_state = e); + var r = function send() { + t._frame.contentWindow.postMessage( + t._client_id + ' ' + t._session_state, + t._frame_origin + ); + }; + r(), (this._timer = window.setInterval(r, this._interval)); + } + }), + (CheckSessionIFrame.prototype.stop = function stop() { + (this._session_state = null), + this._timer && + (n.Log.debug('CheckSessionIFrame.stop'), + window.clearInterval(this._timer), + (this._timer = null)); + }), + CheckSessionIFrame + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.SessionMonitor = void 0); + var n = (function () { + function defineProperties(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + return function (e, t, r) { + return t && defineProperties(e.prototype, t), r && defineProperties(e, r), e; + }; + })(), + i = r(0), + o = r(9); + t.SessionMonitor = (function () { + function SessionMonitor(e) { + var t = this, + r = + arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : o.CheckSessionIFrame; + if ( + ((function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, SessionMonitor), + !e) + ) + throw ( + (i.Log.error('SessionMonitor.ctor: No user manager passed to SessionMonitor'), + new Error('userManager')) + ); + (this._userManager = e), + (this._CheckSessionIFrameCtor = r), + this._userManager.events.addUserLoaded(this._start.bind(this)), + this._userManager.events.addUserUnloaded(this._stop.bind(this)), + this._userManager + .getUser() + .then(function (e) { + e && t._start(e); + }) + .catch(function (e) { + i.Log.error('SessionMonitor ctor: error from getUser:', e.message); + }); + } + return ( + (SessionMonitor.prototype._start = function _start(e) { + var t = this, + r = e.session_state; + r && + ((this._sub = e.profile.sub), + (this._sid = e.profile.sid), + i.Log.debug('SessionMonitor._start: session_state:', r, ', sub:', this._sub), + this._checkSessionIFrame + ? this._checkSessionIFrame.start(r) + : this._metadataService + .getCheckSessionIframe() + .then(function (e) { + if (e) { + i.Log.debug('SessionMonitor._start: Initializing check session iframe'); + var n = t._client_id, + o = t._checkSessionInterval, + s = t._stopCheckSessionOnError; + (t._checkSessionIFrame = new t._CheckSessionIFrameCtor( + t._callback.bind(t), + n, + e, + o, + s + )), + t._checkSessionIFrame.load().then(function () { + t._checkSessionIFrame.start(r); + }); + } else + i.Log.warn( + 'SessionMonitor._start: No check session iframe found in the metadata' + ); + }) + .catch(function (e) { + i.Log.error( + 'SessionMonitor._start: Error from getCheckSessionIframe:', + e.message + ); + })); + }), + (SessionMonitor.prototype._stop = function _stop() { + (this._sub = null), + (this._sid = null), + this._checkSessionIFrame && + (i.Log.debug('SessionMonitor._stop'), this._checkSessionIFrame.stop()); + }), + (SessionMonitor.prototype._callback = function _callback() { + var e = this; + this._userManager + .querySessionStatus() + .then(function (t) { + var r = !0; + t + ? t.sub === e._sub + ? ((r = !1), + e._checkSessionIFrame.start(t.session_state), + t.sid === e._sid + ? i.Log.debug( + 'SessionMonitor._callback: Same sub still logged in at OP, restarting check session iframe; session_state:', + t.session_state + ) + : (i.Log.debug( + 'SessionMonitor._callback: Same sub still logged in at OP, session state has changed, restarting check session iframe; session_state:', + t.session_state + ), + e._userManager.events._raiseUserSessionChanged())) + : i.Log.debug( + 'SessionMonitor._callback: Different subject signed into OP:', + t.sub + ) + : i.Log.debug('SessionMonitor._callback: Subject no longer signed into OP'), + r && + (i.Log.debug( + 'SessionMonitor._callback: SessionMonitor._callback; raising signed out event' + ), + e._userManager.events._raiseUserSignedOut()); + }) + .catch(function (t) { + i.Log.debug( + 'SessionMonitor._callback: Error calling queryCurrentSigninSession; raising signed out event', + t.message + ), + e._userManager.events._raiseUserSignedOut(); + }); + }), + n(SessionMonitor, [ + { + key: '_settings', + get: function get() { + return this._userManager.settings; + }, + }, + { + key: '_metadataService', + get: function get() { + return this._userManager.metadataService; + }, + }, + { + key: '_client_id', + get: function get() { + return this._settings.client_id; + }, + }, + { + key: '_checkSessionInterval', + get: function get() { + return this._settings.checkSessionInterval; + }, + }, + { + key: '_stopCheckSessionOnError', + get: function get() { + return this._settings.stopCheckSessionOnError; + }, + }, + ]), + SessionMonitor + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.Event = void 0); + var n = r(0); + t.Event = (function () { + function Event(e) { + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, Event), + (this._name = e), + (this._callbacks = []); + } + return ( + (Event.prototype.addHandler = function addHandler(e) { + this._callbacks.push(e); + }), + (Event.prototype.removeHandler = function removeHandler(e) { + var t = this._callbacks.findIndex(function (t) { + return t === e; + }); + t >= 0 && this._callbacks.splice(t, 1); + }), + (Event.prototype.raise = function raise() { + n.Log.debug('Event: Raising event: ' + this._name); + for (var e = 0; e < this._callbacks.length; e++) { + var t; + (t = this._callbacks)[e].apply(t, arguments); + } + }), + Event + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.AccessTokenEvents = void 0); + var n = r(0), + i = r(22); + var o = 60; + t.AccessTokenEvents = (function () { + function AccessTokenEvents() { + var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + t = e.accessTokenExpiringNotificationTime, + r = void 0 === t ? o : t, + n = e.accessTokenExpiringTimer, + s = void 0 === n ? new i.Timer('Access token expiring') : n, + a = e.accessTokenExpiredTimer, + u = void 0 === a ? new i.Timer('Access token expired') : a; + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, AccessTokenEvents), + (this._accessTokenExpiringNotificationTime = r), + (this._accessTokenExpiring = s), + (this._accessTokenExpired = u); + } + return ( + (AccessTokenEvents.prototype.load = function load(e) { + if (e.access_token && void 0 !== e.expires_in) { + var t = e.expires_in; + if ( + (n.Log.debug( + 'AccessTokenEvents.load: access token present, remaining duration:', + t + ), + t > 0) + ) { + var r = t - this._accessTokenExpiringNotificationTime; + r <= 0 && (r = 1), + n.Log.debug('AccessTokenEvents.load: registering expiring timer in:', r), + this._accessTokenExpiring.init(r); + } else + n.Log.debug( + "AccessTokenEvents.load: canceling existing expiring timer becase we're past expiration." + ), + this._accessTokenExpiring.cancel(); + var i = t + 1; + n.Log.debug('AccessTokenEvents.load: registering expired timer in:', i), + this._accessTokenExpired.init(i); + } else this._accessTokenExpiring.cancel(), this._accessTokenExpired.cancel(); + }), + (AccessTokenEvents.prototype.unload = function unload() { + n.Log.debug('AccessTokenEvents.unload: canceling existing access token timers'), + this._accessTokenExpiring.cancel(), + this._accessTokenExpired.cancel(); + }), + (AccessTokenEvents.prototype.addAccessTokenExpiring = function addAccessTokenExpiring(e) { + this._accessTokenExpiring.addHandler(e); + }), + (AccessTokenEvents.prototype.removeAccessTokenExpiring = + function removeAccessTokenExpiring(e) { + this._accessTokenExpiring.removeHandler(e); + }), + (AccessTokenEvents.prototype.addAccessTokenExpired = function addAccessTokenExpired(e) { + this._accessTokenExpired.addHandler(e); + }), + (AccessTokenEvents.prototype.removeAccessTokenExpired = function removeAccessTokenExpired( + e + ) { + this._accessTokenExpired.removeHandler(e); + }), + AccessTokenEvents + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.User = void 0); + var n = (function () { + function defineProperties(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + return function (e, t, r) { + return t && defineProperties(e.prototype, t), r && defineProperties(e, r), e; + }; + })(), + i = r(0); + t.User = (function () { + function User(e) { + var t = e.id_token, + r = e.session_state, + n = e.access_token, + i = e.token_type, + o = e.scope, + s = e.profile, + a = e.expires_at, + u = e.state; + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, User), + (this.id_token = t), + (this.session_state = r), + (this.access_token = n), + (this.token_type = i), + (this.scope = o), + (this.profile = s), + (this.expires_at = a), + (this.state = u); + } + return ( + (User.prototype.toStorageString = function toStorageString() { + return ( + i.Log.debug('User.toStorageString'), + JSON.stringify({ + id_token: this.id_token, + session_state: this.session_state, + access_token: this.access_token, + token_type: this.token_type, + scope: this.scope, + profile: this.profile, + expires_at: this.expires_at, + }) + ); + }), + (User.fromStorageString = function fromStorageString(e) { + return i.Log.debug('User.fromStorageString'), new User(JSON.parse(e)); + }), + n(User, [ + { + key: 'expires_in', + get: function get() { + if (this.expires_at) { + var e = parseInt(Date.now() / 1e3); + return this.expires_at - e; + } + }, + }, + { + key: 'expired', + get: function get() { + var e = this.expires_in; + if (void 0 !== e) return e <= 0; + }, + }, + { + key: 'scopes', + get: function get() { + return (this.scope || '').split(' '); + }, + }, + ]), + User + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), + (t.default = + // @preserve Copyright (c) Microsoft Open Technologies, Inc. + function random() { + for ( + var e = 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx', + t = '0123456789abcdef', + r = 0, + n = '', + i = 0; + i < e.length; + i++ + ) + '-' !== e[i] && '4' !== e[i] && (r = (16 * Math.random()) | 0), + 'x' === e[i] + ? (n += t[r]) + : 'y' === e[i] + ? ((r &= 3), (n += t[(r |= 8)])) + : (n += e[i]); + return n; + }), + (e.exports = t.default); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.SigninState = void 0); + var n = (function () { + function defineProperties(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + return function (e, t, r) { + return t && defineProperties(e.prototype, t), r && defineProperties(e, r), e; + }; + })(), + i = r(0), + o = r(4), + s = (function _interopRequireDefault(e) { + return e && e.__esModule ? e : { default: e }; + })(r(14)); + t.SigninState = (function (e) { + function SigninState() { + var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + r = t.nonce, + n = t.authority, + i = t.client_id; + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, SigninState); + var o = (function _possibleConstructorReturn(e, t) { + if (!e) + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + return !t || ('object' != typeof t && 'function' != typeof t) ? e : t; + })(this, e.call(this, arguments[0])); + return ( + !0 === r ? (o._nonce = (0, s.default)()) : r && (o._nonce = r), + (o._authority = n), + (o._client_id = i), + o + ); + } + return ( + (function _inherits(e, t) { + if ('function' != typeof t && null !== t) + throw new TypeError( + 'Super expression must either be null or a function, not ' + typeof t + ); + (e.prototype = Object.create(t && t.prototype, { + constructor: { + value: e, + enumerable: !1, + writable: !0, + configurable: !0, + }, + })), + t && (Object.setPrototypeOf ? Object.setPrototypeOf(e, t) : (e.__proto__ = t)); + })(SigninState, e), + (SigninState.prototype.toStorageString = function toStorageString() { + return ( + i.Log.debug('SigninState.toStorageString'), + JSON.stringify({ + id: this.id, + data: this.data, + created: this.created, + nonce: this.nonce, + authority: this.authority, + client_id: this.client_id, + }) + ); + }), + (SigninState.fromStorageString = function fromStorageString(e) { + return i.Log.debug('SigninState.fromStorageString'), new SigninState(JSON.parse(e)); + }), + n(SigninState, [ + { + key: 'nonce', + get: function get() { + return this._nonce; + }, + }, + { + key: 'authority', + get: function get() { + return this._authority; + }, + }, + { + key: 'client_id', + get: function get() { + return this._client_id; + }, + }, + ]), + SigninState + ); + })(o.State); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.ErrorResponse = void 0); + var n = r(0); + t.ErrorResponse = (function (e) { + function ErrorResponse() { + var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + r = t.error, + i = t.error_description, + o = t.error_uri, + s = t.state; + if ( + ((function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, ErrorResponse), + !r) + ) + throw (n.Log.error('No error passed to ErrorResponse'), new Error('error')); + var a = (function _possibleConstructorReturn(e, t) { + if (!e) + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + return !t || ('object' != typeof t && 'function' != typeof t) ? e : t; + })(this, e.call(this, i || r)); + return ( + (a.name = 'ErrorResponse'), + (a.error = r), + (a.error_description = i), + (a.error_uri = o), + (a.state = s), + a + ); + } + return ( + (function _inherits(e, t) { + if ('function' != typeof t && null !== t) + throw new TypeError( + 'Super expression must either be null or a function, not ' + typeof t + ); + (e.prototype = Object.create(t && t.prototype, { + constructor: { + value: e, + enumerable: !1, + writable: !0, + configurable: !0, + }, + })), + t && (Object.setPrototypeOf ? Object.setPrototypeOf(e, t) : (e.__proto__ = t)); + })(ErrorResponse, e), + ErrorResponse + ); + })(Error); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.JsonService = void 0); + var n = r(0), + i = r(1); + t.JsonService = (function () { + function JsonService() { + var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : null, + t = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : i.Global.XMLHttpRequest; + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, JsonService), + e && Array.isArray(e) ? (this._contentTypes = e.slice()) : (this._contentTypes = []), + this._contentTypes.push('application/json'), + (this._XMLHttpRequest = t); + } + return ( + (JsonService.prototype.getJson = function getJson(e, t) { + var r = this; + if (!e) throw (n.Log.error('JsonService.getJson: No url passed'), new Error('url')); + return ( + n.Log.debug('JsonService.getJson, url: ', e), + new Promise(function (i, o) { + var s = new r._XMLHttpRequest(); + s.open('GET', e); + var a = r._contentTypes; + (s.onload = function () { + if ( + (n.Log.debug('JsonService.getJson: HTTP response received, status', s.status), + 200 === s.status) + ) { + var t = s.getResponseHeader('Content-Type'); + if (t) + if ( + a.find(function (e) { + if (t.startsWith(e)) return !0; + }) + ) + try { + return void i(JSON.parse(s.responseText)); + } catch (e) { + return ( + n.Log.error( + 'JsonService.getJson: Error parsing JSON response', + e.message + ), + void o(e) + ); + } + o(Error('Invalid response Content-Type: ' + t + ', from URL: ' + e)); + } else o(Error(s.statusText + ' (' + s.status + ')')); + }), + (s.onerror = function () { + n.Log.error('JsonService.getJson: network error'), o(Error('Network Error')); + }), + t && + (n.Log.debug('JsonService.getJson: token passed, setting Authorization header'), + s.setRequestHeader('Authorization', 'Bearer ' + t)), + s.send(); + }) + ); + }), + JsonService + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.OidcClient = void 0); + var n = (function () { + function defineProperties(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + return function (e, t, r) { + return t && defineProperties(e.prototype, t), r && defineProperties(e, r), e; + }; + })(), + i = r(0), + o = r(6), + s = r(16), + a = r(35), + u = r(34), + c = r(33), + h = r(32), + f = r(15), + l = r(4); + t.OidcClient = (function () { + function OidcClient() { + var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}; + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, OidcClient), + e instanceof o.OidcClientSettings + ? (this._settings = e) + : (this._settings = new o.OidcClientSettings(e)); + } + return ( + (OidcClient.prototype.createSigninRequest = function createSigninRequest() { + var e = this, + t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + r = t.response_type, + n = t.scope, + o = t.redirect_uri, + s = t.data, + u = t.state, + c = t.prompt, + h = t.display, + f = t.max_age, + l = t.ui_locales, + g = t.id_token_hint, + p = t.login_hint, + d = t.acr_values, + v = t.resource, + y = t.request, + m = t.request_uri, + S = t.extraQueryParams, + F = arguments[1]; + i.Log.debug('OidcClient.createSigninRequest'); + var b = this._settings.client_id; + (r = r || this._settings.response_type), + (n = n || this._settings.scope), + (o = o || this._settings.redirect_uri), + (c = c || this._settings.prompt), + (h = h || this._settings.display), + (f = f || this._settings.max_age), + (l = l || this._settings.ui_locales), + (d = d || this._settings.acr_values), + (v = v || this._settings.resource), + (S = S || this._settings.extraQueryParams); + var _ = this._settings.authority; + return this._metadataService.getAuthorizationEndpoint().then(function (t) { + i.Log.debug('OidcClient.createSigninRequest: Received authorization endpoint', t); + var w = new a.SigninRequest({ + url: t, + client_id: b, + redirect_uri: o, + response_type: r, + scope: n, + data: s || u, + authority: _, + prompt: c, + display: h, + max_age: f, + ui_locales: l, + id_token_hint: g, + login_hint: p, + acr_values: d, + resource: v, + request: y, + request_uri: m, + extraQueryParams: S, + }), + E = w.state; + return (F = F || e._stateStore).set(E.id, E.toStorageString()).then(function () { + return w; + }); + }); + }), + (OidcClient.prototype.processSigninResponse = function processSigninResponse(e, t) { + var r = this; + i.Log.debug('OidcClient.processSigninResponse'); + var n = new u.SigninResponse(e); + return n.state + ? (t = t || this._stateStore).remove(n.state).then(function (e) { + if (!e) + throw ( + (i.Log.error( + 'OidcClient.processSigninResponse: No matching state found in storage' + ), + new Error('No matching state found in storage')) + ); + var t = f.SigninState.fromStorageString(e); + return ( + i.Log.debug( + 'OidcClient.processSigninResponse: Received state from storage; validating response' + ), + r._validator.validateSigninResponse(t, n) + ); + }) + : (i.Log.error('OidcClient.processSigninResponse: No state in response'), + Promise.reject(new Error('No state in response'))); + }), + (OidcClient.prototype.createSignoutRequest = function createSignoutRequest() { + var e = this, + t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + r = t.id_token_hint, + n = t.data, + o = t.state, + s = t.post_logout_redirect_uri, + a = arguments[1]; + return ( + i.Log.debug('OidcClient.createSignoutRequest'), + (s = s || this._settings.post_logout_redirect_uri), + this._metadataService.getEndSessionEndpoint().then(function (t) { + if (!t) + throw ( + (i.Log.error( + 'OidcClient.createSignoutRequest: No end session endpoint url returned' + ), + new Error('no end session endpoint')) + ); + i.Log.debug('OidcClient.createSignoutRequest: Received end session endpoint', t); + var u = new c.SignoutRequest({ + url: t, + id_token_hint: r, + post_logout_redirect_uri: s, + data: n || o, + }), + h = u.state; + return ( + h && + (i.Log.debug( + 'OidcClient.createSignoutRequest: Signout request has state to persist' + ), + (a = a || e._stateStore).set(h.id, h.toStorageString())), + u + ); + }) + ); + }), + (OidcClient.prototype.processSignoutResponse = function processSignoutResponse(e, t) { + var r = this; + i.Log.debug('OidcClient.processSignoutResponse'); + var n = new h.SignoutResponse(e); + if (!n.state) + return ( + i.Log.debug('OidcClient.processSignoutResponse: No state in response'), + n.error + ? (i.Log.warn('OidcClient.processSignoutResponse: Response was error: ', n.error), + Promise.reject(new s.ErrorResponse(n))) + : Promise.resolve(n) + ); + var o = n.state; + return (t = t || this._stateStore).remove(o).then(function (e) { + if (!e) + throw ( + (i.Log.error( + 'OidcClient.processSignoutResponse: No matching state found in storage' + ), + new Error('No matching state found in storage')) + ); + var t = l.State.fromStorageString(e); + return ( + i.Log.debug( + 'OidcClient.processSignoutResponse: Received state from storage; validating response' + ), + r._validator.validateSignoutResponse(t, n) + ); + }); + }), + (OidcClient.prototype.clearStaleState = function clearStaleState(e) { + return ( + i.Log.debug('OidcClient.clearStaleState'), + (e = e || this._stateStore), + l.State.clearStaleState(e, this.settings.staleStateAge) + ); + }), + n(OidcClient, [ + { + key: '_stateStore', + get: function get() { + return this.settings.stateStore; + }, + }, + { + key: '_validator', + get: function get() { + return this.settings.validator; + }, + }, + { + key: '_metadataService', + get: function get() { + return this.settings.metadataService; + }, + }, + { + key: 'settings', + get: function get() { + return this._settings; + }, + }, + { + key: 'metadataService', + get: function get() { + return this._metadataService; + }, + }, + ]), + OidcClient + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.CordovaIFrameNavigator = void 0); + var n = r(7); + t.CordovaIFrameNavigator = (function () { + function CordovaIFrameNavigator() { + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, CordovaIFrameNavigator); + } + return ( + (CordovaIFrameNavigator.prototype.prepare = function prepare(e) { + e.popupWindowFeatures = 'hidden=yes'; + var t = new n.CordovaPopupWindow(e); + return Promise.resolve(t); + }), + CordovaIFrameNavigator + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.CordovaPopupNavigator = void 0); + var n = r(7); + t.CordovaPopupNavigator = (function () { + function CordovaPopupNavigator() { + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, CordovaPopupNavigator); + } + return ( + (CordovaPopupNavigator.prototype.prepare = function prepare(e) { + var t = new n.CordovaPopupWindow(e); + return Promise.resolve(t); + }), + CordovaPopupNavigator + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.SilentRenewService = void 0); + var n = r(0); + t.SilentRenewService = (function () { + function SilentRenewService(e) { + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, SilentRenewService), + (this._userManager = e); + } + return ( + (SilentRenewService.prototype.start = function start() { + this._callback || + ((this._callback = this._tokenExpiring.bind(this)), + this._userManager.events.addAccessTokenExpiring(this._callback), + this._userManager + .getUser() + .then(function (e) {}) + .catch(function (e) { + n.Log.error('SilentRenewService.start: Error from getUser:', e.message); + })); + }), + (SilentRenewService.prototype.stop = function stop() { + this._callback && + (this._userManager.events.removeAccessTokenExpiring(this._callback), + delete this._callback); + }), + (SilentRenewService.prototype._tokenExpiring = function _tokenExpiring() { + var e = this; + this._userManager.signinSilent().then( + function (e) { + n.Log.debug('SilentRenewService._tokenExpiring: Silent token renewal successful'); + }, + function (t) { + n.Log.error( + 'SilentRenewService._tokenExpiring: Error from signinSilent:', + t.message + ), + e._userManager.events._raiseSilentRenewError(t); + } + ); + }), + SilentRenewService + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.Timer = void 0); + var n = (function () { + function defineProperties(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + return function (e, t, r) { + return t && defineProperties(e.prototype, t), r && defineProperties(e, r), e; + }; + })(), + i = r(0), + o = r(1), + s = r(11); + t.Timer = (function (e) { + function Timer(t) { + var r = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : o.Global.timer, + n = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : void 0; + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, Timer); + var i = (function _possibleConstructorReturn(e, t) { + if (!e) + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + return !t || ('object' != typeof t && 'function' != typeof t) ? e : t; + })(this, e.call(this, t)); + return ( + (i._timer = r), + (i._nowFunc = + n || + function () { + return Date.now() / 1e3; + }), + i + ); + } + return ( + (function _inherits(e, t) { + if ('function' != typeof t && null !== t) + throw new TypeError( + 'Super expression must either be null or a function, not ' + typeof t + ); + (e.prototype = Object.create(t && t.prototype, { + constructor: { + value: e, + enumerable: !1, + writable: !0, + configurable: !0, + }, + })), + t && (Object.setPrototypeOf ? Object.setPrototypeOf(e, t) : (e.__proto__ = t)); + })(Timer, e), + (Timer.prototype.init = function init(e) { + e <= 0 && (e = 1), (e = parseInt(e)); + var t = this.now + e; + if (this.expiration === t && this._timerHandle) + i.Log.debug( + 'Timer.init timer ' + + this._name + + ' skipping initialization since already initialized for expiration:', + this.expiration + ); + else { + this.cancel(), + i.Log.debug('Timer.init timer ' + this._name + ' for duration:', e), + (this._expiration = t); + var r = 5; + e < r && (r = e), + (this._timerHandle = this._timer.setInterval(this._callback.bind(this), 1e3 * r)); + } + }), + (Timer.prototype.cancel = function cancel() { + this._timerHandle && + (i.Log.debug('Timer.cancel: ', this._name), + this._timer.clearInterval(this._timerHandle), + (this._timerHandle = null)); + }), + (Timer.prototype._callback = function _callback() { + var t = this._expiration - this.now; + i.Log.debug('Timer.callback; ' + this._name + ' timer expires in:', t), + this._expiration <= this.now && (this.cancel(), e.prototype.raise.call(this)); + }), + n(Timer, [ + { + key: 'now', + get: function get() { + return parseInt(this._nowFunc()); + }, + }, + { + key: 'expiration', + get: function get() { + return this._expiration; + }, + }, + ]), + Timer + ); + })(s.Event); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.UserManagerEvents = void 0); + var n = r(0), + i = r(12), + o = r(11); + t.UserManagerEvents = (function (e) { + function UserManagerEvents(t) { + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, UserManagerEvents); + var r = (function _possibleConstructorReturn(e, t) { + if (!e) + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + return !t || ('object' != typeof t && 'function' != typeof t) ? e : t; + })(this, e.call(this, t)); + return ( + (r._userLoaded = new o.Event('User loaded')), + (r._userUnloaded = new o.Event('User unloaded')), + (r._silentRenewError = new o.Event('Silent renew error')), + (r._userSignedOut = new o.Event('User signed out')), + (r._userSessionChanged = new o.Event('User session changed')), + r + ); + } + return ( + (function _inherits(e, t) { + if ('function' != typeof t && null !== t) + throw new TypeError( + 'Super expression must either be null or a function, not ' + typeof t + ); + (e.prototype = Object.create(t && t.prototype, { + constructor: { + value: e, + enumerable: !1, + writable: !0, + configurable: !0, + }, + })), + t && (Object.setPrototypeOf ? Object.setPrototypeOf(e, t) : (e.__proto__ = t)); + })(UserManagerEvents, e), + (UserManagerEvents.prototype.load = function load(t) { + var r = !(arguments.length > 1 && void 0 !== arguments[1]) || arguments[1]; + n.Log.debug('UserManagerEvents.load'), + e.prototype.load.call(this, t), + r && this._userLoaded.raise(t); + }), + (UserManagerEvents.prototype.unload = function unload() { + n.Log.debug('UserManagerEvents.unload'), + e.prototype.unload.call(this), + this._userUnloaded.raise(); + }), + (UserManagerEvents.prototype.addUserLoaded = function addUserLoaded(e) { + this._userLoaded.addHandler(e); + }), + (UserManagerEvents.prototype.removeUserLoaded = function removeUserLoaded(e) { + this._userLoaded.removeHandler(e); + }), + (UserManagerEvents.prototype.addUserUnloaded = function addUserUnloaded(e) { + this._userUnloaded.addHandler(e); + }), + (UserManagerEvents.prototype.removeUserUnloaded = function removeUserUnloaded(e) { + this._userUnloaded.removeHandler(e); + }), + (UserManagerEvents.prototype.addSilentRenewError = function addSilentRenewError(e) { + this._silentRenewError.addHandler(e); + }), + (UserManagerEvents.prototype.removeSilentRenewError = function removeSilentRenewError(e) { + this._silentRenewError.removeHandler(e); + }), + (UserManagerEvents.prototype._raiseSilentRenewError = function _raiseSilentRenewError(e) { + n.Log.debug('UserManagerEvents._raiseSilentRenewError', e.message), + this._silentRenewError.raise(e); + }), + (UserManagerEvents.prototype.addUserSignedOut = function addUserSignedOut(e) { + this._userSignedOut.addHandler(e); + }), + (UserManagerEvents.prototype.removeUserSignedOut = function removeUserSignedOut(e) { + this._userSignedOut.removeHandler(e); + }), + (UserManagerEvents.prototype._raiseUserSignedOut = function _raiseUserSignedOut(e) { + n.Log.debug('UserManagerEvents._raiseUserSignedOut'), this._userSignedOut.raise(e); + }), + (UserManagerEvents.prototype.addUserSessionChanged = function addUserSessionChanged(e) { + this._userSessionChanged.addHandler(e); + }), + (UserManagerEvents.prototype.removeUserSessionChanged = function removeUserSessionChanged( + e + ) { + this._userSessionChanged.removeHandler(e); + }), + (UserManagerEvents.prototype._raiseUserSessionChanged = function _raiseUserSessionChanged( + e + ) { + n.Log.debug('UserManagerEvents._raiseUserSessionChanged'), + this._userSessionChanged.raise(e); + }), + UserManagerEvents + ); + })(i.AccessTokenEvents); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.IFrameWindow = void 0); + var n = (function () { + function defineProperties(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + return function (e, t, r) { + return t && defineProperties(e.prototype, t), r && defineProperties(e, r), e; + }; + })(), + i = r(0); + t.IFrameWindow = (function () { + function IFrameWindow(e) { + var t = this; + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, IFrameWindow), + (this._promise = new Promise(function (e, r) { + (t._resolve = e), (t._reject = r); + })), + (this._boundMessageEvent = this._message.bind(this)), + window.addEventListener('message', this._boundMessageEvent, !1), + (this._frame = window.document.createElement('iframe')), + (this._frame.style.visibility = 'hidden'), + (this._frame.style.position = 'absolute'), + (this._frame.style.display = 'none'), + (this._frame.style.width = 0), + (this._frame.style.height = 0), + window.document.body.appendChild(this._frame); + } + return ( + (IFrameWindow.prototype.navigate = function navigate(e) { + if (e && e.url) { + var t = e.silentRequestTimeout || 1e4; + i.Log.debug('IFrameWindow.navigate: Using timeout of:', t), + (this._timer = window.setTimeout(this._timeout.bind(this), t)), + (this._frame.src = e.url); + } else this._error('No url provided'); + return this.promise; + }), + (IFrameWindow.prototype._success = function _success(e) { + this._cleanup(), + i.Log.debug('IFrameWindow: Successful response from frame window'), + this._resolve(e); + }), + (IFrameWindow.prototype._error = function _error(e) { + this._cleanup(), i.Log.error(e), this._reject(new Error(e)); + }), + (IFrameWindow.prototype.close = function close() { + this._cleanup(); + }), + (IFrameWindow.prototype._cleanup = function _cleanup() { + this._frame && + (i.Log.debug('IFrameWindow: cleanup'), + window.removeEventListener('message', this._boundMessageEvent, !1), + window.clearTimeout(this._timer), + window.document.body.removeChild(this._frame), + (this._timer = null), + (this._frame = null), + (this._boundMessageEvent = null)); + }), + (IFrameWindow.prototype._timeout = function _timeout() { + i.Log.debug('IFrameWindow.timeout'), this._error('Frame window timed out'); + }), + (IFrameWindow.prototype._message = function _message(e) { + if ( + (i.Log.debug('IFrameWindow.message'), + this._timer && e.origin === this._origin && e.source === this._frame.contentWindow) + ) { + var t = e.data; + t ? this._success({ url: t }) : this._error('Invalid response from frame'); + } + }), + (IFrameWindow.notifyParent = function notifyParent(e) { + i.Log.debug('IFrameWindow.notifyParent'), + window.parent && + window !== window.parent && + (e = e || window.location.href) && + (i.Log.debug('IFrameWindow.notifyParent: posting url message to parent'), + window.parent.postMessage(e, location.protocol + '//' + location.host)); + }), + n(IFrameWindow, [ + { + key: 'promise', + get: function get() { + return this._promise; + }, + }, + { + key: '_origin', + get: function get() { + return location.protocol + '//' + location.host; + }, + }, + ]), + IFrameWindow + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.IFrameNavigator = void 0); + var n = r(0), + i = r(24); + t.IFrameNavigator = (function () { + function IFrameNavigator() { + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, IFrameNavigator); + } + return ( + (IFrameNavigator.prototype.prepare = function prepare(e) { + var t = new i.IFrameWindow(e); + return Promise.resolve(t); + }), + (IFrameNavigator.prototype.callback = function callback(e) { + n.Log.debug('IFrameNavigator.callback'); + try { + return i.IFrameWindow.notifyParent(e), Promise.resolve(); + } catch (e) { + return Promise.reject(e); + } + }), + IFrameNavigator + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.PopupWindow = void 0); + var n = (function () { + function defineProperties(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + return function (e, t, r) { + return t && defineProperties(e.prototype, t), r && defineProperties(e, r), e; + }; + })(), + i = r(0), + o = r(2); + var s = 500, + a = 'location=no,toolbar=no,width=500,height=500,left=100,top=100;', + u = '_blank'; + t.PopupWindow = (function () { + function PopupWindow(e) { + var t = this; + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, PopupWindow), + (this._promise = new Promise(function (e, r) { + (t._resolve = e), (t._reject = r); + })); + var r = e.popupWindowTarget || u, + n = e.popupWindowFeatures || a; + (this._popup = window.open('', r, n)), + this._popup && + (i.Log.debug('PopupWindow.ctor: popup successfully created'), + (this._checkForPopupClosedTimer = window.setInterval( + this._checkForPopupClosed.bind(this), + s + ))); + } + return ( + (PopupWindow.prototype.navigate = function navigate(e) { + return ( + this._popup + ? e && e.url + ? (i.Log.debug('PopupWindow.navigate: Setting URL in popup'), + (this._id = e.id), + this._id && (window['popupCallback_' + e.id] = this._callback.bind(this)), + this._popup.focus(), + (this._popup.window.location = e.url)) + : (this._error('PopupWindow.navigate: no url provided'), + this._error('No url provided')) + : this._error('PopupWindow.navigate: Error opening popup window'), + this.promise + ); + }), + (PopupWindow.prototype._success = function _success(e) { + i.Log.debug('PopupWindow.callback: Successful response from popup window'), + this._cleanup(), + this._resolve(e); + }), + (PopupWindow.prototype._error = function _error(e) { + i.Log.error('PopupWindow.error: ', e), this._cleanup(), this._reject(new Error(e)); + }), + (PopupWindow.prototype.close = function close() { + this._cleanup(!1); + }), + (PopupWindow.prototype._cleanup = function _cleanup(e) { + i.Log.debug('PopupWindow.cleanup'), + window.clearInterval(this._checkForPopupClosedTimer), + (this._checkForPopupClosedTimer = null), + delete window['popupCallback_' + this._id], + this._popup && !e && this._popup.close(), + (this._popup = null); + }), + (PopupWindow.prototype._checkForPopupClosed = function _checkForPopupClosed() { + (this._popup && !this._popup.closed) || this._error('Popup window closed'); + }), + (PopupWindow.prototype._callback = function _callback(e, t) { + this._cleanup(t), + e + ? (i.Log.debug('PopupWindow.callback success'), this._success({ url: e })) + : (i.Log.debug('PopupWindow.callback: Invalid response from popup'), + this._error('Invalid response from popup')); + }), + (PopupWindow.notifyOpener = function notifyOpener(e, t, r) { + if (window.opener) { + if ((e = e || window.location.href)) { + var n = o.UrlUtility.parseUrlFragment(e, r); + if (n.state) { + var s = 'popupCallback_' + n.state, + a = window.opener[s]; + a + ? (i.Log.debug('PopupWindow.notifyOpener: passing url message to opener'), + a(e, t)) + : i.Log.warn('PopupWindow.notifyOpener: no matching callback found on opener'); + } else i.Log.warn('PopupWindow.notifyOpener: no state found in response url'); + } + } else + i.Log.warn( + "PopupWindow.notifyOpener: no window.opener. Can't complete notification." + ); + }), + n(PopupWindow, [ + { + key: 'promise', + get: function get() { + return this._promise; + }, + }, + ]), + PopupWindow + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.PopupNavigator = void 0); + var n = r(0), + i = r(26); + t.PopupNavigator = (function () { + function PopupNavigator() { + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, PopupNavigator); + } + return ( + (PopupNavigator.prototype.prepare = function prepare(e) { + var t = new i.PopupWindow(e); + return Promise.resolve(t); + }), + (PopupNavigator.prototype.callback = function callback(e, t, r) { + n.Log.debug('PopupNavigator.callback'); + try { + return i.PopupWindow.notifyOpener(e, t, r), Promise.resolve(); + } catch (e) { + return Promise.reject(e); + } + }), + PopupNavigator + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.RedirectNavigator = void 0); + var n = (function () { + function defineProperties(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + return function (e, t, r) { + return t && defineProperties(e.prototype, t), r && defineProperties(e, r), e; + }; + })(), + i = r(0); + t.RedirectNavigator = (function () { + function RedirectNavigator() { + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, RedirectNavigator); + } + return ( + (RedirectNavigator.prototype.prepare = function prepare() { + return Promise.resolve(this); + }), + (RedirectNavigator.prototype.navigate = function navigate(e) { + return e && e.url + ? ((window.location = e.url), Promise.resolve()) + : (i.Log.error('RedirectNavigator.navigate: No url provided'), + Promise.reject(new Error('No url provided'))); + }), + n(RedirectNavigator, [ + { + key: 'url', + get: function get() { + return window.location.href; + }, + }, + ]), + RedirectNavigator + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.UserManagerSettings = void 0); + var n = (function () { + function defineProperties(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + return function (e, t, r) { + return t && defineProperties(e.prototype, t), r && defineProperties(e, r), e; + }; + })(), + i = (r(0), r(6)), + o = r(28), + s = r(27), + a = r(25), + u = r(5), + c = r(1); + var h = 60, + f = 2e3; + t.UserManagerSettings = (function (e) { + function UserManagerSettings() { + var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + r = t.popup_redirect_uri, + n = t.popup_post_logout_redirect_uri, + i = t.popupWindowFeatures, + l = t.popupWindowTarget, + g = t.silent_redirect_uri, + p = t.silentRequestTimeout, + d = t.automaticSilentRenew, + v = void 0 !== d && d, + y = t.includeIdTokenInSilentRenew, + m = void 0 === y || y, + S = t.monitorSession, + F = void 0 === S || S, + b = t.checkSessionInterval, + _ = void 0 === b ? f : b, + w = t.stopCheckSessionOnError, + E = void 0 === w || w, + x = t.revokeAccessTokenOnSignout, + C = void 0 !== x && x, + P = t.accessTokenExpiringNotificationTime, + A = void 0 === P ? h : P, + k = t.redirectNavigator, + I = void 0 === k ? new o.RedirectNavigator() : k, + B = t.popupNavigator, + R = void 0 === B ? new s.PopupNavigator() : B, + T = t.iframeNavigator, + U = void 0 === T ? new a.IFrameNavigator() : T, + M = t.userStore, + L = void 0 === M ? new u.WebStorageStateStore({ store: c.Global.sessionStorage }) : M; + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, UserManagerSettings); + var D = (function _possibleConstructorReturn(e, t) { + if (!e) + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + return !t || ('object' != typeof t && 'function' != typeof t) ? e : t; + })(this, e.call(this, arguments[0])); + return ( + (D._popup_redirect_uri = r), + (D._popup_post_logout_redirect_uri = n), + (D._popupWindowFeatures = i), + (D._popupWindowTarget = l), + (D._silent_redirect_uri = g), + (D._silentRequestTimeout = p), + (D._automaticSilentRenew = !!v), + (D._includeIdTokenInSilentRenew = m), + (D._accessTokenExpiringNotificationTime = A), + (D._monitorSession = F), + (D._checkSessionInterval = _), + (D._stopCheckSessionOnError = E), + (D._revokeAccessTokenOnSignout = C), + (D._redirectNavigator = I), + (D._popupNavigator = R), + (D._iframeNavigator = U), + (D._userStore = L), + D + ); + } + return ( + (function _inherits(e, t) { + if ('function' != typeof t && null !== t) + throw new TypeError( + 'Super expression must either be null or a function, not ' + typeof t + ); + (e.prototype = Object.create(t && t.prototype, { + constructor: { + value: e, + enumerable: !1, + writable: !0, + configurable: !0, + }, + })), + t && (Object.setPrototypeOf ? Object.setPrototypeOf(e, t) : (e.__proto__ = t)); + })(UserManagerSettings, e), + n(UserManagerSettings, [ + { + key: 'popup_redirect_uri', + get: function get() { + return this._popup_redirect_uri; + }, + }, + { + key: 'popup_post_logout_redirect_uri', + get: function get() { + return this._popup_post_logout_redirect_uri; + }, + }, + { + key: 'popupWindowFeatures', + get: function get() { + return this._popupWindowFeatures; + }, + }, + { + key: 'popupWindowTarget', + get: function get() { + return this._popupWindowTarget; + }, + }, + { + key: 'silent_redirect_uri', + get: function get() { + return this._silent_redirect_uri; + }, + }, + { + key: 'silentRequestTimeout', + get: function get() { + return this._silentRequestTimeout; + }, + }, + { + key: 'automaticSilentRenew', + get: function get() { + return !(!this.silent_redirect_uri || !this._automaticSilentRenew); + }, + }, + { + key: 'includeIdTokenInSilentRenew', + get: function get() { + return this._includeIdTokenInSilentRenew; + }, + }, + { + key: 'accessTokenExpiringNotificationTime', + get: function get() { + return this._accessTokenExpiringNotificationTime; + }, + }, + { + key: 'monitorSession', + get: function get() { + return this._monitorSession; + }, + }, + { + key: 'checkSessionInterval', + get: function get() { + return this._checkSessionInterval; + }, + }, + { + key: 'stopCheckSessionOnError', + get: function get() { + return this._stopCheckSessionOnError; + }, + }, + { + key: 'revokeAccessTokenOnSignout', + get: function get() { + return this._revokeAccessTokenOnSignout; + }, + }, + { + key: 'redirectNavigator', + get: function get() { + return this._redirectNavigator; + }, + }, + { + key: 'popupNavigator', + get: function get() { + return this._popupNavigator; + }, + }, + { + key: 'iframeNavigator', + get: function get() { + return this._iframeNavigator; + }, + }, + { + key: 'userStore', + get: function get() { + return this._userStore; + }, + }, + ]), + UserManagerSettings + ); + })(i.OidcClientSettings); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.UserManager = void 0); + var n = (function () { + function defineProperties(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + return function (e, t, r) { + return t && defineProperties(e.prototype, t), r && defineProperties(e, r), e; + }; + })(), + i = r(0), + o = r(18), + s = r(29), + a = r(13), + u = r(23), + c = r(21), + h = r(10), + f = r(8); + t.UserManager = (function (e) { + function UserManager() { + var t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + r = + arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : c.SilentRenewService, + n = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : h.SessionMonitor, + o = + arguments.length > 3 && void 0 !== arguments[3] + ? arguments[3] + : f.TokenRevocationClient; + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, UserManager), + t instanceof s.UserManagerSettings || (t = new s.UserManagerSettings(t)); + var a = (function _possibleConstructorReturn(e, t) { + if (!e) + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + return !t || ('object' != typeof t && 'function' != typeof t) ? e : t; + })(this, e.call(this, t)); + return ( + (a._events = new u.UserManagerEvents(t)), + (a._silentRenewService = new r(a)), + a.settings.automaticSilentRenew && + (i.Log.debug( + 'UserManager.ctor: automaticSilentRenew is configured, setting up silent renew' + ), + a.startSilentRenew()), + a.settings.monitorSession && + (i.Log.debug( + 'UserManager.ctor: monitorSession is configured, setting up session monitor' + ), + (a._sessionMonitor = new n(a))), + (a._tokenRevocationClient = new o(a._settings)), + a + ); + } + return ( + (function _inherits(e, t) { + if ('function' != typeof t && null !== t) + throw new TypeError( + 'Super expression must either be null or a function, not ' + typeof t + ); + (e.prototype = Object.create(t && t.prototype, { + constructor: { + value: e, + enumerable: !1, + writable: !0, + configurable: !0, + }, + })), + t && (Object.setPrototypeOf ? Object.setPrototypeOf(e, t) : (e.__proto__ = t)); + })(UserManager, e), + (UserManager.prototype.getUser = function getUser() { + var e = this; + return this._loadUser().then(function (t) { + return t + ? (i.Log.info('UserManager.getUser: user loaded'), e._events.load(t, !1), t) + : (i.Log.info('UserManager.getUser: user not found in storage'), null); + }); + }), + (UserManager.prototype.removeUser = function removeUser() { + var e = this; + return this.storeUser(null).then(function () { + i.Log.info('UserManager.removeUser: user removed from storage'), e._events.unload(); + }); + }), + (UserManager.prototype.signinRedirect = function signinRedirect(e) { + return this._signinStart(e, this._redirectNavigator).then(function () { + i.Log.info('UserManager.signinRedirect: successful'); + }); + }), + (UserManager.prototype.signinRedirectCallback = function signinRedirectCallback(e) { + return this._signinEnd(e || this._redirectNavigator.url).then(function (e) { + return ( + e && + (e.profile && e.profile.sub + ? i.Log.info( + 'UserManager.signinRedirectCallback: successful, signed in sub: ', + e.profile.sub + ) + : i.Log.info('UserManager.signinRedirectCallback: no sub')), + e + ); + }); + }), + (UserManager.prototype.signinPopup = function signinPopup() { + var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + t = e.redirect_uri || this.settings.popup_redirect_uri || this.settings.redirect_uri; + return t + ? ((e.redirect_uri = t), + (e.display = 'popup'), + this._signin(e, this._popupNavigator, { + startUrl: t, + popupWindowFeatures: e.popupWindowFeatures || this.settings.popupWindowFeatures, + popupWindowTarget: e.popupWindowTarget || this.settings.popupWindowTarget, + }).then(function (e) { + return ( + e && + (e.profile && e.profile.sub + ? i.Log.info( + 'UserManager.signinPopup: signinPopup successful, signed in sub: ', + e.profile.sub + ) + : i.Log.info('UserManager.signinPopup: no sub')), + e + ); + })) + : (i.Log.error( + 'UserManager.signinPopup: No popup_redirect_uri or redirect_uri configured' + ), + Promise.reject(new Error('No popup_redirect_uri or redirect_uri configured'))); + }), + (UserManager.prototype.signinPopupCallback = function signinPopupCallback(e) { + return this._signinCallback(e, this._popupNavigator) + .then(function (e) { + return ( + e && + (e.profile && e.profile.sub + ? i.Log.info( + 'UserManager.signinPopupCallback: successful, signed in sub: ', + e.profile.sub + ) + : i.Log.info('UserManager.signinPopupCallback: no sub')), + e + ); + }) + .catch(function (e) { + i.Log.error('UserManager.signinPopupCallback error: ' + e && e.message); + }); + }), + (UserManager.prototype.signinSilent = function signinSilent() { + var e = this, + t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + r = t.redirect_uri || this.settings.silent_redirect_uri; + if (!r) + return ( + i.Log.error('UserManager.signinSilent: No silent_redirect_uri configured'), + Promise.reject(new Error('No silent_redirect_uri configured')) + ); + (t.redirect_uri = r), (t.prompt = 'none'); + return ( + t.id_token_hint || !this.settings.includeIdTokenInSilentRenew + ? Promise.resolve() + : this._loadUser().then(function (e) { + t.id_token_hint = e && e.id_token; + }) + ) + .then(function () { + return e._signin(t, e._iframeNavigator, { + startUrl: r, + silentRequestTimeout: t.silentRequestTimeout || e.settings.silentRequestTimeout, + }); + }) + .then(function (e) { + return ( + e && + (e.profile && e.profile.sub + ? i.Log.info( + 'UserManager.signinSilent: successful, signed in sub: ', + e.profile.sub + ) + : i.Log.info('UserManager.signinSilent: no sub')), + e + ); + }); + }), + (UserManager.prototype.signinSilentCallback = function signinSilentCallback(e) { + return this._signinCallback(e, this._iframeNavigator).then(function (e) { + return ( + e && + (e.profile && e.profile.sub + ? i.Log.info( + 'UserManager.signinSilentCallback: successful, signed in sub: ', + e.profile.sub + ) + : i.Log.info('UserManager.signinSilentCallback: no sub')), + e + ); + }); + }), + (UserManager.prototype.querySessionStatus = function querySessionStatus() { + var e = this, + t = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + r = t.redirect_uri || this.settings.silent_redirect_uri; + return r + ? ((t.redirect_uri = r), + (t.prompt = 'none'), + (t.response_type = 'id_token'), + (t.scope = 'openid'), + this._signinStart(t, this._iframeNavigator, { + startUrl: r, + silentRequestTimeout: + t.silentRequestTimeout || this.settings.silentRequestTimeout, + }).then(function (t) { + return e.processSigninResponse(t.url).then(function (e) { + if ( + (i.Log.debug('UserManager.querySessionStatus: got signin response'), + e.session_state && e.profile.sub && e.profile.sid) + ) + return ( + i.Log.info( + 'UserManager.querySessionStatus: querySessionStatus success for sub: ', + e.profile.sub + ), + { + session_state: e.session_state, + sub: e.profile.sub, + sid: e.profile.sid, + } + ); + i.Log.info('querySessionStatus successful, user not authenticated'); + }); + })) + : (i.Log.error('UserManager.querySessionStatus: No silent_redirect_uri configured'), + Promise.reject(new Error('No silent_redirect_uri configured'))); + }), + (UserManager.prototype._signin = function _signin(e, t) { + var r = this, + n = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {}; + return this._signinStart(e, t, n).then(function (e) { + return r._signinEnd(e.url); + }); + }), + (UserManager.prototype._signinStart = function _signinStart(e, t) { + var r = this, + n = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {}; + return t.prepare(n).then(function (t) { + return ( + i.Log.debug('UserManager._signinStart: got navigator window handle'), + r + .createSigninRequest(e) + .then(function (e) { + return ( + i.Log.debug('UserManager._signinStart: got signin request'), + (n.url = e.url), + (n.id = e.state.id), + t.navigate(n) + ); + }) + .catch(function (e) { + throw ( + (t.close && + (i.Log.debug( + 'UserManager._signinStart: Error after preparing navigator, closing navigator window' + ), + t.close()), + e) + ); + }) + ); + }); + }), + (UserManager.prototype._signinEnd = function _signinEnd(e) { + var t = this; + return this.processSigninResponse(e).then(function (e) { + i.Log.debug('UserManager._signinEnd: got signin response'); + var r = new a.User(e); + return t.storeUser(r).then(function () { + return i.Log.debug('UserManager._signinEnd: user stored'), t._events.load(r), r; + }); + }); + }), + (UserManager.prototype._signinCallback = function _signinCallback(e, t) { + return i.Log.debug('UserManager._signinCallback'), t.callback(e); + }), + (UserManager.prototype.signoutRedirect = function signoutRedirect() { + var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + t = e.post_logout_redirect_uri || this.settings.post_logout_redirect_uri; + return ( + t && (e.post_logout_redirect_uri = t), + this._signoutStart(e, this._redirectNavigator).then(function () { + i.Log.info('UserManager.signoutRedirect: successful'); + }) + ); + }), + (UserManager.prototype.signoutRedirectCallback = function signoutRedirectCallback(e) { + return this._signoutEnd(e || this._redirectNavigator.url).then(function (e) { + return i.Log.info('UserManager.signoutRedirectCallback: successful'), e; + }); + }), + (UserManager.prototype.signoutPopup = function signoutPopup() { + var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + t = + e.post_logout_redirect_uri || + this.settings.popup_post_logout_redirect_uri || + this.settings.post_logout_redirect_uri; + return ( + (e.post_logout_redirect_uri = t), + (e.display = 'popup'), + e.post_logout_redirect_uri && (e.state = e.state || {}), + this._signout(e, this._popupNavigator, { + startUrl: t, + popupWindowFeatures: e.popupWindowFeatures || this.settings.popupWindowFeatures, + popupWindowTarget: e.popupWindowTarget || this.settings.popupWindowTarget, + }).then(function () { + i.Log.info('UserManager.signinPopup: successful'); + }) + ); + }), + (UserManager.prototype.signoutPopupCallback = function signoutPopupCallback(e, t) { + void 0 === t && 'boolean' == typeof e && ((e = null), (t = !0)); + return this._popupNavigator.callback(e, t, '?').then(function () { + i.Log.info('UserManager.signoutPopupCallback: successful'); + }); + }), + (UserManager.prototype._signout = function _signout(e, t) { + var r = this, + n = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {}; + return this._signoutStart(e, t, n).then(function (e) { + return r._signoutEnd(e.url); + }); + }), + (UserManager.prototype._signoutStart = function _signoutStart() { + var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}, + t = this, + r = arguments[1], + n = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {}; + return r.prepare(n).then(function (r) { + return ( + i.Log.debug('UserManager._signoutStart: got navigator window handle'), + t + ._loadUser() + .then(function (o) { + return ( + i.Log.debug('UserManager._signoutStart: loaded current user from storage'), + (t._settings.revokeAccessTokenOnSignout + ? t._revokeInternal(o) + : Promise.resolve() + ).then(function () { + var s = e.id_token_hint || (o && o.id_token); + return ( + s && + (i.Log.debug( + 'UserManager._signoutStart: Setting id_token into signout request' + ), + (e.id_token_hint = s)), + t.removeUser().then(function () { + return ( + i.Log.debug( + 'UserManager._signoutStart: user removed, creating signout request' + ), + t.createSignoutRequest(e).then(function (e) { + return ( + i.Log.debug('UserManager._signoutStart: got signout request'), + (n.url = e.url), + e.state && (n.id = e.state.id), + r.navigate(n) + ); + }) + ); + }) + ); + }) + ); + }) + .catch(function (e) { + throw ( + (r.close && + (i.Log.debug( + 'UserManager._signoutStart: Error after preparing navigator, closing navigator window' + ), + r.close()), + e) + ); + }) + ); + }); + }), + (UserManager.prototype._signoutEnd = function _signoutEnd(e) { + return this.processSignoutResponse(e).then(function (e) { + return i.Log.debug('UserManager._signoutEnd: got signout response'), e; + }); + }), + (UserManager.prototype.revokeAccessToken = function revokeAccessToken() { + var e = this; + return this._loadUser() + .then(function (t) { + return e._revokeInternal(t, !0).then(function (r) { + if (r) + return ( + i.Log.debug( + 'UserManager.revokeAccessToken: removing token properties from user and re-storing' + ), + (t.access_token = null), + (t.expires_at = null), + (t.token_type = null), + e.storeUser(t).then(function () { + i.Log.debug('UserManager.revokeAccessToken: user stored'), + e._events.load(t); + }) + ); + }); + }) + .then(function () { + i.Log.info('UserManager.revokeAccessToken: access token revoked successfully'); + }); + }), + (UserManager.prototype._revokeInternal = function _revokeInternal(e, t) { + var r = e && e.access_token; + return !r || r.indexOf('.') >= 0 + ? (i.Log.debug( + 'UserManager.revokeAccessToken: no need to revoke due to no user, token, or JWT format' + ), + Promise.resolve(!1)) + : this._tokenRevocationClient.revoke(r, t).then(function () { + return !0; + }); + }), + (UserManager.prototype.startSilentRenew = function startSilentRenew() { + this._silentRenewService.start(); + }), + (UserManager.prototype.stopSilentRenew = function stopSilentRenew() { + this._silentRenewService.stop(); + }), + (UserManager.prototype._loadUser = function _loadUser() { + return this._userStore.get(this._userStoreKey).then(function (e) { + return e + ? (i.Log.debug('UserManager._loadUser: user storageString loaded'), + a.User.fromStorageString(e)) + : (i.Log.debug('UserManager._loadUser: no user storageString'), null); + }); + }), + (UserManager.prototype.storeUser = function storeUser(e) { + if (e) { + i.Log.debug('UserManager.storeUser: storing user'); + var t = e.toStorageString(); + return this._userStore.set(this._userStoreKey, t); + } + return ( + i.Log.debug('storeUser.storeUser: removing user'), + this._userStore.remove(this._userStoreKey) + ); + }), + n(UserManager, [ + { + key: '_redirectNavigator', + get: function get() { + return this.settings.redirectNavigator; + }, + }, + { + key: '_popupNavigator', + get: function get() { + return this.settings.popupNavigator; + }, + }, + { + key: '_iframeNavigator', + get: function get() { + return this.settings.iframeNavigator; + }, + }, + { + key: '_userStore', + get: function get() { + return this.settings.userStore; + }, + }, + { + key: 'events', + get: function get() { + return this._events; + }, + }, + { + key: '_userStoreKey', + get: function get() { + return 'user:' + this.settings.authority + ':' + this.settings.client_id; + }, + }, + ]), + UserManager + ); + })(o.OidcClient); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.InMemoryWebStorage = void 0); + var n = (function () { + function defineProperties(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + return function (e, t, r) { + return t && defineProperties(e.prototype, t), r && defineProperties(e, r), e; + }; + })(), + i = r(0); + t.InMemoryWebStorage = (function () { + function InMemoryWebStorage() { + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, InMemoryWebStorage), + (this._data = {}); + } + return ( + (InMemoryWebStorage.prototype.getItem = function getItem(e) { + return i.Log.debug('InMemoryWebStorage.getItem', e), this._data[e]; + }), + (InMemoryWebStorage.prototype.setItem = function setItem(e, t) { + i.Log.debug('InMemoryWebStorage.setItem', e), (this._data[e] = t); + }), + (InMemoryWebStorage.prototype.removeItem = function removeItem(e) { + i.Log.debug('InMemoryWebStorage.removeItem', e), delete this._data[e]; + }), + (InMemoryWebStorage.prototype.key = function key(e) { + return Object.getOwnPropertyNames(this._data)[e]; + }), + n(InMemoryWebStorage, [ + { + key: 'length', + get: function get() { + return Object.getOwnPropertyNames(this._data).length; + }, + }, + ]), + InMemoryWebStorage + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.SignoutResponse = void 0); + var n = r(2); + t.SignoutResponse = function SignoutResponse(e) { + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, SignoutResponse); + var t = n.UrlUtility.parseUrlFragment(e, '?'); + (this.error = t.error), + (this.error_description = t.error_description), + (this.error_uri = t.error_uri), + (this.state = t.state); + }; + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.SignoutRequest = void 0); + var n = r(0), + i = r(2), + o = r(4); + t.SignoutRequest = function SignoutRequest(e) { + var t = e.url, + r = e.id_token_hint, + s = e.post_logout_redirect_uri, + a = e.data; + if ( + ((function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, SignoutRequest), + !t) + ) + throw (n.Log.error('SignoutRequest.ctor: No url passed'), new Error('url')); + r && (t = i.UrlUtility.addQueryParam(t, 'id_token_hint', r)), + s && + ((t = i.UrlUtility.addQueryParam(t, 'post_logout_redirect_uri', s)), + a && + ((this.state = new o.State({ data: a })), + (t = i.UrlUtility.addQueryParam(t, 'state', this.state.id)))), + (this.url = t); + }; + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.SigninResponse = void 0); + var n = (function () { + function defineProperties(e, t) { + for (var r = 0; r < t.length; r++) { + var n = t[r]; + (n.enumerable = n.enumerable || !1), + (n.configurable = !0), + 'value' in n && (n.writable = !0), + Object.defineProperty(e, n.key, n); + } + } + return function (e, t, r) { + return t && defineProperties(e.prototype, t), r && defineProperties(e, r), e; + }; + })(), + i = r(2); + t.SigninResponse = (function () { + function SigninResponse(e) { + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, SigninResponse); + var t = i.UrlUtility.parseUrlFragment(e, '#'); + (this.error = t.error), + (this.error_description = t.error_description), + (this.error_uri = t.error_uri), + (this.state = t.state), + (this.id_token = t.id_token), + (this.session_state = t.session_state), + (this.access_token = t.access_token), + (this.token_type = t.token_type), + (this.scope = t.scope), + (this.profile = void 0); + var r = parseInt(t.expires_in); + if ('number' == typeof r && r > 0) { + var n = parseInt(Date.now() / 1e3); + this.expires_at = n + r; + } + } + return ( + n(SigninResponse, [ + { + key: 'expires_in', + get: function get() { + if (this.expires_at) { + var e = parseInt(Date.now() / 1e3); + return this.expires_at - e; + } + }, + }, + { + key: 'expired', + get: function get() { + var e = this.expires_in; + if (void 0 !== e) return e <= 0; + }, + }, + { + key: 'scopes', + get: function get() { + return (this.scope || '').split(' '); + }, + }, + { + key: 'isOpenIdConnect', + get: function get() { + return this.scopes.indexOf('openid') >= 0 || !!this.id_token; + }, + }, + ]), + SigninResponse + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.SigninRequest = void 0); + var n = r(0), + i = r(2), + o = r(15); + t.SigninRequest = (function () { + function SigninRequest(e) { + var t = e.url, + r = e.client_id, + s = e.redirect_uri, + a = e.response_type, + u = e.scope, + c = e.authority, + h = e.data, + f = e.prompt, + l = e.display, + g = e.max_age, + p = e.ui_locales, + d = e.id_token_hint, + v = e.login_hint, + y = e.acr_values, + m = e.resource, + S = e.request, + F = e.request_uri, + b = e.extraQueryParams; + if ( + ((function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, SigninRequest), + !t) + ) + throw (n.Log.error('SigninRequest.ctor: No url passed'), new Error('url')); + if (!r) + throw (n.Log.error('SigninRequest.ctor: No client_id passed'), new Error('client_id')); + if (!s) + throw ( + (n.Log.error('SigninRequest.ctor: No redirect_uri passed'), new Error('redirect_uri')) + ); + if (!a) + throw ( + (n.Log.error('SigninRequest.ctor: No response_type passed'), + new Error('response_type')) + ); + if (!u) throw (n.Log.error('SigninRequest.ctor: No scope passed'), new Error('scope')); + if (!c) + throw (n.Log.error('SigninRequest.ctor: No authority passed'), new Error('authority')); + var _ = SigninRequest.isOidc(a); + (this.state = new o.SigninState({ + nonce: _, + data: h, + client_id: r, + authority: c, + })), + (t = i.UrlUtility.addQueryParam(t, 'client_id', r)), + (t = i.UrlUtility.addQueryParam(t, 'redirect_uri', s)), + (t = i.UrlUtility.addQueryParam(t, 'response_type', a)), + (t = i.UrlUtility.addQueryParam(t, 'scope', u)), + (t = i.UrlUtility.addQueryParam(t, 'state', this.state.id)), + _ && (t = i.UrlUtility.addQueryParam(t, 'nonce', this.state.nonce)); + var w = { + prompt: f, + display: l, + max_age: g, + ui_locales: p, + id_token_hint: d, + login_hint: v, + acr_values: y, + resource: m, + request: S, + request_uri: F, + }; + for (var E in w) w[E] && (t = i.UrlUtility.addQueryParam(t, E, w[E])); + for (var x in b) t = i.UrlUtility.addQueryParam(t, x, b[x]); + this.url = t; + } + return ( + (SigninRequest.isOidc = function isOidc(e) { + return !!e.split(/\s+/g).filter(function (e) { + return 'id_token' === e; + })[0]; + }), + (SigninRequest.isOAuth = function isOAuth(e) { + return !!e.split(/\s+/g).filter(function (e) { + return 'token' === e; + })[0]; + }), + SigninRequest + ); + })(); + }, + function (e, t) { + var r = {}.toString; + e.exports = + Array.isArray || + function (e) { + return '[object Array]' == r.call(e); + }; + }, + function (e, t) { + (t.read = function (e, t, r, n, i) { + var o, + s, + a = 8 * i - n - 1, + u = (1 << a) - 1, + c = u >> 1, + h = -7, + f = r ? i - 1 : 0, + l = r ? -1 : 1, + g = e[t + f]; + for ( + f += l, o = g & ((1 << -h) - 1), g >>= -h, h += a; + h > 0; + o = 256 * o + e[t + f], f += l, h -= 8 + ); + for ( + s = o & ((1 << -h) - 1), o >>= -h, h += n; + h > 0; + s = 256 * s + e[t + f], f += l, h -= 8 + ); + if (0 === o) o = 1 - c; + else { + if (o === u) return s ? NaN : (1 / 0) * (g ? -1 : 1); + (s += Math.pow(2, n)), (o -= c); + } + return (g ? -1 : 1) * s * Math.pow(2, o - n); + }), + (t.write = function (e, t, r, n, i, o) { + var s, + a, + u, + c = 8 * o - i - 1, + h = (1 << c) - 1, + f = h >> 1, + l = 23 === i ? Math.pow(2, -24) - Math.pow(2, -77) : 0, + g = n ? 0 : o - 1, + p = n ? 1 : -1, + d = t < 0 || (0 === t && 1 / t < 0) ? 1 : 0; + for ( + t = Math.abs(t), + isNaN(t) || t === 1 / 0 + ? ((a = isNaN(t) ? 1 : 0), (s = h)) + : ((s = Math.floor(Math.log(t) / Math.LN2)), + t * (u = Math.pow(2, -s)) < 1 && (s--, (u *= 2)), + (t += s + f >= 1 ? l / u : l * Math.pow(2, 1 - f)) * u >= 2 && (s++, (u /= 2)), + s + f >= h + ? ((a = 0), (s = h)) + : s + f >= 1 + ? ((a = (t * u - 1) * Math.pow(2, i)), (s += f)) + : ((a = t * Math.pow(2, f - 1) * Math.pow(2, i)), (s = 0))); + i >= 8; + e[r + g] = 255 & a, g += p, a /= 256, i -= 8 + ); + for (s = (s << i) | a, c += i; c > 0; e[r + g] = 255 & s, g += p, s /= 256, c -= 8); + e[r + g - p] |= 128 * d; + }); + }, + function (e, t, r) { + 'use strict'; + (t.byteLength = function byteLength(e) { + var t = getLens(e), + r = t[0], + n = t[1]; + return (3 * (r + n)) / 4 - n; + }), + (t.toByteArray = function toByteArray(e) { + for ( + var t, + r = getLens(e), + n = r[0], + s = r[1], + a = new o( + (function _byteLength(e, t, r) { + return (3 * (t + r)) / 4 - r; + })(0, n, s) + ), + u = 0, + c = s > 0 ? n - 4 : n, + h = 0; + h < c; + h += 4 + ) + (t = + (i[e.charCodeAt(h)] << 18) | + (i[e.charCodeAt(h + 1)] << 12) | + (i[e.charCodeAt(h + 2)] << 6) | + i[e.charCodeAt(h + 3)]), + (a[u++] = (t >> 16) & 255), + (a[u++] = (t >> 8) & 255), + (a[u++] = 255 & t); + 2 === s && + ((t = (i[e.charCodeAt(h)] << 2) | (i[e.charCodeAt(h + 1)] >> 4)), (a[u++] = 255 & t)); + 1 === s && + ((t = + (i[e.charCodeAt(h)] << 10) | + (i[e.charCodeAt(h + 1)] << 4) | + (i[e.charCodeAt(h + 2)] >> 2)), + (a[u++] = (t >> 8) & 255), + (a[u++] = 255 & t)); + return a; + }), + (t.fromByteArray = function fromByteArray(e) { + for (var t, r = e.length, i = r % 3, o = [], s = 0, a = r - i; s < a; s += 16383) + o.push(encodeChunk(e, s, s + 16383 > a ? a : s + 16383)); + 1 === i + ? ((t = e[r - 1]), o.push(n[t >> 2] + n[(t << 4) & 63] + '==')) + : 2 === i && + ((t = (e[r - 2] << 8) + e[r - 1]), + o.push(n[t >> 10] + n[(t >> 4) & 63] + n[(t << 2) & 63] + '=')); + return o.join(''); + }); + for ( + var n = [], + i = [], + o = 'undefined' != typeof Uint8Array ? Uint8Array : Array, + s = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', + a = 0, + u = s.length; + a < u; + ++a + ) + (n[a] = s[a]), (i[s.charCodeAt(a)] = a); + function getLens(e) { + var t = e.length; + if (t % 4 > 0) throw new Error('Invalid string. Length must be a multiple of 4'); + var r = e.indexOf('='); + return -1 === r && (r = t), [r, r === t ? 0 : 4 - (r % 4)]; + } + function tripletToBase64(e) { + return n[(e >> 18) & 63] + n[(e >> 12) & 63] + n[(e >> 6) & 63] + n[63 & e]; + } + function encodeChunk(e, t, r) { + for (var n, i = [], o = t; o < r; o += 3) + (n = ((e[o] << 16) & 16711680) + ((e[o + 1] << 8) & 65280) + (255 & e[o + 2])), + i.push(tripletToBase64(n)); + return i.join(''); + } + (i['-'.charCodeAt(0)] = 62), (i['_'.charCodeAt(0)] = 63); + }, + function (e, t) { + var r; + r = (function () { + return this; + })(); + try { + r = r || Function('return this')() || (0, eval)('this'); + } catch (e) { + 'object' == typeof window && (r = window); + } + e.exports = r; + }, + function (e, t, r) { + 'use strict'; + (function (e) { + /*! + * The buffer module from node.js, for the browser. + * + * @author Feross Aboukhadijeh + * @license MIT + */ + var n = r(38), + i = r(37), + o = r(36); + function kMaxLength() { + return Buffer.TYPED_ARRAY_SUPPORT ? 2147483647 : 1073741823; + } + function createBuffer(e, t) { + if (kMaxLength() < t) throw new RangeError('Invalid typed array length'); + return ( + Buffer.TYPED_ARRAY_SUPPORT + ? ((e = new Uint8Array(t)).__proto__ = Buffer.prototype) + : (null === e && (e = new Buffer(t)), (e.length = t)), + e + ); + } + function Buffer(e, t, r) { + if (!(Buffer.TYPED_ARRAY_SUPPORT || this instanceof Buffer)) return new Buffer(e, t, r); + if ('number' == typeof e) { + if ('string' == typeof t) + throw new Error('If encoding is specified then the first argument must be a string'); + return allocUnsafe(this, e); + } + return from(this, e, t, r); + } + function from(e, t, r, n) { + if ('number' == typeof t) throw new TypeError('"value" argument must not be a number'); + return 'undefined' != typeof ArrayBuffer && t instanceof ArrayBuffer + ? (function fromArrayBuffer(e, t, r, n) { + if ((t.byteLength, r < 0 || t.byteLength < r)) + throw new RangeError("'offset' is out of bounds"); + if (t.byteLength < r + (n || 0)) throw new RangeError("'length' is out of bounds"); + t = + void 0 === r && void 0 === n + ? new Uint8Array(t) + : void 0 === n + ? new Uint8Array(t, r) + : new Uint8Array(t, r, n); + Buffer.TYPED_ARRAY_SUPPORT + ? ((e = t).__proto__ = Buffer.prototype) + : (e = fromArrayLike(e, t)); + return e; + })(e, t, r, n) + : 'string' == typeof t + ? (function fromString(e, t, r) { + ('string' == typeof r && '' !== r) || (r = 'utf8'); + if (!Buffer.isEncoding(r)) + throw new TypeError('"encoding" must be a valid string encoding'); + var n = 0 | byteLength(t, r), + i = (e = createBuffer(e, n)).write(t, r); + i !== n && (e = e.slice(0, i)); + return e; + })(e, t, r) + : (function fromObject(e, t) { + if (Buffer.isBuffer(t)) { + var r = 0 | checked(t.length); + return 0 === (e = createBuffer(e, r)).length ? e : (t.copy(e, 0, 0, r), e); + } + if (t) { + if ( + ('undefined' != typeof ArrayBuffer && t.buffer instanceof ArrayBuffer) || + 'length' in t + ) + return 'number' != typeof t.length || + (function isnan(e) { + return e != e; + })(t.length) + ? createBuffer(e, 0) + : fromArrayLike(e, t); + if ('Buffer' === t.type && o(t.data)) return fromArrayLike(e, t.data); + } + throw new TypeError( + 'First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.' + ); + })(e, t); + } + function assertSize(e) { + if ('number' != typeof e) throw new TypeError('"size" argument must be a number'); + if (e < 0) throw new RangeError('"size" argument must not be negative'); + } + function allocUnsafe(e, t) { + if ( + (assertSize(t), + (e = createBuffer(e, t < 0 ? 0 : 0 | checked(t))), + !Buffer.TYPED_ARRAY_SUPPORT) + ) + for (var r = 0; r < t; ++r) e[r] = 0; + return e; + } + function fromArrayLike(e, t) { + var r = t.length < 0 ? 0 : 0 | checked(t.length); + e = createBuffer(e, r); + for (var n = 0; n < r; n += 1) e[n] = 255 & t[n]; + return e; + } + function checked(e) { + if (e >= kMaxLength()) + throw new RangeError( + 'Attempt to allocate Buffer larger than maximum size: 0x' + + kMaxLength().toString(16) + + ' bytes' + ); + return 0 | e; + } + function byteLength(e, t) { + if (Buffer.isBuffer(e)) return e.length; + if ( + 'undefined' != typeof ArrayBuffer && + 'function' == typeof ArrayBuffer.isView && + (ArrayBuffer.isView(e) || e instanceof ArrayBuffer) + ) + return e.byteLength; + 'string' != typeof e && (e = '' + e); + var r = e.length; + if (0 === r) return 0; + for (var n = !1; ; ) + switch (t) { + case 'ascii': + case 'latin1': + case 'binary': + return r; + case 'utf8': + case 'utf-8': + case void 0: + return utf8ToBytes(e).length; + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return 2 * r; + case 'hex': + return r >>> 1; + case 'base64': + return base64ToBytes(e).length; + default: + if (n) return utf8ToBytes(e).length; + (t = ('' + t).toLowerCase()), (n = !0); + } + } + function swap(e, t, r) { + var n = e[t]; + (e[t] = e[r]), (e[r] = n); + } + function bidirectionalIndexOf(e, t, r, n, i) { + if (0 === e.length) return -1; + if ( + ('string' == typeof r + ? ((n = r), (r = 0)) + : r > 2147483647 + ? (r = 2147483647) + : r < -2147483648 && (r = -2147483648), + (r = +r), + isNaN(r) && (r = i ? 0 : e.length - 1), + r < 0 && (r = e.length + r), + r >= e.length) + ) { + if (i) return -1; + r = e.length - 1; + } else if (r < 0) { + if (!i) return -1; + r = 0; + } + if (('string' == typeof t && (t = Buffer.from(t, n)), Buffer.isBuffer(t))) + return 0 === t.length ? -1 : arrayIndexOf(e, t, r, n, i); + if ('number' == typeof t) + return ( + (t &= 255), + Buffer.TYPED_ARRAY_SUPPORT && 'function' == typeof Uint8Array.prototype.indexOf + ? i + ? Uint8Array.prototype.indexOf.call(e, t, r) + : Uint8Array.prototype.lastIndexOf.call(e, t, r) + : arrayIndexOf(e, [t], r, n, i) + ); + throw new TypeError('val must be string, number or Buffer'); + } + function arrayIndexOf(e, t, r, n, i) { + var o, + s = 1, + a = e.length, + u = t.length; + if ( + void 0 !== n && + ('ucs2' === (n = String(n).toLowerCase()) || + 'ucs-2' === n || + 'utf16le' === n || + 'utf-16le' === n) + ) { + if (e.length < 2 || t.length < 2) return -1; + (s = 2), (a /= 2), (u /= 2), (r /= 2); + } + function read(e, t) { + return 1 === s ? e[t] : e.readUInt16BE(t * s); + } + if (i) { + var c = -1; + for (o = r; o < a; o++) + if (read(e, o) === read(t, -1 === c ? 0 : o - c)) { + if ((-1 === c && (c = o), o - c + 1 === u)) return c * s; + } else -1 !== c && (o -= o - c), (c = -1); + } else + for (r + u > a && (r = a - u), o = r; o >= 0; o--) { + for (var h = !0, f = 0; f < u; f++) + if (read(e, o + f) !== read(t, f)) { + h = !1; + break; + } + if (h) return o; + } + return -1; + } + function hexWrite(e, t, r, n) { + r = Number(r) || 0; + var i = e.length - r; + n ? (n = Number(n)) > i && (n = i) : (n = i); + var o = t.length; + if (o % 2 != 0) throw new TypeError('Invalid hex string'); + n > o / 2 && (n = o / 2); + for (var s = 0; s < n; ++s) { + var a = parseInt(t.substr(2 * s, 2), 16); + if (isNaN(a)) return s; + e[r + s] = a; + } + return s; + } + function utf8Write(e, t, r, n) { + return blitBuffer(utf8ToBytes(t, e.length - r), e, r, n); + } + function asciiWrite(e, t, r, n) { + return blitBuffer( + (function asciiToBytes(e) { + for (var t = [], r = 0; r < e.length; ++r) t.push(255 & e.charCodeAt(r)); + return t; + })(t), + e, + r, + n + ); + } + function latin1Write(e, t, r, n) { + return asciiWrite(e, t, r, n); + } + function base64Write(e, t, r, n) { + return blitBuffer(base64ToBytes(t), e, r, n); + } + function ucs2Write(e, t, r, n) { + return blitBuffer( + (function utf16leToBytes(e, t) { + for (var r, n, i, o = [], s = 0; s < e.length && !((t -= 2) < 0); ++s) + (r = e.charCodeAt(s)), (n = r >> 8), (i = r % 256), o.push(i), o.push(n); + return o; + })(t, e.length - r), + e, + r, + n + ); + } + function base64Slice(e, t, r) { + return 0 === t && r === e.length ? n.fromByteArray(e) : n.fromByteArray(e.slice(t, r)); + } + function utf8Slice(e, t, r) { + r = Math.min(e.length, r); + for (var n = [], i = t; i < r; ) { + var o, + a, + u, + c, + h = e[i], + f = null, + l = h > 239 ? 4 : h > 223 ? 3 : h > 191 ? 2 : 1; + if (i + l <= r) + switch (l) { + case 1: + h < 128 && (f = h); + break; + case 2: + 128 == (192 & (o = e[i + 1])) && + (c = ((31 & h) << 6) | (63 & o)) > 127 && + (f = c); + break; + case 3: + (o = e[i + 1]), + (a = e[i + 2]), + 128 == (192 & o) && + 128 == (192 & a) && + (c = ((15 & h) << 12) | ((63 & o) << 6) | (63 & a)) > 2047 && + (c < 55296 || c > 57343) && + (f = c); + break; + case 4: + (o = e[i + 1]), + (a = e[i + 2]), + (u = e[i + 3]), + 128 == (192 & o) && + 128 == (192 & a) && + 128 == (192 & u) && + (c = ((15 & h) << 18) | ((63 & o) << 12) | ((63 & a) << 6) | (63 & u)) > + 65535 && + c < 1114112 && + (f = c); + } + null === f + ? ((f = 65533), (l = 1)) + : f > 65535 && + ((f -= 65536), n.push(((f >>> 10) & 1023) | 55296), (f = 56320 | (1023 & f))), + n.push(f), + (i += l); + } + return (function decodeCodePointsArray(e) { + var t = e.length; + if (t <= s) return String.fromCharCode.apply(String, e); + var r = '', + n = 0; + for (; n < t; ) r += String.fromCharCode.apply(String, e.slice(n, (n += s))); + return r; + })(n); + } + (t.Buffer = Buffer), + (t.SlowBuffer = function SlowBuffer(e) { + +e != e && (e = 0); + return Buffer.alloc(+e); + }), + (t.INSPECT_MAX_BYTES = 50), + (Buffer.TYPED_ARRAY_SUPPORT = + void 0 !== e.TYPED_ARRAY_SUPPORT + ? e.TYPED_ARRAY_SUPPORT + : (function typedArraySupport() { + try { + var e = new Uint8Array(1); + return ( + (e.__proto__ = { + __proto__: Uint8Array.prototype, + foo: function () { + return 42; + }, + }), + 42 === e.foo() && + 'function' == typeof e.subarray && + 0 === e.subarray(1, 1).byteLength + ); + } catch (e) { + return !1; + } + })()), + (t.kMaxLength = kMaxLength()), + (Buffer.poolSize = 8192), + (Buffer._augment = function (e) { + return (e.__proto__ = Buffer.prototype), e; + }), + (Buffer.from = function (e, t, r) { + return from(null, e, t, r); + }), + Buffer.TYPED_ARRAY_SUPPORT && + ((Buffer.prototype.__proto__ = Uint8Array.prototype), + (Buffer.__proto__ = Uint8Array), + 'undefined' != typeof Symbol && + Symbol.species && + Buffer[Symbol.species] === Buffer && + Object.defineProperty(Buffer, Symbol.species, { + value: null, + configurable: !0, + })), + (Buffer.alloc = function (e, t, r) { + return (function alloc(e, t, r, n) { + return ( + assertSize(t), + t <= 0 + ? createBuffer(e, t) + : void 0 !== r + ? 'string' == typeof n + ? createBuffer(e, t).fill(r, n) + : createBuffer(e, t).fill(r) + : createBuffer(e, t) + ); + })(null, e, t, r); + }), + (Buffer.allocUnsafe = function (e) { + return allocUnsafe(null, e); + }), + (Buffer.allocUnsafeSlow = function (e) { + return allocUnsafe(null, e); + }), + (Buffer.isBuffer = function isBuffer(e) { + return !(null == e || !e._isBuffer); + }), + (Buffer.compare = function compare(e, t) { + if (!Buffer.isBuffer(e) || !Buffer.isBuffer(t)) + throw new TypeError('Arguments must be Buffers'); + if (e === t) return 0; + for (var r = e.length, n = t.length, i = 0, o = Math.min(r, n); i < o; ++i) + if (e[i] !== t[i]) { + (r = e[i]), (n = t[i]); + break; + } + return r < n ? -1 : n < r ? 1 : 0; + }), + (Buffer.isEncoding = function isEncoding(e) { + switch (String(e).toLowerCase()) { + case 'hex': + case 'utf8': + case 'utf-8': + case 'ascii': + case 'latin1': + case 'binary': + case 'base64': + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return !0; + default: + return !1; + } + }), + (Buffer.concat = function concat(e, t) { + if (!o(e)) throw new TypeError('"list" argument must be an Array of Buffers'); + if (0 === e.length) return Buffer.alloc(0); + var r; + if (void 0 === t) for (t = 0, r = 0; r < e.length; ++r) t += e[r].length; + var n = Buffer.allocUnsafe(t), + i = 0; + for (r = 0; r < e.length; ++r) { + var s = e[r]; + if (!Buffer.isBuffer(s)) + throw new TypeError('"list" argument must be an Array of Buffers'); + s.copy(n, i), (i += s.length); + } + return n; + }), + (Buffer.byteLength = byteLength), + (Buffer.prototype._isBuffer = !0), + (Buffer.prototype.swap16 = function swap16() { + var e = this.length; + if (e % 2 != 0) throw new RangeError('Buffer size must be a multiple of 16-bits'); + for (var t = 0; t < e; t += 2) swap(this, t, t + 1); + return this; + }), + (Buffer.prototype.swap32 = function swap32() { + var e = this.length; + if (e % 4 != 0) throw new RangeError('Buffer size must be a multiple of 32-bits'); + for (var t = 0; t < e; t += 4) swap(this, t, t + 3), swap(this, t + 1, t + 2); + return this; + }), + (Buffer.prototype.swap64 = function swap64() { + var e = this.length; + if (e % 8 != 0) throw new RangeError('Buffer size must be a multiple of 64-bits'); + for (var t = 0; t < e; t += 8) + swap(this, t, t + 7), + swap(this, t + 1, t + 6), + swap(this, t + 2, t + 5), + swap(this, t + 3, t + 4); + return this; + }), + (Buffer.prototype.toString = function toString() { + var e = 0 | this.length; + return 0 === e + ? '' + : 0 === arguments.length + ? utf8Slice(this, 0, e) + : function slowToString(e, t, r) { + var n = !1; + if (((void 0 === t || t < 0) && (t = 0), t > this.length)) return ''; + if (((void 0 === r || r > this.length) && (r = this.length), r <= 0)) return ''; + if ((r >>>= 0) <= (t >>>= 0)) return ''; + for (e || (e = 'utf8'); ; ) + switch (e) { + case 'hex': + return hexSlice(this, t, r); + case 'utf8': + case 'utf-8': + return utf8Slice(this, t, r); + case 'ascii': + return asciiSlice(this, t, r); + case 'latin1': + case 'binary': + return latin1Slice(this, t, r); + case 'base64': + return base64Slice(this, t, r); + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return utf16leSlice(this, t, r); + default: + if (n) throw new TypeError('Unknown encoding: ' + e); + (e = (e + '').toLowerCase()), (n = !0); + } + }.apply(this, arguments); + }), + (Buffer.prototype.equals = function equals(e) { + if (!Buffer.isBuffer(e)) throw new TypeError('Argument must be a Buffer'); + return this === e || 0 === Buffer.compare(this, e); + }), + (Buffer.prototype.inspect = function inspect() { + var e = '', + r = t.INSPECT_MAX_BYTES; + return ( + this.length > 0 && + ((e = this.toString('hex', 0, r).match(/.{2}/g).join(' ')), + this.length > r && (e += ' ... ')), + '' + ); + }), + (Buffer.prototype.compare = function compare(e, t, r, n, i) { + if (!Buffer.isBuffer(e)) throw new TypeError('Argument must be a Buffer'); + if ( + (void 0 === t && (t = 0), + void 0 === r && (r = e ? e.length : 0), + void 0 === n && (n = 0), + void 0 === i && (i = this.length), + t < 0 || r > e.length || n < 0 || i > this.length) + ) + throw new RangeError('out of range index'); + if (n >= i && t >= r) return 0; + if (n >= i) return -1; + if (t >= r) return 1; + if (((t >>>= 0), (r >>>= 0), (n >>>= 0), (i >>>= 0), this === e)) return 0; + for ( + var o = i - n, + s = r - t, + a = Math.min(o, s), + u = this.slice(n, i), + c = e.slice(t, r), + h = 0; + h < a; + ++h + ) + if (u[h] !== c[h]) { + (o = u[h]), (s = c[h]); + break; + } + return o < s ? -1 : s < o ? 1 : 0; + }), + (Buffer.prototype.includes = function includes(e, t, r) { + return -1 !== this.indexOf(e, t, r); + }), + (Buffer.prototype.indexOf = function indexOf(e, t, r) { + return bidirectionalIndexOf(this, e, t, r, !0); + }), + (Buffer.prototype.lastIndexOf = function lastIndexOf(e, t, r) { + return bidirectionalIndexOf(this, e, t, r, !1); + }), + (Buffer.prototype.write = function write(e, t, r, n) { + if (void 0 === t) (n = 'utf8'), (r = this.length), (t = 0); + else if (void 0 === r && 'string' == typeof t) (n = t), (r = this.length), (t = 0); + else { + if (!isFinite(t)) + throw new Error( + 'Buffer.write(string, encoding, offset[, length]) is no longer supported' + ); + (t |= 0), + isFinite(r) ? ((r |= 0), void 0 === n && (n = 'utf8')) : ((n = r), (r = void 0)); + } + var i = this.length - t; + if ( + ((void 0 === r || r > i) && (r = i), + (e.length > 0 && (r < 0 || t < 0)) || t > this.length) + ) + throw new RangeError('Attempt to write outside buffer bounds'); + n || (n = 'utf8'); + for (var o = !1; ; ) + switch (n) { + case 'hex': + return hexWrite(this, e, t, r); + case 'utf8': + case 'utf-8': + return utf8Write(this, e, t, r); + case 'ascii': + return asciiWrite(this, e, t, r); + case 'latin1': + case 'binary': + return latin1Write(this, e, t, r); + case 'base64': + return base64Write(this, e, t, r); + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return ucs2Write(this, e, t, r); + default: + if (o) throw new TypeError('Unknown encoding: ' + n); + (n = ('' + n).toLowerCase()), (o = !0); + } + }), + (Buffer.prototype.toJSON = function toJSON() { + return { + type: 'Buffer', + data: Array.prototype.slice.call(this._arr || this, 0), + }; + }); + var s = 4096; + function asciiSlice(e, t, r) { + var n = ''; + r = Math.min(e.length, r); + for (var i = t; i < r; ++i) n += String.fromCharCode(127 & e[i]); + return n; + } + function latin1Slice(e, t, r) { + var n = ''; + r = Math.min(e.length, r); + for (var i = t; i < r; ++i) n += String.fromCharCode(e[i]); + return n; + } + function hexSlice(e, t, r) { + var n = e.length; + (!t || t < 0) && (t = 0), (!r || r < 0 || r > n) && (r = n); + for (var i = '', o = t; o < r; ++o) i += toHex(e[o]); + return i; + } + function utf16leSlice(e, t, r) { + for (var n = e.slice(t, r), i = '', o = 0; o < n.length; o += 2) + i += String.fromCharCode(n[o] + 256 * n[o + 1]); + return i; + } + function checkOffset(e, t, r) { + if (e % 1 != 0 || e < 0) throw new RangeError('offset is not uint'); + if (e + t > r) throw new RangeError('Trying to access beyond buffer length'); + } + function checkInt(e, t, r, n, i, o) { + if (!Buffer.isBuffer(e)) + throw new TypeError('"buffer" argument must be a Buffer instance'); + if (t > i || t < o) throw new RangeError('"value" argument is out of bounds'); + if (r + n > e.length) throw new RangeError('Index out of range'); + } + function objectWriteUInt16(e, t, r, n) { + t < 0 && (t = 65535 + t + 1); + for (var i = 0, o = Math.min(e.length - r, 2); i < o; ++i) + e[r + i] = (t & (255 << (8 * (n ? i : 1 - i)))) >>> (8 * (n ? i : 1 - i)); + } + function objectWriteUInt32(e, t, r, n) { + t < 0 && (t = 4294967295 + t + 1); + for (var i = 0, o = Math.min(e.length - r, 4); i < o; ++i) + e[r + i] = (t >>> (8 * (n ? i : 3 - i))) & 255; + } + function checkIEEE754(e, t, r, n, i, o) { + if (r + n > e.length) throw new RangeError('Index out of range'); + if (r < 0) throw new RangeError('Index out of range'); + } + function writeFloat(e, t, r, n, o) { + return o || checkIEEE754(e, 0, r, 4), i.write(e, t, r, n, 23, 4), r + 4; + } + function writeDouble(e, t, r, n, o) { + return o || checkIEEE754(e, 0, r, 8), i.write(e, t, r, n, 52, 8), r + 8; + } + (Buffer.prototype.slice = function slice(e, t) { + var r, + n = this.length; + if ( + ((e = ~~e), + (t = void 0 === t ? n : ~~t), + e < 0 ? (e += n) < 0 && (e = 0) : e > n && (e = n), + t < 0 ? (t += n) < 0 && (t = 0) : t > n && (t = n), + t < e && (t = e), + Buffer.TYPED_ARRAY_SUPPORT) + ) + (r = this.subarray(e, t)).__proto__ = Buffer.prototype; + else { + var i = t - e; + r = new Buffer(i, void 0); + for (var o = 0; o < i; ++o) r[o] = this[o + e]; + } + return r; + }), + (Buffer.prototype.readUIntLE = function readUIntLE(e, t, r) { + (e |= 0), (t |= 0), r || checkOffset(e, t, this.length); + for (var n = this[e], i = 1, o = 0; ++o < t && (i *= 256); ) n += this[e + o] * i; + return n; + }), + (Buffer.prototype.readUIntBE = function readUIntBE(e, t, r) { + (e |= 0), (t |= 0), r || checkOffset(e, t, this.length); + for (var n = this[e + --t], i = 1; t > 0 && (i *= 256); ) n += this[e + --t] * i; + return n; + }), + (Buffer.prototype.readUInt8 = function readUInt8(e, t) { + return t || checkOffset(e, 1, this.length), this[e]; + }), + (Buffer.prototype.readUInt16LE = function readUInt16LE(e, t) { + return t || checkOffset(e, 2, this.length), this[e] | (this[e + 1] << 8); + }), + (Buffer.prototype.readUInt16BE = function readUInt16BE(e, t) { + return t || checkOffset(e, 2, this.length), (this[e] << 8) | this[e + 1]; + }), + (Buffer.prototype.readUInt32LE = function readUInt32LE(e, t) { + return ( + t || checkOffset(e, 4, this.length), + (this[e] | (this[e + 1] << 8) | (this[e + 2] << 16)) + 16777216 * this[e + 3] + ); + }), + (Buffer.prototype.readUInt32BE = function readUInt32BE(e, t) { + return ( + t || checkOffset(e, 4, this.length), + 16777216 * this[e] + ((this[e + 1] << 16) | (this[e + 2] << 8) | this[e + 3]) + ); + }), + (Buffer.prototype.readIntLE = function readIntLE(e, t, r) { + (e |= 0), (t |= 0), r || checkOffset(e, t, this.length); + for (var n = this[e], i = 1, o = 0; ++o < t && (i *= 256); ) n += this[e + o] * i; + return n >= (i *= 128) && (n -= Math.pow(2, 8 * t)), n; + }), + (Buffer.prototype.readIntBE = function readIntBE(e, t, r) { + (e |= 0), (t |= 0), r || checkOffset(e, t, this.length); + for (var n = t, i = 1, o = this[e + --n]; n > 0 && (i *= 256); ) o += this[e + --n] * i; + return o >= (i *= 128) && (o -= Math.pow(2, 8 * t)), o; + }), + (Buffer.prototype.readInt8 = function readInt8(e, t) { + return ( + t || checkOffset(e, 1, this.length), + 128 & this[e] ? -1 * (255 - this[e] + 1) : this[e] + ); + }), + (Buffer.prototype.readInt16LE = function readInt16LE(e, t) { + t || checkOffset(e, 2, this.length); + var r = this[e] | (this[e + 1] << 8); + return 32768 & r ? 4294901760 | r : r; + }), + (Buffer.prototype.readInt16BE = function readInt16BE(e, t) { + t || checkOffset(e, 2, this.length); + var r = this[e + 1] | (this[e] << 8); + return 32768 & r ? 4294901760 | r : r; + }), + (Buffer.prototype.readInt32LE = function readInt32LE(e, t) { + return ( + t || checkOffset(e, 4, this.length), + this[e] | (this[e + 1] << 8) | (this[e + 2] << 16) | (this[e + 3] << 24) + ); + }), + (Buffer.prototype.readInt32BE = function readInt32BE(e, t) { + return ( + t || checkOffset(e, 4, this.length), + (this[e] << 24) | (this[e + 1] << 16) | (this[e + 2] << 8) | this[e + 3] + ); + }), + (Buffer.prototype.readFloatLE = function readFloatLE(e, t) { + return t || checkOffset(e, 4, this.length), i.read(this, e, !0, 23, 4); + }), + (Buffer.prototype.readFloatBE = function readFloatBE(e, t) { + return t || checkOffset(e, 4, this.length), i.read(this, e, !1, 23, 4); + }), + (Buffer.prototype.readDoubleLE = function readDoubleLE(e, t) { + return t || checkOffset(e, 8, this.length), i.read(this, e, !0, 52, 8); + }), + (Buffer.prototype.readDoubleBE = function readDoubleBE(e, t) { + return t || checkOffset(e, 8, this.length), i.read(this, e, !1, 52, 8); + }), + (Buffer.prototype.writeUIntLE = function writeUIntLE(e, t, r, n) { + ((e = +e), (t |= 0), (r |= 0), n) || checkInt(this, e, t, r, Math.pow(2, 8 * r) - 1, 0); + var i = 1, + o = 0; + for (this[t] = 255 & e; ++o < r && (i *= 256); ) this[t + o] = (e / i) & 255; + return t + r; + }), + (Buffer.prototype.writeUIntBE = function writeUIntBE(e, t, r, n) { + ((e = +e), (t |= 0), (r |= 0), n) || checkInt(this, e, t, r, Math.pow(2, 8 * r) - 1, 0); + var i = r - 1, + o = 1; + for (this[t + i] = 255 & e; --i >= 0 && (o *= 256); ) this[t + i] = (e / o) & 255; + return t + r; + }), + (Buffer.prototype.writeUInt8 = function writeUInt8(e, t, r) { + return ( + (e = +e), + (t |= 0), + r || checkInt(this, e, t, 1, 255, 0), + Buffer.TYPED_ARRAY_SUPPORT || (e = Math.floor(e)), + (this[t] = 255 & e), + t + 1 + ); + }), + (Buffer.prototype.writeUInt16LE = function writeUInt16LE(e, t, r) { + return ( + (e = +e), + (t |= 0), + r || checkInt(this, e, t, 2, 65535, 0), + Buffer.TYPED_ARRAY_SUPPORT + ? ((this[t] = 255 & e), (this[t + 1] = e >>> 8)) + : objectWriteUInt16(this, e, t, !0), + t + 2 + ); + }), + (Buffer.prototype.writeUInt16BE = function writeUInt16BE(e, t, r) { + return ( + (e = +e), + (t |= 0), + r || checkInt(this, e, t, 2, 65535, 0), + Buffer.TYPED_ARRAY_SUPPORT + ? ((this[t] = e >>> 8), (this[t + 1] = 255 & e)) + : objectWriteUInt16(this, e, t, !1), + t + 2 + ); + }), + (Buffer.prototype.writeUInt32LE = function writeUInt32LE(e, t, r) { + return ( + (e = +e), + (t |= 0), + r || checkInt(this, e, t, 4, 4294967295, 0), + Buffer.TYPED_ARRAY_SUPPORT + ? ((this[t + 3] = e >>> 24), + (this[t + 2] = e >>> 16), + (this[t + 1] = e >>> 8), + (this[t] = 255 & e)) + : objectWriteUInt32(this, e, t, !0), + t + 4 + ); + }), + (Buffer.prototype.writeUInt32BE = function writeUInt32BE(e, t, r) { + return ( + (e = +e), + (t |= 0), + r || checkInt(this, e, t, 4, 4294967295, 0), + Buffer.TYPED_ARRAY_SUPPORT + ? ((this[t] = e >>> 24), + (this[t + 1] = e >>> 16), + (this[t + 2] = e >>> 8), + (this[t + 3] = 255 & e)) + : objectWriteUInt32(this, e, t, !1), + t + 4 + ); + }), + (Buffer.prototype.writeIntLE = function writeIntLE(e, t, r, n) { + if (((e = +e), (t |= 0), !n)) { + var i = Math.pow(2, 8 * r - 1); + checkInt(this, e, t, r, i - 1, -i); + } + var o = 0, + s = 1, + a = 0; + for (this[t] = 255 & e; ++o < r && (s *= 256); ) + e < 0 && 0 === a && 0 !== this[t + o - 1] && (a = 1), + (this[t + o] = (((e / s) >> 0) - a) & 255); + return t + r; + }), + (Buffer.prototype.writeIntBE = function writeIntBE(e, t, r, n) { + if (((e = +e), (t |= 0), !n)) { + var i = Math.pow(2, 8 * r - 1); + checkInt(this, e, t, r, i - 1, -i); + } + var o = r - 1, + s = 1, + a = 0; + for (this[t + o] = 255 & e; --o >= 0 && (s *= 256); ) + e < 0 && 0 === a && 0 !== this[t + o + 1] && (a = 1), + (this[t + o] = (((e / s) >> 0) - a) & 255); + return t + r; + }), + (Buffer.prototype.writeInt8 = function writeInt8(e, t, r) { + return ( + (e = +e), + (t |= 0), + r || checkInt(this, e, t, 1, 127, -128), + Buffer.TYPED_ARRAY_SUPPORT || (e = Math.floor(e)), + e < 0 && (e = 255 + e + 1), + (this[t] = 255 & e), + t + 1 + ); + }), + (Buffer.prototype.writeInt16LE = function writeInt16LE(e, t, r) { + return ( + (e = +e), + (t |= 0), + r || checkInt(this, e, t, 2, 32767, -32768), + Buffer.TYPED_ARRAY_SUPPORT + ? ((this[t] = 255 & e), (this[t + 1] = e >>> 8)) + : objectWriteUInt16(this, e, t, !0), + t + 2 + ); + }), + (Buffer.prototype.writeInt16BE = function writeInt16BE(e, t, r) { + return ( + (e = +e), + (t |= 0), + r || checkInt(this, e, t, 2, 32767, -32768), + Buffer.TYPED_ARRAY_SUPPORT + ? ((this[t] = e >>> 8), (this[t + 1] = 255 & e)) + : objectWriteUInt16(this, e, t, !1), + t + 2 + ); + }), + (Buffer.prototype.writeInt32LE = function writeInt32LE(e, t, r) { + return ( + (e = +e), + (t |= 0), + r || checkInt(this, e, t, 4, 2147483647, -2147483648), + Buffer.TYPED_ARRAY_SUPPORT + ? ((this[t] = 255 & e), + (this[t + 1] = e >>> 8), + (this[t + 2] = e >>> 16), + (this[t + 3] = e >>> 24)) + : objectWriteUInt32(this, e, t, !0), + t + 4 + ); + }), + (Buffer.prototype.writeInt32BE = function writeInt32BE(e, t, r) { + return ( + (e = +e), + (t |= 0), + r || checkInt(this, e, t, 4, 2147483647, -2147483648), + e < 0 && (e = 4294967295 + e + 1), + Buffer.TYPED_ARRAY_SUPPORT + ? ((this[t] = e >>> 24), + (this[t + 1] = e >>> 16), + (this[t + 2] = e >>> 8), + (this[t + 3] = 255 & e)) + : objectWriteUInt32(this, e, t, !1), + t + 4 + ); + }), + (Buffer.prototype.writeFloatLE = function writeFloatLE(e, t, r) { + return writeFloat(this, e, t, !0, r); + }), + (Buffer.prototype.writeFloatBE = function writeFloatBE(e, t, r) { + return writeFloat(this, e, t, !1, r); + }), + (Buffer.prototype.writeDoubleLE = function writeDoubleLE(e, t, r) { + return writeDouble(this, e, t, !0, r); + }), + (Buffer.prototype.writeDoubleBE = function writeDoubleBE(e, t, r) { + return writeDouble(this, e, t, !1, r); + }), + (Buffer.prototype.copy = function copy(e, t, r, n) { + if ( + (r || (r = 0), + n || 0 === n || (n = this.length), + t >= e.length && (t = e.length), + t || (t = 0), + n > 0 && n < r && (n = r), + n === r) + ) + return 0; + if (0 === e.length || 0 === this.length) return 0; + if (t < 0) throw new RangeError('targetStart out of bounds'); + if (r < 0 || r >= this.length) throw new RangeError('sourceStart out of bounds'); + if (n < 0) throw new RangeError('sourceEnd out of bounds'); + n > this.length && (n = this.length), e.length - t < n - r && (n = e.length - t + r); + var i, + o = n - r; + if (this === e && r < t && t < n) for (i = o - 1; i >= 0; --i) e[i + t] = this[i + r]; + else if (o < 1e3 || !Buffer.TYPED_ARRAY_SUPPORT) + for (i = 0; i < o; ++i) e[i + t] = this[i + r]; + else Uint8Array.prototype.set.call(e, this.subarray(r, r + o), t); + return o; + }), + (Buffer.prototype.fill = function fill(e, t, r, n) { + if ('string' == typeof e) { + if ( + ('string' == typeof t + ? ((n = t), (t = 0), (r = this.length)) + : 'string' == typeof r && ((n = r), (r = this.length)), + 1 === e.length) + ) { + var i = e.charCodeAt(0); + i < 256 && (e = i); + } + if (void 0 !== n && 'string' != typeof n) + throw new TypeError('encoding must be a string'); + if ('string' == typeof n && !Buffer.isEncoding(n)) + throw new TypeError('Unknown encoding: ' + n); + } else 'number' == typeof e && (e &= 255); + if (t < 0 || this.length < t || this.length < r) + throw new RangeError('Out of range index'); + if (r <= t) return this; + var o; + if ( + ((t >>>= 0), + (r = void 0 === r ? this.length : r >>> 0), + e || (e = 0), + 'number' == typeof e) + ) + for (o = t; o < r; ++o) this[o] = e; + else { + var s = Buffer.isBuffer(e) ? e : utf8ToBytes(new Buffer(e, n).toString()), + a = s.length; + for (o = 0; o < r - t; ++o) this[o + t] = s[o % a]; + } + return this; + }); + var a = /[^+\/0-9A-Za-z-_]/g; + function toHex(e) { + return e < 16 ? '0' + e.toString(16) : e.toString(16); + } + function utf8ToBytes(e, t) { + var r; + t = t || 1 / 0; + for (var n = e.length, i = null, o = [], s = 0; s < n; ++s) { + if ((r = e.charCodeAt(s)) > 55295 && r < 57344) { + if (!i) { + if (r > 56319) { + (t -= 3) > -1 && o.push(239, 191, 189); + continue; + } + if (s + 1 === n) { + (t -= 3) > -1 && o.push(239, 191, 189); + continue; + } + i = r; + continue; + } + if (r < 56320) { + (t -= 3) > -1 && o.push(239, 191, 189), (i = r); + continue; + } + r = 65536 + (((i - 55296) << 10) | (r - 56320)); + } else i && (t -= 3) > -1 && o.push(239, 191, 189); + if (((i = null), r < 128)) { + if ((t -= 1) < 0) break; + o.push(r); + } else if (r < 2048) { + if ((t -= 2) < 0) break; + o.push((r >> 6) | 192, (63 & r) | 128); + } else if (r < 65536) { + if ((t -= 3) < 0) break; + o.push((r >> 12) | 224, ((r >> 6) & 63) | 128, (63 & r) | 128); + } else { + if (!(r < 1114112)) throw new Error('Invalid code point'); + if ((t -= 4) < 0) break; + o.push( + (r >> 18) | 240, + ((r >> 12) & 63) | 128, + ((r >> 6) & 63) | 128, + (63 & r) | 128 + ); + } + } + return o; + } + function base64ToBytes(e) { + return n.toByteArray( + (function base64clean(e) { + if ( + (e = (function stringtrim(e) { + return e.trim ? e.trim() : e.replace(/^\s+|\s+$/g, ''); + })(e).replace(a, '')).length < 2 + ) + return ''; + for (; e.length % 4 != 0; ) e += '='; + return e; + })(e) + ); + } + function blitBuffer(e, t, r, n) { + for (var i = 0; i < n && !(i + r >= t.length || i >= e.length); ++i) t[i + r] = e[i]; + return i; + } + }).call(this, r(39)); + }, + function (e, t, r) { + 'use strict'; + (function (n) { + var i = + 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator + ? function (e) { + return typeof e; + } + : function (e) { + return e && + 'function' == typeof Symbol && + e.constructor === Symbol && + e !== Symbol.prototype + ? 'symbol' + : typeof e; + }, + u = { userAgent: !1 }, + p = {}; + /*! + Copyright (c) 2011, Yahoo! Inc. All rights reserved. + Code licensed under the BSD License: + http://developer.yahoo.com/yui/license.html + version: 2.9.0 + */ + if (void 0 === v) var v = {}; + v.lang = { + extend: function extend(t, r, n) { + if (!r || !t) + throw new Error( + 'YAHOO.lang.extend failed, please check that all dependencies are included.' + ); + var i = function d() {}; + if ( + ((i.prototype = r.prototype), + (t.prototype = new i()), + (t.prototype.constructor = t), + (t.superclass = r.prototype), + r.prototype.constructor == Object.prototype.constructor && + (r.prototype.constructor = r), + n) + ) { + var o; + for (o in n) t.prototype[o] = n[o]; + var s = function e() {}, + a = ['toString', 'valueOf']; + try { + /MSIE/.test(u.userAgent) && + (s = function e(t, r) { + for (o = 0; o < a.length; o += 1) { + var n = a[o], + i = r[n]; + 'function' == typeof i && i != Object.prototype[n] && (t[n] = i); + } + }); + } catch (e) {} + s(t.prototype, n); + } + }, + }; + /*! CryptoJS v3.1.2 core-fix.js + * code.google.com/p/crypto-js + * (c) 2009-2013 by Jeff Mott. All rights reserved. + * code.google.com/p/crypto-js/wiki/License + * THIS IS FIX of 'core.js' to fix Hmac issue. + * https://code.google.com/p/crypto-js/issues/detail?id=84 + * https://crypto-js.googlecode.com/svn-history/r667/branches/3.x/src/core.js + */ + var y = + y || + (function (e, t) { + var r = {}, + n = (r.lib = {}), + i = (n.Base = (function () { + function n() {} + return { + extend: function extend(e) { + n.prototype = this; + var t = new n(); + return ( + e && t.mixIn(e), + t.hasOwnProperty('init') || + (t.init = function () { + t.$super.init.apply(this, arguments); + }), + (t.init.prototype = t), + (t.$super = this), + t + ); + }, + create: function create() { + var e = this.extend(); + return e.init.apply(e, arguments), e; + }, + init: function init() {}, + mixIn: function mixIn(e) { + for (var t in e) e.hasOwnProperty(t) && (this[t] = e[t]); + e.hasOwnProperty('toString') && (this.toString = e.toString); + }, + clone: function clone() { + return this.init.prototype.extend(this); + }, + }; + })()), + o = (n.WordArray = i.extend({ + init: function init(e, t) { + (e = this.words = e || []), (this.sigBytes = void 0 != t ? t : 4 * e.length); + }, + toString: function toString(e) { + return (e || a).stringify(this); + }, + concat: function concat(e) { + var t = this.words, + r = e.words, + n = this.sigBytes, + i = e.sigBytes; + if ((this.clamp(), n % 4)) + for (var o = 0; o < i; o++) { + var s = (r[o >>> 2] >>> (24 - (o % 4) * 8)) & 255; + t[(n + o) >>> 2] |= s << (24 - ((n + o) % 4) * 8); + } + else for (o = 0; o < i; o += 4) t[(n + o) >>> 2] = r[o >>> 2]; + return (this.sigBytes += i), this; + }, + clamp: function clamp() { + var t = this.words, + r = this.sigBytes; + (t[r >>> 2] &= 4294967295 << (32 - (r % 4) * 8)), (t.length = e.ceil(r / 4)); + }, + clone: function clone() { + var e = i.clone.call(this); + return (e.words = this.words.slice(0)), e; + }, + random: function random(t) { + for (var r = [], n = 0; n < t; n += 4) r.push((4294967296 * e.random()) | 0); + return new o.init(r, t); + }, + })), + s = (r.enc = {}), + a = (s.Hex = { + stringify: function stringify(e) { + for (var t = e.words, r = e.sigBytes, n = [], i = 0; i < r; i++) { + var o = (t[i >>> 2] >>> (24 - (i % 4) * 8)) & 255; + n.push((o >>> 4).toString(16)), n.push((15 & o).toString(16)); + } + return n.join(''); + }, + parse: function parse(e) { + for (var t = e.length, r = [], n = 0; n < t; n += 2) + r[n >>> 3] |= parseInt(e.substr(n, 2), 16) << (24 - (n % 8) * 4); + return new o.init(r, t / 2); + }, + }), + u = (s.Latin1 = { + stringify: function stringify(e) { + for (var t = e.words, r = e.sigBytes, n = [], i = 0; i < r; i++) { + var o = (t[i >>> 2] >>> (24 - (i % 4) * 8)) & 255; + n.push(String.fromCharCode(o)); + } + return n.join(''); + }, + parse: function parse(e) { + for (var t = e.length, r = [], n = 0; n < t; n++) + r[n >>> 2] |= (255 & e.charCodeAt(n)) << (24 - (n % 4) * 8); + return new o.init(r, t); + }, + }), + c = (s.Utf8 = { + stringify: function stringify(e) { + try { + return decodeURIComponent(escape(u.stringify(e))); + } catch (e) { + throw new Error('Malformed UTF-8 data'); + } + }, + parse: function parse(e) { + return u.parse(unescape(encodeURIComponent(e))); + }, + }), + h = (n.BufferedBlockAlgorithm = i.extend({ + reset: function reset() { + (this._data = new o.init()), (this._nDataBytes = 0); + }, + _append: function _append(e) { + 'string' == typeof e && (e = c.parse(e)), + this._data.concat(e), + (this._nDataBytes += e.sigBytes); + }, + _process: function _process(t) { + var r = this._data, + n = r.words, + i = r.sigBytes, + s = this.blockSize, + a = i / (4 * s), + u = (a = t ? e.ceil(a) : e.max((0 | a) - this._minBufferSize, 0)) * s, + c = e.min(4 * u, i); + if (u) { + for (var h = 0; h < u; h += s) this._doProcessBlock(n, h); + var f = n.splice(0, u); + r.sigBytes -= c; + } + return new o.init(f, c); + }, + clone: function clone() { + var e = i.clone.call(this); + return (e._data = this._data.clone()), e; + }, + _minBufferSize: 0, + })), + f = + ((n.Hasher = h.extend({ + cfg: i.extend(), + init: function init(e) { + (this.cfg = this.cfg.extend(e)), this.reset(); + }, + reset: function reset() { + h.reset.call(this), this._doReset(); + }, + update: function update(e) { + return this._append(e), this._process(), this; + }, + finalize: function finalize(e) { + return e && this._append(e), this._doFinalize(); + }, + blockSize: 16, + _createHelper: function _createHelper(e) { + return function (t, r) { + return new e.init(r).finalize(t); + }; + }, + _createHmacHelper: function _createHmacHelper(e) { + return function (t, r) { + return new f.HMAC.init(e, r).finalize(t); + }; + }, + })), + (r.algo = {})); + return r; + })(Math); + !(function (e) { + var t, + r = (t = y).lib, + n = r.Base, + i = r.WordArray; + ((t = t.x64 = {}).Word = n.extend({ + init: function init(e, t) { + (this.high = e), (this.low = t); + }, + })), + (t.WordArray = n.extend({ + init: function init(e, t) { + (e = this.words = e || []), (this.sigBytes = void 0 != t ? t : 8 * e.length); + }, + toX32: function toX32() { + for (var e = this.words, t = e.length, r = [], n = 0; n < t; n++) { + var o = e[n]; + r.push(o.high), r.push(o.low); + } + return i.create(r, this.sigBytes); + }, + clone: function clone() { + for ( + var e = n.clone.call(this), + t = (e.words = this.words.slice(0)), + r = t.length, + i = 0; + i < r; + i++ + ) + t[i] = t[i].clone(); + return e; + }, + })); + })(), + (function () { + var e = y, + t = e.lib.WordArray; + e.enc.Base64 = { + stringify: function stringify(e) { + var t = e.words, + r = e.sigBytes, + n = this._map; + e.clamp(), (e = []); + for (var i = 0; i < r; i += 3) + for ( + var o = + (((t[i >>> 2] >>> (24 - (i % 4) * 8)) & 255) << 16) | + (((t[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 255) << 8) | + ((t[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 255), + s = 0; + 4 > s && i + 0.75 * s < r; + s++ + ) + e.push(n.charAt((o >>> (6 * (3 - s))) & 63)); + if ((t = n.charAt(64))) for (; e.length % 4; ) e.push(t); + return e.join(''); + }, + parse: function parse(e) { + var r = e.length, + n = this._map; + (i = n.charAt(64)) && -1 != (i = e.indexOf(i)) && (r = i); + for (var i = [], o = 0, s = 0; s < r; s++) + if (s % 4) { + var a = n.indexOf(e.charAt(s - 1)) << ((s % 4) * 2), + u = n.indexOf(e.charAt(s)) >>> (6 - (s % 4) * 2); + (i[o >>> 2] |= (a | u) << (24 - (o % 4) * 8)), o++; + } + return t.create(i, o); + }, + _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', + }; + })(), + (function (e) { + for ( + var t = y, + r = (i = t.lib).WordArray, + n = i.Hasher, + i = t.algo, + o = [], + s = [], + a = function u(e) { + return (4294967296 * (e - (0 | e))) | 0; + }, + u = 2, + c = 0; + 64 > c; + + ) { + var h; + e: { + h = u; + for (var f = e.sqrt(h), l = 2; l <= f; l++) + if (!(h % l)) { + h = !1; + break e; + } + h = !0; + } + h && (8 > c && (o[c] = a(e.pow(u, 0.5))), (s[c] = a(e.pow(u, 1 / 3))), c++), u++; + } + var g = []; + i = i.SHA256 = n.extend({ + _doReset: function _doReset() { + this._hash = new r.init(o.slice(0)); + }, + _doProcessBlock: function _doProcessBlock(e, t) { + for ( + var r = this._hash.words, + n = r[0], + i = r[1], + o = r[2], + a = r[3], + u = r[4], + c = r[5], + h = r[6], + f = r[7], + l = 0; + 64 > l; + l++ + ) { + if (16 > l) g[l] = 0 | e[t + l]; + else { + var p = g[l - 15], + d = g[l - 2]; + g[l] = + (((p << 25) | (p >>> 7)) ^ ((p << 14) | (p >>> 18)) ^ (p >>> 3)) + + g[l - 7] + + (((d << 15) | (d >>> 17)) ^ ((d << 13) | (d >>> 19)) ^ (d >>> 10)) + + g[l - 16]; + } + (p = + f + + (((u << 26) | (u >>> 6)) ^ ((u << 21) | (u >>> 11)) ^ ((u << 7) | (u >>> 25))) + + ((u & c) ^ (~u & h)) + + s[l] + + g[l]), + (d = + (((n << 30) | (n >>> 2)) ^ + ((n << 19) | (n >>> 13)) ^ + ((n << 10) | (n >>> 22))) + + ((n & i) ^ (n & o) ^ (i & o))), + (f = h), + (h = c), + (c = u), + (u = (a + p) | 0), + (a = o), + (o = i), + (i = n), + (n = (p + d) | 0); + } + (r[0] = (r[0] + n) | 0), + (r[1] = (r[1] + i) | 0), + (r[2] = (r[2] + o) | 0), + (r[3] = (r[3] + a) | 0), + (r[4] = (r[4] + u) | 0), + (r[5] = (r[5] + c) | 0), + (r[6] = (r[6] + h) | 0), + (r[7] = (r[7] + f) | 0); + }, + _doFinalize: function _doFinalize() { + var t = this._data, + r = t.words, + n = 8 * this._nDataBytes, + i = 8 * t.sigBytes; + return ( + (r[i >>> 5] |= 128 << (24 - (i % 32))), + (r[14 + (((i + 64) >>> 9) << 4)] = e.floor(n / 4294967296)), + (r[15 + (((i + 64) >>> 9) << 4)] = n), + (t.sigBytes = 4 * r.length), + this._process(), + this._hash + ); + }, + clone: function clone() { + var e = n.clone.call(this); + return (e._hash = this._hash.clone()), e; + }, + }); + (t.SHA256 = n._createHelper(i)), (t.HmacSHA256 = n._createHmacHelper(i)); + })(Math), + (function () { + function a() { + return r.create.apply(r, arguments); + } + for ( + var e = y, + t = e.lib.Hasher, + r = (i = e.x64).Word, + n = i.WordArray, + i = e.algo, + o = [ + a(1116352408, 3609767458), + a(1899447441, 602891725), + a(3049323471, 3964484399), + a(3921009573, 2173295548), + a(961987163, 4081628472), + a(1508970993, 3053834265), + a(2453635748, 2937671579), + a(2870763221, 3664609560), + a(3624381080, 2734883394), + a(310598401, 1164996542), + a(607225278, 1323610764), + a(1426881987, 3590304994), + a(1925078388, 4068182383), + a(2162078206, 991336113), + a(2614888103, 633803317), + a(3248222580, 3479774868), + a(3835390401, 2666613458), + a(4022224774, 944711139), + a(264347078, 2341262773), + a(604807628, 2007800933), + a(770255983, 1495990901), + a(1249150122, 1856431235), + a(1555081692, 3175218132), + a(1996064986, 2198950837), + a(2554220882, 3999719339), + a(2821834349, 766784016), + a(2952996808, 2566594879), + a(3210313671, 3203337956), + a(3336571891, 1034457026), + a(3584528711, 2466948901), + a(113926993, 3758326383), + a(338241895, 168717936), + a(666307205, 1188179964), + a(773529912, 1546045734), + a(1294757372, 1522805485), + a(1396182291, 2643833823), + a(1695183700, 2343527390), + a(1986661051, 1014477480), + a(2177026350, 1206759142), + a(2456956037, 344077627), + a(2730485921, 1290863460), + a(2820302411, 3158454273), + a(3259730800, 3505952657), + a(3345764771, 106217008), + a(3516065817, 3606008344), + a(3600352804, 1432725776), + a(4094571909, 1467031594), + a(275423344, 851169720), + a(430227734, 3100823752), + a(506948616, 1363258195), + a(659060556, 3750685593), + a(883997877, 3785050280), + a(958139571, 3318307427), + a(1322822218, 3812723403), + a(1537002063, 2003034995), + a(1747873779, 3602036899), + a(1955562222, 1575990012), + a(2024104815, 1125592928), + a(2227730452, 2716904306), + a(2361852424, 442776044), + a(2428436474, 593698344), + a(2756734187, 3733110249), + a(3204031479, 2999351573), + a(3329325298, 3815920427), + a(3391569614, 3928383900), + a(3515267271, 566280711), + a(3940187606, 3454069534), + a(4118630271, 4000239992), + a(116418474, 1914138554), + a(174292421, 2731055270), + a(289380356, 3203993006), + a(460393269, 320620315), + a(685471733, 587496836), + a(852142971, 1086792851), + a(1017036298, 365543100), + a(1126000580, 2618297676), + a(1288033470, 3409855158), + a(1501505948, 4234509866), + a(1607167915, 987167468), + a(1816402316, 1246189591), + ], + s = [], + u = 0; + 80 > u; + u++ + ) + s[u] = a(); + (i = i.SHA512 = + t.extend({ + _doReset: function _doReset() { + this._hash = new n.init([ + new r.init(1779033703, 4089235720), + new r.init(3144134277, 2227873595), + new r.init(1013904242, 4271175723), + new r.init(2773480762, 1595750129), + new r.init(1359893119, 2917565137), + new r.init(2600822924, 725511199), + new r.init(528734635, 4215389547), + new r.init(1541459225, 327033209), + ]); + }, + _doProcessBlock: function _doProcessBlock(e, t) { + for ( + var r = (f = this._hash.words)[0], + n = f[1], + i = f[2], + a = f[3], + u = f[4], + c = f[5], + h = f[6], + f = f[7], + l = r.high, + g = r.low, + p = n.high, + d = n.low, + v = i.high, + y = i.low, + m = a.high, + S = a.low, + F = u.high, + b = u.low, + _ = c.high, + w = c.low, + E = h.high, + x = h.low, + C = f.high, + P = f.low, + A = l, + k = g, + I = p, + B = d, + R = v, + T = y, + U = m, + M = S, + L = F, + D = b, + N = _, + O = w, + H = E, + j = x, + K = C, + q = P, + W = 0; + 80 > W; + W++ + ) { + var V = s[W]; + if (16 > W) + var J = (V.high = 0 | e[t + 2 * W]), + z = (V.low = 0 | e[t + 2 * W + 1]); + else { + J = + (((z = (J = s[W - 15]).high) >>> 1) | ((Y = J.low) << 31)) ^ + ((z >>> 8) | (Y << 24)) ^ + (z >>> 7); + var Y = + ((Y >>> 1) | (z << 31)) ^ + ((Y >>> 8) | (z << 24)) ^ + ((Y >>> 7) | (z << 25)), + G = + (((z = (G = s[W - 2]).high) >>> 19) | ((X = G.low) << 13)) ^ + ((z << 3) | (X >>> 29)) ^ + (z >>> 6), + X = + ((X >>> 19) | (z << 13)) ^ + ((X << 3) | (z >>> 29)) ^ + ((X >>> 6) | (z << 26)), + Q = (z = s[W - 7]).high, + Z = ($ = s[W - 16]).high, + $ = $.low; + J = + (J = + (J = J + Q + ((z = Y + z.low) >>> 0 < Y >>> 0 ? 1 : 0)) + + G + + ((z = z + X) >>> 0 < X >>> 0 ? 1 : 0)) + + Z + + ((z = z + $) >>> 0 < $ >>> 0 ? 1 : 0); + (V.high = J), (V.low = z); + } + (Q = (L & N) ^ (~L & H)), + ($ = (D & O) ^ (~D & j)), + (V = (A & I) ^ (A & R) ^ (I & R)); + var ee = (k & B) ^ (k & T) ^ (B & T), + te = + ((Y = + ((A >>> 28) | (k << 4)) ^ + ((A << 30) | (k >>> 2)) ^ + ((A << 25) | (k >>> 7))), + (G = + ((k >>> 28) | (A << 4)) ^ + ((k << 30) | (A >>> 2)) ^ + ((k << 25) | (A >>> 7))), + (X = o[W]).high), + re = X.low; + (Z = + (Z = + (Z = + (Z = + K + + (((L >>> 14) | (D << 18)) ^ + ((L >>> 18) | (D << 14)) ^ + ((L << 23) | (D >>> 9))) + + ((X = + q + + (((D >>> 14) | (L << 18)) ^ + ((D >>> 18) | (L << 14)) ^ + ((D << 23) | (L >>> 9)))) >>> + 0 < + q >>> 0 + ? 1 + : 0)) + + Q + + ((X = X + $) >>> 0 < $ >>> 0 ? 1 : 0)) + + te + + ((X = X + re) >>> 0 < re >>> 0 ? 1 : 0)) + + J + + ((X = X + z) >>> 0 < z >>> 0 ? 1 : 0)), + (V = Y + V + ((z = G + ee) >>> 0 < G >>> 0 ? 1 : 0)), + (K = H), + (q = j), + (H = N), + (j = O), + (N = L), + (O = D), + (L = (U + Z + ((D = (M + X) | 0) >>> 0 < M >>> 0 ? 1 : 0)) | 0), + (U = R), + (M = T), + (R = I), + (T = B), + (I = A), + (B = k), + (A = (Z + V + ((k = (X + z) | 0) >>> 0 < X >>> 0 ? 1 : 0)) | 0); + } + (g = r.low = g + k), + (r.high = l + A + (g >>> 0 < k >>> 0 ? 1 : 0)), + (d = n.low = d + B), + (n.high = p + I + (d >>> 0 < B >>> 0 ? 1 : 0)), + (y = i.low = y + T), + (i.high = v + R + (y >>> 0 < T >>> 0 ? 1 : 0)), + (S = a.low = S + M), + (a.high = m + U + (S >>> 0 < M >>> 0 ? 1 : 0)), + (b = u.low = b + D), + (u.high = F + L + (b >>> 0 < D >>> 0 ? 1 : 0)), + (w = c.low = w + O), + (c.high = _ + N + (w >>> 0 < O >>> 0 ? 1 : 0)), + (x = h.low = x + j), + (h.high = E + H + (x >>> 0 < j >>> 0 ? 1 : 0)), + (P = f.low = P + q), + (f.high = C + K + (P >>> 0 < q >>> 0 ? 1 : 0)); + }, + _doFinalize: function _doFinalize() { + var e = this._data, + t = e.words, + r = 8 * this._nDataBytes, + n = 8 * e.sigBytes; + return ( + (t[n >>> 5] |= 128 << (24 - (n % 32))), + (t[30 + (((n + 128) >>> 10) << 5)] = Math.floor(r / 4294967296)), + (t[31 + (((n + 128) >>> 10) << 5)] = r), + (e.sigBytes = 4 * t.length), + this._process(), + this._hash.toX32() + ); + }, + clone: function clone() { + var e = t.clone.call(this); + return (e._hash = this._hash.clone()), e; + }, + blockSize: 32, + })), + (e.SHA512 = t._createHelper(i)), + (e.HmacSHA512 = t._createHmacHelper(i)); + })(), + (function () { + var e = y, + t = (i = e.x64).Word, + r = i.WordArray, + n = (i = e.algo).SHA512, + i = (i.SHA384 = n.extend({ + _doReset: function _doReset() { + this._hash = new r.init([ + new t.init(3418070365, 3238371032), + new t.init(1654270250, 914150663), + new t.init(2438529370, 812702999), + new t.init(355462360, 4144912697), + new t.init(1731405415, 4290775857), + new t.init(2394180231, 1750603025), + new t.init(3675008525, 1694076839), + new t.init(1203062813, 3204075428), + ]); + }, + _doFinalize: function _doFinalize() { + var e = n._doFinalize.call(this); + return (e.sigBytes -= 16), e; + }, + })); + (e.SHA384 = n._createHelper(i)), (e.HmacSHA384 = n._createHmacHelper(i)); + })(); + /*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ + */ + var S, + F = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', + _ = '='; + function hex2b64(e) { + var t, + r, + n = ''; + for (t = 0; t + 3 <= e.length; t += 3) + (r = parseInt(e.substring(t, t + 3), 16)), (n += F.charAt(r >> 6) + F.charAt(63 & r)); + if ( + (t + 1 == e.length + ? ((r = parseInt(e.substring(t, t + 1), 16)), (n += F.charAt(r << 2))) + : t + 2 == e.length && + ((r = parseInt(e.substring(t, t + 2), 16)), + (n += F.charAt(r >> 2) + F.charAt((3 & r) << 4))), + _) + ) + for (; (3 & n.length) > 0; ) n += _; + return n; + } + function b64tohex(e) { + var t, + r, + n, + i = '', + o = 0; + for (t = 0; t < e.length && e.charAt(t) != _; ++t) + (n = F.indexOf(e.charAt(t))) < 0 || + (0 == o + ? ((i += int2char(n >> 2)), (r = 3 & n), (o = 1)) + : 1 == o + ? ((i += int2char((r << 2) | (n >> 4))), (r = 15 & n), (o = 2)) + : 2 == o + ? ((i += int2char(r)), (i += int2char(n >> 2)), (r = 3 & n), (o = 3)) + : ((i += int2char((r << 2) | (n >> 4))), (i += int2char(15 & n)), (o = 0))); + return 1 == o && (i += int2char(r << 2)), i; + } + function b64toBA(e) { + var t, + r = b64tohex(e), + n = new Array(); + for (t = 0; 2 * t < r.length; ++t) n[t] = parseInt(r.substring(2 * t, 2 * t + 2), 16); + return n; + } + function BigInteger(e, t, r) { + null != e && + ('number' == typeof e + ? this.fromNumber(e, t, r) + : null == t && 'string' != typeof e + ? this.fromString(e, 256) + : this.fromString(e, t)); + } + function nbi() { + return new BigInteger(null); + } + 'Microsoft Internet Explorer' == u.appName + ? ((BigInteger.prototype.am = function am2(e, t, r, n, i, o) { + for (var s = 32767 & t, a = t >> 15; --o >= 0; ) { + var u = 32767 & this[e], + c = this[e++] >> 15, + h = a * u + c * s; + (i = + ((u = s * u + ((32767 & h) << 15) + r[n] + (1073741823 & i)) >>> 30) + + (h >>> 15) + + a * c + + (i >>> 30)), + (r[n++] = 1073741823 & u); + } + return i; + }), + (S = 30)) + : 'Netscape' != u.appName + ? ((BigInteger.prototype.am = function am1(e, t, r, n, i, o) { + for (; --o >= 0; ) { + var s = t * this[e++] + r[n] + i; + (i = Math.floor(s / 67108864)), (r[n++] = 67108863 & s); + } + return i; + }), + (S = 26)) + : ((BigInteger.prototype.am = function am3(e, t, r, n, i, o) { + for (var s = 16383 & t, a = t >> 14; --o >= 0; ) { + var u = 16383 & this[e], + c = this[e++] >> 14, + h = a * u + c * s; + (i = ((u = s * u + ((16383 & h) << 14) + r[n] + i) >> 28) + (h >> 14) + a * c), + (r[n++] = 268435455 & u); + } + return i; + }), + (S = 28)), + (BigInteger.prototype.DB = S), + (BigInteger.prototype.DM = (1 << S) - 1), + (BigInteger.prototype.DV = 1 << S); + (BigInteger.prototype.FV = Math.pow(2, 52)), + (BigInteger.prototype.F1 = 52 - S), + (BigInteger.prototype.F2 = 2 * S - 52); + var w, + E, + C = '0123456789abcdefghijklmnopqrstuvwxyz', + P = new Array(); + for (w = '0'.charCodeAt(0), E = 0; E <= 9; ++E) P[w++] = E; + for (w = 'a'.charCodeAt(0), E = 10; E < 36; ++E) P[w++] = E; + for (w = 'A'.charCodeAt(0), E = 10; E < 36; ++E) P[w++] = E; + function int2char(e) { + return C.charAt(e); + } + function intAt(e, t) { + var r = P[e.charCodeAt(t)]; + return null == r ? -1 : r; + } + function nbv(e) { + var t = nbi(); + return t.fromInt(e), t; + } + function nbits(e) { + var t, + r = 1; + return ( + 0 != (t = e >>> 16) && ((e = t), (r += 16)), + 0 != (t = e >> 8) && ((e = t), (r += 8)), + 0 != (t = e >> 4) && ((e = t), (r += 4)), + 0 != (t = e >> 2) && ((e = t), (r += 2)), + 0 != (t = e >> 1) && ((e = t), (r += 1)), + r + ); + } + function Classic(e) { + this.m = e; + } + function Montgomery(e) { + (this.m = e), + (this.mp = e.invDigit()), + (this.mpl = 32767 & this.mp), + (this.mph = this.mp >> 15), + (this.um = (1 << (e.DB - 15)) - 1), + (this.mt2 = 2 * e.t); + } + function op_and(e, t) { + return e & t; + } + function op_or(e, t) { + return e | t; + } + function op_xor(e, t) { + return e ^ t; + } + function op_andnot(e, t) { + return e & ~t; + } + function lbit(e) { + if (0 == e) return -1; + var t = 0; + return ( + 0 == (65535 & e) && ((e >>= 16), (t += 16)), + 0 == (255 & e) && ((e >>= 8), (t += 8)), + 0 == (15 & e) && ((e >>= 4), (t += 4)), + 0 == (3 & e) && ((e >>= 2), (t += 2)), + 0 == (1 & e) && ++t, + t + ); + } + function cbit(e) { + for (var t = 0; 0 != e; ) (e &= e - 1), ++t; + return t; + } + function NullExp() {} + function nNop(e) { + return e; + } + function Barrett(e) { + (this.r2 = nbi()), + (this.q3 = nbi()), + BigInteger.ONE.dlShiftTo(2 * e.t, this.r2), + (this.mu = this.r2.divide(e)), + (this.m = e); + } + (Classic.prototype.convert = function cConvert(e) { + return e.s < 0 || e.compareTo(this.m) >= 0 ? e.mod(this.m) : e; + }), + (Classic.prototype.revert = function cRevert(e) { + return e; + }), + (Classic.prototype.reduce = function cReduce(e) { + e.divRemTo(this.m, null, e); + }), + (Classic.prototype.mulTo = function cMulTo(e, t, r) { + e.multiplyTo(t, r), this.reduce(r); + }), + (Classic.prototype.sqrTo = function cSqrTo(e, t) { + e.squareTo(t), this.reduce(t); + }), + (Montgomery.prototype.convert = function montConvert(e) { + var t = nbi(); + return ( + e.abs().dlShiftTo(this.m.t, t), + t.divRemTo(this.m, null, t), + e.s < 0 && t.compareTo(BigInteger.ZERO) > 0 && this.m.subTo(t, t), + t + ); + }), + (Montgomery.prototype.revert = function montRevert(e) { + var t = nbi(); + return e.copyTo(t), this.reduce(t), t; + }), + (Montgomery.prototype.reduce = function montReduce(e) { + for (; e.t <= this.mt2; ) e[e.t++] = 0; + for (var t = 0; t < this.m.t; ++t) { + var r = 32767 & e[t], + n = + (r * this.mpl + (((r * this.mph + (e[t] >> 15) * this.mpl) & this.um) << 15)) & + e.DM; + for (e[(r = t + this.m.t)] += this.m.am(0, n, e, t, 0, this.m.t); e[r] >= e.DV; ) + (e[r] -= e.DV), e[++r]++; + } + e.clamp(), e.drShiftTo(this.m.t, e), e.compareTo(this.m) >= 0 && e.subTo(this.m, e); + }), + (Montgomery.prototype.mulTo = function montMulTo(e, t, r) { + e.multiplyTo(t, r), this.reduce(r); + }), + (Montgomery.prototype.sqrTo = function montSqrTo(e, t) { + e.squareTo(t), this.reduce(t); + }), + (BigInteger.prototype.copyTo = function bnpCopyTo(e) { + for (var t = this.t - 1; t >= 0; --t) e[t] = this[t]; + (e.t = this.t), (e.s = this.s); + }), + (BigInteger.prototype.fromInt = function bnpFromInt(e) { + (this.t = 1), + (this.s = e < 0 ? -1 : 0), + e > 0 ? (this[0] = e) : e < -1 ? (this[0] = e + this.DV) : (this.t = 0); + }), + (BigInteger.prototype.fromString = function bnpFromString(e, t) { + var r; + if (16 == t) r = 4; + else if (8 == t) r = 3; + else if (256 == t) r = 8; + else if (2 == t) r = 1; + else if (32 == t) r = 5; + else { + if (4 != t) return void this.fromRadix(e, t); + r = 2; + } + (this.t = 0), (this.s = 0); + for (var n = e.length, i = !1, o = 0; --n >= 0; ) { + var s = 8 == r ? 255 & e[n] : intAt(e, n); + s < 0 + ? '-' == e.charAt(n) && (i = !0) + : ((i = !1), + 0 == o + ? (this[this.t++] = s) + : o + r > this.DB + ? ((this[this.t - 1] |= (s & ((1 << (this.DB - o)) - 1)) << o), + (this[this.t++] = s >> (this.DB - o))) + : (this[this.t - 1] |= s << o), + (o += r) >= this.DB && (o -= this.DB)); + } + 8 == r && + 0 != (128 & e[0]) && + ((this.s = -1), o > 0 && (this[this.t - 1] |= ((1 << (this.DB - o)) - 1) << o)), + this.clamp(), + i && BigInteger.ZERO.subTo(this, this); + }), + (BigInteger.prototype.clamp = function bnpClamp() { + for (var e = this.s & this.DM; this.t > 0 && this[this.t - 1] == e; ) --this.t; + }), + (BigInteger.prototype.dlShiftTo = function bnpDLShiftTo(e, t) { + var r; + for (r = this.t - 1; r >= 0; --r) t[r + e] = this[r]; + for (r = e - 1; r >= 0; --r) t[r] = 0; + (t.t = this.t + e), (t.s = this.s); + }), + (BigInteger.prototype.drShiftTo = function bnpDRShiftTo(e, t) { + for (var r = e; r < this.t; ++r) t[r - e] = this[r]; + (t.t = Math.max(this.t - e, 0)), (t.s = this.s); + }), + (BigInteger.prototype.lShiftTo = function bnpLShiftTo(e, t) { + var r, + n = e % this.DB, + i = this.DB - n, + o = (1 << i) - 1, + s = Math.floor(e / this.DB), + a = (this.s << n) & this.DM; + for (r = this.t - 1; r >= 0; --r) + (t[r + s + 1] = (this[r] >> i) | a), (a = (this[r] & o) << n); + for (r = s - 1; r >= 0; --r) t[r] = 0; + (t[s] = a), (t.t = this.t + s + 1), (t.s = this.s), t.clamp(); + }), + (BigInteger.prototype.rShiftTo = function bnpRShiftTo(e, t) { + t.s = this.s; + var r = Math.floor(e / this.DB); + if (r >= this.t) t.t = 0; + else { + var n = e % this.DB, + i = this.DB - n, + o = (1 << n) - 1; + t[0] = this[r] >> n; + for (var s = r + 1; s < this.t; ++s) + (t[s - r - 1] |= (this[s] & o) << i), (t[s - r] = this[s] >> n); + n > 0 && (t[this.t - r - 1] |= (this.s & o) << i), (t.t = this.t - r), t.clamp(); + } + }), + (BigInteger.prototype.subTo = function bnpSubTo(e, t) { + for (var r = 0, n = 0, i = Math.min(e.t, this.t); r < i; ) + (n += this[r] - e[r]), (t[r++] = n & this.DM), (n >>= this.DB); + if (e.t < this.t) { + for (n -= e.s; r < this.t; ) (n += this[r]), (t[r++] = n & this.DM), (n >>= this.DB); + n += this.s; + } else { + for (n += this.s; r < e.t; ) (n -= e[r]), (t[r++] = n & this.DM), (n >>= this.DB); + n -= e.s; + } + (t.s = n < 0 ? -1 : 0), + n < -1 ? (t[r++] = this.DV + n) : n > 0 && (t[r++] = n), + (t.t = r), + t.clamp(); + }), + (BigInteger.prototype.multiplyTo = function bnpMultiplyTo(e, t) { + var r = this.abs(), + n = e.abs(), + i = r.t; + for (t.t = i + n.t; --i >= 0; ) t[i] = 0; + for (i = 0; i < n.t; ++i) t[i + r.t] = r.am(0, n[i], t, i, 0, r.t); + (t.s = 0), t.clamp(), this.s != e.s && BigInteger.ZERO.subTo(t, t); + }), + (BigInteger.prototype.squareTo = function bnpSquareTo(e) { + for (var t = this.abs(), r = (e.t = 2 * t.t); --r >= 0; ) e[r] = 0; + for (r = 0; r < t.t - 1; ++r) { + var n = t.am(r, t[r], e, 2 * r, 0, 1); + (e[r + t.t] += t.am(r + 1, 2 * t[r], e, 2 * r + 1, n, t.t - r - 1)) >= t.DV && + ((e[r + t.t] -= t.DV), (e[r + t.t + 1] = 1)); + } + e.t > 0 && (e[e.t - 1] += t.am(r, t[r], e, 2 * r, 0, 1)), (e.s = 0), e.clamp(); + }), + (BigInteger.prototype.divRemTo = function bnpDivRemTo(e, t, r) { + var n = e.abs(); + if (!(n.t <= 0)) { + var i = this.abs(); + if (i.t < n.t) return null != t && t.fromInt(0), void (null != r && this.copyTo(r)); + null == r && (r = nbi()); + var o = nbi(), + s = this.s, + a = e.s, + u = this.DB - nbits(n[n.t - 1]); + u > 0 ? (n.lShiftTo(u, o), i.lShiftTo(u, r)) : (n.copyTo(o), i.copyTo(r)); + var c = o.t, + h = o[c - 1]; + if (0 != h) { + var f = h * (1 << this.F1) + (c > 1 ? o[c - 2] >> this.F2 : 0), + l = this.FV / f, + g = (1 << this.F1) / f, + p = 1 << this.F2, + d = r.t, + v = d - c, + y = null == t ? nbi() : t; + for ( + o.dlShiftTo(v, y), + r.compareTo(y) >= 0 && ((r[r.t++] = 1), r.subTo(y, r)), + BigInteger.ONE.dlShiftTo(c, y), + y.subTo(o, o); + o.t < c; + + ) + o[o.t++] = 0; + for (; --v >= 0; ) { + var m = r[--d] == h ? this.DM : Math.floor(r[d] * l + (r[d - 1] + p) * g); + if ((r[d] += o.am(0, m, r, v, 0, c)) < m) + for (o.dlShiftTo(v, y), r.subTo(y, r); r[d] < --m; ) r.subTo(y, r); + } + null != t && (r.drShiftTo(c, t), s != a && BigInteger.ZERO.subTo(t, t)), + (r.t = c), + r.clamp(), + u > 0 && r.rShiftTo(u, r), + s < 0 && BigInteger.ZERO.subTo(r, r); + } + } + }), + (BigInteger.prototype.invDigit = function bnpInvDigit() { + if (this.t < 1) return 0; + var e = this[0]; + if (0 == (1 & e)) return 0; + var t = 3 & e; + return (t = + ((t = + ((t = ((t = (t * (2 - (15 & e) * t)) & 15) * (2 - (255 & e) * t)) & 255) * + (2 - (((65535 & e) * t) & 65535))) & + 65535) * + (2 - ((e * t) % this.DV))) % + this.DV) > 0 + ? this.DV - t + : -t; + }), + (BigInteger.prototype.isEven = function bnpIsEven() { + return 0 == (this.t > 0 ? 1 & this[0] : this.s); + }), + (BigInteger.prototype.exp = function bnpExp(e, t) { + if (e > 4294967295 || e < 1) return BigInteger.ONE; + var r = nbi(), + n = nbi(), + i = t.convert(this), + o = nbits(e) - 1; + for (i.copyTo(r); --o >= 0; ) + if ((t.sqrTo(r, n), (e & (1 << o)) > 0)) t.mulTo(n, i, r); + else { + var s = r; + (r = n), (n = s); + } + return t.revert(r); + }), + (BigInteger.prototype.toString = function bnToString(e) { + if (this.s < 0) return '-' + this.negate().toString(e); + var t; + if (16 == e) t = 4; + else if (8 == e) t = 3; + else if (2 == e) t = 1; + else if (32 == e) t = 5; + else { + if (4 != e) return this.toRadix(e); + t = 2; + } + var r, + n = (1 << t) - 1, + i = !1, + o = '', + s = this.t, + a = this.DB - ((s * this.DB) % t); + if (s-- > 0) + for (a < this.DB && (r = this[s] >> a) > 0 && ((i = !0), (o = int2char(r))); s >= 0; ) + a < t + ? ((r = (this[s] & ((1 << a) - 1)) << (t - a)), + (r |= this[--s] >> (a += this.DB - t))) + : ((r = (this[s] >> (a -= t)) & n), a <= 0 && ((a += this.DB), --s)), + r > 0 && (i = !0), + i && (o += int2char(r)); + return i ? o : '0'; + }), + (BigInteger.prototype.negate = function bnNegate() { + var e = nbi(); + return BigInteger.ZERO.subTo(this, e), e; + }), + (BigInteger.prototype.abs = function bnAbs() { + return this.s < 0 ? this.negate() : this; + }), + (BigInteger.prototype.compareTo = function bnCompareTo(e) { + var t = this.s - e.s; + if (0 != t) return t; + var r = this.t; + if (0 != (t = r - e.t)) return this.s < 0 ? -t : t; + for (; --r >= 0; ) if (0 != (t = this[r] - e[r])) return t; + return 0; + }), + (BigInteger.prototype.bitLength = function bnBitLength() { + return this.t <= 0 + ? 0 + : this.DB * (this.t - 1) + nbits(this[this.t - 1] ^ (this.s & this.DM)); + }), + (BigInteger.prototype.mod = function bnMod(e) { + var t = nbi(); + return ( + this.abs().divRemTo(e, null, t), + this.s < 0 && t.compareTo(BigInteger.ZERO) > 0 && e.subTo(t, t), + t + ); + }), + (BigInteger.prototype.modPowInt = function bnModPowInt(e, t) { + var r; + return (r = e < 256 || t.isEven() ? new Classic(t) : new Montgomery(t)), this.exp(e, r); + }), + (BigInteger.ZERO = nbv(0)), + (BigInteger.ONE = nbv(1)), + (NullExp.prototype.convert = nNop), + (NullExp.prototype.revert = nNop), + (NullExp.prototype.mulTo = function nMulTo(e, t, r) { + e.multiplyTo(t, r); + }), + (NullExp.prototype.sqrTo = function nSqrTo(e, t) { + e.squareTo(t); + }), + (Barrett.prototype.convert = function barrettConvert(e) { + if (e.s < 0 || e.t > 2 * this.m.t) return e.mod(this.m); + if (e.compareTo(this.m) < 0) return e; + var t = nbi(); + return e.copyTo(t), this.reduce(t), t; + }), + (Barrett.prototype.revert = function barrettRevert(e) { + return e; + }), + (Barrett.prototype.reduce = function barrettReduce(e) { + for ( + e.drShiftTo(this.m.t - 1, this.r2), + e.t > this.m.t + 1 && ((e.t = this.m.t + 1), e.clamp()), + this.mu.multiplyUpperTo(this.r2, this.m.t + 1, this.q3), + this.m.multiplyLowerTo(this.q3, this.m.t + 1, this.r2); + e.compareTo(this.r2) < 0; + + ) + e.dAddOffset(1, this.m.t + 1); + for (e.subTo(this.r2, e); e.compareTo(this.m) >= 0; ) e.subTo(this.m, e); + }), + (Barrett.prototype.mulTo = function barrettMulTo(e, t, r) { + e.multiplyTo(t, r), this.reduce(r); + }), + (Barrett.prototype.sqrTo = function barrettSqrTo(e, t) { + e.squareTo(t), this.reduce(t); + }); + var I = [ + 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, + 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, + 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, + 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, + 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, + 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, + 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, + 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, + 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, + 947, 953, 967, 971, 977, 983, 991, 997, + ], + R = (1 << 26) / I[I.length - 1]; + /*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ + */ + function Arcfour() { + (this.i = 0), (this.j = 0), (this.S = new Array()); + } + (BigInteger.prototype.chunkSize = function bnpChunkSize(e) { + return Math.floor((Math.LN2 * this.DB) / Math.log(e)); + }), + (BigInteger.prototype.toRadix = function bnpToRadix(e) { + if ((null == e && (e = 10), 0 == this.signum() || e < 2 || e > 36)) return '0'; + var t = this.chunkSize(e), + r = Math.pow(e, t), + n = nbv(r), + i = nbi(), + o = nbi(), + s = ''; + for (this.divRemTo(n, i, o); i.signum() > 0; ) + (s = (r + o.intValue()).toString(e).substr(1) + s), i.divRemTo(n, i, o); + return o.intValue().toString(e) + s; + }), + (BigInteger.prototype.fromRadix = function bnpFromRadix(e, t) { + this.fromInt(0), null == t && (t = 10); + for ( + var r = this.chunkSize(t), n = Math.pow(t, r), i = !1, o = 0, s = 0, a = 0; + a < e.length; + ++a + ) { + var u = intAt(e, a); + u < 0 + ? '-' == e.charAt(a) && 0 == this.signum() && (i = !0) + : ((s = t * s + u), + ++o >= r && (this.dMultiply(n), this.dAddOffset(s, 0), (o = 0), (s = 0))); + } + o > 0 && (this.dMultiply(Math.pow(t, o)), this.dAddOffset(s, 0)), + i && BigInteger.ZERO.subTo(this, this); + }), + (BigInteger.prototype.fromNumber = function bnpFromNumber(e, t, r) { + if ('number' == typeof t) + if (e < 2) this.fromInt(1); + else + for ( + this.fromNumber(e, r), + this.testBit(e - 1) || + this.bitwiseTo(BigInteger.ONE.shiftLeft(e - 1), op_or, this), + this.isEven() && this.dAddOffset(1, 0); + !this.isProbablePrime(t); + + ) + this.dAddOffset(2, 0), + this.bitLength() > e && this.subTo(BigInteger.ONE.shiftLeft(e - 1), this); + else { + var n = new Array(), + i = 7 & e; + (n.length = 1 + (e >> 3)), + t.nextBytes(n), + i > 0 ? (n[0] &= (1 << i) - 1) : (n[0] = 0), + this.fromString(n, 256); + } + }), + (BigInteger.prototype.bitwiseTo = function bnpBitwiseTo(e, t, r) { + var n, + i, + o = Math.min(e.t, this.t); + for (n = 0; n < o; ++n) r[n] = t(this[n], e[n]); + if (e.t < this.t) { + for (i = e.s & this.DM, n = o; n < this.t; ++n) r[n] = t(this[n], i); + r.t = this.t; + } else { + for (i = this.s & this.DM, n = o; n < e.t; ++n) r[n] = t(i, e[n]); + r.t = e.t; + } + (r.s = t(this.s, e.s)), r.clamp(); + }), + (BigInteger.prototype.changeBit = function bnpChangeBit(e, t) { + var r = BigInteger.ONE.shiftLeft(e); + return this.bitwiseTo(r, t, r), r; + }), + (BigInteger.prototype.addTo = function bnpAddTo(e, t) { + for (var r = 0, n = 0, i = Math.min(e.t, this.t); r < i; ) + (n += this[r] + e[r]), (t[r++] = n & this.DM), (n >>= this.DB); + if (e.t < this.t) { + for (n += e.s; r < this.t; ) (n += this[r]), (t[r++] = n & this.DM), (n >>= this.DB); + n += this.s; + } else { + for (n += this.s; r < e.t; ) (n += e[r]), (t[r++] = n & this.DM), (n >>= this.DB); + n += e.s; + } + (t.s = n < 0 ? -1 : 0), + n > 0 ? (t[r++] = n) : n < -1 && (t[r++] = this.DV + n), + (t.t = r), + t.clamp(); + }), + (BigInteger.prototype.dMultiply = function bnpDMultiply(e) { + (this[this.t] = this.am(0, e - 1, this, 0, 0, this.t)), ++this.t, this.clamp(); + }), + (BigInteger.prototype.dAddOffset = function bnpDAddOffset(e, t) { + if (0 != e) { + for (; this.t <= t; ) this[this.t++] = 0; + for (this[t] += e; this[t] >= this.DV; ) + (this[t] -= this.DV), ++t >= this.t && (this[this.t++] = 0), ++this[t]; + } + }), + (BigInteger.prototype.multiplyLowerTo = function bnpMultiplyLowerTo(e, t, r) { + var n, + i = Math.min(this.t + e.t, t); + for (r.s = 0, r.t = i; i > 0; ) r[--i] = 0; + for (n = r.t - this.t; i < n; ++i) r[i + this.t] = this.am(0, e[i], r, i, 0, this.t); + for (n = Math.min(e.t, t); i < n; ++i) this.am(0, e[i], r, i, 0, t - i); + r.clamp(); + }), + (BigInteger.prototype.multiplyUpperTo = function bnpMultiplyUpperTo(e, t, r) { + --t; + var n = (r.t = this.t + e.t - t); + for (r.s = 0; --n >= 0; ) r[n] = 0; + for (n = Math.max(t - this.t, 0); n < e.t; ++n) + r[this.t + n - t] = this.am(t - n, e[n], r, 0, 0, this.t + n - t); + r.clamp(), r.drShiftTo(1, r); + }), + (BigInteger.prototype.modInt = function bnpModInt(e) { + if (e <= 0) return 0; + var t = this.DV % e, + r = this.s < 0 ? e - 1 : 0; + if (this.t > 0) + if (0 == t) r = this[0] % e; + else for (var n = this.t - 1; n >= 0; --n) r = (t * r + this[n]) % e; + return r; + }), + (BigInteger.prototype.millerRabin = function bnpMillerRabin(e) { + var t = this.subtract(BigInteger.ONE), + r = t.getLowestSetBit(); + if (r <= 0) return !1; + var n = t.shiftRight(r); + (e = (e + 1) >> 1) > I.length && (e = I.length); + for (var i = nbi(), o = 0; o < e; ++o) { + i.fromInt(I[Math.floor(Math.random() * I.length)]); + var s = i.modPow(n, this); + if (0 != s.compareTo(BigInteger.ONE) && 0 != s.compareTo(t)) { + for (var a = 1; a++ < r && 0 != s.compareTo(t); ) + if (0 == (s = s.modPowInt(2, this)).compareTo(BigInteger.ONE)) return !1; + if (0 != s.compareTo(t)) return !1; + } + } + return !0; + }), + (BigInteger.prototype.clone = + /*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ + */ + function bnClone() { + var e = nbi(); + return this.copyTo(e), e; + }), + (BigInteger.prototype.intValue = function bnIntValue() { + if (this.s < 0) { + if (1 == this.t) return this[0] - this.DV; + if (0 == this.t) return -1; + } else { + if (1 == this.t) return this[0]; + if (0 == this.t) return 0; + } + return ((this[1] & ((1 << (32 - this.DB)) - 1)) << this.DB) | this[0]; + }), + (BigInteger.prototype.byteValue = function bnByteValue() { + return 0 == this.t ? this.s : (this[0] << 24) >> 24; + }), + (BigInteger.prototype.shortValue = function bnShortValue() { + return 0 == this.t ? this.s : (this[0] << 16) >> 16; + }), + (BigInteger.prototype.signum = function bnSigNum() { + return this.s < 0 ? -1 : this.t <= 0 || (1 == this.t && this[0] <= 0) ? 0 : 1; + }), + (BigInteger.prototype.toByteArray = function bnToByteArray() { + var e = this.t, + t = new Array(); + t[0] = this.s; + var r, + n = this.DB - ((e * this.DB) % 8), + i = 0; + if (e-- > 0) + for ( + n < this.DB && + (r = this[e] >> n) != (this.s & this.DM) >> n && + (t[i++] = r | (this.s << (this.DB - n))); + e >= 0; + + ) + n < 8 + ? ((r = (this[e] & ((1 << n) - 1)) << (8 - n)), + (r |= this[--e] >> (n += this.DB - 8))) + : ((r = (this[e] >> (n -= 8)) & 255), n <= 0 && ((n += this.DB), --e)), + 0 != (128 & r) && (r |= -256), + 0 == i && (128 & this.s) != (128 & r) && ++i, + (i > 0 || r != this.s) && (t[i++] = r); + return t; + }), + (BigInteger.prototype.equals = function bnEquals(e) { + return 0 == this.compareTo(e); + }), + (BigInteger.prototype.min = function bnMin(e) { + return this.compareTo(e) < 0 ? this : e; + }), + (BigInteger.prototype.max = function bnMax(e) { + return this.compareTo(e) > 0 ? this : e; + }), + (BigInteger.prototype.and = function bnAnd(e) { + var t = nbi(); + return this.bitwiseTo(e, op_and, t), t; + }), + (BigInteger.prototype.or = function bnOr(e) { + var t = nbi(); + return this.bitwiseTo(e, op_or, t), t; + }), + (BigInteger.prototype.xor = function bnXor(e) { + var t = nbi(); + return this.bitwiseTo(e, op_xor, t), t; + }), + (BigInteger.prototype.andNot = function bnAndNot(e) { + var t = nbi(); + return this.bitwiseTo(e, op_andnot, t), t; + }), + (BigInteger.prototype.not = function bnNot() { + for (var e = nbi(), t = 0; t < this.t; ++t) e[t] = this.DM & ~this[t]; + return (e.t = this.t), (e.s = ~this.s), e; + }), + (BigInteger.prototype.shiftLeft = function bnShiftLeft(e) { + var t = nbi(); + return e < 0 ? this.rShiftTo(-e, t) : this.lShiftTo(e, t), t; + }), + (BigInteger.prototype.shiftRight = function bnShiftRight(e) { + var t = nbi(); + return e < 0 ? this.lShiftTo(-e, t) : this.rShiftTo(e, t), t; + }), + (BigInteger.prototype.getLowestSetBit = function bnGetLowestSetBit() { + for (var e = 0; e < this.t; ++e) if (0 != this[e]) return e * this.DB + lbit(this[e]); + return this.s < 0 ? this.t * this.DB : -1; + }), + (BigInteger.prototype.bitCount = function bnBitCount() { + for (var e = 0, t = this.s & this.DM, r = 0; r < this.t; ++r) e += cbit(this[r] ^ t); + return e; + }), + (BigInteger.prototype.testBit = function bnTestBit(e) { + var t = Math.floor(e / this.DB); + return t >= this.t ? 0 != this.s : 0 != (this[t] & (1 << e % this.DB)); + }), + (BigInteger.prototype.setBit = function bnSetBit(e) { + return this.changeBit(e, op_or); + }), + (BigInteger.prototype.clearBit = function bnClearBit(e) { + return this.changeBit(e, op_andnot); + }), + (BigInteger.prototype.flipBit = function bnFlipBit(e) { + return this.changeBit(e, op_xor); + }), + (BigInteger.prototype.add = function bnAdd(e) { + var t = nbi(); + return this.addTo(e, t), t; + }), + (BigInteger.prototype.subtract = function bnSubtract(e) { + var t = nbi(); + return this.subTo(e, t), t; + }), + (BigInteger.prototype.multiply = function bnMultiply(e) { + var t = nbi(); + return this.multiplyTo(e, t), t; + }), + (BigInteger.prototype.divide = function bnDivide(e) { + var t = nbi(); + return this.divRemTo(e, t, null), t; + }), + (BigInteger.prototype.remainder = function bnRemainder(e) { + var t = nbi(); + return this.divRemTo(e, null, t), t; + }), + (BigInteger.prototype.divideAndRemainder = function bnDivideAndRemainder(e) { + var t = nbi(), + r = nbi(); + return this.divRemTo(e, t, r), new Array(t, r); + }), + (BigInteger.prototype.modPow = function bnModPow(e, t) { + var r, + n, + i = e.bitLength(), + o = nbv(1); + if (i <= 0) return o; + (r = i < 18 ? 1 : i < 48 ? 3 : i < 144 ? 4 : i < 768 ? 5 : 6), + (n = i < 8 ? new Classic(t) : t.isEven() ? new Barrett(t) : new Montgomery(t)); + var s = new Array(), + a = 3, + u = r - 1, + c = (1 << r) - 1; + if (((s[1] = n.convert(this)), r > 1)) { + var h = nbi(); + for (n.sqrTo(s[1], h); a <= c; ) (s[a] = nbi()), n.mulTo(h, s[a - 2], s[a]), (a += 2); + } + var f, + l, + g = e.t - 1, + p = !0, + d = nbi(); + for (i = nbits(e[g]) - 1; g >= 0; ) { + for ( + i >= u + ? (f = (e[g] >> (i - u)) & c) + : ((f = (e[g] & ((1 << (i + 1)) - 1)) << (u - i)), + g > 0 && (f |= e[g - 1] >> (this.DB + i - u))), + a = r; + 0 == (1 & f); + + ) + (f >>= 1), --a; + if (((i -= a) < 0 && ((i += this.DB), --g), p)) s[f].copyTo(o), (p = !1); + else { + for (; a > 1; ) n.sqrTo(o, d), n.sqrTo(d, o), (a -= 2); + a > 0 ? n.sqrTo(o, d) : ((l = o), (o = d), (d = l)), n.mulTo(d, s[f], o); + } + for (; g >= 0 && 0 == (e[g] & (1 << i)); ) + n.sqrTo(o, d), (l = o), (o = d), (d = l), --i < 0 && ((i = this.DB - 1), --g); + } + return n.revert(o); + }), + (BigInteger.prototype.modInverse = function bnModInverse(e) { + var t = e.isEven(); + if ((this.isEven() && t) || 0 == e.signum()) return BigInteger.ZERO; + for ( + var r = e.clone(), n = this.clone(), i = nbv(1), o = nbv(0), s = nbv(0), a = nbv(1); + 0 != r.signum(); + + ) { + for (; r.isEven(); ) + r.rShiftTo(1, r), + t + ? ((i.isEven() && o.isEven()) || (i.addTo(this, i), o.subTo(e, o)), + i.rShiftTo(1, i)) + : o.isEven() || o.subTo(e, o), + o.rShiftTo(1, o); + for (; n.isEven(); ) + n.rShiftTo(1, n), + t + ? ((s.isEven() && a.isEven()) || (s.addTo(this, s), a.subTo(e, a)), + s.rShiftTo(1, s)) + : a.isEven() || a.subTo(e, a), + a.rShiftTo(1, a); + r.compareTo(n) >= 0 + ? (r.subTo(n, r), t && i.subTo(s, i), o.subTo(a, o)) + : (n.subTo(r, n), t && s.subTo(i, s), a.subTo(o, a)); + } + return 0 != n.compareTo(BigInteger.ONE) + ? BigInteger.ZERO + : a.compareTo(e) >= 0 + ? a.subtract(e) + : a.signum() < 0 + ? (a.addTo(e, a), a.signum() < 0 ? a.add(e) : a) + : a; + }), + (BigInteger.prototype.pow = function bnPow(e) { + return this.exp(e, new NullExp()); + }), + (BigInteger.prototype.gcd = function bnGCD(e) { + var t = this.s < 0 ? this.negate() : this.clone(), + r = e.s < 0 ? e.negate() : e.clone(); + if (t.compareTo(r) < 0) { + var n = t; + (t = r), (r = n); + } + var i = t.getLowestSetBit(), + o = r.getLowestSetBit(); + if (o < 0) return t; + for (i < o && (o = i), o > 0 && (t.rShiftTo(o, t), r.rShiftTo(o, r)); t.signum() > 0; ) + (i = t.getLowestSetBit()) > 0 && t.rShiftTo(i, t), + (i = r.getLowestSetBit()) > 0 && r.rShiftTo(i, r), + t.compareTo(r) >= 0 + ? (t.subTo(r, t), t.rShiftTo(1, t)) + : (r.subTo(t, r), r.rShiftTo(1, r)); + return o > 0 && r.lShiftTo(o, r), r; + }), + (BigInteger.prototype.isProbablePrime = function bnIsProbablePrime(e) { + var t, + r = this.abs(); + if (1 == r.t && r[0] <= I[I.length - 1]) { + for (t = 0; t < I.length; ++t) if (r[0] == I[t]) return !0; + return !1; + } + if (r.isEven()) return !1; + for (t = 1; t < I.length; ) { + for (var n = I[t], i = t + 1; i < I.length && n < R; ) n *= I[i++]; + for (n = r.modInt(n); t < i; ) if (n % I[t++] == 0) return !1; + } + return r.millerRabin(e); + }), + (BigInteger.prototype.square = function bnSquare() { + var e = nbi(); + return this.squareTo(e), e; + }), + (Arcfour.prototype.init = function ARC4init(e) { + var t, r, n; + for (t = 0; t < 256; ++t) this.S[t] = t; + for (r = 0, t = 0; t < 256; ++t) + (r = (r + this.S[t] + e[t % e.length]) & 255), + (n = this.S[t]), + (this.S[t] = this.S[r]), + (this.S[r] = n); + (this.i = 0), (this.j = 0); + }), + (Arcfour.prototype.next = function ARC4next() { + var e; + return ( + (this.i = (this.i + 1) & 255), + (this.j = (this.j + this.S[this.i]) & 255), + (e = this.S[this.i]), + (this.S[this.i] = this.S[this.j]), + (this.S[this.j] = e), + this.S[(e + this.S[this.i]) & 255] + ); + }); + var T, + U, + M, + L = 256; + /*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ + */ function rng_seed_time() { + !(function rng_seed_int(e) { + (U[M++] ^= 255 & e), + (U[M++] ^= (e >> 8) & 255), + (U[M++] ^= (e >> 16) & 255), + (U[M++] ^= (e >> 24) & 255), + M >= L && (M -= L); + })(new Date().getTime()); + } + if (null == U) { + var D; + if ( + ((U = new Array()), + (M = 0), + void 0 !== p && (void 0 !== p.crypto || void 0 !== p.msCrypto)) + ) { + var N = p.crypto || p.msCrypto; + if (N.getRandomValues) { + var O = new Uint8Array(32); + for (N.getRandomValues(O), D = 0; D < 32; ++D) U[M++] = O[D]; + } else if ('Netscape' == u.appName && u.appVersion < '5') { + var H = p.crypto.random(32); + for (D = 0; D < H.length; ++D) U[M++] = 255 & H.charCodeAt(D); + } + } + for (; M < L; ) + (D = Math.floor(65536 * Math.random())), (U[M++] = D >>> 8), (U[M++] = 255 & D); + (M = 0), rng_seed_time(); + } + function rng_get_byte() { + if (null == T) { + for ( + rng_seed_time(), + (T = (function prng_newstate() { + return new Arcfour(); + })()).init(U), + M = 0; + M < U.length; + ++M + ) + U[M] = 0; + M = 0; + } + return T.next(); + } + function SecureRandom() {} + /*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ + */ + function parseBigInt(e, t) { + return new BigInteger(e, t); + } + function oaep_mgf1_arr(e, t, r) { + for (var n = '', i = 0; n.length < t; ) + (n += r( + String.fromCharCode.apply( + String, + e.concat([(4278190080 & i) >> 24, (16711680 & i) >> 16, (65280 & i) >> 8, 255 & i]) + ) + )), + (i += 1); + return n; + } + function RSAKey() { + (this.n = null), + (this.e = 0), + (this.d = null), + (this.p = null), + (this.q = null), + (this.dmp1 = null), + (this.dmq1 = null), + (this.coeff = null); + } + /*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ + */ + function ECFieldElementFp(e, t) { + (this.x = t), (this.q = e); + } + function ECPointFp(e, t, r, n) { + (this.curve = e), + (this.x = t), + (this.y = r), + (this.z = null == n ? BigInteger.ONE : n), + (this.zinv = null); + } + function ECCurveFp(e, t, r) { + (this.q = e), + (this.a = this.fromBigInteger(t)), + (this.b = this.fromBigInteger(r)), + (this.infinity = new ECPointFp(this, null, null)); + } + (SecureRandom.prototype.nextBytes = function rng_get_bytes(e) { + var t; + for (t = 0; t < e.length; ++t) e[t] = rng_get_byte(); + }), + (RSAKey.prototype.doPublic = function RSADoPublic(e) { + return e.modPowInt(this.e, this.n); + }), + (RSAKey.prototype.setPublic = function RSASetPublic(e, t) { + if (((this.isPublic = !0), (this.isPrivate = !1), 'string' != typeof e)) + (this.n = e), (this.e = t); + else { + if (!(null != e && null != t && e.length > 0 && t.length > 0)) + throw 'Invalid RSA public key'; + (this.n = parseBigInt(e, 16)), (this.e = parseInt(t, 16)); + } + }), + (RSAKey.prototype.encrypt = function RSAEncrypt(e) { + var t = (function pkcs1pad2(e, t) { + if (t < e.length + 11) throw 'Message too long for RSA'; + for (var r = new Array(), n = e.length - 1; n >= 0 && t > 0; ) { + var i = e.charCodeAt(n--); + i < 128 + ? (r[--t] = i) + : i > 127 && i < 2048 + ? ((r[--t] = (63 & i) | 128), (r[--t] = (i >> 6) | 192)) + : ((r[--t] = (63 & i) | 128), + (r[--t] = ((i >> 6) & 63) | 128), + (r[--t] = (i >> 12) | 224)); + } + r[--t] = 0; + for (var o = new SecureRandom(), s = new Array(); t > 2; ) { + for (s[0] = 0; 0 == s[0]; ) o.nextBytes(s); + r[--t] = s[0]; + } + return (r[--t] = 2), (r[--t] = 0), new BigInteger(r); + })(e, (this.n.bitLength() + 7) >> 3); + if (null == t) return null; + var r = this.doPublic(t); + if (null == r) return null; + var n = r.toString(16); + return 0 == (1 & n.length) ? n : '0' + n; + }), + (RSAKey.prototype.encryptOAEP = function RSAEncryptOAEP(e, t, r) { + var n = (function oaep_pad(e, t, r, n) { + var i = K.crypto.MessageDigest, + o = K.crypto.Util, + s = null; + if ( + (r || (r = 'sha1'), + 'string' == typeof r && + ((s = i.getCanonicalAlgName(r)), + (n = i.getHashLength(s)), + (r = function f(e) { + return hextorstr(o.hashHex(rstrtohex(e), s)); + })), + e.length + 2 * n + 2 > t) + ) + throw 'Message too long for RSA'; + var a, + u = ''; + for (a = 0; a < t - e.length - 2 * n - 2; a += 1) u += '\0'; + var c = r('') + u + '' + e, + h = new Array(n); + new SecureRandom().nextBytes(h); + var l = oaep_mgf1_arr(h, c.length, r), + g = []; + for (a = 0; a < c.length; a += 1) g[a] = c.charCodeAt(a) ^ l.charCodeAt(a); + var p = oaep_mgf1_arr(g, h.length, r), + d = [0]; + for (a = 0; a < h.length; a += 1) d[a + 1] = h[a] ^ p.charCodeAt(a); + return new BigInteger(d.concat(g)); + })(e, (this.n.bitLength() + 7) >> 3, t, r); + if (null == n) return null; + var i = this.doPublic(n); + if (null == i) return null; + var o = i.toString(16); + return 0 == (1 & o.length) ? o : '0' + o; + }), + (RSAKey.prototype.type = 'RSA'), + (ECFieldElementFp.prototype.equals = function feFpEquals(e) { + return e == this || (this.q.equals(e.q) && this.x.equals(e.x)); + }), + (ECFieldElementFp.prototype.toBigInteger = function feFpToBigInteger() { + return this.x; + }), + (ECFieldElementFp.prototype.negate = function feFpNegate() { + return new ECFieldElementFp(this.q, this.x.negate().mod(this.q)); + }), + (ECFieldElementFp.prototype.add = function feFpAdd(e) { + return new ECFieldElementFp(this.q, this.x.add(e.toBigInteger()).mod(this.q)); + }), + (ECFieldElementFp.prototype.subtract = function feFpSubtract(e) { + return new ECFieldElementFp(this.q, this.x.subtract(e.toBigInteger()).mod(this.q)); + }), + (ECFieldElementFp.prototype.multiply = function feFpMultiply(e) { + return new ECFieldElementFp(this.q, this.x.multiply(e.toBigInteger()).mod(this.q)); + }), + (ECFieldElementFp.prototype.square = function feFpSquare() { + return new ECFieldElementFp(this.q, this.x.square().mod(this.q)); + }), + (ECFieldElementFp.prototype.divide = function feFpDivide(e) { + return new ECFieldElementFp( + this.q, + this.x.multiply(e.toBigInteger().modInverse(this.q)).mod(this.q) + ); + }), + (ECPointFp.prototype.getX = function pointFpGetX() { + return ( + null == this.zinv && (this.zinv = this.z.modInverse(this.curve.q)), + this.curve.fromBigInteger(this.x.toBigInteger().multiply(this.zinv).mod(this.curve.q)) + ); + }), + (ECPointFp.prototype.getY = function pointFpGetY() { + return ( + null == this.zinv && (this.zinv = this.z.modInverse(this.curve.q)), + this.curve.fromBigInteger(this.y.toBigInteger().multiply(this.zinv).mod(this.curve.q)) + ); + }), + (ECPointFp.prototype.equals = function pointFpEquals(e) { + return ( + e == this || + (this.isInfinity() + ? e.isInfinity() + : e.isInfinity() + ? this.isInfinity() + : !!e.y + .toBigInteger() + .multiply(this.z) + .subtract(this.y.toBigInteger().multiply(e.z)) + .mod(this.curve.q) + .equals(BigInteger.ZERO) && + e.x + .toBigInteger() + .multiply(this.z) + .subtract(this.x.toBigInteger().multiply(e.z)) + .mod(this.curve.q) + .equals(BigInteger.ZERO)) + ); + }), + (ECPointFp.prototype.isInfinity = function pointFpIsInfinity() { + return ( + (null == this.x && null == this.y) || + (this.z.equals(BigInteger.ZERO) && !this.y.toBigInteger().equals(BigInteger.ZERO)) + ); + }), + (ECPointFp.prototype.negate = function pointFpNegate() { + return new ECPointFp(this.curve, this.x, this.y.negate(), this.z); + }), + (ECPointFp.prototype.add = function pointFpAdd(e) { + if (this.isInfinity()) return e; + if (e.isInfinity()) return this; + var t = e.y + .toBigInteger() + .multiply(this.z) + .subtract(this.y.toBigInteger().multiply(e.z)) + .mod(this.curve.q), + r = e.x + .toBigInteger() + .multiply(this.z) + .subtract(this.x.toBigInteger().multiply(e.z)) + .mod(this.curve.q); + if (BigInteger.ZERO.equals(r)) + return BigInteger.ZERO.equals(t) ? this.twice() : this.curve.getInfinity(); + var n = new BigInteger('3'), + i = this.x.toBigInteger(), + o = this.y.toBigInteger(), + s = (e.x.toBigInteger(), e.y.toBigInteger(), r.square()), + a = s.multiply(r), + u = i.multiply(s), + c = t.square().multiply(this.z), + h = c + .subtract(u.shiftLeft(1)) + .multiply(e.z) + .subtract(a) + .multiply(r) + .mod(this.curve.q), + f = u + .multiply(n) + .multiply(t) + .subtract(o.multiply(a)) + .subtract(c.multiply(t)) + .multiply(e.z) + .add(t.multiply(a)) + .mod(this.curve.q), + l = a.multiply(this.z).multiply(e.z).mod(this.curve.q); + return new ECPointFp( + this.curve, + this.curve.fromBigInteger(h), + this.curve.fromBigInteger(f), + l + ); + }), + (ECPointFp.prototype.twice = function pointFpTwice() { + if (this.isInfinity()) return this; + if (0 == this.y.toBigInteger().signum()) return this.curve.getInfinity(); + var e = new BigInteger('3'), + t = this.x.toBigInteger(), + r = this.y.toBigInteger(), + n = r.multiply(this.z), + i = n.multiply(r).mod(this.curve.q), + o = this.curve.a.toBigInteger(), + s = t.square().multiply(e); + BigInteger.ZERO.equals(o) || (s = s.add(this.z.square().multiply(o))); + var a = (s = s.mod(this.curve.q)) + .square() + .subtract(t.shiftLeft(3).multiply(i)) + .shiftLeft(1) + .multiply(n) + .mod(this.curve.q), + u = s + .multiply(e) + .multiply(t) + .subtract(i.shiftLeft(1)) + .shiftLeft(2) + .multiply(i) + .subtract(s.square().multiply(s)) + .mod(this.curve.q), + c = n.square().multiply(n).shiftLeft(3).mod(this.curve.q); + return new ECPointFp( + this.curve, + this.curve.fromBigInteger(a), + this.curve.fromBigInteger(u), + c + ); + }), + (ECPointFp.prototype.multiply = function pointFpMultiply(e) { + if (this.isInfinity()) return this; + if (0 == e.signum()) return this.curve.getInfinity(); + var t, + r = e, + n = r.multiply(new BigInteger('3')), + i = this.negate(), + o = this; + for (t = n.bitLength() - 2; t > 0; --t) { + o = o.twice(); + var s = n.testBit(t); + s != r.testBit(t) && (o = o.add(s ? this : i)); + } + return o; + }), + (ECPointFp.prototype.multiplyTwo = function pointFpMultiplyTwo(e, t, r) { + var n; + n = e.bitLength() > r.bitLength() ? e.bitLength() - 1 : r.bitLength() - 1; + for (var i = this.curve.getInfinity(), o = this.add(t); n >= 0; ) + (i = i.twice()), + e.testBit(n) + ? (i = r.testBit(n) ? i.add(o) : i.add(this)) + : r.testBit(n) && (i = i.add(t)), + --n; + return i; + }), + (ECCurveFp.prototype.getQ = function curveFpGetQ() { + return this.q; + }), + (ECCurveFp.prototype.getA = function curveFpGetA() { + return this.a; + }), + (ECCurveFp.prototype.getB = function curveFpGetB() { + return this.b; + }), + (ECCurveFp.prototype.equals = function curveFpEquals(e) { + return e == this || (this.q.equals(e.q) && this.a.equals(e.a) && this.b.equals(e.b)); + }), + (ECCurveFp.prototype.getInfinity = function curveFpGetInfinity() { + return this.infinity; + }), + (ECCurveFp.prototype.fromBigInteger = function curveFpFromBigInteger(e) { + return new ECFieldElementFp(this.q, e); + }), + (ECCurveFp.prototype.decodePointHex = function curveFpDecodePointHex(e) { + switch (parseInt(e.substr(0, 2), 16)) { + case 0: + return this.infinity; + case 2: + case 3: + return null; + case 4: + case 6: + case 7: + var t = (e.length - 2) / 2, + r = e.substr(2, t), + n = e.substr(t + 2, t); + return new ECPointFp( + this, + this.fromBigInteger(new BigInteger(r, 16)), + this.fromBigInteger(new BigInteger(n, 16)) + ); + default: + return null; + } + }); + /*! Mike Samuel (c) 2009 | code.google.com/p/json-sans-eval + */ + var K, + q, + W, + V = (function () { + var e = new RegExp( + '(?:false|true|null|[\\{\\}\\[\\]]|(?:-?\\b(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\\b)|(?:"(?:[^\\0-\\x08\\x0a-\\x1f"\\\\]|\\\\(?:["/\\\\bfnrt]|u[0-9A-Fa-f]{4}))*"))', + 'g' + ), + t = new RegExp('\\\\(?:([^u])|u(.{4}))', 'g'), + r = { + '"': '"', + '/': '/', + '\\': '\\', + b: '\b', + f: '\f', + n: '\n', + r: '\r', + t: '\t', + }; + function h(e, t, n) { + return t ? r[t] : String.fromCharCode(parseInt(n, 16)); + } + var n = new String(''), + o = (Object, Array, Object.hasOwnProperty); + return function (r, a) { + var u, + c, + f = r.match(e), + l = f[0], + g = !1; + '{' === l ? (u = {}) : '[' === l ? (u = []) : ((u = []), (g = !0)); + for (var p = [u], d = 1 - g, v = f.length; d < v; ++d) { + var y; + switch ((l = f[d]).charCodeAt(0)) { + default: + ((y = p[0])[c || y.length] = +l), (c = void 0); + break; + case 34: + if ( + (-1 !== (l = l.substring(1, l.length - 1)).indexOf('\\') && + (l = l.replace(t, h)), + (y = p[0]), + !c) + ) { + if (!(y instanceof Array)) { + c = l || n; + break; + } + c = y.length; + } + (y[c] = l), (c = void 0); + break; + case 91: + (y = p[0]), p.unshift((y[c || y.length] = [])), (c = void 0); + break; + case 93: + p.shift(); + break; + case 102: + ((y = p[0])[c || y.length] = !1), (c = void 0); + break; + case 110: + ((y = p[0])[c || y.length] = null), (c = void 0); + break; + case 116: + ((y = p[0])[c || y.length] = !0), (c = void 0); + break; + case 123: + (y = p[0]), p.unshift((y[c || y.length] = {})), (c = void 0); + break; + case 125: + p.shift(); + } + } + if (g) { + if (1 !== p.length) throw new Error(); + u = u[0]; + } else if (p.length) throw new Error(); + if (a) { + u = (function s(e, t) { + var r = e[t]; + if (r && 'object' === (void 0 === r ? 'undefined' : i(r))) { + var n = null; + for (var u in r) + if (o.call(r, u) && r !== e) { + var c = s(r, u); + void 0 !== c ? (r[u] = c) : (n || (n = []), n.push(u)); + } + if (n) for (var h = n.length; --h >= 0; ) delete r[n[h]]; + } + return a.call(e, t, r); + })({ '': u }, ''); + } + return u; + }; + })(), + J = new (function () {})(); + function stoBA(e) { + for (var t = new Array(), r = 0; r < e.length; r++) t[r] = e.charCodeAt(r); + return t; + } + function BAtos(e) { + for (var t = '', r = 0; r < e.length; r++) t += String.fromCharCode(e[r]); + return t; + } + function BAtohex(e) { + for (var t = '', r = 0; r < e.length; r++) { + var n = e[r].toString(16); + 1 == n.length && (n = '0' + n), (t += n); + } + return t; + } + function stohex(e) { + return BAtohex(stoBA(e)); + } + function b64tob64u(e) { + return (e = (e = (e = e.replace(/\=/g, '')).replace(/\+/g, '-')).replace(/\//g, '_')); + } + function b64utob64(e) { + return ( + e.length % 4 == 2 ? (e += '==') : e.length % 4 == 3 && (e += '='), + (e = (e = e.replace(/-/g, '+')).replace(/_/g, '/')) + ); + } + function hextob64u(e) { + return e.length % 2 == 1 && (e = '0' + e), b64tob64u(hex2b64(e)); + } + function b64utohex(e) { + return b64tohex(b64utob64(e)); + } + function utf8tohex(e) { + return uricmptohex(encodeURIComponentAll(e)); + } + function hextoutf8(e) { + return decodeURIComponent(hextouricmp(e)); + } + function hextorstr(e) { + for (var t = '', r = 0; r < e.length - 1; r += 2) + t += String.fromCharCode(parseInt(e.substr(r, 2), 16)); + return t; + } + function rstrtohex(e) { + for (var t = '', r = 0; r < e.length; r++) + t += ('0' + e.charCodeAt(r).toString(16)).slice(-2); + return t; + } + function hextob64(e) { + return hex2b64(e); + } + function hextob64nl(e) { + var t = hextob64(e).replace(/(.{64})/g, '$1\r\n'); + return (t = t.replace(/\r\n$/, '')); + } + function b64nltohex(e) { + return b64tohex(e.replace(/[^0-9A-Za-z\/+=]*/g, '')); + } + function hextopem(e, t) { + return ( + '-----BEGIN ' + t + '-----\r\n' + hextob64nl(e) + '\r\n-----END ' + t + '-----\r\n' + ); + } + function pemtohex(e, t) { + if (-1 == e.indexOf('-----BEGIN ')) throw "can't find PEM header: " + t; + return b64nltohex( + (e = + void 0 !== t + ? (e = e.replace('-----BEGIN ' + t + '-----', '')).replace( + '-----END ' + t + '-----', + '' + ) + : (e = e.replace(/-----BEGIN [^-]+-----/, '')).replace(/-----END [^-]+-----/, '')) + ); + } + function zulutomsec(e) { + var t, r, n, i, o, s, a, u, c, h, f; + if ((f = e.match(/^(\d{2}|\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(|\.\d+)Z$/))) + return ( + (u = f[1]), + (t = parseInt(u)), + 2 === u.length && + (50 <= t && t < 100 ? (t = 1900 + t) : 0 <= t && t < 50 && (t = 2e3 + t)), + (r = parseInt(f[2]) - 1), + (n = parseInt(f[3])), + (i = parseInt(f[4])), + (o = parseInt(f[5])), + (s = parseInt(f[6])), + (a = 0), + '' !== (c = f[7]) && ((h = (c.substr(1) + '00').substr(0, 3)), (a = parseInt(h))), + Date.UTC(t, r, n, i, o, s, a) + ); + throw 'unsupported zulu format: ' + e; + } + function zulutosec(e) { + return ~~(zulutomsec(e) / 1e3); + } + function uricmptohex(e) { + return e.replace(/%/g, ''); + } + function hextouricmp(e) { + return e.replace(/(..)/g, '%$1'); + } + function ipv6tohex(e) { + var t = 'malformed IPv6 address'; + if (!e.match(/^[0-9A-Fa-f:]+$/)) throw t; + var r = (e = e.toLowerCase()).split(':').length - 1; + if (r < 2) throw t; + var n = ':'.repeat(7 - r + 2), + i = (e = e.replace('::', n)).split(':'); + if (8 != i.length) throw t; + for (var o = 0; o < 8; o++) i[o] = ('0000' + i[o]).slice(-4); + return i.join(''); + } + function hextoipv6(e) { + if (!e.match(/^[0-9A-Fa-f]{32}$/)) throw 'malformed IPv6 address octet'; + for (var t = (e = e.toLowerCase()).match(/.{1,4}/g), r = 0; r < 8; r++) + (t[r] = t[r].replace(/^0+/, '')), '' == t[r] && (t[r] = '0'); + var n = (e = ':' + t.join(':') + ':').match(/:(0:){2,}/g); + if (null === n) return e.slice(1, -1); + var i = ''; + for (r = 0; r < n.length; r++) n[r].length > i.length && (i = n[r]); + return (e = e.replace(i, '::')).slice(1, -1); + } + function hextoip(e) { + var t = 'malformed hex value'; + if (!e.match(/^([0-9A-Fa-f][0-9A-Fa-f]){1,}$/)) throw t; + if (8 != e.length) return 32 == e.length ? hextoipv6(e) : e; + try { + return ( + parseInt(e.substr(0, 2), 16) + + '.' + + parseInt(e.substr(2, 2), 16) + + '.' + + parseInt(e.substr(4, 2), 16) + + '.' + + parseInt(e.substr(6, 2), 16) + ); + } catch (e) { + throw t; + } + } + function encodeURIComponentAll(e) { + for (var t = encodeURIComponent(e), r = '', n = 0; n < t.length; n++) + '%' == t[n] ? ((r += t.substr(n, 3)), (n += 2)) : (r = r + '%' + stohex(t[n])); + return r; + } + function hextoposhex(e) { + return e.length % 2 == 1 ? '0' + e : e.substr(0, 1) > '7' ? '00' + e : e; + } + (J.getLblen = function (e, t) { + if ('8' != e.substr(t + 2, 1)) return 1; + var r = parseInt(e.substr(t + 3, 1)); + return 0 == r ? -1 : 0 < r && r < 10 ? r + 1 : -2; + }), + (J.getL = function (e, t) { + var r = J.getLblen(e, t); + return r < 1 ? '' : e.substr(t + 2, 2 * r); + }), + (J.getVblen = function (e, t) { + var r; + return '' == (r = J.getL(e, t)) + ? -1 + : ('8' === r.substr(0, 1) + ? new BigInteger(r.substr(2), 16) + : new BigInteger(r, 16) + ).intValue(); + }), + (J.getVidx = function (e, t) { + var r = J.getLblen(e, t); + return r < 0 ? r : t + 2 * (r + 1); + }), + (J.getV = function (e, t) { + var r = J.getVidx(e, t), + n = J.getVblen(e, t); + return e.substr(r, 2 * n); + }), + (J.getTLV = function (e, t) { + return e.substr(t, 2) + J.getL(e, t) + J.getV(e, t); + }), + (J.getNextSiblingIdx = function (e, t) { + return J.getVidx(e, t) + 2 * J.getVblen(e, t); + }), + (J.getChildIdx = function (e, t) { + var r = J, + n = new Array(), + i = r.getVidx(e, t); + '03' == e.substr(t, 2) ? n.push(i + 2) : n.push(i); + for (var o = r.getVblen(e, t), s = i, a = 0; ; ) { + var u = r.getNextSiblingIdx(e, s); + if (null == u || u - i >= 2 * o) break; + if (a >= 200) break; + n.push(u), (s = u), a++; + } + return n; + }), + (J.getNthChildIdx = function (e, t, r) { + return J.getChildIdx(e, t)[r]; + }), + (J.getIdxbyList = function (e, t, r, n) { + var i, + o, + s = J; + if (0 == r.length) { + if (void 0 !== n && e.substr(t, 2) !== n) + throw "checking tag doesn't match: " + e.substr(t, 2) + '!=' + n; + return t; + } + return (i = r.shift()), (o = s.getChildIdx(e, t)), s.getIdxbyList(e, o[i], r, n); + }), + (J.getTLVbyList = function (e, t, r, n) { + var i = J, + o = i.getIdxbyList(e, t, r); + if (void 0 === o) throw "can't find nthList object"; + if (void 0 !== n && e.substr(o, 2) != n) + throw "checking tag doesn't match: " + e.substr(o, 2) + '!=' + n; + return i.getTLV(e, o); + }), + (J.getVbyList = function (e, t, r, n, i) { + var o, + s, + a = J; + if (void 0 === (o = a.getIdxbyList(e, t, r, n))) throw "can't find nthList object"; + return (s = a.getV(e, o)), !0 === i && (s = s.substr(2)), s; + }), + (J.hextooidstr = function (e) { + var t = function h(e, t) { + return e.length >= t ? e : new Array(t - e.length + 1).join('0') + e; + }, + r = [], + n = e.substr(0, 2), + i = parseInt(n, 16); + (r[0] = new String(Math.floor(i / 40))), (r[1] = new String(i % 40)); + for (var o = e.substr(2), s = [], a = 0; a < o.length / 2; a++) + s.push(parseInt(o.substr(2 * a, 2), 16)); + var u = [], + c = ''; + for (a = 0; a < s.length; a++) + 128 & s[a] + ? (c += t((127 & s[a]).toString(2), 7)) + : ((c += t((127 & s[a]).toString(2), 7)), + u.push(new String(parseInt(c, 2))), + (c = '')); + var h = r.join('.'); + return u.length > 0 && (h = h + '.' + u.join('.')), h; + }), + (J.dump = function (e, t, r, n) { + var i = J, + o = i.getV, + s = i.dump, + a = i.getChildIdx, + u = e; + e instanceof K.asn1.ASN1Object && (u = e.getEncodedHex()); + var c = function q(e, t) { + return e.length <= 2 * t + ? e + : e.substr(0, t) + + '..(total ' + + e.length / 2 + + 'bytes)..' + + e.substr(e.length - t, t); + }; + void 0 === t && (t = { ommit_long_octet: 32 }), + void 0 === r && (r = 0), + void 0 === n && (n = ''); + var h = t.ommit_long_octet; + if ('01' == u.substr(r, 2)) + return '00' == (f = o(u, r)) ? n + 'BOOLEAN FALSE\n' : n + 'BOOLEAN TRUE\n'; + if ('02' == u.substr(r, 2)) return n + 'INTEGER ' + c((f = o(u, r)), h) + '\n'; + if ('03' == u.substr(r, 2)) return n + 'BITSTRING ' + c((f = o(u, r)), h) + '\n'; + if ('04' == u.substr(r, 2)) { + var f = o(u, r); + if (i.isASN1HEX(f)) { + var l = n + 'OCTETSTRING, encapsulates\n'; + return (l += s(f, t, 0, n + ' ')); + } + return n + 'OCTETSTRING ' + c(f, h) + '\n'; + } + if ('05' == u.substr(r, 2)) return n + 'NULL\n'; + if ('06' == u.substr(r, 2)) { + var g = o(u, r), + p = K.asn1.ASN1Util.oidHexToInt(g), + d = K.asn1.x509.OID.oid2name(p), + v = p.replace(/\./g, ' '); + return '' != d + ? n + 'ObjectIdentifier ' + d + ' (' + v + ')\n' + : n + 'ObjectIdentifier (' + v + ')\n'; + } + if ('0c' == u.substr(r, 2)) return n + "UTF8String '" + hextoutf8(o(u, r)) + "'\n"; + if ('13' == u.substr(r, 2)) return n + "PrintableString '" + hextoutf8(o(u, r)) + "'\n"; + if ('14' == u.substr(r, 2)) return n + "TeletexString '" + hextoutf8(o(u, r)) + "'\n"; + if ('16' == u.substr(r, 2)) return n + "IA5String '" + hextoutf8(o(u, r)) + "'\n"; + if ('17' == u.substr(r, 2)) return n + 'UTCTime ' + hextoutf8(o(u, r)) + '\n'; + if ('18' == u.substr(r, 2)) return n + 'GeneralizedTime ' + hextoutf8(o(u, r)) + '\n'; + if ('30' == u.substr(r, 2)) { + if ('3000' == u.substr(r, 4)) return n + 'SEQUENCE {}\n'; + l = n + 'SEQUENCE\n'; + var y = t; + if ( + (2 == (F = a(u, r)).length || 3 == F.length) && + '06' == u.substr(F[0], 2) && + '04' == u.substr(F[F.length - 1], 2) + ) { + d = i.oidname(o(u, F[0])); + var m = JSON.parse(JSON.stringify(t)); + (m.x509ExtName = d), (y = m); + } + for (var S = 0; S < F.length; S++) l += s(u, y, F[S], n + ' '); + return l; + } + if ('31' == u.substr(r, 2)) { + l = n + 'SET\n'; + var F = a(u, r); + for (S = 0; S < F.length; S++) l += s(u, t, F[S], n + ' '); + return l; + } + var b = parseInt(u.substr(r, 2), 16); + if (0 != (128 & b)) { + var _ = 31 & b; + if (0 != (32 & b)) { + var l = n + '[' + _ + ']\n'; + for (F = a(u, r), S = 0; S < F.length; S++) l += s(u, t, F[S], n + ' '); + return l; + } + return ( + '68747470' == (f = o(u, r)).substr(0, 8) && (f = hextoutf8(f)), + 'subjectAltName' === t.x509ExtName && 2 == _ && (f = hextoutf8(f)), + (l = n + '[' + _ + '] ' + f + '\n') + ); + } + return n + 'UNKNOWN(' + u.substr(r, 2) + ') ' + o(u, r) + '\n'; + }), + (J.isASN1HEX = function (e) { + var t = J; + if (e.length % 2 == 1) return !1; + var r = t.getVblen(e, 0), + n = e.substr(0, 2), + i = t.getL(e, 0); + return e.length - n.length - i.length == 2 * r; + }), + (J.oidname = function (e) { + var t = K.asn1; + K.lang.String.isHex(e) && (e = t.ASN1Util.oidHexToInt(e)); + var r = t.x509.OID.oid2name(e); + return '' === r && (r = e), r; + }), + (void 0 !== K && K) || (K = {}), + (void 0 !== K.lang && K.lang) || (K.lang = {}), + (K.lang.String = function () {}), + 'function' == typeof n + ? ((q = function utf8tob64u(e) { + return b64tob64u(new n(e, 'utf8').toString('base64')); + }), + (W = function b64utoutf8(e) { + return new n(b64utob64(e), 'base64').toString('utf8'); + })) + : ((q = function utf8tob64u(e) { + return hextob64u(uricmptohex(encodeURIComponentAll(e))); + }), + (W = function b64utoutf8(e) { + return decodeURIComponent(hextouricmp(b64utohex(e))); + })), + (K.lang.String.isInteger = function (e) { + return !!e.match(/^[0-9]+$/) || !!e.match(/^-[0-9]+$/); + }), + (K.lang.String.isHex = function (e) { + return !(e.length % 2 != 0 || (!e.match(/^[0-9a-f]+$/) && !e.match(/^[0-9A-F]+$/))); + }), + (K.lang.String.isBase64 = function (e) { + return !( + !(e = e.replace(/\s+/g, '')).match(/^[0-9A-Za-z+\/]+={0,3}$/) || e.length % 4 != 0 + ); + }), + (K.lang.String.isBase64URL = function (e) { + return !e.match(/[+/=]/) && ((e = b64utob64(e)), K.lang.String.isBase64(e)); + }), + (K.lang.String.isIntegerArray = function (e) { + return !!(e = e.replace(/\s+/g, '')).match(/^\[[0-9,]+\]$/); + }); + (void 0 !== K && K) || (K = {}), + (void 0 !== K.crypto && K.crypto) || (K.crypto = {}), + (K.crypto.Util = new (function () { + (this.DIGESTINFOHEAD = { + sha1: '3021300906052b0e03021a05000414', + sha224: '302d300d06096086480165030402040500041c', + sha256: '3031300d060960864801650304020105000420', + sha384: '3041300d060960864801650304020205000430', + sha512: '3051300d060960864801650304020305000440', + md2: '3020300c06082a864886f70d020205000410', + md5: '3020300c06082a864886f70d020505000410', + ripemd160: '3021300906052b2403020105000414', + }), + (this.DEFAULTPROVIDER = { + md5: 'cryptojs', + sha1: 'cryptojs', + sha224: 'cryptojs', + sha256: 'cryptojs', + sha384: 'cryptojs', + sha512: 'cryptojs', + ripemd160: 'cryptojs', + hmacmd5: 'cryptojs', + hmacsha1: 'cryptojs', + hmacsha224: 'cryptojs', + hmacsha256: 'cryptojs', + hmacsha384: 'cryptojs', + hmacsha512: 'cryptojs', + hmacripemd160: 'cryptojs', + MD5withRSA: 'cryptojs/jsrsa', + SHA1withRSA: 'cryptojs/jsrsa', + SHA224withRSA: 'cryptojs/jsrsa', + SHA256withRSA: 'cryptojs/jsrsa', + SHA384withRSA: 'cryptojs/jsrsa', + SHA512withRSA: 'cryptojs/jsrsa', + RIPEMD160withRSA: 'cryptojs/jsrsa', + MD5withECDSA: 'cryptojs/jsrsa', + SHA1withECDSA: 'cryptojs/jsrsa', + SHA224withECDSA: 'cryptojs/jsrsa', + SHA256withECDSA: 'cryptojs/jsrsa', + SHA384withECDSA: 'cryptojs/jsrsa', + SHA512withECDSA: 'cryptojs/jsrsa', + RIPEMD160withECDSA: 'cryptojs/jsrsa', + SHA1withDSA: 'cryptojs/jsrsa', + SHA224withDSA: 'cryptojs/jsrsa', + SHA256withDSA: 'cryptojs/jsrsa', + MD5withRSAandMGF1: 'cryptojs/jsrsa', + SHA1withRSAandMGF1: 'cryptojs/jsrsa', + SHA224withRSAandMGF1: 'cryptojs/jsrsa', + SHA256withRSAandMGF1: 'cryptojs/jsrsa', + SHA384withRSAandMGF1: 'cryptojs/jsrsa', + SHA512withRSAandMGF1: 'cryptojs/jsrsa', + RIPEMD160withRSAandMGF1: 'cryptojs/jsrsa', + }), + (this.CRYPTOJSMESSAGEDIGESTNAME = { + md5: y.algo.MD5, + sha1: y.algo.SHA1, + sha224: y.algo.SHA224, + sha256: y.algo.SHA256, + sha384: y.algo.SHA384, + sha512: y.algo.SHA512, + ripemd160: y.algo.RIPEMD160, + }), + (this.getDigestInfoHex = function (e, t) { + if (void 0 === this.DIGESTINFOHEAD[t]) + throw 'alg not supported in Util.DIGESTINFOHEAD: ' + t; + return this.DIGESTINFOHEAD[t] + e; + }), + (this.getPaddedDigestInfoHex = function (e, t, r) { + var n = this.getDigestInfoHex(e, t), + i = r / 4; + if (n.length + 22 > i) throw 'key is too short for SigAlg: keylen=' + r + ',' + t; + for ( + var o = '0001', s = '00' + n, a = '', u = i - o.length - s.length, c = 0; + c < u; + c += 2 + ) + a += 'ff'; + return o + a + s; + }), + (this.hashString = function (e, t) { + return new K.crypto.MessageDigest({ alg: t }).digestString(e); + }), + (this.hashHex = function (e, t) { + return new K.crypto.MessageDigest({ alg: t }).digestHex(e); + }), + (this.sha1 = function (e) { + return new K.crypto.MessageDigest({ + alg: 'sha1', + prov: 'cryptojs', + }).digestString(e); + }), + (this.sha256 = function (e) { + return new K.crypto.MessageDigest({ + alg: 'sha256', + prov: 'cryptojs', + }).digestString(e); + }), + (this.sha256Hex = function (e) { + return new K.crypto.MessageDigest({ + alg: 'sha256', + prov: 'cryptojs', + }).digestHex(e); + }), + (this.sha512 = function (e) { + return new K.crypto.MessageDigest({ + alg: 'sha512', + prov: 'cryptojs', + }).digestString(e); + }), + (this.sha512Hex = function (e) { + return new K.crypto.MessageDigest({ + alg: 'sha512', + prov: 'cryptojs', + }).digestHex(e); + }); + })()), + (K.crypto.Util.md5 = function (e) { + return new K.crypto.MessageDigest({ + alg: 'md5', + prov: 'cryptojs', + }).digestString(e); + }), + (K.crypto.Util.ripemd160 = function (e) { + return new K.crypto.MessageDigest({ + alg: 'ripemd160', + prov: 'cryptojs', + }).digestString(e); + }), + (K.crypto.Util.SECURERANDOMGEN = new SecureRandom()), + (K.crypto.Util.getRandomHexOfNbytes = function (e) { + var t = new Array(e); + return K.crypto.Util.SECURERANDOMGEN.nextBytes(t), BAtohex(t); + }), + (K.crypto.Util.getRandomBigIntegerOfNbytes = function (e) { + return new BigInteger(K.crypto.Util.getRandomHexOfNbytes(e), 16); + }), + (K.crypto.Util.getRandomHexOfNbits = function (e) { + var t = e % 8, + r = new Array((e - t) / 8 + 1); + return ( + K.crypto.Util.SECURERANDOMGEN.nextBytes(r), + (r[0] = (((255 << t) & 255) ^ 255) & r[0]), + BAtohex(r) + ); + }), + (K.crypto.Util.getRandomBigIntegerOfNbits = function (e) { + return new BigInteger(K.crypto.Util.getRandomHexOfNbits(e), 16); + }), + (K.crypto.Util.getRandomBigIntegerZeroToMax = function (e) { + for (var t = e.bitLength(); ; ) { + var r = K.crypto.Util.getRandomBigIntegerOfNbits(t); + if (-1 != e.compareTo(r)) return r; + } + }), + (K.crypto.Util.getRandomBigIntegerMinToMax = function (e, t) { + var r = e.compareTo(t); + if (1 == r) throw 'biMin is greater than biMax'; + if (0 == r) return e; + var n = t.subtract(e); + return K.crypto.Util.getRandomBigIntegerZeroToMax(n).add(e); + }), + (K.crypto.MessageDigest = function (e) { + (this.setAlgAndProvider = function (e, t) { + if ( + (null !== (e = K.crypto.MessageDigest.getCanonicalAlgName(e)) && + void 0 === t && + (t = K.crypto.Util.DEFAULTPROVIDER[e]), + -1 != ':md5:sha1:sha224:sha256:sha384:sha512:ripemd160:'.indexOf(e) && + 'cryptojs' == t) + ) { + try { + this.md = K.crypto.Util.CRYPTOJSMESSAGEDIGESTNAME[e].create(); + } catch (t) { + throw 'setAlgAndProvider hash alg set fail alg=' + e + '/' + t; + } + (this.updateString = function (e) { + this.md.update(e); + }), + (this.updateHex = function (e) { + var t = y.enc.Hex.parse(e); + this.md.update(t); + }), + (this.digest = function () { + return this.md.finalize().toString(y.enc.Hex); + }), + (this.digestString = function (e) { + return this.updateString(e), this.digest(); + }), + (this.digestHex = function (e) { + return this.updateHex(e), this.digest(); + }); + } + if (-1 != ':sha256:'.indexOf(e) && 'sjcl' == t) { + try { + this.md = new sjcl.hash.sha256(); + } catch (t) { + throw 'setAlgAndProvider hash alg set fail alg=' + e + '/' + t; + } + (this.updateString = function (e) { + this.md.update(e); + }), + (this.updateHex = function (e) { + var t = sjcl.codec.hex.toBits(e); + this.md.update(t); + }), + (this.digest = function () { + var e = this.md.finalize(); + return sjcl.codec.hex.fromBits(e); + }), + (this.digestString = function (e) { + return this.updateString(e), this.digest(); + }), + (this.digestHex = function (e) { + return this.updateHex(e), this.digest(); + }); + } + }), + (this.updateString = function (e) { + throw ( + 'updateString(str) not supported for this alg/prov: ' + + this.algName + + '/' + + this.provName + ); + }), + (this.updateHex = function (e) { + throw ( + 'updateHex(hex) not supported for this alg/prov: ' + + this.algName + + '/' + + this.provName + ); + }), + (this.digest = function () { + throw ( + 'digest() not supported for this alg/prov: ' + this.algName + '/' + this.provName + ); + }), + (this.digestString = function (e) { + throw ( + 'digestString(str) not supported for this alg/prov: ' + + this.algName + + '/' + + this.provName + ); + }), + (this.digestHex = function (e) { + throw ( + 'digestHex(hex) not supported for this alg/prov: ' + + this.algName + + '/' + + this.provName + ); + }), + void 0 !== e && + void 0 !== e.alg && + ((this.algName = e.alg), + void 0 === e.prov && (this.provName = K.crypto.Util.DEFAULTPROVIDER[this.algName]), + this.setAlgAndProvider(this.algName, this.provName)); + }), + (K.crypto.MessageDigest.getCanonicalAlgName = function (e) { + return 'string' == typeof e && (e = (e = e.toLowerCase()).replace(/-/, '')), e; + }), + (K.crypto.MessageDigest.getHashLength = function (e) { + var t = K.crypto.MessageDigest, + r = t.getCanonicalAlgName(e); + if (void 0 === t.HASHLENGTH[r]) throw 'not supported algorithm: ' + e; + return t.HASHLENGTH[r]; + }), + (K.crypto.MessageDigest.HASHLENGTH = { + md5: 16, + sha1: 20, + sha224: 28, + sha256: 32, + sha384: 48, + sha512: 64, + ripemd160: 20, + }), + (K.crypto.Mac = function (e) { + (this.setAlgAndProvider = function (e, t) { + if ( + (null == (e = e.toLowerCase()) && (e = 'hmacsha1'), + 'hmac' != (e = e.toLowerCase()).substr(0, 4)) + ) + throw 'setAlgAndProvider unsupported HMAC alg: ' + e; + void 0 === t && (t = K.crypto.Util.DEFAULTPROVIDER[e]), (this.algProv = e + '/' + t); + var r = e.substr(4); + if ( + -1 != ':md5:sha1:sha224:sha256:sha384:sha512:ripemd160:'.indexOf(r) && + 'cryptojs' == t + ) { + try { + var n = K.crypto.Util.CRYPTOJSMESSAGEDIGESTNAME[r]; + this.mac = y.algo.HMAC.create(n, this.pass); + } catch (e) { + throw 'setAlgAndProvider hash alg set fail hashAlg=' + r + '/' + e; + } + (this.updateString = function (e) { + this.mac.update(e); + }), + (this.updateHex = function (e) { + var t = y.enc.Hex.parse(e); + this.mac.update(t); + }), + (this.doFinal = function () { + return this.mac.finalize().toString(y.enc.Hex); + }), + (this.doFinalString = function (e) { + return this.updateString(e), this.doFinal(); + }), + (this.doFinalHex = function (e) { + return this.updateHex(e), this.doFinal(); + }); + } + }), + (this.updateString = function (e) { + throw 'updateString(str) not supported for this alg/prov: ' + this.algProv; + }), + (this.updateHex = function (e) { + throw 'updateHex(hex) not supported for this alg/prov: ' + this.algProv; + }), + (this.doFinal = function () { + throw 'digest() not supported for this alg/prov: ' + this.algProv; + }), + (this.doFinalString = function (e) { + throw 'digestString(str) not supported for this alg/prov: ' + this.algProv; + }), + (this.doFinalHex = function (e) { + throw 'digestHex(hex) not supported for this alg/prov: ' + this.algProv; + }), + (this.setPassword = function (e) { + if ('string' == typeof e) { + var t = e; + return ( + (e.length % 2 != 1 && e.match(/^[0-9A-Fa-f]+$/)) || (t = rstrtohex(e)), + void (this.pass = y.enc.Hex.parse(t)) + ); + } + if ('object' != (void 0 === e ? 'undefined' : i(e))) + throw 'KJUR.crypto.Mac unsupported password type: ' + e; + t = null; + if (void 0 !== e.hex) { + if (e.hex.length % 2 != 0 || !e.hex.match(/^[0-9A-Fa-f]+$/)) + throw 'Mac: wrong hex password: ' + e.hex; + t = e.hex; + } + if ( + (void 0 !== e.utf8 && (t = utf8tohex(e.utf8)), + void 0 !== e.rstr && (t = rstrtohex(e.rstr)), + void 0 !== e.b64 && (t = b64tohex(e.b64)), + void 0 !== e.b64u && (t = b64utohex(e.b64u)), + null == t) + ) + throw 'KJUR.crypto.Mac unsupported password type: ' + e; + this.pass = y.enc.Hex.parse(t); + }), + void 0 !== e && + (void 0 !== e.pass && this.setPassword(e.pass), + void 0 !== e.alg && + ((this.algName = e.alg), + void 0 === e.prov && + (this.provName = K.crypto.Util.DEFAULTPROVIDER[this.algName]), + this.setAlgAndProvider(this.algName, this.provName))); + }), + (K.crypto.Signature = function (e) { + var t = null; + if ( + ((this._setAlgNames = function () { + var e = this.algName.match(/^(.+)with(.+)$/); + e && + ((this.mdAlgName = e[1].toLowerCase()), + (this.pubkeyAlgName = e[2].toLowerCase())); + }), + (this._zeroPaddingOfSignature = function (e, t) { + for (var r = '', n = t / 4 - e.length, i = 0; i < n; i++) r += '0'; + return r + e; + }), + (this.setAlgAndProvider = function (e, t) { + if ((this._setAlgNames(), 'cryptojs/jsrsa' != t)) + throw 'provider not supported: ' + t; + if ( + -1 != ':md5:sha1:sha224:sha256:sha384:sha512:ripemd160:'.indexOf(this.mdAlgName) + ) { + try { + this.md = new K.crypto.MessageDigest({ + alg: this.mdAlgName, + }); + } catch (e) { + throw 'setAlgAndProvider hash alg set fail alg=' + this.mdAlgName + '/' + e; + } + (this.init = function (e, t) { + var r = null; + try { + r = void 0 === t ? z.getKey(e) : z.getKey(e, t); + } catch (e) { + throw 'init failed:' + e; + } + if (!0 === r.isPrivate) (this.prvKey = r), (this.state = 'SIGN'); + else { + if (!0 !== r.isPublic) throw 'init failed.:' + r; + (this.pubKey = r), (this.state = 'VERIFY'); + } + }), + (this.updateString = function (e) { + this.md.updateString(e); + }), + (this.updateHex = function (e) { + this.md.updateHex(e); + }), + (this.sign = function () { + if ( + ((this.sHashHex = this.md.digest()), + void 0 !== this.ecprvhex && void 0 !== this.eccurvename) + ) { + var e = new K.crypto.ECDSA({ curve: this.eccurvename }); + this.hSign = e.signHex(this.sHashHex, this.ecprvhex); + } else if ( + this.prvKey instanceof RSAKey && + 'rsaandmgf1' === this.pubkeyAlgName + ) + this.hSign = this.prvKey.signWithMessageHashPSS( + this.sHashHex, + this.mdAlgName, + this.pssSaltLen + ); + else if (this.prvKey instanceof RSAKey && 'rsa' === this.pubkeyAlgName) + this.hSign = this.prvKey.signWithMessageHash(this.sHashHex, this.mdAlgName); + else if (this.prvKey instanceof K.crypto.ECDSA) + this.hSign = this.prvKey.signWithMessageHash(this.sHashHex); + else { + if (!(this.prvKey instanceof K.crypto.DSA)) + throw 'Signature: unsupported private key alg: ' + this.pubkeyAlgName; + this.hSign = this.prvKey.signWithMessageHash(this.sHashHex); + } + return this.hSign; + }), + (this.signString = function (e) { + return this.updateString(e), this.sign(); + }), + (this.signHex = function (e) { + return this.updateHex(e), this.sign(); + }), + (this.verify = function (e) { + if ( + ((this.sHashHex = this.md.digest()), + void 0 !== this.ecpubhex && void 0 !== this.eccurvename) + ) + return new K.crypto.ECDSA({ + curve: this.eccurvename, + }).verifyHex(this.sHashHex, e, this.ecpubhex); + if (this.pubKey instanceof RSAKey && 'rsaandmgf1' === this.pubkeyAlgName) + return this.pubKey.verifyWithMessageHashPSS( + this.sHashHex, + e, + this.mdAlgName, + this.pssSaltLen + ); + if (this.pubKey instanceof RSAKey && 'rsa' === this.pubkeyAlgName) + return this.pubKey.verifyWithMessageHash(this.sHashHex, e); + if (void 0 !== K.crypto.ECDSA && this.pubKey instanceof K.crypto.ECDSA) + return this.pubKey.verifyWithMessageHash(this.sHashHex, e); + if (void 0 !== K.crypto.DSA && this.pubKey instanceof K.crypto.DSA) + return this.pubKey.verifyWithMessageHash(this.sHashHex, e); + throw 'Signature: unsupported public key alg: ' + this.pubkeyAlgName; + }); + } + }), + (this.init = function (e, t) { + throw 'init(key, pass) not supported for this alg:prov=' + this.algProvName; + }), + (this.updateString = function (e) { + throw 'updateString(str) not supported for this alg:prov=' + this.algProvName; + }), + (this.updateHex = function (e) { + throw 'updateHex(hex) not supported for this alg:prov=' + this.algProvName; + }), + (this.sign = function () { + throw 'sign() not supported for this alg:prov=' + this.algProvName; + }), + (this.signString = function (e) { + throw 'digestString(str) not supported for this alg:prov=' + this.algProvName; + }), + (this.signHex = function (e) { + throw 'digestHex(hex) not supported for this alg:prov=' + this.algProvName; + }), + (this.verify = function (e) { + throw 'verify(hSigVal) not supported for this alg:prov=' + this.algProvName; + }), + (this.initParams = e), + void 0 !== e && + (void 0 !== e.alg && + ((this.algName = e.alg), + void 0 === e.prov + ? (this.provName = K.crypto.Util.DEFAULTPROVIDER[this.algName]) + : (this.provName = e.prov), + (this.algProvName = this.algName + ':' + this.provName), + this.setAlgAndProvider(this.algName, this.provName), + this._setAlgNames()), + void 0 !== e.psssaltlen && (this.pssSaltLen = e.psssaltlen), + void 0 !== e.prvkeypem)) + ) { + if (void 0 !== e.prvkeypas) + throw 'both prvkeypem and prvkeypas parameters not supported'; + try { + t = z.getKey(e.prvkeypem); + this.init(t); + } catch (e) { + throw 'fatal error to load pem private key: ' + e; + } + } + }), + (K.crypto.Cipher = function (e) {}), + (K.crypto.Cipher.encrypt = function (e, t, r) { + if (t instanceof RSAKey && t.isPublic) { + var n = K.crypto.Cipher.getAlgByKeyAndName(t, r); + if ('RSA' === n) return t.encrypt(e); + if ('RSAOAEP' === n) return t.encryptOAEP(e, 'sha1'); + var i = n.match(/^RSAOAEP(\d+)$/); + if (null !== i) return t.encryptOAEP(e, 'sha' + i[1]); + throw 'Cipher.encrypt: unsupported algorithm for RSAKey: ' + r; + } + throw 'Cipher.encrypt: unsupported key or algorithm'; + }), + (K.crypto.Cipher.decrypt = function (e, t, r) { + if (t instanceof RSAKey && t.isPrivate) { + var n = K.crypto.Cipher.getAlgByKeyAndName(t, r); + if ('RSA' === n) return t.decrypt(e); + if ('RSAOAEP' === n) return t.decryptOAEP(e, 'sha1'); + var i = n.match(/^RSAOAEP(\d+)$/); + if (null !== i) return t.decryptOAEP(e, 'sha' + i[1]); + throw 'Cipher.decrypt: unsupported algorithm for RSAKey: ' + r; + } + throw 'Cipher.decrypt: unsupported key or algorithm'; + }), + (K.crypto.Cipher.getAlgByKeyAndName = function (e, t) { + if (e instanceof RSAKey) { + if (-1 != ':RSA:RSAOAEP:RSAOAEP224:RSAOAEP256:RSAOAEP384:RSAOAEP512:'.indexOf(t)) + return t; + if (null === t || void 0 === t) return 'RSA'; + throw 'getAlgByKeyAndName: not supported algorithm name for RSAKey: ' + t; + } + throw 'getAlgByKeyAndName: not supported algorithm name: ' + t; + }), + (K.crypto.OID = new (function () { + this.oidhex2name = { + '2a864886f70d010101': 'rsaEncryption', + '2a8648ce3d0201': 'ecPublicKey', + '2a8648ce380401': 'dsa', + '2a8648ce3d030107': 'secp256r1', + '2b8104001f': 'secp192k1', + '2b81040021': 'secp224r1', + '2b8104000a': 'secp256k1', + '2b81040023': 'secp521r1', + '2b81040022': 'secp384r1', + '2a8648ce380403': 'SHA1withDSA', + '608648016503040301': 'SHA224withDSA', + '608648016503040302': 'SHA256withDSA', + }; + })()), + (void 0 !== K && K) || (K = {}), + (void 0 !== K.crypto && K.crypto) || (K.crypto = {}), + (K.crypto.ECDSA = function (e) { + var t = new SecureRandom(); + (this.type = 'EC'), + (this.isPrivate = !1), + (this.isPublic = !1), + (this.getBigRandom = function (e) { + return new BigInteger(e.bitLength(), t) + .mod(e.subtract(BigInteger.ONE)) + .add(BigInteger.ONE); + }), + (this.setNamedCurve = function (e) { + (this.ecparams = K.crypto.ECParameterDB.getByName(e)), + (this.prvKeyHex = null), + (this.pubKeyHex = null), + (this.curveName = e); + }), + (this.setPrivateKeyHex = function (e) { + (this.isPrivate = !0), (this.prvKeyHex = e); + }), + (this.setPublicKeyHex = function (e) { + (this.isPublic = !0), (this.pubKeyHex = e); + }), + (this.getPublicKeyXYHex = function () { + var e = this.pubKeyHex; + if ('04' !== e.substr(0, 2)) + throw 'this method supports uncompressed format(04) only'; + var t = this.ecparams.keylen / 4; + if (e.length !== 2 + 2 * t) throw 'malformed public key hex length'; + var r = {}; + return (r.x = e.substr(2, t)), (r.y = e.substr(2 + t)), r; + }), + (this.getShortNISTPCurveName = function () { + var e = this.curveName; + return 'secp256r1' === e || + 'NIST P-256' === e || + 'P-256' === e || + 'prime256v1' === e + ? 'P-256' + : 'secp384r1' === e || 'NIST P-384' === e || 'P-384' === e + ? 'P-384' + : null; + }), + (this.generateKeyPairHex = function () { + var e = this.ecparams.n, + t = this.getBigRandom(e), + r = this.ecparams.G.multiply(t), + n = r.getX().toBigInteger(), + i = r.getY().toBigInteger(), + o = this.ecparams.keylen / 4, + s = ('0000000000' + t.toString(16)).slice(-o), + a = + '04' + + ('0000000000' + n.toString(16)).slice(-o) + + ('0000000000' + i.toString(16)).slice(-o); + return ( + this.setPrivateKeyHex(s), this.setPublicKeyHex(a), { ecprvhex: s, ecpubhex: a } + ); + }), + (this.signWithMessageHash = function (e) { + return this.signHex(e, this.prvKeyHex); + }), + (this.signHex = function (e, t) { + var r = new BigInteger(t, 16), + n = this.ecparams.n, + i = new BigInteger(e, 16); + do { + var o = this.getBigRandom(n), + s = this.ecparams.G.multiply(o).getX().toBigInteger().mod(n); + } while (s.compareTo(BigInteger.ZERO) <= 0); + var a = o + .modInverse(n) + .multiply(i.add(r.multiply(s))) + .mod(n); + return K.crypto.ECDSA.biRSSigToASN1Sig(s, a); + }), + (this.sign = function (e, t) { + var r = t, + n = this.ecparams.n, + i = BigInteger.fromByteArrayUnsigned(e); + do { + var o = this.getBigRandom(n), + s = this.ecparams.G.multiply(o).getX().toBigInteger().mod(n); + } while (s.compareTo(BigInteger.ZERO) <= 0); + var a = o + .modInverse(n) + .multiply(i.add(r.multiply(s))) + .mod(n); + return this.serializeSig(s, a); + }), + (this.verifyWithMessageHash = function (e, t) { + return this.verifyHex(e, t, this.pubKeyHex); + }), + (this.verifyHex = function (e, t, r) { + var n, + i, + o, + s = K.crypto.ECDSA.parseSigHex(t); + (n = s.r), (i = s.s), (o = ECPointFp.decodeFromHex(this.ecparams.curve, r)); + var a = new BigInteger(e, 16); + return this.verifyRaw(a, n, i, o); + }), + (this.verify = function (e, t, r) { + var n, o, s; + if (Bitcoin.Util.isArray(t)) { + var a = this.parseSig(t); + (n = a.r), (o = a.s); + } else { + if ('object' !== (void 0 === t ? 'undefined' : i(t)) || !t.r || !t.s) + throw 'Invalid value for signature'; + (n = t.r), (o = t.s); + } + if (r instanceof ECPointFp) s = r; + else { + if (!Bitcoin.Util.isArray(r)) + throw 'Invalid format for pubkey value, must be byte array or ECPointFp'; + s = ECPointFp.decodeFrom(this.ecparams.curve, r); + } + var u = BigInteger.fromByteArrayUnsigned(e); + return this.verifyRaw(u, n, o, s); + }), + (this.verifyRaw = function (e, t, r, n) { + var i = this.ecparams.n, + o = this.ecparams.G; + if (t.compareTo(BigInteger.ONE) < 0 || t.compareTo(i) >= 0) return !1; + if (r.compareTo(BigInteger.ONE) < 0 || r.compareTo(i) >= 0) return !1; + var s = r.modInverse(i), + a = e.multiply(s).mod(i), + u = t.multiply(s).mod(i); + return o.multiply(a).add(n.multiply(u)).getX().toBigInteger().mod(i).equals(t); + }), + (this.serializeSig = function (e, t) { + var r = e.toByteArraySigned(), + n = t.toByteArraySigned(), + i = []; + return ( + i.push(2), + i.push(r.length), + (i = i.concat(r)).push(2), + i.push(n.length), + (i = i.concat(n)).unshift(i.length), + i.unshift(48), + i + ); + }), + (this.parseSig = function (e) { + var t; + if (48 != e[0]) throw new Error('Signature not a valid DERSequence'); + if (2 != e[(t = 2)]) + throw new Error('First element in signature must be a DERInteger'); + var r = e.slice(t + 2, t + 2 + e[t + 1]); + if (2 != e[(t += 2 + e[t + 1])]) + throw new Error('Second element in signature must be a DERInteger'); + var n = e.slice(t + 2, t + 2 + e[t + 1]); + return ( + (t += 2 + e[t + 1]), + { + r: BigInteger.fromByteArrayUnsigned(r), + s: BigInteger.fromByteArrayUnsigned(n), + } + ); + }), + (this.parseSigCompact = function (e) { + if (65 !== e.length) throw 'Signature has the wrong length'; + var t = e[0] - 27; + if (t < 0 || t > 7) throw 'Invalid signature type'; + var r = this.ecparams.n; + return { + r: BigInteger.fromByteArrayUnsigned(e.slice(1, 33)).mod(r), + s: BigInteger.fromByteArrayUnsigned(e.slice(33, 65)).mod(r), + i: t, + }; + }), + (this.readPKCS5PrvKeyHex = function (e) { + var t, + r, + n, + i = J, + o = K.crypto.ECDSA.getName, + s = i.getVbyList; + if (!1 === i.isASN1HEX(e)) throw 'not ASN.1 hex string'; + try { + (t = s(e, 0, [2, 0], '06')), (r = s(e, 0, [1], '04')); + try { + n = s(e, 0, [3, 0], '03').substr(2); + } catch (e) {} + } catch (e) { + throw 'malformed PKCS#1/5 plain ECC private key'; + } + if (((this.curveName = o(t)), void 0 === this.curveName)) + throw 'unsupported curve name'; + this.setNamedCurve(this.curveName), + this.setPublicKeyHex(n), + this.setPrivateKeyHex(r), + (this.isPublic = !1); + }), + (this.readPKCS8PrvKeyHex = function (e) { + var t, + r, + n, + i = J, + o = K.crypto.ECDSA.getName, + s = i.getVbyList; + if (!1 === i.isASN1HEX(e)) throw 'not ASN.1 hex string'; + try { + s(e, 0, [1, 0], '06'), + (t = s(e, 0, [1, 1], '06')), + (r = s(e, 0, [2, 0, 1], '04')); + try { + n = s(e, 0, [2, 0, 2, 0], '03').substr(2); + } catch (e) {} + } catch (e) { + throw 'malformed PKCS#8 plain ECC private key'; + } + if (((this.curveName = o(t)), void 0 === this.curveName)) + throw 'unsupported curve name'; + this.setNamedCurve(this.curveName), + this.setPublicKeyHex(n), + this.setPrivateKeyHex(r), + (this.isPublic = !1); + }), + (this.readPKCS8PubKeyHex = function (e) { + var t, + r, + n = J, + i = K.crypto.ECDSA.getName, + o = n.getVbyList; + if (!1 === n.isASN1HEX(e)) throw 'not ASN.1 hex string'; + try { + o(e, 0, [0, 0], '06'), + (t = o(e, 0, [0, 1], '06')), + (r = o(e, 0, [1], '03').substr(2)); + } catch (e) { + throw 'malformed PKCS#8 ECC public key'; + } + if (((this.curveName = i(t)), null === this.curveName)) + throw 'unsupported curve name'; + this.setNamedCurve(this.curveName), this.setPublicKeyHex(r); + }), + (this.readCertPubKeyHex = function (e, t) { + 5 !== t && (t = 6); + var r, + n, + i = J, + o = K.crypto.ECDSA.getName, + s = i.getVbyList; + if (!1 === i.isASN1HEX(e)) throw 'not ASN.1 hex string'; + try { + (r = s(e, 0, [0, t, 0, 1], '06')), (n = s(e, 0, [0, t, 1], '03').substr(2)); + } catch (e) { + throw 'malformed X.509 certificate ECC public key'; + } + if (((this.curveName = o(r)), null === this.curveName)) + throw 'unsupported curve name'; + this.setNamedCurve(this.curveName), this.setPublicKeyHex(n); + }), + void 0 !== e && void 0 !== e.curve && (this.curveName = e.curve), + void 0 === this.curveName && (this.curveName = 'secp256r1'), + this.setNamedCurve(this.curveName), + void 0 !== e && + (void 0 !== e.prv && this.setPrivateKeyHex(e.prv), + void 0 !== e.pub && this.setPublicKeyHex(e.pub)); + }), + (K.crypto.ECDSA.parseSigHex = function (e) { + var t = K.crypto.ECDSA.parseSigHexInHexRS(e); + return { r: new BigInteger(t.r, 16), s: new BigInteger(t.s, 16) }; + }), + (K.crypto.ECDSA.parseSigHexInHexRS = function (e) { + var t = J, + r = t.getChildIdx, + n = t.getV; + if ('30' != e.substr(0, 2)) throw 'signature is not a ASN.1 sequence'; + var i = r(e, 0); + if (2 != i.length) throw 'number of signature ASN.1 sequence elements seem wrong'; + var o = i[0], + s = i[1]; + if ('02' != e.substr(o, 2)) + throw '1st item of sequene of signature is not ASN.1 integer'; + if ('02' != e.substr(s, 2)) + throw '2nd item of sequene of signature is not ASN.1 integer'; + return { r: n(e, o), s: n(e, s) }; + }), + (K.crypto.ECDSA.asn1SigToConcatSig = function (e) { + var t = K.crypto.ECDSA.parseSigHexInHexRS(e), + r = t.r, + n = t.s; + if ( + ('00' == r.substr(0, 2) && r.length % 32 == 2 && (r = r.substr(2)), + '00' == n.substr(0, 2) && n.length % 32 == 2 && (n = n.substr(2)), + r.length % 32 == 30 && (r = '00' + r), + n.length % 32 == 30 && (n = '00' + n), + r.length % 32 != 0) + ) + throw 'unknown ECDSA sig r length error'; + if (n.length % 32 != 0) throw 'unknown ECDSA sig s length error'; + return r + n; + }), + (K.crypto.ECDSA.concatSigToASN1Sig = function (e) { + if (((e.length / 2) * 8) % 128 != 0) + throw 'unknown ECDSA concatinated r-s sig length error'; + var t = e.substr(0, e.length / 2), + r = e.substr(e.length / 2); + return K.crypto.ECDSA.hexRSSigToASN1Sig(t, r); + }), + (K.crypto.ECDSA.hexRSSigToASN1Sig = function (e, t) { + var r = new BigInteger(e, 16), + n = new BigInteger(t, 16); + return K.crypto.ECDSA.biRSSigToASN1Sig(r, n); + }), + (K.crypto.ECDSA.biRSSigToASN1Sig = function (e, t) { + var r = K.asn1, + n = new r.DERInteger({ bigint: e }), + i = new r.DERInteger({ bigint: t }); + return new r.DERSequence({ array: [n, i] }).getEncodedHex(); + }), + (K.crypto.ECDSA.getName = function (e) { + return '2a8648ce3d030107' === e + ? 'secp256r1' + : '2b8104000a' === e + ? 'secp256k1' + : '2b81040022' === e + ? 'secp384r1' + : -1 !== '|secp256r1|NIST P-256|P-256|prime256v1|'.indexOf(e) + ? 'secp256r1' + : -1 !== '|secp256k1|'.indexOf(e) + ? 'secp256k1' + : -1 !== '|secp384r1|NIST P-384|P-384|'.indexOf(e) + ? 'secp384r1' + : null; + }), + (void 0 !== K && K) || (K = {}), + (void 0 !== K.crypto && K.crypto) || (K.crypto = {}), + (K.crypto.ECParameterDB = new (function () { + var e = {}, + t = {}; + function a(e) { + return new BigInteger(e, 16); + } + (this.getByName = function (r) { + var n = r; + if ((void 0 !== t[n] && (n = t[r]), void 0 !== e[n])) return e[n]; + throw 'unregistered EC curve name: ' + n; + }), + (this.regist = function (r, n, i, o, s, u, c, h, f, l, g, p) { + e[r] = {}; + var d = a(i), + v = a(o), + y = a(s), + m = a(u), + S = a(c), + F = new ECCurveFp(d, v, y), + b = F.decodePointHex('04' + h + f); + (e[r].name = r), + (e[r].keylen = n), + (e[r].curve = F), + (e[r].G = b), + (e[r].n = m), + (e[r].h = S), + (e[r].oid = g), + (e[r].info = p); + for (var _ = 0; _ < l.length; _++) t[l[_]] = r; + }); + })()), + K.crypto.ECParameterDB.regist( + 'secp128r1', + 128, + 'FFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFF', + 'FFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFC', + 'E87579C11079F43DD824993C2CEE5ED3', + 'FFFFFFFE0000000075A30D1B9038A115', + '1', + '161FF7528B899B2D0C28607CA52C5B86', + 'CF5AC8395BAFEB13C02DA292DDED7A83', + [], + '', + 'secp128r1 : SECG curve over a 128 bit prime field' + ), + K.crypto.ECParameterDB.regist( + 'secp160k1', + 160, + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC73', + '0', + '7', + '0100000000000000000001B8FA16DFAB9ACA16B6B3', + '1', + '3B4C382CE37AA192A4019E763036F4F5DD4D7EBB', + '938CF935318FDCED6BC28286531733C3F03C4FEE', + [], + '', + 'secp160k1 : SECG curve over a 160 bit prime field' + ), + K.crypto.ECParameterDB.regist( + 'secp160r1', + 160, + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFF', + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFC', + '1C97BEFC54BD7A8B65ACF89F81D4D4ADC565FA45', + '0100000000000000000001F4C8F927AED3CA752257', + '1', + '4A96B5688EF573284664698968C38BB913CBFC82', + '23A628553168947D59DCC912042351377AC5FB32', + [], + '', + 'secp160r1 : SECG curve over a 160 bit prime field' + ), + K.crypto.ECParameterDB.regist( + 'secp192k1', + 192, + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFEE37', + '0', + '3', + 'FFFFFFFFFFFFFFFFFFFFFFFE26F2FC170F69466A74DEFD8D', + '1', + 'DB4FF10EC057E9AE26B07D0280B7F4341DA5D1B1EAE06C7D', + '9B2F2F6D9C5628A7844163D015BE86344082AA88D95E2F9D', + [] + ), + K.crypto.ECParameterDB.regist( + 'secp192r1', + 192, + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFF', + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFC', + '64210519E59C80E70FA7E9AB72243049FEB8DEECC146B9B1', + 'FFFFFFFFFFFFFFFFFFFFFFFF99DEF836146BC9B1B4D22831', + '1', + '188DA80EB03090F67CBF20EB43A18800F4FF0AFD82FF1012', + '07192B95FFC8DA78631011ED6B24CDD573F977A11E794811', + [] + ), + K.crypto.ECParameterDB.regist( + 'secp224r1', + 224, + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000001', + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFE', + 'B4050A850C04B3ABF54132565044B0B7D7BFD8BA270B39432355FFB4', + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D', + '1', + 'B70E0CBD6BB4BF7F321390B94A03C1D356C21122343280D6115C1D21', + 'BD376388B5F723FB4C22DFE6CD4375A05A07476444D5819985007E34', + [] + ), + K.crypto.ECParameterDB.regist( + 'secp256k1', + 256, + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F', + '0', + '7', + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', + '1', + '79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798', + '483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8', + [] + ), + K.crypto.ECParameterDB.regist( + 'secp256r1', + 256, + 'FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF', + 'FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC', + '5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B', + 'FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551', + '1', + '6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296', + '4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5', + ['NIST P-256', 'P-256', 'prime256v1'] + ), + K.crypto.ECParameterDB.regist( + 'secp384r1', + 384, + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF', + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC', + 'B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF', + 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973', + '1', + 'AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7', + '3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f', + ['NIST P-384', 'P-384'] + ), + K.crypto.ECParameterDB.regist( + 'secp521r1', + 521, + '1FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', + '1FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC', + '051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00', + '1FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409', + '1', + 'C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66', + '011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650', + ['NIST P-521', 'P-521'] + ); + var z = (function () { + var t = function d(e, t, n) { + return r(y.AES, e, t, n); + }, + r = function k(e, t, r, n) { + var i = y.enc.Hex.parse(t), + o = y.enc.Hex.parse(r), + s = y.enc.Hex.parse(n), + a = {}; + (a.key = o), (a.iv = s), (a.ciphertext = i); + var u = e.decrypt(a, o, { iv: s }); + return y.enc.Hex.stringify(u); + }, + n = function l(e, t, r) { + return i(y.AES, e, t, r); + }, + i = function g(e, t, r, n) { + var i = y.enc.Hex.parse(t), + o = y.enc.Hex.parse(r), + s = y.enc.Hex.parse(n), + a = e.encrypt(i, o, { iv: s }), + u = y.enc.Hex.parse(a.toString()); + return y.enc.Base64.stringify(u); + }, + s = { + 'AES-256-CBC': { proc: t, eproc: n, keylen: 32, ivlen: 16 }, + 'AES-192-CBC': { proc: t, eproc: n, keylen: 24, ivlen: 16 }, + 'AES-128-CBC': { proc: t, eproc: n, keylen: 16, ivlen: 16 }, + 'DES-EDE3-CBC': { + proc: function e(t, n, i) { + return r(y.TripleDES, t, n, i); + }, + eproc: function o(e, t, r) { + return i(y.TripleDES, e, t, r); + }, + keylen: 24, + ivlen: 8, + }, + 'DES-CBC': { + proc: function a(e, t, n) { + return r(y.DES, e, t, n); + }, + eproc: function f(e, t, r) { + return i(y.DES, e, t, r); + }, + keylen: 8, + ivlen: 8, + }, + }, + u = function n(e) { + var t = {}, + r = e.match(new RegExp('DEK-Info: ([^,]+),([0-9A-Fa-f]+)', 'm')); + r && ((t.cipher = r[1]), (t.ivsalt = r[2])); + var i = e.match(new RegExp('-----BEGIN ([A-Z]+) PRIVATE KEY-----')); + i && (t.type = i[1]); + var o = -1, + s = 0; + -1 != e.indexOf('\r\n\r\n') && ((o = e.indexOf('\r\n\r\n')), (s = 2)), + -1 != e.indexOf('\n\n') && ((o = e.indexOf('\n\n')), (s = 1)); + var a = e.indexOf('-----END'); + if (-1 != o && -1 != a) { + var u = e.substring(o + 2 * s, a - s); + (u = u.replace(/\s+/g, '')), (t.data = u); + } + return t; + }, + c = function j(e, t, r) { + for ( + var n = r.substring(0, 16), + i = y.enc.Hex.parse(n), + o = y.enc.Utf8.parse(t), + a = s[e].keylen + s[e].ivlen, + u = '', + c = null; + ; + + ) { + var h = y.algo.MD5.create(); + if ( + (null != c && h.update(c), + h.update(o), + h.update(i), + (c = h.finalize()), + (u += y.enc.Hex.stringify(c)).length >= 2 * a) + ) + break; + } + var f = {}; + return ( + (f.keyhex = u.substr(0, 2 * s[e].keylen)), + (f.ivhex = u.substr(2 * s[e].keylen, 2 * s[e].ivlen)), + f + ); + }, + g = function b(e, t, r, n) { + var i = y.enc.Base64.parse(e), + o = y.enc.Hex.stringify(i); + return (0, s[t].proc)(o, r, n); + }; + return { + version: '1.0.0', + parsePKCS5PEM: function parsePKCS5PEM(e) { + return u(e); + }, + getKeyAndUnusedIvByPasscodeAndIvsalt: function getKeyAndUnusedIvByPasscodeAndIvsalt( + e, + t, + r + ) { + return c(e, t, r); + }, + decryptKeyB64: function decryptKeyB64(e, t, r, n) { + return g(e, t, r, n); + }, + getDecryptedKeyHex: function getDecryptedKeyHex(e, t) { + var r = u(e), + n = (r.type, r.cipher), + i = r.ivsalt, + o = r.data, + s = c(n, t, i).keyhex; + return g(o, n, s, i); + }, + getEncryptedPKCS5PEMFromPrvKeyHex: function getEncryptedPKCS5PEMFromPrvKeyHex( + e, + t, + r, + n, + i + ) { + var o = ''; + if (((void 0 !== n && null != n) || (n = 'AES-256-CBC'), void 0 === s[n])) + throw 'KEYUTIL unsupported algorithm: ' + n; + (void 0 !== i && null != i) || + (i = (function m(e) { + var t = y.lib.WordArray.random(e); + return y.enc.Hex.stringify(t); + })(s[n].ivlen).toUpperCase()); + var a = (function h(e, t, r, n) { + return (0, s[t].eproc)(e, r, n); + })(t, n, c(n, r, i).keyhex, i); + o = '-----BEGIN ' + e + ' PRIVATE KEY-----\r\n'; + return ( + (o += 'Proc-Type: 4,ENCRYPTED\r\n'), + (o += 'DEK-Info: ' + n + ',' + i + '\r\n'), + (o += '\r\n'), + (o += a.replace(/(.{64})/g, '$1\r\n')), + (o += '\r\n-----END ' + e + ' PRIVATE KEY-----\r\n') + ); + }, + parseHexOfEncryptedPKCS8: function parseHexOfEncryptedPKCS8(e) { + var t = J, + r = t.getChildIdx, + n = t.getV, + i = {}, + o = r(e, 0); + if (2 != o.length) throw 'malformed format: SEQUENCE(0).items != 2: ' + o.length; + i.ciphertext = n(e, o[1]); + var s = r(e, o[0]); + if (2 != s.length) throw 'malformed format: SEQUENCE(0.0).items != 2: ' + s.length; + if ('2a864886f70d01050d' != n(e, s[0])) throw 'this only supports pkcs5PBES2'; + var a = r(e, s[1]); + if (2 != s.length) throw 'malformed format: SEQUENCE(0.0.1).items != 2: ' + a.length; + var u = r(e, a[1]); + if (2 != u.length) + throw 'malformed format: SEQUENCE(0.0.1.1).items != 2: ' + u.length; + if ('2a864886f70d0307' != n(e, u[0])) throw 'this only supports TripleDES'; + (i.encryptionSchemeAlg = 'TripleDES'), (i.encryptionSchemeIV = n(e, u[1])); + var c = r(e, a[0]); + if (2 != c.length) + throw 'malformed format: SEQUENCE(0.0.1.0).items != 2: ' + c.length; + if ('2a864886f70d01050c' != n(e, c[0])) throw 'this only supports pkcs5PBKDF2'; + var h = r(e, c[1]); + if (h.length < 2) + throw 'malformed format: SEQUENCE(0.0.1.0.1).items < 2: ' + h.length; + i.pbkdf2Salt = n(e, h[0]); + var f = n(e, h[1]); + try { + i.pbkdf2Iter = parseInt(f, 16); + } catch (e) { + throw 'malformed format pbkdf2Iter: ' + f; + } + return i; + }, + getPBKDF2KeyHexFromParam: function getPBKDF2KeyHexFromParam(e, t) { + var r = y.enc.Hex.parse(e.pbkdf2Salt), + n = e.pbkdf2Iter, + i = y.PBKDF2(t, r, { keySize: 6, iterations: n }); + return y.enc.Hex.stringify(i); + }, + _getPlainPKCS8HexFromEncryptedPKCS8PEM: function _getPlainPKCS8HexFromEncryptedPKCS8PEM( + e, + t + ) { + var r = pemtohex(e, 'ENCRYPTED PRIVATE KEY'), + n = this.parseHexOfEncryptedPKCS8(r), + i = z.getPBKDF2KeyHexFromParam(n, t), + o = {}; + o.ciphertext = y.enc.Hex.parse(n.ciphertext); + var s = y.enc.Hex.parse(i), + a = y.enc.Hex.parse(n.encryptionSchemeIV), + u = y.TripleDES.decrypt(o, s, { iv: a }); + return y.enc.Hex.stringify(u); + }, + getKeyFromEncryptedPKCS8PEM: function getKeyFromEncryptedPKCS8PEM(e, t) { + var r = this._getPlainPKCS8HexFromEncryptedPKCS8PEM(e, t); + return this.getKeyFromPlainPrivatePKCS8Hex(r); + }, + parsePlainPrivatePKCS8Hex: function parsePlainPrivatePKCS8Hex(e) { + var t = J, + r = t.getChildIdx, + n = t.getV, + i = { algparam: null }; + if ('30' != e.substr(0, 2)) throw 'malformed plain PKCS8 private key(code:001)'; + var o = r(e, 0); + if (3 != o.length) throw 'malformed plain PKCS8 private key(code:002)'; + if ('30' != e.substr(o[1], 2)) throw 'malformed PKCS8 private key(code:003)'; + var s = r(e, o[1]); + if (2 != s.length) throw 'malformed PKCS8 private key(code:004)'; + if ('06' != e.substr(s[0], 2)) throw 'malformed PKCS8 private key(code:005)'; + if ( + ((i.algoid = n(e, s[0])), + '06' == e.substr(s[1], 2) && (i.algparam = n(e, s[1])), + '04' != e.substr(o[2], 2)) + ) + throw 'malformed PKCS8 private key(code:006)'; + return (i.keyidx = t.getVidx(e, o[2])), i; + }, + getKeyFromPlainPrivatePKCS8PEM: function getKeyFromPlainPrivatePKCS8PEM(e) { + var t = pemtohex(e, 'PRIVATE KEY'); + return this.getKeyFromPlainPrivatePKCS8Hex(t); + }, + getKeyFromPlainPrivatePKCS8Hex: function getKeyFromPlainPrivatePKCS8Hex(e) { + var t, + r = this.parsePlainPrivatePKCS8Hex(e); + if ('2a864886f70d010101' == r.algoid) t = new RSAKey(); + else if ('2a8648ce380401' == r.algoid) t = new K.crypto.DSA(); + else { + if ('2a8648ce3d0201' != r.algoid) throw 'unsupported private key algorithm'; + t = new K.crypto.ECDSA(); + } + return t.readPKCS8PrvKeyHex(e), t; + }, + _getKeyFromPublicPKCS8Hex: function _getKeyFromPublicPKCS8Hex(e) { + var t, + r = J.getVbyList(e, 0, [0, 0], '06'); + if ('2a864886f70d010101' === r) t = new RSAKey(); + else if ('2a8648ce380401' === r) t = new K.crypto.DSA(); + else { + if ('2a8648ce3d0201' !== r) throw 'unsupported PKCS#8 public key hex'; + t = new K.crypto.ECDSA(); + } + return t.readPKCS8PubKeyHex(e), t; + }, + parsePublicRawRSAKeyHex: function parsePublicRawRSAKeyHex(e) { + var t = J, + r = t.getChildIdx, + n = t.getV, + i = {}; + if ('30' != e.substr(0, 2)) throw 'malformed RSA key(code:001)'; + var o = r(e, 0); + if (2 != o.length) throw 'malformed RSA key(code:002)'; + if ('02' != e.substr(o[0], 2)) throw 'malformed RSA key(code:003)'; + if (((i.n = n(e, o[0])), '02' != e.substr(o[1], 2))) + throw 'malformed RSA key(code:004)'; + return (i.e = n(e, o[1])), i; + }, + parsePublicPKCS8Hex: function parsePublicPKCS8Hex(e) { + var t = J, + r = t.getChildIdx, + n = t.getV, + i = { algparam: null }, + o = r(e, 0); + if (2 != o.length) throw 'outer DERSequence shall have 2 elements: ' + o.length; + var s = o[0]; + if ('30' != e.substr(s, 2)) throw 'malformed PKCS8 public key(code:001)'; + var a = r(e, s); + if (2 != a.length) throw 'malformed PKCS8 public key(code:002)'; + if ('06' != e.substr(a[0], 2)) throw 'malformed PKCS8 public key(code:003)'; + if ( + ((i.algoid = n(e, a[0])), + '06' == e.substr(a[1], 2) + ? (i.algparam = n(e, a[1])) + : '30' == e.substr(a[1], 2) && + ((i.algparam = {}), + (i.algparam.p = t.getVbyList(e, a[1], [0], '02')), + (i.algparam.q = t.getVbyList(e, a[1], [1], '02')), + (i.algparam.g = t.getVbyList(e, a[1], [2], '02'))), + '03' != e.substr(o[1], 2)) + ) + throw 'malformed PKCS8 public key(code:004)'; + return (i.key = n(e, o[1]).substr(2)), i; + }, + }; + })(); + (z.getKey = function (e, t, r) { + var n = (v = J).getChildIdx, + i = (v.getV, v.getVbyList), + o = K.crypto, + s = o.ECDSA, + a = o.DSA, + u = RSAKey, + c = pemtohex, + h = z; + if (void 0 !== u && e instanceof u) return e; + if (void 0 !== s && e instanceof s) return e; + if (void 0 !== a && e instanceof a) return e; + if (void 0 !== e.curve && void 0 !== e.xy && void 0 === e.d) + return new s({ pub: e.xy, curve: e.curve }); + if (void 0 !== e.curve && void 0 !== e.d) return new s({ prv: e.d, curve: e.curve }); + if (void 0 === e.kty && void 0 !== e.n && void 0 !== e.e && void 0 === e.d) + return (P = new u()).setPublic(e.n, e.e), P; + if ( + void 0 === e.kty && + void 0 !== e.n && + void 0 !== e.e && + void 0 !== e.d && + void 0 !== e.p && + void 0 !== e.q && + void 0 !== e.dp && + void 0 !== e.dq && + void 0 !== e.co && + void 0 === e.qi + ) + return (P = new u()).setPrivateEx(e.n, e.e, e.d, e.p, e.q, e.dp, e.dq, e.co), P; + if ( + void 0 === e.kty && + void 0 !== e.n && + void 0 !== e.e && + void 0 !== e.d && + void 0 === e.p + ) + return (P = new u()).setPrivate(e.n, e.e, e.d), P; + if ( + void 0 !== e.p && + void 0 !== e.q && + void 0 !== e.g && + void 0 !== e.y && + void 0 === e.x + ) + return (P = new a()).setPublic(e.p, e.q, e.g, e.y), P; + if ( + void 0 !== e.p && + void 0 !== e.q && + void 0 !== e.g && + void 0 !== e.y && + void 0 !== e.x + ) + return (P = new a()).setPrivate(e.p, e.q, e.g, e.y, e.x), P; + if ('RSA' === e.kty && void 0 !== e.n && void 0 !== e.e && void 0 === e.d) + return (P = new u()).setPublic(b64utohex(e.n), b64utohex(e.e)), P; + if ( + 'RSA' === e.kty && + void 0 !== e.n && + void 0 !== e.e && + void 0 !== e.d && + void 0 !== e.p && + void 0 !== e.q && + void 0 !== e.dp && + void 0 !== e.dq && + void 0 !== e.qi + ) + return ( + (P = new u()).setPrivateEx( + b64utohex(e.n), + b64utohex(e.e), + b64utohex(e.d), + b64utohex(e.p), + b64utohex(e.q), + b64utohex(e.dp), + b64utohex(e.dq), + b64utohex(e.qi) + ), + P + ); + if ('RSA' === e.kty && void 0 !== e.n && void 0 !== e.e && void 0 !== e.d) + return (P = new u()).setPrivate(b64utohex(e.n), b64utohex(e.e), b64utohex(e.d)), P; + if ( + 'EC' === e.kty && + void 0 !== e.crv && + void 0 !== e.x && + void 0 !== e.y && + void 0 === e.d + ) { + var f = (C = new s({ curve: e.crv })).ecparams.keylen / 4, + l = + '04' + + ('0000000000' + b64utohex(e.x)).slice(-f) + + ('0000000000' + b64utohex(e.y)).slice(-f); + return C.setPublicKeyHex(l), C; + } + if ( + 'EC' === e.kty && + void 0 !== e.crv && + void 0 !== e.x && + void 0 !== e.y && + void 0 !== e.d + ) { + (f = (C = new s({ curve: e.crv })).ecparams.keylen / 4), + (l = + '04' + + ('0000000000' + b64utohex(e.x)).slice(-f) + + ('0000000000' + b64utohex(e.y)).slice(-f)); + var g = ('0000000000' + b64utohex(e.d)).slice(-f); + return C.setPublicKeyHex(l), C.setPrivateKeyHex(g), C; + } + if ('pkcs5prv' === r) { + var p, + d = e, + v = J; + if (9 === (p = n(d, 0)).length) (P = new u()).readPKCS5PrvKeyHex(d); + else if (6 === p.length) (P = new a()).readPKCS5PrvKeyHex(d); + else { + if (!(p.length > 2 && '04' === d.substr(p[1], 2))) + throw 'unsupported PKCS#1/5 hexadecimal key'; + (P = new s()).readPKCS5PrvKeyHex(d); + } + return P; + } + if ('pkcs8prv' === r) return (P = h.getKeyFromPlainPrivatePKCS8Hex(e)); + if ('pkcs8pub' === r) return h._getKeyFromPublicPKCS8Hex(e); + if ('x509pub' === r) return X509.getPublicKeyFromCertHex(e); + if ( + -1 != e.indexOf('-END CERTIFICATE-', 0) || + -1 != e.indexOf('-END X509 CERTIFICATE-', 0) || + -1 != e.indexOf('-END TRUSTED CERTIFICATE-', 0) + ) + return X509.getPublicKeyFromCertPEM(e); + if (-1 != e.indexOf('-END PUBLIC KEY-')) { + var y = pemtohex(e, 'PUBLIC KEY'); + return h._getKeyFromPublicPKCS8Hex(y); + } + if (-1 != e.indexOf('-END RSA PRIVATE KEY-') && -1 == e.indexOf('4,ENCRYPTED')) { + var m = c(e, 'RSA PRIVATE KEY'); + return h.getKey(m, null, 'pkcs5prv'); + } + if (-1 != e.indexOf('-END DSA PRIVATE KEY-') && -1 == e.indexOf('4,ENCRYPTED')) { + var S = i((I = c(e, 'DSA PRIVATE KEY')), 0, [1], '02'), + F = i(I, 0, [2], '02'), + b = i(I, 0, [3], '02'), + _ = i(I, 0, [4], '02'), + w = i(I, 0, [5], '02'); + return ( + (P = new a()).setPrivate( + new BigInteger(S, 16), + new BigInteger(F, 16), + new BigInteger(b, 16), + new BigInteger(_, 16), + new BigInteger(w, 16) + ), + P + ); + } + if (-1 != e.indexOf('-END PRIVATE KEY-')) return h.getKeyFromPlainPrivatePKCS8PEM(e); + if (-1 != e.indexOf('-END RSA PRIVATE KEY-') && -1 != e.indexOf('4,ENCRYPTED')) { + var E = h.getDecryptedKeyHex(e, t), + x = new RSAKey(); + return x.readPKCS5PrvKeyHex(E), x; + } + if (-1 != e.indexOf('-END EC PRIVATE KEY-') && -1 != e.indexOf('4,ENCRYPTED')) { + var C, + P = i((I = h.getDecryptedKeyHex(e, t)), 0, [1], '04'), + A = i(I, 0, [2, 0], '06'), + k = i(I, 0, [3, 0], '03').substr(2); + if (void 0 === K.crypto.OID.oidhex2name[A]) + throw 'undefined OID(hex) in KJUR.crypto.OID: ' + A; + return ( + (C = new s({ + curve: K.crypto.OID.oidhex2name[A], + })).setPublicKeyHex(k), + C.setPrivateKeyHex(P), + (C.isPublic = !1), + C + ); + } + if (-1 != e.indexOf('-END DSA PRIVATE KEY-') && -1 != e.indexOf('4,ENCRYPTED')) { + var I; + (S = i((I = h.getDecryptedKeyHex(e, t)), 0, [1], '02')), + (F = i(I, 0, [2], '02')), + (b = i(I, 0, [3], '02')), + (_ = i(I, 0, [4], '02')), + (w = i(I, 0, [5], '02')); + return ( + (P = new a()).setPrivate( + new BigInteger(S, 16), + new BigInteger(F, 16), + new BigInteger(b, 16), + new BigInteger(_, 16), + new BigInteger(w, 16) + ), + P + ); + } + if (-1 != e.indexOf('-END ENCRYPTED PRIVATE KEY-')) + return h.getKeyFromEncryptedPKCS8PEM(e, t); + throw 'not supported argument'; + }), + (z.generateKeypair = function (e, t) { + if ('RSA' == e) { + var r = t; + (s = new RSAKey()).generate(r, '10001'), (s.isPrivate = !0), (s.isPublic = !0); + var n = new RSAKey(), + i = s.n.toString(16), + o = s.e.toString(16); + return ( + n.setPublic(i, o), + (n.isPrivate = !1), + (n.isPublic = !0), + ((a = {}).prvKeyObj = s), + (a.pubKeyObj = n), + a + ); + } + if ('EC' == e) { + var s, + a, + u = t, + c = new K.crypto.ECDSA({ curve: u }).generateKeyPairHex(); + return ( + (s = new K.crypto.ECDSA({ curve: u })).setPublicKeyHex(c.ecpubhex), + s.setPrivateKeyHex(c.ecprvhex), + (s.isPrivate = !0), + (s.isPublic = !1), + (n = new K.crypto.ECDSA({ curve: u })).setPublicKeyHex(c.ecpubhex), + (n.isPrivate = !1), + (n.isPublic = !0), + ((a = {}).prvKeyObj = s), + (a.pubKeyObj = n), + a + ); + } + throw 'unknown algorithm: ' + e; + }), + (z.getPEM = function (e, t, r, n, i, s) { + var a = K, + u = a.asn1, + c = u.DERObjectIdentifier, + h = u.DERInteger, + f = u.ASN1Util.newObject, + l = u.x509.SubjectPublicKeyInfo, + g = a.crypto, + p = g.DSA, + d = g.ECDSA, + v = RSAKey; + function A(e) { + return f({ + seq: [ + { int: 0 }, + { int: { bigint: e.n } }, + { int: e.e }, + { int: { bigint: e.d } }, + { int: { bigint: e.p } }, + { int: { bigint: e.q } }, + { int: { bigint: e.dmp1 } }, + { int: { bigint: e.dmq1 } }, + { int: { bigint: e.coeff } }, + ], + }); + } + function B(e) { + return f({ + seq: [ + { int: 1 }, + { octstr: { hex: e.prvKeyHex } }, + { tag: ['a0', !0, { oid: { name: e.curveName } }] }, + { tag: ['a1', !0, { bitstr: { hex: '00' + e.pubKeyHex } }] }, + ], + }); + } + function x(e) { + return f({ + seq: [ + { int: 0 }, + { int: { bigint: e.p } }, + { int: { bigint: e.q } }, + { int: { bigint: e.g } }, + { int: { bigint: e.y } }, + { int: { bigint: e.x } }, + ], + }); + } + if ( + ((void 0 !== v && e instanceof v) || + (void 0 !== p && e instanceof p) || + (void 0 !== d && e instanceof d)) && + 1 == e.isPublic && + (void 0 === t || 'PKCS8PUB' == t) + ) + return hextopem((b = new l(e).getEncodedHex()), 'PUBLIC KEY'); + if ( + 'PKCS1PRV' == t && + void 0 !== v && + e instanceof v && + (void 0 === r || null == r) && + 1 == e.isPrivate + ) + return hextopem((b = A(e).getEncodedHex()), 'RSA PRIVATE KEY'); + if ( + 'PKCS1PRV' == t && + void 0 !== d && + e instanceof d && + (void 0 === r || null == r) && + 1 == e.isPrivate + ) { + var m = new c({ name: e.curveName }).getEncodedHex(), + S = B(e).getEncodedHex(), + F = ''; + return (F += hextopem(m, 'EC PARAMETERS')), (F += hextopem(S, 'EC PRIVATE KEY')); + } + if ( + 'PKCS1PRV' == t && + void 0 !== p && + e instanceof p && + (void 0 === r || null == r) && + 1 == e.isPrivate + ) + return hextopem((b = x(e).getEncodedHex()), 'DSA PRIVATE KEY'); + if ( + 'PKCS5PRV' == t && + void 0 !== v && + e instanceof v && + void 0 !== r && + null != r && + 1 == e.isPrivate + ) { + var b = A(e).getEncodedHex(); + return ( + void 0 === n && (n = 'DES-EDE3-CBC'), + this.getEncryptedPKCS5PEMFromPrvKeyHex('RSA', b, r, n, s) + ); + } + if ( + 'PKCS5PRV' == t && + void 0 !== d && + e instanceof d && + void 0 !== r && + null != r && + 1 == e.isPrivate + ) { + b = B(e).getEncodedHex(); + return ( + void 0 === n && (n = 'DES-EDE3-CBC'), + this.getEncryptedPKCS5PEMFromPrvKeyHex('EC', b, r, n, s) + ); + } + if ( + 'PKCS5PRV' == t && + void 0 !== p && + e instanceof p && + void 0 !== r && + null != r && + 1 == e.isPrivate + ) { + b = x(e).getEncodedHex(); + return ( + void 0 === n && (n = 'DES-EDE3-CBC'), + this.getEncryptedPKCS5PEMFromPrvKeyHex('DSA', b, r, n, s) + ); + } + var _ = function o(e, t) { + var r = w(e, t); + return new f({ + seq: [ + { + seq: [ + { oid: { name: 'pkcs5PBES2' } }, + { + seq: [ + { + seq: [ + { oid: { name: 'pkcs5PBKDF2' } }, + { + seq: [{ octstr: { hex: r.pbkdf2Salt } }, { int: r.pbkdf2Iter }], + }, + ], + }, + { + seq: [ + { oid: { name: 'des-EDE3-CBC' } }, + { octstr: { hex: r.encryptionSchemeIV } }, + ], + }, + ], + }, + ], + }, + { octstr: { hex: r.ciphertext } }, + ], + }).getEncodedHex(); + }, + w = function c(e, t) { + var r = y.lib.WordArray.random(8), + n = y.lib.WordArray.random(8), + i = y.PBKDF2(t, r, { keySize: 6, iterations: 100 }), + o = y.enc.Hex.parse(e), + s = y.TripleDES.encrypt(o, i, { iv: n }) + '', + a = {}; + return ( + (a.ciphertext = s), + (a.pbkdf2Salt = y.enc.Hex.stringify(r)), + (a.pbkdf2Iter = 100), + (a.encryptionSchemeAlg = 'DES-EDE3-CBC'), + (a.encryptionSchemeIV = y.enc.Hex.stringify(n)), + a + ); + }; + if ('PKCS8PRV' == t && void 0 != v && e instanceof v && 1 == e.isPrivate) { + var E = A(e).getEncodedHex(); + b = f({ + seq: [ + { int: 0 }, + { seq: [{ oid: { name: 'rsaEncryption' } }, { null: !0 }] }, + { octstr: { hex: E } }, + ], + }).getEncodedHex(); + return void 0 === r || null == r + ? hextopem(b, 'PRIVATE KEY') + : hextopem((S = _(b, r)), 'ENCRYPTED PRIVATE KEY'); + } + if ('PKCS8PRV' == t && void 0 !== d && e instanceof d && 1 == e.isPrivate) { + (E = new f({ + seq: [ + { int: 1 }, + { octstr: { hex: e.prvKeyHex } }, + { tag: ['a1', !0, { bitstr: { hex: '00' + e.pubKeyHex } }] }, + ], + }).getEncodedHex()), + (b = f({ + seq: [ + { int: 0 }, + { + seq: [{ oid: { name: 'ecPublicKey' } }, { oid: { name: e.curveName } }], + }, + { octstr: { hex: E } }, + ], + }).getEncodedHex()); + return void 0 === r || null == r + ? hextopem(b, 'PRIVATE KEY') + : hextopem((S = _(b, r)), 'ENCRYPTED PRIVATE KEY'); + } + if ('PKCS8PRV' == t && void 0 !== p && e instanceof p && 1 == e.isPrivate) { + (E = new h({ bigint: e.x }).getEncodedHex()), + (b = f({ + seq: [ + { int: 0 }, + { + seq: [ + { oid: { name: 'dsa' } }, + { + seq: [ + { int: { bigint: e.p } }, + { int: { bigint: e.q } }, + { int: { bigint: e.g } }, + ], + }, + ], + }, + { octstr: { hex: E } }, + ], + }).getEncodedHex()); + return void 0 === r || null == r + ? hextopem(b, 'PRIVATE KEY') + : hextopem((S = _(b, r)), 'ENCRYPTED PRIVATE KEY'); + } + throw 'unsupported object nor format'; + }), + (z.getKeyFromCSRPEM = function (e) { + var t = pemtohex(e, 'CERTIFICATE REQUEST'); + return z.getKeyFromCSRHex(t); + }), + (z.getKeyFromCSRHex = function (e) { + var t = z.parseCSRHex(e); + return z.getKey(t.p8pubkeyhex, null, 'pkcs8pub'); + }), + (z.parseCSRHex = function (e) { + var t = J, + r = t.getChildIdx, + n = t.getTLV, + i = {}, + o = e; + if ('30' != o.substr(0, 2)) throw 'malformed CSR(code:001)'; + var s = r(o, 0); + if (s.length < 1) throw 'malformed CSR(code:002)'; + if ('30' != o.substr(s[0], 2)) throw 'malformed CSR(code:003)'; + var a = r(o, s[0]); + if (a.length < 3) throw 'malformed CSR(code:004)'; + return (i.p8pubkeyhex = n(o, a[2])), i; + }), + (z.getJWKFromKey = function (e) { + var t = {}; + if (e instanceof RSAKey && e.isPrivate) + return ( + (t.kty = 'RSA'), + (t.n = hextob64u(e.n.toString(16))), + (t.e = hextob64u(e.e.toString(16))), + (t.d = hextob64u(e.d.toString(16))), + (t.p = hextob64u(e.p.toString(16))), + (t.q = hextob64u(e.q.toString(16))), + (t.dp = hextob64u(e.dmp1.toString(16))), + (t.dq = hextob64u(e.dmq1.toString(16))), + (t.qi = hextob64u(e.coeff.toString(16))), + t + ); + if (e instanceof RSAKey && e.isPublic) + return ( + (t.kty = 'RSA'), + (t.n = hextob64u(e.n.toString(16))), + (t.e = hextob64u(e.e.toString(16))), + t + ); + if (e instanceof K.crypto.ECDSA && e.isPrivate) { + if ('P-256' !== (n = e.getShortNISTPCurveName()) && 'P-384' !== n) + throw 'unsupported curve name for JWT: ' + n; + var r = e.getPublicKeyXYHex(); + return ( + (t.kty = 'EC'), + (t.crv = n), + (t.x = hextob64u(r.x)), + (t.y = hextob64u(r.y)), + (t.d = hextob64u(e.prvKeyHex)), + t + ); + } + if (e instanceof K.crypto.ECDSA && e.isPublic) { + var n; + if ('P-256' !== (n = e.getShortNISTPCurveName()) && 'P-384' !== n) + throw 'unsupported curve name for JWT: ' + n; + r = e.getPublicKeyXYHex(); + return (t.kty = 'EC'), (t.crv = n), (t.x = hextob64u(r.x)), (t.y = hextob64u(r.y)), t; + } + throw 'not supported key object'; + }), + (RSAKey.getPosArrayOfChildrenFromHex = function (e) { + return J.getChildIdx(e, 0); + }), + (RSAKey.getHexValueArrayOfChildrenFromHex = function (e) { + var t, + r = J.getV, + n = r(e, (t = RSAKey.getPosArrayOfChildrenFromHex(e))[0]), + i = r(e, t[1]), + o = r(e, t[2]), + s = r(e, t[3]), + a = r(e, t[4]), + u = r(e, t[5]), + c = r(e, t[6]), + h = r(e, t[7]), + f = r(e, t[8]); + return (t = new Array()).push(n, i, o, s, a, u, c, h, f), t; + }), + (RSAKey.prototype.readPrivateKeyFromPEMString = function (e) { + var t = pemtohex(e), + r = RSAKey.getHexValueArrayOfChildrenFromHex(t); + this.setPrivateEx(r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8]); + }), + (RSAKey.prototype.readPKCS5PrvKeyHex = function (e) { + var t = RSAKey.getHexValueArrayOfChildrenFromHex(e); + this.setPrivateEx(t[1], t[2], t[3], t[4], t[5], t[6], t[7], t[8]); + }), + (RSAKey.prototype.readPKCS8PrvKeyHex = function (e) { + var t, + r, + n, + i, + o, + s, + a, + u, + c = J, + h = c.getVbyList; + if (!1 === c.isASN1HEX(e)) throw 'not ASN.1 hex string'; + try { + (t = h(e, 0, [2, 0, 1], '02')), + (r = h(e, 0, [2, 0, 2], '02')), + (n = h(e, 0, [2, 0, 3], '02')), + (i = h(e, 0, [2, 0, 4], '02')), + (o = h(e, 0, [2, 0, 5], '02')), + (s = h(e, 0, [2, 0, 6], '02')), + (a = h(e, 0, [2, 0, 7], '02')), + (u = h(e, 0, [2, 0, 8], '02')); + } catch (e) { + throw 'malformed PKCS#8 plain RSA private key'; + } + this.setPrivateEx(t, r, n, i, o, s, a, u); + }), + (RSAKey.prototype.readPKCS5PubKeyHex = function (e) { + var t = J, + r = t.getV; + if (!1 === t.isASN1HEX(e)) throw 'keyHex is not ASN.1 hex string'; + var n = t.getChildIdx(e, 0); + if (2 !== n.length || '02' !== e.substr(n[0], 2) || '02' !== e.substr(n[1], 2)) + throw 'wrong hex for PKCS#5 public key'; + var i = r(e, n[0]), + o = r(e, n[1]); + this.setPublic(i, o); + }), + (RSAKey.prototype.readPKCS8PubKeyHex = function (e) { + var t = J; + if (!1 === t.isASN1HEX(e)) throw 'not ASN.1 hex string'; + if ('06092a864886f70d010101' !== t.getTLVbyList(e, 0, [0, 0])) + throw 'not PKCS8 RSA public key'; + var r = t.getTLVbyList(e, 0, [1, 0]); + this.readPKCS5PubKeyHex(r); + }), + (RSAKey.prototype.readCertPubKeyHex = function (e, t) { + var r, n; + (r = new X509()).readCertHex(e), (n = r.getPublicKeyHex()), this.readPKCS8PubKeyHex(n); + }); + var Y = new RegExp(''); + function _zeroPaddingOfSignature(e, t) { + for (var r = '', n = t / 4 - e.length, i = 0; i < n; i++) r += '0'; + return r + e; + } + function pss_mgf1_str(e, t, r) { + for (var n = '', i = 0; n.length < t; ) + (n += hextorstr( + r( + rstrtohex( + e + + String.fromCharCode.apply(String, [ + (4278190080 & i) >> 24, + (16711680 & i) >> 16, + (65280 & i) >> 8, + 255 & i, + ]) + ) + ) + )), + (i += 1); + return n; + } + function _rsasign_getAlgNameAndHashFromHexDisgestInfo(e) { + for (var t in K.crypto.Util.DIGESTINFOHEAD) { + var r = K.crypto.Util.DIGESTINFOHEAD[t], + n = r.length; + if (e.substring(0, n) == r) return [t, e.substring(n)]; + } + return []; + } + function X509() { + var e = J, + t = e.getChildIdx, + r = e.getV, + n = e.getTLV, + i = e.getVbyList, + o = e.getTLVbyList, + s = e.getIdxbyList, + a = e.getVidx, + u = e.oidname, + c = X509, + h = pemtohex; + (this.hex = null), + (this.version = 0), + (this.foffset = 0), + (this.aExtInfo = null), + (this.getVersion = function () { + return null === this.hex || 0 !== this.version + ? this.version + : 'a003020102' !== o(this.hex, 0, [0, 0]) + ? ((this.version = 1), (this.foffset = -1), 1) + : ((this.version = 3), 3); + }), + (this.getSerialNumberHex = function () { + return i(this.hex, 0, [0, 1 + this.foffset], '02'); + }), + (this.getSignatureAlgorithmField = function () { + return u(i(this.hex, 0, [0, 2 + this.foffset, 0], '06')); + }), + (this.getIssuerHex = function () { + return o(this.hex, 0, [0, 3 + this.foffset], '30'); + }), + (this.getIssuerString = function () { + return c.hex2dn(this.getIssuerHex()); + }), + (this.getSubjectHex = function () { + return o(this.hex, 0, [0, 5 + this.foffset], '30'); + }), + (this.getSubjectString = function () { + return c.hex2dn(this.getSubjectHex()); + }), + (this.getNotBefore = function () { + var e = i(this.hex, 0, [0, 4 + this.foffset, 0]); + return (e = e.replace(/(..)/g, '%$1')), (e = decodeURIComponent(e)); + }), + (this.getNotAfter = function () { + var e = i(this.hex, 0, [0, 4 + this.foffset, 1]); + return (e = e.replace(/(..)/g, '%$1')), (e = decodeURIComponent(e)); + }), + (this.getPublicKeyHex = function () { + return e.getTLVbyList(this.hex, 0, [0, 6 + this.foffset], '30'); + }), + (this.getPublicKeyIdx = function () { + return s(this.hex, 0, [0, 6 + this.foffset], '30'); + }), + (this.getPublicKeyContentIdx = function () { + var e = this.getPublicKeyIdx(); + return s(this.hex, e, [1, 0], '30'); + }), + (this.getPublicKey = function () { + return z.getKey(this.getPublicKeyHex(), null, 'pkcs8pub'); + }), + (this.getSignatureAlgorithmName = function () { + return u(i(this.hex, 0, [1, 0], '06')); + }), + (this.getSignatureValueHex = function () { + return i(this.hex, 0, [2], '03', !0); + }), + (this.verifySignature = function (e) { + var t = this.getSignatureAlgorithmName(), + r = this.getSignatureValueHex(), + n = o(this.hex, 0, [0], '30'), + i = new K.crypto.Signature({ alg: t }); + return i.init(e), i.updateHex(n), i.verify(r); + }), + (this.parseExt = function () { + if (3 !== this.version) return -1; + var r = s(this.hex, 0, [0, 7, 0], '30'), + n = t(this.hex, r); + this.aExtInfo = new Array(); + for (var o = 0; o < n.length; o++) { + var u = { critical: !1 }, + c = 0; + 3 === t(this.hex, n[o]).length && ((u.critical = !0), (c = 1)), + (u.oid = e.hextooidstr(i(this.hex, n[o], [0], '06'))); + var h = s(this.hex, n[o], [1 + c]); + (u.vidx = a(this.hex, h)), this.aExtInfo.push(u); + } + }), + (this.getExtInfo = function (e) { + var t = this.aExtInfo, + r = e; + if ((e.match(/^[0-9.]+$/) || (r = K.asn1.x509.OID.name2oid(e)), '' !== r)) + for (var n = 0; n < t.length; n++) if (t[n].oid === r) return t[n]; + }), + (this.getExtBasicConstraints = function () { + var e = this.getExtInfo('basicConstraints'); + if (void 0 === e) return e; + var t = r(this.hex, e.vidx); + if ('' === t) return {}; + if ('0101ff' === t) return { cA: !0 }; + if ('0101ff02' === t.substr(0, 8)) { + var n = r(t, 6); + return { cA: !0, pathLen: parseInt(n, 16) }; + } + throw 'basicConstraints parse error'; + }), + (this.getExtKeyUsageBin = function () { + var e = this.getExtInfo('keyUsage'); + if (void 0 === e) return ''; + var t = r(this.hex, e.vidx); + if (t.length % 2 != 0 || t.length <= 2) throw 'malformed key usage value'; + var n = parseInt(t.substr(0, 2)), + i = parseInt(t.substr(2), 16).toString(2); + return i.substr(0, i.length - n); + }), + (this.getExtKeyUsageString = function () { + for (var e = this.getExtKeyUsageBin(), t = new Array(), r = 0; r < e.length; r++) + '1' == e.substr(r, 1) && t.push(X509.KEYUSAGE_NAME[r]); + return t.join(','); + }), + (this.getExtSubjectKeyIdentifier = function () { + var e = this.getExtInfo('subjectKeyIdentifier'); + return void 0 === e ? e : r(this.hex, e.vidx); + }), + (this.getExtAuthorityKeyIdentifier = function () { + var e = this.getExtInfo('authorityKeyIdentifier'); + if (void 0 === e) return e; + for (var i = {}, o = n(this.hex, e.vidx), s = t(o, 0), a = 0; a < s.length; a++) + '80' === o.substr(s[a], 2) && (i.kid = r(o, s[a])); + return i; + }), + (this.getExtExtKeyUsageName = function () { + var e = this.getExtInfo('extKeyUsage'); + if (void 0 === e) return e; + var i = new Array(), + o = n(this.hex, e.vidx); + if ('' === o) return i; + for (var s = t(o, 0), a = 0; a < s.length; a++) i.push(u(r(o, s[a]))); + return i; + }), + (this.getExtSubjectAltName = function () { + for (var e = this.getExtSubjectAltName2(), t = new Array(), r = 0; r < e.length; r++) + 'DNS' === e[r][0] && t.push(e[r][1]); + return t; + }), + (this.getExtSubjectAltName2 = function () { + var e, + i, + o, + s = this.getExtInfo('subjectAltName'); + if (void 0 === s) return s; + for ( + var a = new Array(), u = n(this.hex, s.vidx), c = t(u, 0), h = 0; + h < c.length; + h++ + ) + (o = u.substr(c[h], 2)), + (e = r(u, c[h])), + '81' === o && ((i = hextoutf8(e)), a.push(['MAIL', i])), + '82' === o && ((i = hextoutf8(e)), a.push(['DNS', i])), + '84' === o && ((i = X509.hex2dn(e, 0)), a.push(['DN', i])), + '86' === o && ((i = hextoutf8(e)), a.push(['URI', i])), + '87' === o && ((i = hextoip(e)), a.push(['IP', i])); + return a; + }), + (this.getExtCRLDistributionPointsURI = function () { + var e = this.getExtInfo('cRLDistributionPoints'); + if (void 0 === e) return e; + for (var r = new Array(), n = t(this.hex, e.vidx), o = 0; o < n.length; o++) + try { + var s = hextoutf8(i(this.hex, n[o], [0, 0, 0], '86')); + r.push(s); + } catch (e) {} + return r; + }), + (this.getExtAIAInfo = function () { + var e = this.getExtInfo('authorityInfoAccess'); + if (void 0 === e) return e; + for ( + var r = { ocsp: [], caissuer: [] }, n = t(this.hex, e.vidx), o = 0; + o < n.length; + o++ + ) { + var s = i(this.hex, n[o], [0], '06'), + a = i(this.hex, n[o], [1], '86'); + '2b06010505073001' === s && r.ocsp.push(hextoutf8(a)), + '2b06010505073002' === s && r.caissuer.push(hextoutf8(a)); + } + return r; + }), + (this.getExtCertificatePolicies = function () { + var e = this.getExtInfo('certificatePolicies'); + if (void 0 === e) return e; + for (var o = n(this.hex, e.vidx), s = [], a = t(o, 0), c = 0; c < a.length; c++) { + var h = {}, + f = t(o, a[c]); + if (((h.id = u(r(o, f[0]))), 2 === f.length)) + for (var l = t(o, f[1]), g = 0; g < l.length; g++) { + var p = i(o, l[g], [0], '06'); + '2b06010505070201' === p + ? (h.cps = hextoutf8(i(o, l[g], [1]))) + : '2b06010505070202' === p && (h.unotice = hextoutf8(i(o, l[g], [1, 0]))); + } + s.push(h); + } + return s; + }), + (this.readCertPEM = function (e) { + this.readCertHex(h(e)); + }), + (this.readCertHex = function (e) { + (this.hex = e), this.getVersion(); + try { + s(this.hex, 0, [0, 7], 'a3'), this.parseExt(); + } catch (e) {} + }), + (this.getInfo = function () { + var e, t, r; + if ( + ((e = 'Basic Fields\n'), + (e += ' serial number: ' + this.getSerialNumberHex() + '\n'), + (e += ' signature algorithm: ' + this.getSignatureAlgorithmField() + '\n'), + (e += ' issuer: ' + this.getIssuerString() + '\n'), + (e += ' notBefore: ' + this.getNotBefore() + '\n'), + (e += ' notAfter: ' + this.getNotAfter() + '\n'), + (e += ' subject: ' + this.getSubjectString() + '\n'), + (e += ' subject public key info: \n'), + (e += ' key algorithm: ' + (t = this.getPublicKey()).type + '\n'), + 'RSA' === t.type && + ((e += ' n=' + hextoposhex(t.n.toString(16)).substr(0, 16) + '...\n'), + (e += ' e=' + hextoposhex(t.e.toString(16)) + '\n')), + void 0 !== (r = this.aExtInfo) && null !== r) + ) { + e += 'X509v3 Extensions:\n'; + for (var n = 0; n < r.length; n++) { + var i = r[n], + o = K.asn1.x509.OID.oid2name(i.oid); + '' === o && (o = i.oid); + var s = ''; + if ( + (!0 === i.critical && (s = 'CRITICAL'), + (e += ' ' + o + ' ' + s + ':\n'), + 'basicConstraints' === o) + ) { + var a = this.getExtBasicConstraints(); + void 0 === a.cA + ? (e += ' {}\n') + : ((e += ' cA=true'), + void 0 !== a.pathLen && (e += ', pathLen=' + a.pathLen), + (e += '\n')); + } else if ('keyUsage' === o) e += ' ' + this.getExtKeyUsageString() + '\n'; + else if ('subjectKeyIdentifier' === o) + e += ' ' + this.getExtSubjectKeyIdentifier() + '\n'; + else if ('authorityKeyIdentifier' === o) { + var u = this.getExtAuthorityKeyIdentifier(); + void 0 !== u.kid && (e += ' kid=' + u.kid + '\n'); + } else { + if ('extKeyUsage' === o) + e += ' ' + this.getExtExtKeyUsageName().join(', ') + '\n'; + else if ('subjectAltName' === o) + e += ' ' + this.getExtSubjectAltName2() + '\n'; + else if ('cRLDistributionPoints' === o) + e += ' ' + this.getExtCRLDistributionPointsURI() + '\n'; + else if ('authorityInfoAccess' === o) { + var c = this.getExtAIAInfo(); + void 0 !== c.ocsp && (e += ' ocsp: ' + c.ocsp.join(',') + '\n'), + void 0 !== c.caissuer && + (e += ' caissuer: ' + c.caissuer.join(',') + '\n'); + } else if ('certificatePolicies' === o) + for (var h = this.getExtCertificatePolicies(), f = 0; f < h.length; f++) + void 0 !== h[f].id && (e += ' policy oid: ' + h[f].id + '\n'), + void 0 !== h[f].cps && (e += ' cps: ' + h[f].cps + '\n'); + } + } + } + return ( + (e += 'signature algorithm: ' + this.getSignatureAlgorithmName() + '\n'), + (e += 'signature: ' + this.getSignatureValueHex().substr(0, 16) + '...\n') + ); + }); + } + Y.compile('[^0-9a-f]', 'gi'), + (RSAKey.prototype.sign = function (e, t) { + var r = (function b(e) { + return K.crypto.Util.hashString(e, t); + })(e); + return this.signWithMessageHash(r, t); + }), + (RSAKey.prototype.signWithMessageHash = function (e, t) { + var r = parseBigInt(K.crypto.Util.getPaddedDigestInfoHex(e, t, this.n.bitLength()), 16); + return _zeroPaddingOfSignature(this.doPrivate(r).toString(16), this.n.bitLength()); + }), + (RSAKey.prototype.signPSS = function (e, t, r) { + var n = (function c(e) { + return K.crypto.Util.hashHex(e, t); + })(rstrtohex(e)); + return void 0 === r && (r = -1), this.signWithMessageHashPSS(n, t, r); + }), + (RSAKey.prototype.signWithMessageHashPSS = function (e, t, r) { + var n, + i = hextorstr(e), + s = i.length, + a = this.n.bitLength() - 1, + u = Math.ceil(a / 8), + c = function o(e) { + return K.crypto.Util.hashHex(e, t); + }; + if (-1 === r || void 0 === r) r = s; + else if (-2 === r) r = u - s - 2; + else if (r < -2) throw 'invalid salt length'; + if (u < s + r + 2) throw 'data too long'; + var h = ''; + r > 0 && + ((h = new Array(r)), + new SecureRandom().nextBytes(h), + (h = String.fromCharCode.apply(String, h))); + var f = hextorstr(c(rstrtohex('\0\0\0\0\0\0\0\0' + i + h))), + l = []; + for (n = 0; n < u - r - s - 2; n += 1) l[n] = 0; + var g = String.fromCharCode.apply(String, l) + '' + h, + p = pss_mgf1_str(f, g.length, c), + d = []; + for (n = 0; n < g.length; n += 1) d[n] = g.charCodeAt(n) ^ p.charCodeAt(n); + var v = (65280 >> (8 * u - a)) & 255; + for (d[0] &= ~v, n = 0; n < s; n++) d.push(f.charCodeAt(n)); + return ( + d.push(188), + _zeroPaddingOfSignature( + this.doPrivate(new BigInteger(d)).toString(16), + this.n.bitLength() + ) + ); + }), + (RSAKey.prototype.verify = function (e, t) { + var r = parseBigInt((t = (t = t.replace(Y, '')).replace(/[ \n]+/g, '')), 16); + if (r.bitLength() > this.n.bitLength()) return 0; + var n = _rsasign_getAlgNameAndHashFromHexDisgestInfo( + this.doPublic(r) + .toString(16) + .replace(/^1f+00/, '') + ); + if (0 == n.length) return !1; + var i = n[0]; + return ( + n[1] == + (function a(e) { + return K.crypto.Util.hashString(e, i); + })(e) + ); + }), + (RSAKey.prototype.verifyWithMessageHash = function (e, t) { + var r = parseBigInt((t = (t = t.replace(Y, '')).replace(/[ \n]+/g, '')), 16); + if (r.bitLength() > this.n.bitLength()) return 0; + var n = _rsasign_getAlgNameAndHashFromHexDisgestInfo( + this.doPublic(r) + .toString(16) + .replace(/^1f+00/, '') + ); + if (0 == n.length) return !1; + n[0]; + return n[1] == e; + }), + (RSAKey.prototype.verifyPSS = function (t, r, n, i) { + var o = (function e(t) { + return K.crypto.Util.hashHex(t, n); + })(rstrtohex(t)); + return void 0 === i && (i = -1), this.verifyWithMessageHashPSS(o, r, n, i); + }), + (RSAKey.prototype.verifyWithMessageHashPSS = function (e, t, n, i) { + var o = new BigInteger(t, 16); + if (o.bitLength() > this.n.bitLength()) return !1; + var s, + a = function r(e) { + return K.crypto.Util.hashHex(e, n); + }, + u = hextorstr(e), + c = u.length, + h = this.n.bitLength() - 1, + f = Math.ceil(h / 8); + if (-1 === i || void 0 === i) i = c; + else if (-2 === i) i = f - c - 2; + else if (i < -2) throw 'invalid salt length'; + if (f < c + i + 2) throw 'data too long'; + var l = this.doPublic(o).toByteArray(); + for (s = 0; s < l.length; s += 1) l[s] &= 255; + for (; l.length < f; ) l.unshift(0); + if (188 !== l[f - 1]) throw 'encoded message does not end in 0xbc'; + var g = (l = String.fromCharCode.apply(String, l)).substr(0, f - c - 1), + p = l.substr(g.length, c), + d = (65280 >> (8 * f - h)) & 255; + if (0 != (g.charCodeAt(0) & d)) throw 'bits beyond keysize not zero'; + var v = pss_mgf1_str(p, g.length, a), + y = []; + for (s = 0; s < g.length; s += 1) y[s] = g.charCodeAt(s) ^ v.charCodeAt(s); + y[0] &= ~d; + var m = f - c - i - 2; + for (s = 0; s < m; s += 1) if (0 !== y[s]) throw 'leftmost octets not zero'; + if (1 !== y[m]) throw '0x01 marker not found'; + return ( + p === + hextorstr( + a( + rstrtohex('\0\0\0\0\0\0\0\0' + u + String.fromCharCode.apply(String, y.slice(-i))) + ) + ) + ); + }), + (RSAKey.SALT_LEN_HLEN = -1), + (RSAKey.SALT_LEN_MAX = -2), + (RSAKey.SALT_LEN_RECOVER = -2), + (X509.hex2dn = function (e, t) { + if ((void 0 === t && (t = 0), '30' !== e.substr(t, 2))) throw 'malformed DN'; + for (var r = new Array(), n = J.getChildIdx(e, t), i = 0; i < n.length; i++) + r.push(X509.hex2rdn(e, n[i])); + return ( + '/' + + (r = r.map(function (e) { + return e.replace('/', '\\/'); + })).join('/') + ); + }), + (X509.hex2rdn = function (e, t) { + if ((void 0 === t && (t = 0), '31' !== e.substr(t, 2))) throw 'malformed RDN'; + for (var r = new Array(), n = J.getChildIdx(e, t), i = 0; i < n.length; i++) + r.push(X509.hex2attrTypeValue(e, n[i])); + return (r = r.map(function (e) { + return e.replace('+', '\\+'); + })).join('+'); + }), + (X509.hex2attrTypeValue = function (e, t) { + var r = J, + n = r.getV; + if ((void 0 === t && (t = 0), '30' !== e.substr(t, 2))) + throw 'malformed attribute type and value'; + var i = r.getChildIdx(e, t); + 2 !== i.length || e.substr(i[0], 2); + var o = n(e, i[0]), + s = K.asn1.ASN1Util.oidHexToInt(o); + return K.asn1.x509.OID.oid2atype(s) + '=' + hextorstr(n(e, i[1])); + }), + (X509.getPublicKeyFromCertHex = function (e) { + var t = new X509(); + return t.readCertHex(e), t.getPublicKey(); + }), + (X509.getPublicKeyFromCertPEM = function (e) { + var t = new X509(); + return t.readCertPEM(e), t.getPublicKey(); + }), + (X509.getPublicKeyInfoPropOfCertPEM = function (e) { + var t, + r, + n = J.getVbyList, + i = {}; + return ( + (i.algparam = null), + (t = new X509()).readCertPEM(e), + (r = t.getPublicKeyHex()), + (i.keyhex = n(r, 0, [1], '03').substr(2)), + (i.algoid = n(r, 0, [0, 0], '06')), + '2a8648ce3d0201' === i.algoid && (i.algparam = n(r, 0, [0, 1], '06')), + i + ); + }), + (X509.KEYUSAGE_NAME = [ + 'digitalSignature', + 'nonRepudiation', + 'keyEncipherment', + 'dataEncipherment', + 'keyAgreement', + 'keyCertSign', + 'cRLSign', + 'encipherOnly', + 'decipherOnly', + ]), + (void 0 !== K && K) || (K = {}), + (void 0 !== K.jws && K.jws) || (K.jws = {}), + (K.jws.JWS = function () { + var e = K.jws.JWS.isSafeJSONString; + this.parseJWS = function (t, r) { + if (void 0 === this.parsedJWS || (!r && void 0 === this.parsedJWS.sigvalH)) { + var n = t.match(/^([^.]+)\.([^.]+)\.([^.]+)$/); + if (null == n) throw "JWS signature is not a form of 'Head.Payload.SigValue'."; + var i = n[1], + o = n[2], + s = n[3], + a = i + '.' + o; + if ( + ((this.parsedJWS = {}), + (this.parsedJWS.headB64U = i), + (this.parsedJWS.payloadB64U = o), + (this.parsedJWS.sigvalB64U = s), + (this.parsedJWS.si = a), + !r) + ) { + var u = b64utohex(s), + c = parseBigInt(u, 16); + (this.parsedJWS.sigvalH = u), (this.parsedJWS.sigvalBI = c); + } + var h = W(i), + f = W(o); + if ( + ((this.parsedJWS.headS = h), + (this.parsedJWS.payloadS = f), + !e(h, this.parsedJWS, 'headP')) + ) + throw 'malformed JSON string for JWS Head: ' + h; + } + }; + }), + (K.jws.JWS.sign = function (e, t, r, n, o) { + var s, + a, + u, + c = K, + h = c.jws.JWS, + f = h.readSafeJSONString, + l = h.isSafeJSONString, + g = c.crypto, + p = (g.ECDSA, g.Mac), + d = g.Signature, + v = JSON; + if ('string' != typeof t && 'object' != (void 0 === t ? 'undefined' : i(t))) + throw 'spHeader must be JSON string or object: ' + t; + if ( + ('object' == (void 0 === t ? 'undefined' : i(t)) && ((a = t), (s = v.stringify(a))), + 'string' == typeof t) + ) { + if (!l((s = t))) throw 'JWS Head is not safe JSON string: ' + s; + a = f(s); + } + if ( + ((u = r), + 'object' == (void 0 === r ? 'undefined' : i(r)) && (u = v.stringify(r)), + ('' != e && null != e) || void 0 === a.alg || (e = a.alg), + '' != e && null != e && void 0 === a.alg && ((a.alg = e), (s = v.stringify(a))), + e !== a.alg) + ) + throw "alg and sHeader.alg doesn't match: " + e + '!=' + a.alg; + var y = null; + if (void 0 === h.jwsalg2sigalg[e]) throw 'unsupported alg name: ' + e; + y = h.jwsalg2sigalg[e]; + var m = q(s) + '.' + q(u), + S = ''; + if ('Hmac' == y.substr(0, 4)) { + if (void 0 === n) throw 'mac key shall be specified for HS* alg'; + var F = new p({ alg: y, prov: 'cryptojs', pass: n }); + F.updateString(m), (S = F.doFinal()); + } else { + var b; + if (-1 != y.indexOf('withECDSA')) + (b = new d({ alg: y })).init(n, o), + b.updateString(m), + (hASN1Sig = b.sign()), + (S = K.crypto.ECDSA.asn1SigToConcatSig(hASN1Sig)); + else if ('none' != y) + (b = new d({ alg: y })).init(n, o), b.updateString(m), (S = b.sign()); + } + return m + '.' + hextob64u(S); + }), + (K.jws.JWS.verify = function (e, t, r) { + var n, + o = K, + s = o.jws.JWS, + a = s.readSafeJSONString, + u = o.crypto, + c = u.ECDSA, + h = u.Mac, + f = u.Signature; + void 0 !== i(RSAKey) && (n = RSAKey); + var l = e.split('.'); + if (3 !== l.length) return !1; + var g = l[0] + '.' + l[1], + p = b64utohex(l[2]), + d = a(W(l[0])), + v = null, + y = null; + if (void 0 === d.alg) throw 'algorithm not specified in header'; + if ( + ((y = (v = d.alg).substr(0, 2)), + null != r && + '[object Array]' === Object.prototype.toString.call(r) && + r.length > 0) && + -1 == (':' + r.join(':') + ':').indexOf(':' + v + ':') + ) + throw "algorithm '" + v + "' not accepted in the list"; + if ('none' != v && null === t) throw 'key shall be specified to verify.'; + if ( + ('string' == typeof t && -1 != t.indexOf('-----BEGIN ') && (t = z.getKey(t)), + !(('RS' != y && 'PS' != y) || t instanceof n)) + ) + throw 'key shall be a RSAKey obj for RS* and PS* algs'; + if ('ES' == y && !(t instanceof c)) throw 'key shall be a ECDSA obj for ES* algs'; + var m = null; + if (void 0 === s.jwsalg2sigalg[d.alg]) throw 'unsupported alg name: ' + v; + if ('none' == (m = s.jwsalg2sigalg[v])) throw 'not supported'; + if ('Hmac' == m.substr(0, 4)) { + if (void 0 === t) throw 'hexadecimal key shall be specified for HMAC'; + var S = new h({ alg: m, pass: t }); + return S.updateString(g), p == S.doFinal(); + } + if (-1 != m.indexOf('withECDSA')) { + var F, + b = null; + try { + b = c.concatSigToASN1Sig(p); + } catch (e) { + return !1; + } + return (F = new f({ alg: m })).init(t), F.updateString(g), F.verify(b); + } + return (F = new f({ alg: m })).init(t), F.updateString(g), F.verify(p); + }), + (K.jws.JWS.parse = function (e) { + var t, + r, + n, + i = e.split('.'), + o = {}; + if (2 != i.length && 3 != i.length) + throw "malformed sJWS: wrong number of '.' splitted elements"; + return ( + (t = i[0]), + (r = i[1]), + 3 == i.length && (n = i[2]), + (o.headerObj = K.jws.JWS.readSafeJSONString(W(t))), + (o.payloadObj = K.jws.JWS.readSafeJSONString(W(r))), + (o.headerPP = JSON.stringify(o.headerObj, null, ' ')), + null == o.payloadObj + ? (o.payloadPP = W(r)) + : (o.payloadPP = JSON.stringify(o.payloadObj, null, ' ')), + void 0 !== n && (o.sigHex = b64utohex(n)), + o + ); + }), + (K.jws.JWS.verifyJWT = function (e, t, r) { + var n = K.jws, + o = n.JWS, + s = o.readSafeJSONString, + a = o.inArray, + u = o.includedArray, + c = e.split('.'), + h = c[0], + f = c[1], + l = (b64utohex(c[2]), s(W(h))), + g = s(W(f)); + if (void 0 === l.alg) return !1; + if (void 0 === r.alg) throw 'acceptField.alg shall be specified'; + if (!a(l.alg, r.alg)) return !1; + if (void 0 !== g.iss && 'object' === i(r.iss) && !a(g.iss, r.iss)) return !1; + if (void 0 !== g.sub && 'object' === i(r.sub) && !a(g.sub, r.sub)) return !1; + if (void 0 !== g.aud && 'object' === i(r.aud)) + if ('string' == typeof g.aud) { + if (!a(g.aud, r.aud)) return !1; + } else if ('object' == i(g.aud) && !u(g.aud, r.aud)) return !1; + var p = n.IntDate.getNow(); + return ( + void 0 !== r.verifyAt && 'number' == typeof r.verifyAt && (p = r.verifyAt), + (void 0 !== r.gracePeriod && 'number' == typeof r.gracePeriod) || (r.gracePeriod = 0), + !(void 0 !== g.exp && 'number' == typeof g.exp && g.exp + r.gracePeriod < p) && + !(void 0 !== g.nbf && 'number' == typeof g.nbf && p < g.nbf - r.gracePeriod) && + !(void 0 !== g.iat && 'number' == typeof g.iat && p < g.iat - r.gracePeriod) && + (void 0 === g.jti || void 0 === r.jti || g.jti === r.jti) && + !!o.verify(e, t, r.alg) + ); + }), + (K.jws.JWS.includedArray = function (e, t) { + var r = K.jws.JWS.inArray; + if (null === e) return !1; + if ('object' !== (void 0 === e ? 'undefined' : i(e))) return !1; + if ('number' != typeof e.length) return !1; + for (var n = 0; n < e.length; n++) if (!r(e[n], t)) return !1; + return !0; + }), + (K.jws.JWS.inArray = function (e, t) { + if (null === t) return !1; + if ('object' !== (void 0 === t ? 'undefined' : i(t))) return !1; + if ('number' != typeof t.length) return !1; + for (var r = 0; r < t.length; r++) if (t[r] == e) return !0; + return !1; + }), + (K.jws.JWS.jwsalg2sigalg = { + HS256: 'HmacSHA256', + HS384: 'HmacSHA384', + HS512: 'HmacSHA512', + RS256: 'SHA256withRSA', + RS384: 'SHA384withRSA', + RS512: 'SHA512withRSA', + ES256: 'SHA256withECDSA', + ES384: 'SHA384withECDSA', + PS256: 'SHA256withRSAandMGF1', + PS384: 'SHA384withRSAandMGF1', + PS512: 'SHA512withRSAandMGF1', + none: 'none', + }), + (K.jws.JWS.isSafeJSONString = function (e, t, r) { + var n = null; + try { + return 'object' != (void 0 === (n = V(e)) ? 'undefined' : i(n)) + ? 0 + : n.constructor === Array + ? 0 + : (t && (t[r] = n), 1); + } catch (e) { + return 0; + } + }), + (K.jws.JWS.readSafeJSONString = function (e) { + var t = null; + try { + return 'object' != (void 0 === (t = V(e)) ? 'undefined' : i(t)) + ? null + : t.constructor === Array + ? null + : t; + } catch (e) { + return null; + } + }), + (K.jws.JWS.getEncodedSignatureValueFromJWS = function (e) { + var t = e.match(/^[^.]+\.[^.]+\.([^.]+)$/); + if (null == t) throw "JWS signature is not a form of 'Head.Payload.SigValue'."; + return t[1]; + }), + (K.jws.JWS.getJWKthumbprint = function (e) { + if ('RSA' !== e.kty && 'EC' !== e.kty && 'oct' !== e.kty) + throw 'unsupported algorithm for JWK Thumprint'; + var t = '{'; + if ('RSA' === e.kty) { + if ('string' != typeof e.n || 'string' != typeof e.e) + throw 'wrong n and e value for RSA key'; + (t += '"e":"' + e.e + '",'), + (t += '"kty":"' + e.kty + '",'), + (t += '"n":"' + e.n + '"}'); + } else if ('EC' === e.kty) { + if ('string' != typeof e.crv || 'string' != typeof e.x || 'string' != typeof e.y) + throw 'wrong crv, x and y value for EC key'; + (t += '"crv":"' + e.crv + '",'), + (t += '"kty":"' + e.kty + '",'), + (t += '"x":"' + e.x + '",'), + (t += '"y":"' + e.y + '"}'); + } else if ('oct' === e.kty) { + if ('string' != typeof e.k) throw 'wrong k value for oct(symmetric) key'; + (t += '"kty":"' + e.kty + '",'), (t += '"k":"' + e.k + '"}'); + } + var r = rstrtohex(t); + return hextob64u(K.crypto.Util.hashHex(r, 'sha256')); + }), + (K.jws.IntDate = {}), + (K.jws.IntDate.get = function (e) { + var t = K.jws.IntDate, + r = t.getNow, + n = t.getZulu; + if ('now' == e) return r(); + if ('now + 1hour' == e) return r() + 3600; + if ('now + 1day' == e) return r() + 86400; + if ('now + 1month' == e) return r() + 2592e3; + if ('now + 1year' == e) return r() + 31536e3; + if (e.match(/Z$/)) return n(e); + if (e.match(/^[0-9]+$/)) return parseInt(e); + throw 'unsupported format: ' + e; + }), + (K.jws.IntDate.getZulu = function (e) { + return zulutosec(e); + }), + (K.jws.IntDate.getNow = function () { + return ~~(new Date() / 1e3); + }), + (K.jws.IntDate.intDate2UTCString = function (e) { + return new Date(1e3 * e).toUTCString(); + }), + (K.jws.IntDate.intDate2Zulu = function (e) { + var t = new Date(1e3 * e); + return ( + ('0000' + t.getUTCFullYear()).slice(-4) + + ('00' + (t.getUTCMonth() + 1)).slice(-2) + + ('00' + t.getUTCDate()).slice(-2) + + ('00' + t.getUTCHours()).slice(-2) + + ('00' + t.getUTCMinutes()).slice(-2) + + ('00' + t.getUTCSeconds()).slice(-2) + + 'Z' + ); + }), + (t.SecureRandom = SecureRandom), + (t.rng_seed_time = rng_seed_time), + (t.BigInteger = BigInteger), + (t.RSAKey = RSAKey), + (t.ECDSA = K.crypto.ECDSA), + (t.DSA = K.crypto.DSA), + (t.Signature = K.crypto.Signature), + (t.MessageDigest = K.crypto.MessageDigest), + (t.Mac = K.crypto.Mac), + (t.Cipher = K.crypto.Cipher), + (t.KEYUTIL = z), + (t.ASN1HEX = J), + (t.X509 = X509), + (t.CryptoJS = y), + (t.b64tohex = b64tohex), + (t.b64toBA = b64toBA), + (t.stoBA = stoBA), + (t.BAtos = BAtos), + (t.BAtohex = BAtohex), + (t.stohex = stohex), + (t.stob64 = function stob64(e) { + return hex2b64(stohex(e)); + }), + (t.stob64u = function stob64u(e) { + return b64tob64u(hex2b64(stohex(e))); + }), + (t.b64utos = function b64utos(e) { + return BAtos(b64toBA(b64utob64(e))); + }), + (t.b64tob64u = b64tob64u), + (t.b64utob64 = b64utob64), + (t.hex2b64 = hex2b64), + (t.hextob64u = hextob64u), + (t.b64utohex = b64utohex), + (t.utf8tob64u = q), + (t.b64utoutf8 = W), + (t.utf8tob64 = function utf8tob64(e) { + return hex2b64(uricmptohex(encodeURIComponentAll(e))); + }), + (t.b64toutf8 = function b64toutf8(e) { + return decodeURIComponent(hextouricmp(b64tohex(e))); + }), + (t.utf8tohex = utf8tohex), + (t.hextoutf8 = hextoutf8), + (t.hextorstr = hextorstr), + (t.rstrtohex = rstrtohex), + (t.hextob64 = hextob64), + (t.hextob64nl = hextob64nl), + (t.b64nltohex = b64nltohex), + (t.hextopem = hextopem), + (t.pemtohex = pemtohex), + (t.hextoArrayBuffer = function hextoArrayBuffer(e) { + if (e.length % 2 != 0) throw 'input is not even length'; + if (null == e.match(/^[0-9A-Fa-f]+$/)) throw 'input is not hexadecimal'; + for ( + var t = new ArrayBuffer(e.length / 2), r = new DataView(t), n = 0; + n < e.length / 2; + n++ + ) + r.setUint8(n, parseInt(e.substr(2 * n, 2), 16)); + return t; + }), + (t.ArrayBuffertohex = function ArrayBuffertohex(e) { + for (var t = '', r = new DataView(e), n = 0; n < e.byteLength; n++) + t += ('00' + r.getUint8(n).toString(16)).slice(-2); + return t; + }), + (t.zulutomsec = zulutomsec), + (t.zulutosec = zulutosec), + (t.zulutodate = function zulutodate(e) { + return new Date(zulutomsec(e)); + }), + (t.datetozulu = function datetozulu(e, t, r) { + var n, + i = e.getUTCFullYear(); + if (t) { + if (i < 1950 || 2049 < i) throw 'not proper year for UTCTime: ' + i; + n = ('' + i).slice(-2); + } else n = ('000' + i).slice(-4); + if ( + ((n += ('0' + (e.getUTCMonth() + 1)).slice(-2)), + (n += ('0' + e.getUTCDate()).slice(-2)), + (n += ('0' + e.getUTCHours()).slice(-2)), + (n += ('0' + e.getUTCMinutes()).slice(-2)), + (n += ('0' + e.getUTCSeconds()).slice(-2)), + r) + ) { + var o = e.getUTCMilliseconds(); + 0 !== o && (n += '.' + (o = (o = ('00' + o).slice(-3)).replace(/0+$/g, ''))); + } + return (n += 'Z'); + }), + (t.uricmptohex = uricmptohex), + (t.hextouricmp = hextouricmp), + (t.ipv6tohex = ipv6tohex), + (t.hextoipv6 = hextoipv6), + (t.hextoip = hextoip), + (t.iptohex = function iptohex(e) { + var t = 'malformed IP address'; + if (!(e = e.toLowerCase(e)).match(/^[0-9.]+$/)) { + if (e.match(/^[0-9a-f:]+$/) && -1 !== e.indexOf(':')) return ipv6tohex(e); + throw t; + } + var r = e.split('.'); + if (4 !== r.length) throw t; + var n = ''; + try { + for (var i = 0; i < 4; i++) n += ('0' + parseInt(r[i]).toString(16)).slice(-2); + return n; + } catch (e) { + throw t; + } + }), + (t.encodeURIComponentAll = encodeURIComponentAll), + (t.newline_toUnix = function newline_toUnix(e) { + return (e = e.replace(/\r\n/gm, '\n')); + }), + (t.newline_toDos = function newline_toDos(e) { + return (e = (e = e.replace(/\r\n/gm, '\n')).replace(/\n/gm, '\r\n')); + }), + (t.hextoposhex = hextoposhex), + (t.intarystrtohex = function intarystrtohex(e) { + e = (e = (e = e.replace(/^\s*\[\s*/, '')).replace(/\s*\]\s*$/, '')).replace(/\s*/g, ''); + try { + return e + .split(/,/) + .map(function (e, t, r) { + var n = parseInt(e); + if (n < 0 || 255 < n) throw 'integer not in range 0-255'; + return ('00' + n.toString(16)).slice(-2); + }) + .join(''); + } catch (e) { + throw 'malformed integer array string: ' + e; + } + }), + (t.strdiffidx = function strdiffidx(e, t) { + var r = e.length; + e.length > t.length && (r = t.length); + for (var n = 0; n < r; n++) if (e.charCodeAt(n) != t.charCodeAt(n)) return n; + return e.length != t.length ? r : -1; + }), + (t.KJUR = K), + (t.crypto = K.crypto), + (t.asn1 = K.asn1), + (t.jws = K.jws), + (t.lang = K.lang); + }).call(this, r(40).Buffer); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.JoseUtil = void 0); + var n = r(41), + i = r(0); + var o = ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES384', 'ES512']; + t.JoseUtil = (function () { + function JoseUtil() { + !(function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, JoseUtil); + } + return ( + (JoseUtil.parseJwt = function parseJwt(e) { + i.Log.debug('JoseUtil.parseJwt'); + try { + var t = n.jws.JWS.parse(e); + return { header: t.headerObj, payload: t.payloadObj }; + } catch (e) { + i.Log.error(e); + } + }), + (JoseUtil.validateJwt = function validateJwt(e, t, r, o, s, a) { + i.Log.debug('JoseUtil.validateJwt'); + try { + if ('RSA' === t.kty) + if (t.e && t.n) t = n.KEYUTIL.getKey(t); + else { + if (!t.x5c || !t.x5c.length) + return ( + i.Log.error('JoseUtil.validateJwt: RSA key missing key material', t), + Promise.reject(new Error('RSA key missing key material')) + ); + var u = (0, n.b64tohex)(t.x5c[0]); + t = n.X509.getPublicKeyFromCertHex(u); + } + else { + if ('EC' !== t.kty) + return ( + i.Log.error('JoseUtil.validateJwt: Unsupported key type', t && t.kty), + Promise.reject(new Error('Unsupported key type: ' + t && t.kty)) + ); + if (!(t.crv && t.x && t.y)) + return ( + i.Log.error('JoseUtil.validateJwt: EC key missing key material', t), + Promise.reject(new Error('EC key missing key material')) + ); + t = n.KEYUTIL.getKey(t); + } + return JoseUtil._validateJwt(e, t, r, o, s, a); + } catch (e) { + return i.Log.error((e && e.message) || e), Promise.reject('JWT validation failed'); + } + }), + (JoseUtil._validateJwt = function _validateJwt(e, t, r, s, a, u) { + a || (a = 0), u || (u = parseInt(Date.now() / 1e3)); + var c = JoseUtil.parseJwt(e).payload; + if (!c.iss) + return ( + i.Log.error('JoseUtil._validateJwt: issuer was not provided'), + Promise.reject(new Error('issuer was not provided')) + ); + if (c.iss !== r) + return ( + i.Log.error('JoseUtil._validateJwt: Invalid issuer in token', c.iss), + Promise.reject(new Error('Invalid issuer in token: ' + c.iss)) + ); + if (!c.aud) + return ( + i.Log.error('JoseUtil._validateJwt: aud was not provided'), + Promise.reject(new Error('aud was not provided')) + ); + if (!(c.aud === s || (Array.isArray(c.aud) && c.aud.indexOf(s) >= 0))) + return ( + i.Log.error('JoseUtil._validateJwt: Invalid audience in token', c.aud), + Promise.reject(new Error('Invalid audience in token: ' + c.aud)) + ); + var h = u + a, + f = u - a; + if (!c.iat) + return ( + i.Log.error('JoseUtil._validateJwt: iat was not provided'), + Promise.reject(new Error('iat was not provided')) + ); + if (h < c.iat) + return ( + i.Log.error('JoseUtil._validateJwt: iat is in the future', c.iat), + Promise.reject(new Error('iat is in the future: ' + c.iat)) + ); + if (c.nbf && h < c.nbf) + return ( + i.Log.error('JoseUtil._validateJwt: nbf is in the future', c.nbf), + Promise.reject(new Error('nbf is in the future: ' + c.nbf)) + ); + if (!c.exp) + return ( + i.Log.error('JoseUtil._validateJwt: exp was not provided'), + Promise.reject(new Error('exp was not provided')) + ); + if (c.exp < f) + return ( + i.Log.error('JoseUtil._validateJwt: exp is in the past', c.exp), + Promise.reject(new Error('exp is in the past:' + c.exp)) + ); + try { + if (!n.jws.JWS.verify(e, t, o)) + return ( + i.Log.error('JoseUtil._validateJwt: signature validation failed'), + Promise.reject(new Error('signature validation failed')) + ); + } catch (e) { + return ( + i.Log.error((e && e.message) || e), + Promise.reject(new Error('signature validation failed')) + ); + } + return Promise.resolve(); + }), + (JoseUtil.hashString = function hashString(e, t) { + try { + return n.crypto.Util.hashString(e, t); + } catch (e) { + i.Log.error(e); + } + }), + (JoseUtil.hexToBase64Url = function hexToBase64Url(e) { + try { + return (0, n.hextob64u)(e); + } catch (e) { + i.Log.error(e); + } + }), + JoseUtil + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.UserInfoService = void 0); + var n = r(17), + i = r(3), + o = r(0); + t.UserInfoService = (function () { + function UserInfoService(e) { + var t = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : n.JsonService, + r = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : i.MetadataService; + if ( + ((function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, UserInfoService), + !e) + ) + throw (o.Log.error('UserInfoService.ctor: No settings passed'), new Error('settings')); + (this._settings = e), + (this._jsonService = new t()), + (this._metadataService = new r(this._settings)); + } + return ( + (UserInfoService.prototype.getClaims = function getClaims(e) { + var t = this; + return e + ? this._metadataService.getUserInfoEndpoint().then(function (r) { + return ( + o.Log.debug('UserInfoService.getClaims: received userinfo url', r), + t._jsonService.getJson(r, e).then(function (e) { + return o.Log.debug('UserInfoService.getClaims: claims received', e), e; + }) + ); + }) + : (o.Log.error('UserInfoService.getClaims: No token passed'), + Promise.reject(new Error('A token is required'))); + }), + UserInfoService + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }), (t.ResponseValidator = void 0); + var n = r(0), + i = r(3), + o = r(43), + s = r(16), + a = r(42); + var u = ['nonce', 'at_hash', 'iat', 'nbf', 'exp', 'aud', 'iss', 'c_hash']; + t.ResponseValidator = (function () { + function ResponseValidator(e) { + var t = + arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : i.MetadataService, + r = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : o.UserInfoService, + s = arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : a.JoseUtil; + if ( + ((function _classCallCheck(e, t) { + if (!(e instanceof t)) throw new TypeError('Cannot call a class as a function'); + })(this, ResponseValidator), + !e) + ) + throw ( + (n.Log.error('ResponseValidator.ctor: No settings passed to ResponseValidator'), + new Error('settings')) + ); + (this._settings = e), + (this._metadataService = new t(this._settings)), + (this._userInfoService = new r(this._settings)), + (this._joseUtil = s); + } + return ( + (ResponseValidator.prototype.validateSigninResponse = function validateSigninResponse( + e, + t + ) { + var r = this; + return ( + n.Log.debug('ResponseValidator.validateSigninResponse'), + this._processSigninParams(e, t).then(function (t) { + return ( + n.Log.debug('ResponseValidator.validateSigninResponse: state processed'), + r._validateTokens(e, t).then(function (e) { + return ( + n.Log.debug('ResponseValidator.validateSigninResponse: tokens validated'), + r._processClaims(e).then(function (e) { + return ( + n.Log.debug('ResponseValidator.validateSigninResponse: claims processed'), + e + ); + }) + ); + }) + ); + }) + ); + }), + (ResponseValidator.prototype.validateSignoutResponse = function validateSignoutResponse( + e, + t + ) { + return e.id !== t.state + ? (n.Log.error('ResponseValidator.validateSignoutResponse: State does not match'), + Promise.reject(new Error('State does not match'))) + : (n.Log.debug('ResponseValidator.validateSignoutResponse: state validated'), + (t.state = e.data), + t.error + ? (n.Log.warn( + 'ResponseValidator.validateSignoutResponse: Response was error', + t.error + ), + Promise.reject(new s.ErrorResponse(t))) + : Promise.resolve(t)); + }), + (ResponseValidator.prototype._processSigninParams = function _processSigninParams(e, t) { + if (e.id !== t.state) + return ( + n.Log.error('ResponseValidator._processSigninParams: State does not match'), + Promise.reject(new Error('State does not match')) + ); + if (!e.client_id) + return ( + n.Log.error('ResponseValidator._processSigninParams: No client_id on state'), + Promise.reject(new Error('No client_id on state')) + ); + if (!e.authority) + return ( + n.Log.error('ResponseValidator._processSigninParams: No authority on state'), + Promise.reject(new Error('No authority on state')) + ); + if (this._settings.authority) { + if (this._settings.authority && this._settings.authority !== e.authority) + return ( + n.Log.error( + 'ResponseValidator._processSigninParams: authority mismatch on settings vs. signin state' + ), + Promise.reject(new Error('authority mismatch on settings vs. signin state')) + ); + } else this._settings.authority = e.authority; + if (this._settings.client_id) { + if (this._settings.client_id && this._settings.client_id !== e.client_id) + return ( + n.Log.error( + 'ResponseValidator._processSigninParams: client_id mismatch on settings vs. signin state' + ), + Promise.reject(new Error('client_id mismatch on settings vs. signin state')) + ); + } else this._settings.client_id = e.client_id; + return ( + n.Log.debug('ResponseValidator._processSigninParams: state validated'), + (t.state = e.data), + t.error + ? (n.Log.warn( + 'ResponseValidator._processSigninParams: Response was error', + t.error + ), + Promise.reject(new s.ErrorResponse(t))) + : e.nonce && !t.id_token + ? (n.Log.error( + 'ResponseValidator._processSigninParams: Expecting id_token in response' + ), + Promise.reject(new Error('No id_token in response'))) + : !e.nonce && t.id_token + ? (n.Log.error( + 'ResponseValidator._processSigninParams: Not expecting id_token in response' + ), + Promise.reject(new Error('Unexpected id_token in response'))) + : Promise.resolve(t) + ); + }), + (ResponseValidator.prototype._processClaims = function _processClaims(e) { + var t = this; + if (e.isOpenIdConnect) { + if ( + (n.Log.debug( + 'ResponseValidator._processClaims: response is OIDC, processing claims' + ), + (e.profile = this._filterProtocolClaims(e.profile)), + this._settings.loadUserInfo && e.access_token) + ) + return ( + n.Log.debug('ResponseValidator._processClaims: loading user info'), + this._userInfoService.getClaims(e.access_token).then(function (r) { + return ( + n.Log.debug( + 'ResponseValidator._processClaims: user info claims received from user info endpoint' + ), + r.sub !== e.profile.sub + ? (n.Log.error( + 'ResponseValidator._processClaims: sub from user info endpoint does not match sub in access_token' + ), + Promise.reject( + new Error( + 'sub from user info endpoint does not match sub in access_token' + ) + )) + : ((e.profile = t._mergeClaims(e.profile, r)), + n.Log.debug( + 'ResponseValidator._processClaims: user info claims received, updated profile:', + e.profile + ), + e) + ); + }) + ); + n.Log.debug('ResponseValidator._processClaims: not loading user info'); + } else + n.Log.debug( + 'ResponseValidator._processClaims: response is not OIDC, not processing claims' + ); + return Promise.resolve(e); + }), + (ResponseValidator.prototype._mergeClaims = function _mergeClaims(e, t) { + var r = Object.assign({}, e); + for (var n in t) { + var i = t[n]; + Array.isArray(i) || (i = [i]); + for (var o = 0; o < i.length; o++) { + var s = i[o]; + r[n] + ? Array.isArray(r[n]) + ? r[n].indexOf(s) < 0 && r[n].push(s) + : r[n] !== s && (r[n] = [r[n], s]) + : (r[n] = s); + } + } + return r; + }), + (ResponseValidator.prototype._filterProtocolClaims = function _filterProtocolClaims(e) { + n.Log.debug('ResponseValidator._filterProtocolClaims, incoming claims:', e); + var t = Object.assign({}, e); + return ( + this._settings._filterProtocolClaims + ? (u.forEach(function (e) { + delete t[e]; + }), + n.Log.debug( + 'ResponseValidator._filterProtocolClaims: protocol claims filtered', + t + )) + : n.Log.debug( + 'ResponseValidator._filterProtocolClaims: protocol claims not filtered' + ), + t + ); + }), + (ResponseValidator.prototype._validateTokens = function _validateTokens(e, t) { + return t.id_token + ? t.access_token + ? (n.Log.debug( + 'ResponseValidator._validateTokens: Validating id_token and access_token' + ), + this._validateIdTokenAndAccessToken(e, t)) + : (n.Log.debug('ResponseValidator._validateTokens: Validating id_token'), + this._validateIdToken(e, t)) + : (n.Log.debug('ResponseValidator._validateTokens: No id_token to validate'), + Promise.resolve(t)); + }), + (ResponseValidator.prototype._validateIdTokenAndAccessToken = + function _validateIdTokenAndAccessToken(e, t) { + var r = this; + return this._validateIdToken(e, t).then(function (e) { + return r._validateAccessToken(e); + }); + }), + (ResponseValidator.prototype._validateIdToken = function _validateIdToken(e, t) { + var r = this; + if (!e.nonce) + return ( + n.Log.error('ResponseValidator._validateIdToken: No nonce on state'), + Promise.reject(new Error('No nonce on state')) + ); + var i = this._joseUtil.parseJwt(t.id_token); + if (!i || !i.header || !i.payload) + return ( + n.Log.error('ResponseValidator._validateIdToken: Failed to parse id_token', i), + Promise.reject(new Error('Failed to parse id_token')) + ); + if (e.nonce !== i.payload.nonce) + return ( + n.Log.error('ResponseValidator._validateIdToken: Invalid nonce in id_token'), + Promise.reject(new Error('Invalid nonce in id_token')) + ); + var o = i.header.kid; + return this._metadataService.getIssuer().then(function (s) { + return ( + n.Log.debug('ResponseValidator._validateIdToken: Received issuer'), + r._metadataService.getSigningKeys().then(function (a) { + if (!a) + return ( + n.Log.error( + 'ResponseValidator._validateIdToken: No signing keys from metadata' + ), + Promise.reject(new Error('No signing keys from metadata')) + ); + n.Log.debug('ResponseValidator._validateIdToken: Received signing keys'); + var u = void 0; + if (o) + u = a.filter(function (e) { + return e.kid === o; + })[0]; + else { + if ((a = r._filterByAlg(a, i.header.alg)).length > 1) + return ( + n.Log.error( + 'ResponseValidator._validateIdToken: No kid found in id_token and more than one key found in metadata' + ), + Promise.reject( + new Error( + 'No kid found in id_token and more than one key found in metadata' + ) + ) + ); + u = a[0]; + } + if (!u) + return ( + n.Log.error( + 'ResponseValidator._validateIdToken: No key matching kid or alg found in signing keys' + ), + Promise.reject(new Error('No key matching kid or alg found in signing keys')) + ); + var c = e.client_id, + h = r._settings.clockSkew; + return ( + n.Log.debug( + 'ResponseValidator._validateIdToken: Validaing JWT; using clock skew (in seconds) of: ', + h + ), + r._joseUtil.validateJwt(t.id_token, u, s, c, h).then(function () { + return ( + n.Log.debug( + 'ResponseValidator._validateIdToken: JWT validation successful' + ), + i.payload.sub + ? ((t.profile = i.payload), t) + : (n.Log.error( + 'ResponseValidator._validateIdToken: No sub present in id_token' + ), + Promise.reject(new Error('No sub present in id_token'))) + ); + }) + ); + }) + ); + }); + }), + (ResponseValidator.prototype._filterByAlg = function _filterByAlg(e, t) { + var r = null; + if (t.startsWith('RS')) r = 'RSA'; + else if (t.startsWith('PS')) r = 'PS'; + else { + if (!t.startsWith('ES')) + return n.Log.debug('ResponseValidator._filterByAlg: alg not supported: ', t), []; + r = 'EC'; + } + return ( + n.Log.debug('ResponseValidator._filterByAlg: Looking for keys that match kty: ', r), + (e = e.filter(function (e) { + return e.kty === r; + })), + n.Log.debug( + 'ResponseValidator._filterByAlg: Number of keys that match kty: ', + r, + e.length + ), + e + ); + }), + (ResponseValidator.prototype._validateAccessToken = function _validateAccessToken(e) { + if (!e.profile) + return ( + n.Log.error( + 'ResponseValidator._validateAccessToken: No profile loaded from id_token' + ), + Promise.reject(new Error('No profile loaded from id_token')) + ); + if (!e.profile.at_hash) + return ( + n.Log.error('ResponseValidator._validateAccessToken: No at_hash in id_token'), + Promise.reject(new Error('No at_hash in id_token')) + ); + if (!e.id_token) + return ( + n.Log.error('ResponseValidator._validateAccessToken: No id_token'), + Promise.reject(new Error('No id_token')) + ); + var t = this._joseUtil.parseJwt(e.id_token); + if (!t || !t.header) + return ( + n.Log.error('ResponseValidator._validateAccessToken: Failed to parse id_token', t), + Promise.reject(new Error('Failed to parse id_token')) + ); + var r = t.header.alg; + if (!r || 5 !== r.length) + return ( + n.Log.error('ResponseValidator._validateAccessToken: Unsupported alg:', r), + Promise.reject(new Error('Unsupported alg: ' + r)) + ); + var i = r.substr(2, 3); + if (!i) + return ( + n.Log.error('ResponseValidator._validateAccessToken: Unsupported alg:', r, i), + Promise.reject(new Error('Unsupported alg: ' + r)) + ); + if (256 !== (i = parseInt(i)) && 384 !== i && 512 !== i) + return ( + n.Log.error('ResponseValidator._validateAccessToken: Unsupported alg:', r, i), + Promise.reject(new Error('Unsupported alg: ' + r)) + ); + var o = 'sha' + i, + s = this._joseUtil.hashString(e.access_token, o); + if (!s) + return ( + n.Log.error('ResponseValidator._validateAccessToken: access_token hash failed:', o), + Promise.reject(new Error('Failed to validate at_hash')) + ); + var a = s.substr(0, s.length / 2), + u = this._joseUtil.hexToBase64Url(a); + return u !== e.profile.at_hash + ? (n.Log.error( + 'ResponseValidator._validateAccessToken: Failed to validate at_hash', + u, + e.profile.at_hash + ), + Promise.reject(new Error('Failed to validate at_hash'))) + : (n.Log.debug('ResponseValidator._validateAccessToken: success'), + Promise.resolve(e)); + }), + ResponseValidator + ); + })(); + }, + function (e, t, r) { + 'use strict'; + Object.defineProperty(t, '__esModule', { value: !0 }); + var n = r(0), + i = r(18), + o = r(6), + s = r(5), + a = r(31), + u = r(30), + c = r(12), + h = r(3), + f = r(20), + l = r(19), + g = r(9), + p = r(8), + d = r(10), + v = r(1), + y = r(13); + (t.default = { + Log: n.Log, + OidcClient: i.OidcClient, + OidcClientSettings: o.OidcClientSettings, + WebStorageStateStore: s.WebStorageStateStore, + InMemoryWebStorage: a.InMemoryWebStorage, + UserManager: u.UserManager, + AccessTokenEvents: c.AccessTokenEvents, + MetadataService: h.MetadataService, + CordovaPopupNavigator: f.CordovaPopupNavigator, + CordovaIFrameNavigator: l.CordovaIFrameNavigator, + CheckSessionIFrame: g.CheckSessionIFrame, + TokenRevocationClient: p.TokenRevocationClient, + SessionMonitor: d.SessionMonitor, + Global: v.Global, + User: y.User, + }), + (e.exports = t.default); + }, + ]); +}); diff --git a/platform/app/public/polyfill.min.js b/platform/app/public/polyfill.min.js new file mode 100644 index 0000000..0786328 --- /dev/null +++ b/platform/app/public/polyfill.min.js @@ -0,0 +1,184 @@ +!(function (e, n) { + 'object' == typeof exports && 'undefined' != typeof module + ? n() + : 'function' == typeof define && define.amd + ? define(n) + : n(); +})(0, function () { + 'use strict'; + function e(e) { + var n = this.constructor; + return this.then( + function (t) { + return n.resolve(e()).then(function () { + return t; + }); + }, + function (t) { + return n.resolve(e()).then(function () { + return n.reject(t); + }); + } + ); + } + function n() {} + function t(e) { + if (!(this instanceof t)) throw new TypeError('Promises must be constructed via new'); + if ('function' != typeof e) throw new TypeError('not a function'); + (this._state = 0), + (this._handled = !1), + (this._value = undefined), + (this._deferreds = []), + u(e, this); + } + function o(e, n) { + for (; 3 === e._state; ) e = e._value; + 0 !== e._state + ? ((e._handled = !0), + t._immediateFn(function () { + var t = 1 === e._state ? n.onFulfilled : n.onRejected; + if (null !== t) { + var o; + try { + o = t(e._value); + } catch (f) { + return void i(n.promise, f); + } + r(n.promise, o); + } else (1 === e._state ? r : i)(n.promise, e._value); + })) + : e._deferreds.push(n); + } + function r(e, n) { + try { + if (n === e) throw new TypeError('A promise cannot be resolved with itself.'); + if (n && ('object' == typeof n || 'function' == typeof n)) { + var o = n.then; + if (n instanceof t) return (e._state = 3), (e._value = n), void f(e); + if ('function' == typeof o) + return void u( + (function (e, n) { + return function () { + e.apply(n, arguments); + }; + })(o, n), + e + ); + } + (e._state = 1), (e._value = n), f(e); + } catch (r) { + i(e, r); + } + } + function i(e, n) { + (e._state = 2), (e._value = n), f(e); + } + function f(e) { + 2 === e._state && + 0 === e._deferreds.length && + t._immediateFn(function () { + e._handled || t._unhandledRejectionFn(e._value); + }); + for (var n = 0, r = e._deferreds.length; r > n; n++) o(e, e._deferreds[n]); + e._deferreds = null; + } + function u(e, n) { + var t = !1; + try { + e( + function (e) { + t || ((t = !0), r(n, e)); + }, + function (e) { + t || ((t = !0), i(n, e)); + } + ); + } catch (o) { + if (t) return; + (t = !0), i(n, o); + } + } + var c = setTimeout; + (t.prototype['catch'] = function (e) { + return this.then(null, e); + }), + (t.prototype.then = function (e, t) { + var r = new this.constructor(n); + return ( + o( + this, + new (function (e, n, t) { + (this.onFulfilled = 'function' == typeof e ? e : null), + (this.onRejected = 'function' == typeof n ? n : null), + (this.promise = t); + })(e, t, r) + ), + r + ); + }), + (t.prototype['finally'] = e), + (t.all = function (e) { + return new t(function (n, t) { + function o(e, f) { + try { + if (f && ('object' == typeof f || 'function' == typeof f)) { + var u = f.then; + if ('function' == typeof u) + return void u.call( + f, + function (n) { + o(e, n); + }, + t + ); + } + (r[e] = f), 0 == --i && n(r); + } catch (c) { + t(c); + } + } + if (!e || 'undefined' == typeof e.length) + throw new TypeError('Promise.all accepts an array'); + var r = Array.prototype.slice.call(e); + if (0 === r.length) return n([]); + for (var i = r.length, f = 0; r.length > f; f++) o(f, r[f]); + }); + }), + (t.resolve = function (e) { + return e && 'object' == typeof e && e.constructor === t + ? e + : new t(function (n) { + n(e); + }); + }), + (t.reject = function (e) { + return new t(function (n, t) { + t(e); + }); + }), + (t.race = function (e) { + return new t(function (n, t) { + for (var o = 0, r = e.length; r > o; o++) e[o].then(n, t); + }); + }), + (t._immediateFn = + ('function' == typeof setImmediate && + function (e) { + setImmediate(e); + }) || + function (e) { + c(e, 0); + }), + (t._unhandledRejectionFn = function (e) { + void 0 !== console && console && console.warn('Possible Unhandled Promise Rejection:', e); + }); + var l = (function () { + if ('undefined' != typeof self) return self; + if ('undefined' != typeof window) return window; + if ('undefined' != typeof global) return global; + throw Error('unable to locate global object'); + })(); + 'Promise' in l + ? l.Promise.prototype['finally'] || (l.Promise.prototype['finally'] = e) + : (l.Promise = t); +}); diff --git a/platform/app/public/serve.json b/platform/app/public/serve.json new file mode 100644 index 0000000..8616a96 --- /dev/null +++ b/platform/app/public/serve.json @@ -0,0 +1,3 @@ +{ + "rewrites": [{ "source": "*", "destination": "index.html" }] +} diff --git a/platform/app/public/silent-refresh.html b/platform/app/public/silent-refresh.html new file mode 100644 index 0000000..089c94b --- /dev/null +++ b/platform/app/public/silent-refresh.html @@ -0,0 +1,25 @@ + + + + + Silent OpenID Connect Token Refresh Page + + + + + + + + + diff --git a/platform/app/src/App.tsx b/platform/app/src/App.tsx new file mode 100644 index 0000000..df31b2c --- /dev/null +++ b/platform/app/src/App.tsx @@ -0,0 +1,197 @@ +// External + +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import i18n from '@ohif/i18n'; +import { I18nextProvider } from 'react-i18next'; +import { BrowserRouter } from 'react-router-dom'; + +import Compose from './routes/Mode/Compose'; +import { + ExtensionManager, + CommandsManager, + HotkeysManager, + ServiceProvidersManager, + SystemContextProvider, +} from '@ohif/core'; +import { + DialogProvider, + Modal, + ModalProvider, + ThemeWrapper, + ViewportDialogProvider, + CineProvider, + UserAuthenticationProvider, +} from '@ohif/ui'; +import { + ThemeWrapper as ThemeWrapperNext, + NotificationProvider, + ViewportGridProvider, + TooltipProvider, + ToolboxProvider, +} from '@ohif/ui-next'; +// Viewer Project +// TODO: Should this influence study list? +import { AppConfigProvider } from '@state'; +import createRoutes from './routes'; +import appInit from './appInit.js'; +import OpenIdConnectRoutes from './utils/OpenIdConnectRoutes'; +import { ShepherdJourneyProvider } from 'react-shepherd'; + +let commandsManager: CommandsManager, + extensionManager: ExtensionManager, + servicesManager: AppTypes.ServicesManager, + serviceProvidersManager: ServiceProvidersManager, + hotkeysManager: HotkeysManager; + +function App({ + config = { + /** + * Relative route from domain root that OHIF instance is installed at. + * For example: + * + * Hosted at: https://ohif.org/where-i-host-the/viewer/ + * Value: `/where-i-host-the/viewer/` + * */ + routerBaseName: '/', + /** + * + */ + showLoadingIndicator: true, + showStudyList: true, + oidc: [], + extensions: [], + }, + defaultExtensions = [], + defaultModes = [], +}) { + const [init, setInit] = useState(null); + useEffect(() => { + const run = async () => { + appInit(config, defaultExtensions, defaultModes).then(setInit).catch(console.error); + }; + + run(); + }, []); + + if (!init) { + return null; + } + + // Set above for named export + commandsManager = init.commandsManager; + extensionManager = init.extensionManager; + servicesManager = init.servicesManager; + serviceProvidersManager = init.serviceProvidersManager; + hotkeysManager = init.hotkeysManager; + + // Set appConfig + const appConfigState = init.appConfig; + const { routerBasename, modes, dataSources, oidc, showStudyList } = appConfigState; + + // get the maximum 3D texture size + const canvas = document.createElement('canvas'); + const gl = canvas.getContext('webgl2'); + + if (gl) { + const max3DTextureSize = gl.getParameter(gl.MAX_3D_TEXTURE_SIZE); + appConfigState.max3DTextureSize = max3DTextureSize; + } + + const { + uiDialogService, + uiModalService, + uiViewportDialogService, + viewportGridService, + cineService, + userAuthenticationService, + uiNotificationService, + customizationService, + } = servicesManager.services; + + const providers = [ + [AppConfigProvider, { value: appConfigState }], + [UserAuthenticationProvider, { service: userAuthenticationService }], + [I18nextProvider, { i18n }], + [ThemeWrapperNext], + [ThemeWrapper], + [SystemContextProvider, { commandsManager, extensionManager, hotkeysManager, servicesManager }], + [ToolboxProvider], + [ViewportGridProvider, { service: viewportGridService }], + [ViewportDialogProvider, { service: uiViewportDialogService }], + [CineProvider, { service: cineService }], + [NotificationProvider, { service: uiNotificationService }], + [TooltipProvider], + [DialogProvider, { service: uiDialogService }], + [ModalProvider, { service: uiModalService, modal: Modal }], + [ShepherdJourneyProvider], + ]; + + // Loop through and register each of the service providers registered with the ServiceProvidersManager. + const providersFromManager = Object.entries(serviceProvidersManager.providers); + if (providersFromManager.length > 0) { + providersFromManager.forEach(([serviceName, provider]) => { + providers.push([provider, { service: servicesManager.services[serviceName] }]); + }); + } + + const CombinedProviders = ({ children }) => Compose({ components: providers, children }); + + let authRoutes = null; + + // Should there be a generic call to init on the extension manager? + customizationService.init(extensionManager); + + // Use config to create routes + const appRoutes = createRoutes({ + modes, + dataSources, + extensionManager, + servicesManager, + commandsManager, + hotkeysManager, + routerBasename, + showStudyList, + }); + + if (oidc) { + authRoutes = ( + + ); + } + + return ( + + + {authRoutes} + {appRoutes} + + + ); +} + +App.propTypes = { + config: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ + routerBasename: PropTypes.string.isRequired, + oidc: PropTypes.array, + whiteLabeling: PropTypes.object, + extensions: PropTypes.array, + }), + ]).isRequired, + /* Extensions that are "bundled" or "baked-in" to the application. + * These would be provided at build time as part of they entry point. */ + defaultExtensions: PropTypes.array, + /* Modes that are "bundled" or "baked-in" to the application. + * These would be provided at build time as part of they entry point. */ + defaultModes: PropTypes.array, +}; + +export default App; + +export { commandsManager, extensionManager, servicesManager }; diff --git a/platform/app/src/__mocks__/fileMock.js b/platform/app/src/__mocks__/fileMock.js new file mode 100644 index 0000000..407d724 --- /dev/null +++ b/platform/app/src/__mocks__/fileMock.js @@ -0,0 +1,3 @@ +// https://jestjs.io/docs/en/webpack#handling-static-assets + +module.exports = 'test-file-stub'; diff --git a/platform/app/src/__tests__/globalSetup.js b/platform/app/src/__tests__/globalSetup.js new file mode 100644 index 0000000..0aae7f5 --- /dev/null +++ b/platform/app/src/__tests__/globalSetup.js @@ -0,0 +1,11 @@ +const _ = require('lodash'); +const originalConsoleError = console.error; + +// JSDom's CSS Parser has limited support for certain features +// This suppresses error warnings caused by it +console.error = function (msg) { + if (_.startsWith(msg, 'Error: Could not parse CSS stylesheet')) { + return; + } + originalConsoleError(msg); +}; diff --git a/platform/app/src/appInit.js b/platform/app/src/appInit.js new file mode 100644 index 0000000..db1c544 --- /dev/null +++ b/platform/app/src/appInit.js @@ -0,0 +1,147 @@ +import { + CommandsManager, + ExtensionManager, + ServicesManager, + ServiceProvidersManager, + HotkeysManager, + UINotificationService, + UIModalService, + UIDialogService, + UIViewportDialogService, + MeasurementService, + DisplaySetService, + ToolbarService, + ViewportGridService, + HangingProtocolService, + CineService, + UserAuthenticationService, + errorHandler, + CustomizationService, + PanelService, + WorkflowStepsService, + StudyPrefetcherService, + MultiMonitorService, + // utils, +} from '@ohif/core'; + +import loadModules, { loadModule as peerImport } from './pluginImports'; + +/** + * @param {object|func} appConfigOrFunc - application configuration, or a function that returns application configuration + * @param {object[]} defaultExtensions - array of extension objects + */ +async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) { + const commandsManagerConfig = { + getAppState: () => {}, + }; + + const commandsManager = new CommandsManager(commandsManagerConfig); + const servicesManager = new ServicesManager(commandsManager); + const serviceProvidersManager = new ServiceProvidersManager(); + const hotkeysManager = new HotkeysManager(commandsManager, servicesManager); + + const appConfig = { + ...(typeof appConfigOrFunc === 'function' + ? await appConfigOrFunc({ servicesManager, peerImport }) + : appConfigOrFunc), + }; + // Default the peer import function + appConfig.peerImport ||= peerImport; + + const extensionManager = new ExtensionManager({ + commandsManager, + servicesManager, + serviceProvidersManager, + hotkeysManager, + appConfig, + }); + + servicesManager.setExtensionManager(extensionManager); + + servicesManager.registerServices([ + [MultiMonitorService.REGISTRATION, appConfig.multimonitor], + UINotificationService.REGISTRATION, + UIModalService.REGISTRATION, + UIDialogService.REGISTRATION, + UIViewportDialogService.REGISTRATION, + MeasurementService.REGISTRATION, + DisplaySetService.REGISTRATION, + [CustomizationService.REGISTRATION, appConfig.customizationService], + ToolbarService.REGISTRATION, + ViewportGridService.REGISTRATION, + HangingProtocolService.REGISTRATION, + CineService.REGISTRATION, + UserAuthenticationService.REGISTRATION, + PanelService.REGISTRATION, + WorkflowStepsService.REGISTRATION, + [StudyPrefetcherService.REGISTRATION, appConfig.studyPrefetcher], + ]); + + errorHandler.getHTTPErrorHandler = () => { + if (typeof appConfig.httpErrorHandler === 'function') { + return appConfig.httpErrorHandler; + } + }; + + /** + * Example: [ext1, ext2, ext3] + * Example2: [[ext1, config], ext2, [ext3, config]] + */ + const loadedExtensions = await loadModules([...defaultExtensions, ...appConfig.extensions]); + await extensionManager.registerExtensions(loadedExtensions, appConfig.dataSources); + + // TODO: We no longer use `utils.addServer` + // TODO: We no longer init webWorkers at app level + // TODO: We no longer init the user Manager + + if (!appConfig.modes) { + throw new Error('No modes are defined! Check your app-config.js'); + } + + const loadedModes = await loadModules([...(appConfig.modes || []), ...defaultModes]); + + // This is the name for the loaded instance object + appConfig.loadedModes = []; + const modesById = new Set(); + for (let i = 0; i < loadedModes.length; i++) { + let mode = loadedModes[i]; + if (!mode) { + continue; + } + const { id } = mode; + + if (mode.modeFactory) { + // If the appConfig contains configuration for this mode, use it. + const modeConfiguration = + appConfig.modesConfiguration && appConfig.modesConfiguration[id] + ? appConfig.modesConfiguration[id] + : {}; + + mode = await mode.modeFactory({ modeConfiguration, loadModules }); + } + + if (modesById.has(id)) { + continue; + } + // Prevent duplication + modesById.add(id); + if (!mode || typeof mode !== 'object') { + continue; + } + appConfig.loadedModes.push(mode); + } + // Hack alert - don't touch the original modes definition, + // but there are still dependencies on having the appConfig modes defined + appConfig.modes = appConfig.loadedModes; + + return { + appConfig, + commandsManager, + extensionManager, + servicesManager, + serviceProvidersManager, + hotkeysManager, + }; +} + +export default appInit; diff --git a/platform/app/src/components/EmptyViewport.tsx b/platform/app/src/components/EmptyViewport.tsx new file mode 100644 index 0000000..a6fa6f3 --- /dev/null +++ b/platform/app/src/components/EmptyViewport.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +function EmptyViewport() { + return
; +} + +export default EmptyViewport; diff --git a/platform/app/src/components/ViewportGrid.tsx b/platform/app/src/components/ViewportGrid.tsx new file mode 100644 index 0000000..9d95eb6 --- /dev/null +++ b/platform/app/src/components/ViewportGrid.tsx @@ -0,0 +1,389 @@ +import React, { useEffect, useCallback, useRef } from 'react'; +import { useResizeDetector } from 'react-resize-detector'; +import { Types, MeasurementService } from '@ohif/core'; +import { ViewportGrid, ViewportPane } from '@ohif/ui'; +import { useViewportGrid } from '@ohif/ui-next'; +import EmptyViewport from './EmptyViewport'; +import classNames from 'classnames'; +import { useAppConfig } from '@state'; + +function ViewerViewportGrid(props: withAppTypes) { + const { servicesManager, viewportComponents = [], dataSource } = props; + const [viewportGrid, viewportGridService] = useViewportGrid(); + const [appConfig] = useAppConfig(); + + const { layout, activeViewportId, viewports, isHangingProtocolLayout } = viewportGrid; + const { numCols, numRows } = layout; + const { ref: resizeRef } = useResizeDetector({ + refreshMode: 'debounce', + refreshRate: 7, + refreshOptions: { leading: true }, + onResize: () => { + viewportGridService.setViewportGridSizeChanged(); + }, + }); + const layoutHash = useRef(null); + + const { + displaySetService, + measurementService, + hangingProtocolService, + uiNotificationService, + customizationService, + } = servicesManager.services; + + const generateLayoutHash = () => `${numCols}-${numRows}`; + + /** + * This callback runs after the viewports structure has changed in any way. + * On initial display, that means if it has changed by applying a HangingProtocol, + * while subsequently it may mean by changing the stage or by manually adjusting + * the layout. + + */ + const updateDisplaySetsFromProtocol = ( + protocol: Types.HangingProtocol.Protocol, + stage, + activeStudyUID, + viewportMatchDetails + ) => { + const availableDisplaySets = displaySetService.getActiveDisplaySets(); + + if (!availableDisplaySets.length) { + console.log('No available display sets', availableDisplaySets); + return; + } + + // Match each viewport individually + const { layoutType } = stage.viewportStructure; + const stageProps = stage.viewportStructure.properties; + const { columns: numCols, rows: numRows, layoutOptions = [] } = stageProps; + + /** + * This find or create viewport uses the hanging protocol results to + * specify the viewport match details, which specifies the size and + * setup of the various viewports. + */ + const findOrCreateViewport = pos => { + const viewportId = Array.from(viewportMatchDetails.keys())[pos]; + const details = viewportMatchDetails.get(viewportId); + if (!details) { + console.log('No match details for viewport', viewportId); + return; + } + + const { displaySetsInfo, viewportOptions } = details; + const displaySetUIDsToHang = []; + const displaySetUIDsToHangOptions = []; + + displaySetsInfo.forEach(({ displaySetInstanceUID, displaySetOptions }) => { + if (displaySetInstanceUID) { + displaySetUIDsToHang.push(displaySetInstanceUID); + } + + displaySetUIDsToHangOptions.push(displaySetOptions); + }); + + const computedViewportOptions = hangingProtocolService.getComputedOptions( + viewportOptions, + displaySetUIDsToHang + ); + + const computedDisplaySetOptions = hangingProtocolService.getComputedOptions( + displaySetUIDsToHangOptions, + displaySetUIDsToHang + ); + + return { + displaySetInstanceUIDs: displaySetUIDsToHang, + displaySetOptions: computedDisplaySetOptions, + viewportOptions: computedViewportOptions, + }; + }; + + viewportGridService.setLayout({ + numRows, + numCols, + layoutType, + layoutOptions, + findOrCreateViewport, + isHangingProtocolLayout: true, + }); + }; + + const _getUpdatedViewports = useCallback( + (viewportId, displaySetInstanceUID) => { + if (!displaySetInstanceUID) { + return []; + } + + let updatedViewports = []; + try { + updatedViewports = hangingProtocolService.getViewportsRequireUpdate( + viewportId, + displaySetInstanceUID, + isHangingProtocolLayout + ); + } catch (error) { + console.warn(error); + uiNotificationService.show({ + title: 'Drag and Drop', + message: + 'The selected display sets could not be added to the viewport due to a mismatch in the Hanging Protocol rules.', + type: 'error', + duration: 3000, + }); + } + + return updatedViewports; + }, + [hangingProtocolService, uiNotificationService, isHangingProtocolLayout] + ); + + // Using Hanging protocol engine to match the displaySets + useEffect(() => { + const { unsubscribe } = hangingProtocolService.subscribe( + hangingProtocolService.EVENTS.PROTOCOL_CHANGED, + ({ protocol, stage, activeStudyUID, viewportMatchDetails }) => { + updateDisplaySetsFromProtocol(protocol, stage, activeStudyUID, viewportMatchDetails); + } + ); + + return () => { + unsubscribe(); + }; + }, []); + + // Check viewport readiness in useEffect + useEffect(() => { + const allReady = viewportGridService.getGridViewportsReady(); + const sameLayoutHash = layoutHash.current === generateLayoutHash(); + if (allReady && !sameLayoutHash) { + layoutHash.current = generateLayoutHash(); + viewportGridService.publishViewportsReady(); + } + }, [viewportGridService, generateLayoutHash]); + + useEffect(() => { + const { unsubscribe } = measurementService.subscribe( + MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_LAYOUT, + ({ viewportId, measurement, isConsumed }) => { + if (isConsumed) { + return; + } + // This occurs when no viewport has elected to consume the event + // so we need to change layouts into a layout which can consume + // the event. + const { displaySetInstanceUID: referencedDisplaySetInstanceUID } = measurement; + + const updatedViewports = _getUpdatedViewports(viewportId, referencedDisplaySetInstanceUID); + if (!updatedViewports[0]) { + console.warn( + 'ViewportGrid::Unable to navigate to viewport containing', + referencedDisplaySetInstanceUID + ); + return; + } + + // Arbitrarily assign the viewport to element 0 + // TODO - this should perform a search to find the most suitable viewport. + updatedViewports[0] = { ...updatedViewports[0] }; + const [viewport] = updatedViewports; + + // Copy the viewport options to prevent modifying the internal data + viewport.viewportOptions = { + ...viewport.viewportOptions, + orientation: 'acquisition', + // The preferred way to jump to the measurement view is to set the + // view reference, as this can hold information such as the orientation + // or zoom level required to display an annotation. The metadata attribute + // of the measurement is a viewReference, so use it to show the measurement. + // Longer term this should clear the view reference data + viewReference: measurement.metadata, + viewportType: measurement.metadata.volumeId ? 'volume' : null, + }; + + viewportGridService.setDisplaySetsForViewports(updatedViewports); + } + ); + + return () => { + unsubscribe(); + }; + }, [viewports]); + + const onDropHandler = (viewportId, { displaySetInstanceUID }) => { + const customOnDropHandler = customizationService.getCustomization('customOnDropHandler'); + const dropHandlerPromise = customOnDropHandler({ + ...props, + viewportId, + displaySetInstanceUID, + appConfig, + }); + + dropHandlerPromise.then(({ handled }) => { + if (!handled) { + const updatedViewports = _getUpdatedViewports(viewportId, displaySetInstanceUID); + viewportGridService.setDisplaySetsForViewports(updatedViewports); + } + }); + }; + + const getViewportPanes = useCallback(() => { + const viewportPanes = []; + + const numViewportPanes = viewportGridService.getNumViewportPanes(); + for (let i = 0; i < numViewportPanes; i++) { + const paneMetadata = Array.from(viewports.values())[i] || {}; + const { + displaySetInstanceUIDs, + viewportOptions, + displaySetOptions, // array of options for each display set in the viewport + x: viewportX, + y: viewportY, + width: viewportWidth, + height: viewportHeight, + viewportLabel, + } = paneMetadata; + + const viewportId = viewportOptions.viewportId; + const isActive = activeViewportId === viewportId; + + const displaySetInstanceUIDsToUse = displaySetInstanceUIDs || []; + + // This is causing the viewport components re-render when the activeViewportId changes + const displaySets = displaySetInstanceUIDsToUse + .map(displaySetInstanceUID => { + return displaySetService.getDisplaySetByUID(displaySetInstanceUID) || {}; + }) + .filter(displaySet => { + return !displaySet?.unsupported; + }); + + const ViewportComponent = _getViewportComponent( + displaySets, + viewportComponents, + uiNotificationService + ); + + // look inside displaySets to see if they need reRendering + const displaySetsNeedsRerendering = displaySets.some(displaySet => { + return displaySet.needsRerendering; + }); + + const onInteractionHandler = event => { + if (isActive) { + return; + } + + if (event && (appConfig?.activateViewportBeforeInteraction ?? true)) { + event.preventDefault(); + event.stopPropagation(); + } + + viewportGridService.setActiveViewportId(viewportId); + }; + + viewportPanes[i] = ( + +
+ 1 ? viewportLabel : ''} + viewportId={viewportId} + dataSource={dataSource} + viewportOptions={viewportOptions} + displaySetOptions={displaySetOptions} + needsRerendering={displaySetsNeedsRerendering} + isHangingProtocolLayout={isHangingProtocolLayout} + onElementEnabled={() => { + viewportGridService.setViewportIsReady(viewportId, true); + }} + /> +
+
+ ); + } + + return viewportPanes; + }, [viewports, activeViewportId, viewportComponents, dataSource]); + + /** + * Loading indicator until numCols and numRows are gotten from the HangingProtocolService + */ + if (!numRows || !numCols) { + return null; + } + + return ( +
+ + {getViewportPanes()} + +
+ ); +} + +function _getViewportComponent(displaySets, viewportComponents, uiNotificationService) { + if (!displaySets || !displaySets.length) { + return EmptyViewport; + } + + // Todo: Do we have a viewport that has two different SOPClassHandlerIds? + const SOPClassHandlerId = displaySets[0].SOPClassHandlerId; + + for (let i = 0; i < viewportComponents.length; i++) { + if (!viewportComponents[i]) { + throw new Error('viewport components not defined'); + } + if (!viewportComponents[i].displaySetsToDisplay) { + throw new Error('displaySetsToDisplay is null'); + } + if (viewportComponents[i].displaySetsToDisplay.includes(SOPClassHandlerId)) { + const { component } = viewportComponents[i]; + return component; + } + } + + console.log("Can't show displaySet", SOPClassHandlerId, displaySets[0]); + uiNotificationService.show({ + title: 'Viewport Not Supported Yet', + message: `Cannot display SOPClassUID of ${displaySets[0].SOPClassUID} yet`, + type: 'error', + }); + + return EmptyViewport; +} + +export default ViewerViewportGrid; diff --git a/platform/app/src/hooks/index.js b/platform/app/src/hooks/index.js new file mode 100644 index 0000000..b2e9756 --- /dev/null +++ b/platform/app/src/hooks/index.js @@ -0,0 +1,4 @@ +import useDebounce from './useDebounce.js'; +import useSearchParams from './useSearchParams'; + +export { useDebounce, useSearchParams }; diff --git a/platform/app/src/hooks/useDebounce.js b/platform/app/src/hooks/useDebounce.js new file mode 100644 index 0000000..45dde5b --- /dev/null +++ b/platform/app/src/hooks/useDebounce.js @@ -0,0 +1,31 @@ +import { useState, useEffect } from 'react'; + +/** + * See: https://usehooks.com/useDebounce/ + * + * @param {*} value + * @param {number} delay - delat in ms + */ +export default function useDebounce(value, delay) { + // State and setters for debounced value + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect( + () => { + // Update debounced value after delay + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + // Cancel the timeout if value changes (also on delay change or unmount) + // This is how we prevent debounced value from updating if value is changed ... + // .. within the delay period. Timeout gets cleared and restarted. + return () => { + clearTimeout(handler); + }; + }, + [value, delay] // Only re-call effect if value or delay changes + ); + + return debouncedValue; +} diff --git a/platform/app/src/hooks/useSearchParams.ts b/platform/app/src/hooks/useSearchParams.ts new file mode 100644 index 0000000..ccac9df --- /dev/null +++ b/platform/app/src/hooks/useSearchParams.ts @@ -0,0 +1,30 @@ +import { useLocation } from 'react-router'; + +/** + * It returns a URLSearchParams of the query parameters in the URL, where the keys are + * either lowercase or maintain their case based on the lowerCaseKeys parameter. + * This will automatically include the hash parameters as preferred parameters + * @param {lowerCaseKeys:boolean} true to return lower case keys; false (default) to maintain casing; + * @returns {URLSearchParams} + */ +export default function useSearchParams(options = { lowerCaseKeys: false }) { + const { lowerCaseKeys } = options; + const location = useLocation(); + const searchParams = new URLSearchParams(location.search); + const hashParams = new URLSearchParams(location.hash?.substring(1) || ''); + + for (const [key, value] of hashParams) { + searchParams.set(key, value); + } + if (!lowerCaseKeys) { + return searchParams; + } + + const lowerCaseSearchParams = new URLSearchParams(); + + for (const [key, value] of searchParams) { + lowerCaseSearchParams.set(key.toLowerCase(), value); + } + + return lowerCaseSearchParams; +} diff --git a/platform/app/src/index.js b/platform/app/src/index.js new file mode 100644 index 0000000..4600014 --- /dev/null +++ b/platform/app/src/index.js @@ -0,0 +1,44 @@ +/** + * Entry point for development and production PWA builds. + */ +import 'regenerator-runtime/runtime'; +import { createRoot } from 'react-dom/client'; +import App from './App'; +import React from 'react'; + +/** + * EXTENSIONS AND MODES + * ================= + * pluginImports.js is dynamically generated from extension and mode + * configuration at build time. + * + * pluginImports.js imports all of the modes and extensions and adds them + * to the window for processing. + */ +import { modes as defaultModes, extensions as defaultExtensions } from './pluginImports'; +import loadDynamicConfig from './loadDynamicConfig'; +export { history } from './utils/history'; +export { preserveQueryParameters, preserveQueryStrings } from './utils/preserveQueryParameters'; +export { publicUrl } from './utils/publicUrl'; + +loadDynamicConfig(window.config).then(config_json => { + // Reset Dynamic config if defined + if (config_json !== null) { + window.config = config_json; + } + + /** + * Combine our appConfiguration with installed extensions and modes. + * In the future appConfiguration may contain modes added at runtime. + * */ + const appProps = { + config: window ? window.config : {}, + defaultExtensions, + defaultModes, + }; + + const container = document.getElementById('root'); + + const root = createRoot(container); + root.render(React.createElement(App, appProps)); +}); diff --git a/platform/app/src/loadDynamicConfig.js b/platform/app/src/loadDynamicConfig.js new file mode 100644 index 0000000..f646eef --- /dev/null +++ b/platform/app/src/loadDynamicConfig.js @@ -0,0 +1,23 @@ +export default async config => { + const useDynamicConfig = config.dangerouslyUseDynamicConfig; + + // Check if dangerouslyUseDynamicConfig enabled + if (useDynamicConfig?.enabled) { + // If enabled then get configUrl query-string + let query = new URLSearchParams(window.location.search); + let configUrl = query.get('configUrl'); + + if (configUrl) { + // validate regex + const regex = useDynamicConfig.regex; + + if (configUrl.match(regex)) { + const response = await fetch(configUrl); + return response.json(); + } else { + return null; + } + } + } + return null; +}; diff --git a/platform/app/src/routes/CallbackPage.tsx b/platform/app/src/routes/CallbackPage.tsx new file mode 100644 index 0000000..bf938ec --- /dev/null +++ b/platform/app/src/routes/CallbackPage.tsx @@ -0,0 +1,23 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; + +function CallbackPage({ userManager, onRedirectSuccess }) { + const onRedirectError = error => { + throw new Error(error); + }; + + useEffect(() => { + userManager + .signinRedirectCallback() + .then(user => onRedirectSuccess(user)) + .catch(error => onRedirectError(error)); + }, [userManager, onRedirectSuccess]); + + return null; +} + +CallbackPage.propTypes = { + userManager: PropTypes.object.isRequired, +}; + +export default CallbackPage; diff --git a/platform/app/src/routes/DataSourceWrapper.tsx b/platform/app/src/routes/DataSourceWrapper.tsx new file mode 100644 index 0000000..d652562 --- /dev/null +++ b/platform/app/src/routes/DataSourceWrapper.tsx @@ -0,0 +1,305 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import React, { useCallback, useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { Enums, ExtensionManager, MODULE_TYPES, log } from '@ohif/core'; +// +import { extensionManager } from '../App'; +import { useParams, useLocation } from 'react-router'; +import { useNavigate } from 'react-router-dom'; +import useSearchParams from '../hooks/useSearchParams'; + +/** + * Determines if two React Router location objects are the same. + */ +const areLocationsTheSame = (location0, location1) => { + return ( + location0.pathname === location1.pathname && + location0.search === location1.search && + location0.hash === location1.hash + ); +}; + +/** + * Uses route properties to determine the data source that should be passed + * to the child layout template. In some instances, initiates requests and + * passes data as props. + * + * @param {object} props + * @param {function} props.children - Layout Template React Component + */ +function DataSourceWrapper(props: withAppTypes) { + const { servicesManager } = props; + const navigate = useNavigate(); + const { children: LayoutTemplate, ...rest } = props; + const params = useParams(); + const location = useLocation(); + const lowerCaseSearchParams = useSearchParams({ lowerCaseKeys: true }); + const query = useSearchParams(); + // Route props --> studies.mapParams + // mapParams --> studies.search + // studies.search --> studies.processResults + // studies.processResults --> + // But only for LayoutTemplate type of 'list'? + // Or no data fetching here, and just hand down my source + const STUDIES_LIMIT = 101; + const DEFAULT_DATA = { + studies: [], + total: 0, + resultsPerPage: 25, + pageNumber: 1, + location: 'Not a valid location, causes first load to occur', + }; + + const getInitialDataSourceName = useCallback(() => { + // TODO - get the variable from the props all the time... + let dataSourceName = lowerCaseSearchParams.get('datasources'); + + if (!dataSourceName && window.config.defaultDataSourceName) { + return ''; + } + + if (!dataSourceName) { + // Gets the first defined datasource with the right name + // Mostly for historical reasons - new configs should use the defaultDataSourceName + const dataSourceModules = extensionManager.modules[MODULE_TYPES.DATA_SOURCE]; + // TODO: Good usecase for flatmap? + const webApiDataSources = dataSourceModules.reduce((acc, curr) => { + const mods = []; + curr.module.forEach(mod => { + if (mod.type === 'webApi') { + mods.push(mod); + } + }); + return acc.concat(mods); + }, []); + dataSourceName = webApiDataSources + .map(ds => ds.name) + .find(it => extensionManager.getDataSources(it)?.[0] !== undefined); + } + + return dataSourceName; + }, []); + + const [isDataSourceInitialized, setIsDataSourceInitialized] = useState(false); + + // The path to the data source to be used in the URL for a mode (e.g. mode/dataSourcePath?StudyIntanceUIDs=1.2.3) + const [dataSourcePath, setDataSourcePath] = useState(() => { + const dataSourceName = getInitialDataSourceName(); + return dataSourceName ? `/${dataSourceName}` : ''; + }); + + const [dataSource, setDataSource] = useState(() => { + const dataSourceName = getInitialDataSourceName(); + + if (!dataSourceName) { + return extensionManager.getActiveDataSource()[0]; + } + + const dataSource = extensionManager.getDataSources(dataSourceName)?.[0]; + if (!dataSource) { + throw new Error(`No data source found for ${dataSourceName}`); + } + + return dataSource; + }); + + const [data, setData] = useState(DEFAULT_DATA); + const [isLoading, setIsLoading] = useState(false); + + /** + * The effect to initialize the data source whenever it changes. Similar to + * whenever a different Mode is entered, the Mode's data source is initialized, so + * too this DataSourceWrapper must initialize its data source whenever a different + * data source is activated. Furthermore, a data source might be initialized + * several times as it gets activated/deactivated because the location URL + * might change and data sources initialize based on the URL. + */ + useEffect(() => { + const initializeDataSource = async () => { + await dataSource.initialize({ params, query }); + setIsDataSourceInitialized(true); + }; + + initializeDataSource(); + }, [dataSource]); + + useEffect(() => { + const dataSourceChangedCallback = () => { + setIsLoading(false); + setIsDataSourceInitialized(false); + setDataSourcePath(''); + setDataSource(extensionManager.getActiveDataSource()[0]); + // Setting data to DEFAULT_DATA triggers a new query just like it does for the initial load. + setData(DEFAULT_DATA); + }; + + const sub = extensionManager.subscribe( + ExtensionManager.EVENTS.ACTIVE_DATA_SOURCE_CHANGED, + dataSourceChangedCallback + ); + return () => sub.unsubscribe(); + }, []); + + useEffect(() => { + if (!isDataSourceInitialized) { + return; + } + + const queryFilterValues = _getQueryFilterValues(location.search, STUDIES_LIMIT); + + // 204: no content + async function getData() { + setIsLoading(true); + log.time(Enums.TimingEnum.SEARCH_TO_LIST); + const studies = await dataSource.query.studies.search(queryFilterValues); + + setData({ + studies: studies || [], + total: studies.length, + resultsPerPage: queryFilterValues.resultsPerPage, + pageNumber: queryFilterValues.pageNumber, + location, + }); + log.timeEnd(Enums.TimingEnum.SCRIPT_TO_VIEW); + log.timeEnd(Enums.TimingEnum.SEARCH_TO_LIST); + + setIsLoading(false); + } + + try { + // Cache invalidation :thinking: + // - Anytime change is not just next/previous page + // - And we didn't cross a result offset range + const isSamePage = data.pageNumber === queryFilterValues.pageNumber; + const previousOffset = + Math.floor((data.pageNumber * data.resultsPerPage) / STUDIES_LIMIT) * (STUDIES_LIMIT - 1); + const newOffset = + Math.floor( + (queryFilterValues.pageNumber * queryFilterValues.resultsPerPage) / STUDIES_LIMIT + ) * + (STUDIES_LIMIT - 1); + // Simply checking data.location !== location is not sufficient because even though the location href (i.e. entire URL) + // has not changed, the React Router still provides a new location reference and would result in two study queries + // on initial load. Alternatively, window.location.href could be used. + const isLocationUpdated = + typeof data.location === 'string' || !areLocationsTheSame(data.location, location); + const isDataInvalid = + !isSamePage || (!isLoading && (newOffset !== previousOffset || isLocationUpdated)); + + if (isDataInvalid) { + getData().catch(e => { + console.error(e); + + const { configurationAPI, friendlyName } = dataSource.getConfig(); + // If there is a data source configuration API, then the Worklist will popup the dialog to attempt to configure it + // and attempt to resolve this issue. + if (configurationAPI) { + return; + } + + servicesManager.services.uiModalService.show({ + title: 'Data Source Connection Error', + containerDimensions: 'w-1/2', + content: () => { + return ( +
+

Error: {e.message}

+

+ Please ensure the following data source is configured correctly or is running: +

+
{friendlyName}
+
+ ); + }, + }); + }); + } + } catch (ex) { + console.warn(ex); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data, location, params, isLoading, setIsLoading, dataSource, isDataSourceInitialized]); + // queryFilterValues + + // TODO: Better way to pass DataSource? + return ( + setData(DEFAULT_DATA)} + /> + ); +} + +DataSourceWrapper.propTypes = { + /** Layout Component to wrap with a Data Source */ + children: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired, +}; + +export default DataSourceWrapper; + +/** + * Duplicated in `workList` + * Need generic that can be shared? Isn't this what qs is for? + * @param {*} query + */ +function _getQueryFilterValues(query, queryLimit) { + query = new URLSearchParams(query); + const newParams = new URLSearchParams(); + for (const [key, value] of query) { + newParams.set(key.toLowerCase(), value); + } + query = newParams; + + const pageNumber = _tryParseInt(query.get('pagenumber'), 1); + const resultsPerPage = _tryParseInt(query.get('resultsperpage'), 25); + + const queryFilterValues = { + // DCM + patientId: query.get('mrn'), + patientName: query.get('patientname'), + studyDescription: query.get('description'), + modalitiesInStudy: query.get('modalities') && query.get('modalities').split(','), + accessionNumber: query.get('accession'), + // + startDate: query.get('startdate'), + endDate: query.get('enddate'), + page: _tryParseInt(query.get('page'), undefined), + pageNumber, + resultsPerPage, + // Rarely supported server-side + sortBy: query.get('sortby'), + sortDirection: query.get('sortdirection'), + // Offset... + offset: Math.floor((pageNumber * resultsPerPage) / queryLimit) * (queryLimit - 1), + config: query.get('configurl'), + }; + + // patientName: good + // studyDescription: good + // accessionNumber: good + + // Delete null/undefined keys + Object.keys(queryFilterValues).forEach( + key => queryFilterValues[key] == null && delete queryFilterValues[key] + ); + + return queryFilterValues; + + function _tryParseInt(str, defaultValue) { + let retValue = defaultValue; + if (str !== null) { + if (str.length > 0) { + if (!isNaN(str)) { + retValue = parseInt(str); + } + } + } + return retValue; + } +} diff --git a/platform/app/src/routes/Debug.tsx b/platform/app/src/routes/Debug.tsx new file mode 100644 index 0000000..0933bd9 --- /dev/null +++ b/platform/app/src/routes/Debug.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Icons } from '@ohif/ui-next'; + +// this is a debug component that is used to list various things that might +// be useful for debugging such as cross origin errors, etc. +function Debug() { + return ( +
+
+
+ OHIF +
+
+

Debug Information

+
+

Cross Origin Isolated (COOP/COEP)

+ +
+
+
+
+
+
+ ); +} + +export default Debug; diff --git a/platform/app/src/routes/Local/Local.tsx b/platform/app/src/routes/Local/Local.tsx new file mode 100644 index 0000000..7f7806e --- /dev/null +++ b/platform/app/src/routes/Local/Local.tsx @@ -0,0 +1,168 @@ +import React, { useEffect, useRef } from 'react'; +import classnames from 'classnames'; +import { useNavigate } from 'react-router-dom'; +import { DicomMetadataStore, MODULE_TYPES, useSystem } from '@ohif/core'; + +import Dropzone from 'react-dropzone'; +import filesToStudies from './filesToStudies'; + +import { extensionManager } from '../../App'; + +import { Button } from '@ohif/ui'; +import { Icons } from '@ohif/ui-next'; + +const getLoadButton = (onDrop, text, isDir) => { + return ( + + {({ getRootProps, getInputProps }) => ( +
+ +
+ )} +
+ ); +}; + +type LocalProps = { + modePath: string; +}; + +function Local({ modePath }: LocalProps) { + const { servicesManager } = useSystem(); + const { customizationService } = servicesManager.services; + const navigate = useNavigate(); + const dropzoneRef = useRef(); + const [dropInitiated, setDropInitiated] = React.useState(false); + + const LoadingIndicatorProgress = customizationService.getCustomization( + 'ui.loadingIndicatorProgress' + ); + + // Initializing the dicom local dataSource + const dataSourceModules = extensionManager.modules[MODULE_TYPES.DATA_SOURCE]; + const localDataSources = dataSourceModules.reduce((acc, curr) => { + const mods = []; + curr.module.forEach(mod => { + if (mod.type === 'localApi') { + mods.push(mod); + } + }); + return acc.concat(mods); + }, []); + + const firstLocalDataSource = localDataSources[0]; + const dataSource = firstLocalDataSource.createDataSource({}); + + const microscopyExtensionLoaded = extensionManager.registeredExtensionIds.includes( + '@ohif/extension-dicom-microscopy' + ); + + const onDrop = async acceptedFiles => { + const studies = await filesToStudies(acceptedFiles, dataSource); + + const query = new URLSearchParams(); + + if (microscopyExtensionLoaded) { + // TODO: for microscopy, we are forcing microscopy mode, which is not ideal. + // we should make the local drag and drop navigate to the worklist and + // there user can select microscopy mode + const smStudies = studies.filter(id => { + const study = DicomMetadataStore.getStudy(id); + return ( + study.series.findIndex(s => s.Modality === 'SM' || s.instances[0].Modality === 'SM') >= 0 + ); + }); + + if (smStudies.length > 0) { + smStudies.forEach(id => query.append('StudyInstanceUIDs', id)); + + modePath = 'microscopy'; + } + } + + // Todo: navigate to work list and let user select a mode + studies.forEach(id => query.append('StudyInstanceUIDs', id)); + query.append('datasources', 'dicomlocal'); + + navigate(`/${modePath}?${decodeURIComponent(query.toString())}`); + }; + + // Set body style + useEffect(() => { + document.body.classList.add('bg-black'); + return () => { + document.body.classList.remove('bg-black'); + }; + }, []); + + return ( + { + setDropInitiated(true); + onDrop(acceptedFiles); + }} + noClick + > + {({ getRootProps }) => ( +
+
+
+
+ +
+
+ {dropInitiated ? ( +
+ +
+ ) : ( +
+

+ Note: You data is not uploaded to any server, it will stay in your local + browser application +

+

+ Drag and Drop DICOM files here to load them in the Viewer +

+

Or click to

+
+ )} +
+
+ {getLoadButton(onDrop, 'Load files', false)} + {getLoadButton(onDrop, 'Load folders', true)} +
+
+
+
+ )} +
+ ); +} + +export default Local; diff --git a/platform/app/src/routes/Local/dicomFileLoader.js b/platform/app/src/routes/Local/dicomFileLoader.js new file mode 100644 index 0000000..b5446da --- /dev/null +++ b/platform/app/src/routes/Local/dicomFileLoader.js @@ -0,0 +1,27 @@ +import dcmjs from 'dcmjs'; +import dicomImageLoader from '@cornerstonejs/dicom-image-loader'; +import FileLoader from './fileLoader'; + +const DICOMFileLoader = new (class extends FileLoader { + fileType = 'application/dicom'; + loadFile(file, imageId) { + return dicomImageLoader.wadouri.loadFileRequest(imageId); + } + + getDataset(image, imageId) { + const dicomData = dcmjs.data.DicomMessage.readFile(image); + + const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomData.dict); + + dataset.url = imageId; + + dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(dicomData.meta); + + dataset.AvailableTransferSyntaxUID = + dataset.AvailableTransferSyntaxUID || dataset._meta.TransferSyntaxUID?.Value?.[0]; + + return dataset; + } +})(); + +export default DICOMFileLoader; diff --git a/platform/app/src/routes/Local/fileLoader.js b/platform/app/src/routes/Local/fileLoader.js new file mode 100644 index 0000000..71bda52 --- /dev/null +++ b/platform/app/src/routes/Local/fileLoader.js @@ -0,0 +1,6 @@ +export default class FileLoader { + fileType; + loadFile(file, imageId) {} + getDataset(image, imageId) {} + getStudies(dataset, imageId) {} +} diff --git a/platform/app/src/routes/Local/fileLoaderService.js b/platform/app/src/routes/Local/fileLoaderService.js new file mode 100644 index 0000000..9100789 --- /dev/null +++ b/platform/app/src/routes/Local/fileLoaderService.js @@ -0,0 +1,39 @@ +import dicomImageLoader from '@cornerstonejs/dicom-image-loader'; + +import FileLoader from './fileLoader'; +import PDFFileLoader from './pdfFileLoader'; +import DICOMFileLoader from './dicomFileLoader'; + +class FileLoaderService extends FileLoader { + fileType; + loader; + constructor(file) { + super(); + const fileType = file && file.type; + this.loader = this.getLoader(fileType); + this.fileType = this.loader.fileType; + } + + addFile(file) { + return dicomImageLoader.wadouri.fileManager.add(file); + } + + loadFile(file, imageId) { + return this.loader.loadFile(file, imageId); + } + + getDataset(image, imageId) { + return this.loader.getDataset(image, imageId); + } + + getLoader(fileType) { + if (fileType === 'application/pdf') { + return PDFFileLoader; + } else { + // Default to dicom loader + return DICOMFileLoader; + } + } +} + +export default FileLoaderService; diff --git a/platform/app/src/routes/Local/filesToStudies.js b/platform/app/src/routes/Local/filesToStudies.js new file mode 100644 index 0000000..63148b1 --- /dev/null +++ b/platform/app/src/routes/Local/filesToStudies.js @@ -0,0 +1,22 @@ +import FileLoaderService from './fileLoaderService'; +import { DicomMetadataStore } from '@ohif/core'; + +const processFile = async file => { + try { + const fileLoaderService = new FileLoaderService(file); + const imageId = fileLoaderService.addFile(file); + const image = await fileLoaderService.loadFile(file, imageId); + const dicomJSONDataset = await fileLoaderService.getDataset(image, imageId); + + DicomMetadataStore.addInstance(dicomJSONDataset); + } catch (error) { + console.log(error.name, ':Error when trying to load and process local files:', error.message); + } +}; + +export default async function filesToStudies(files) { + const processFilesPromises = files.map(processFile); + await Promise.all(processFilesPromises); + + return DicomMetadataStore.getStudyInstanceUIDs(); +} diff --git a/platform/app/src/routes/Local/index.js b/platform/app/src/routes/Local/index.js new file mode 100644 index 0000000..bd6b8f7 --- /dev/null +++ b/platform/app/src/routes/Local/index.js @@ -0,0 +1 @@ +export { default } from './Local'; diff --git a/platform/app/src/routes/Local/pdfFileLoader.js b/platform/app/src/routes/Local/pdfFileLoader.js new file mode 100644 index 0000000..6d44d49 --- /dev/null +++ b/platform/app/src/routes/Local/pdfFileLoader.js @@ -0,0 +1,17 @@ +import dicomImageLoader from '@cornerstonejs/dicom-image-loader'; +import FileLoader from './fileLoader'; + +const PDFFileLoader = new (class extends FileLoader { + fileType = 'application/pdf'; + loadFile(file, imageId) { + return dicomImageLoader.wadouri.loadFileRequest(imageId); + } + + getDataset(image, imageId) { + const dataset = {}; + dataset.imageId = image.imageId || imageId; + return dataset; + } +})(); + +export default PDFFileLoader; diff --git a/platform/app/src/routes/Mode/Compose.tsx b/platform/app/src/routes/Mode/Compose.tsx new file mode 100644 index 0000000..7f8802f --- /dev/null +++ b/platform/app/src/routes/Mode/Compose.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +/** + * Nests React components as ordered in array. We use this to + * simplify composition a Mode specify's in it's configuration + * for React Contexts that should wrap a Mode Route. + */ +export default function Compose(props) { + const { components = [], children } = props; + + return ( + + {components.reduceRight((acc, curr) => { + const [Comp, props] = Array.isArray(curr) ? [curr[0], curr[1]] : [curr, {}]; + return {acc}; + }, children)} + + ); +} + +// https://juliuskoronci.medium.com/avoid-a-long-list-of-react-providers-c45a269d80c1 +Compose.propTypes = { + components: PropTypes.array, + children: PropTypes.node.isRequired, +}; diff --git a/platform/app/src/routes/Mode/Mode.tsx b/platform/app/src/routes/Mode/Mode.tsx new file mode 100644 index 0000000..86651aa --- /dev/null +++ b/platform/app/src/routes/Mode/Mode.tsx @@ -0,0 +1,408 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { useParams, useLocation, useNavigate } from 'react-router'; +import PropTypes from 'prop-types'; +import { utils } from '@ohif/core'; +import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui'; +import { useSearchParams } from '@hooks'; +import { useAppConfig } from '@state'; +import ViewportGrid from '@components/ViewportGrid'; +import Compose from './Compose'; +import { history } from '../../utils/history'; +import loadModules from '../../pluginImports'; +import { defaultRouteInit } from './defaultRouteInit'; +import { updateAuthServiceAndCleanUrl } from './updateAuthServiceAndCleanUrl'; + +const { getSplitParam } = utils; + +export default function ModeRoute({ + mode, + dataSourceName, + extensionManager, + servicesManager, + commandsManager, + hotkeysManager, +}: withAppTypes) { + const [appConfig] = useAppConfig(); + + // Parse route params/querystring + const location = useLocation(); + + // The react router DOM placeholder map (see https://reactrouter.com/en/main/hooks/use-params). + const params = useParams(); + // The URL's query search parameters where the keys casing is maintained + const query = useSearchParams(); + + mode?.onModeInit?.({ + servicesManager, + extensionManager, + commandsManager, + appConfig, + query, + }); + + // The URL's query search parameters where the keys are all lower case. + const lowerCaseSearchParams = useSearchParams({ lowerCaseKeys: true }); + + const [studyInstanceUIDs, setStudyInstanceUIDs] = useState(null); + + const [refresh, setRefresh] = useState(false); + const [ExtensionDependenciesLoaded, setExtensionDependenciesLoaded] = useState(false); + + const layoutTemplateData = useRef(false); + const locationRef = useRef(null); + const isMounted = useRef(false); + + // Expose the react router dom navigation. + history.navigate = useNavigate(); + + if (location !== locationRef.current) { + layoutTemplateData.current = null; + locationRef.current = location; + } + + const { + displaySetService, + panelService, + hangingProtocolService, + userAuthenticationService, + customizationService, + } = servicesManager.services; + + const { extensions, sopClassHandlers, hangingProtocol } = mode; + + const runTimeHangingProtocolId = lowerCaseSearchParams.get('hangingprotocolid'); + const runTimeStageId = lowerCaseSearchParams.get('stageid'); + const token = lowerCaseSearchParams.get('token'); + + if (token) { + updateAuthServiceAndCleanUrl(token, location, userAuthenticationService); + } + + // An undefined dataSourceName implies that the active data source that is already set in the ExtensionManager should be used. + if (dataSourceName !== undefined) { + extensionManager.setActiveDataSource(dataSourceName); + } + + const dataSource = extensionManager.getActiveDataSource()[0]; + + // Only handling one route per mode for now + const route = mode.routes[0]; + + useEffect(() => { + const loadExtensions = async () => { + const loadedExtensions = await loadModules(Object.keys(extensions)); + for (const extension of loadedExtensions) { + const { id: extensionId } = extension; + if (extensionManager.registeredExtensionIds.indexOf(extensionId) === -1) { + await extensionManager.registerExtension(extension); + } + } + + if (isMounted.current) { + setExtensionDependenciesLoaded(true); + } + }; + + loadExtensions(); + }, []); + + useEffect(() => { + // Preventing state update for unmounted component + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + + useEffect(() => { + if (!ExtensionDependenciesLoaded) { + return; + } + + // Todo: this should not be here, data source should not care about params + const initializeDataSource = async (params, query) => { + await dataSource.initialize({ + params, + query, + }); + setStudyInstanceUIDs(dataSource.getStudyInstanceUIDs({ params, query })); + }; + + initializeDataSource(params, query); + return () => { + layoutTemplateData.current = null; + }; + }, [location, ExtensionDependenciesLoaded]); + + useEffect(() => { + if (!ExtensionDependenciesLoaded || !studyInstanceUIDs?.length) { + return; + } + + const retrieveLayoutData = async () => { + const layoutData = await route.layoutTemplate({ + location, + servicesManager, + studyInstanceUIDs, + }); + + if (isMounted.current) { + const { leftPanels = [], rightPanels = [], ...layoutProps } = layoutData.props; + + panelService.reset(); + panelService.addPanels(panelService.PanelPosition.Left, leftPanels); + panelService.addPanels(panelService.PanelPosition.Right, rightPanels); + + // layoutProps contains all props but leftPanels and rightPanels + layoutData.props = layoutProps; + + layoutTemplateData.current = layoutData; + setRefresh(!refresh); + } + }; + if (Array.isArray(studyInstanceUIDs) && studyInstanceUIDs[0]) { + retrieveLayoutData(); + } + return () => { + layoutTemplateData.current = null; + }; + }, [studyInstanceUIDs, ExtensionDependenciesLoaded]); + + useEffect(() => { + if (!layoutTemplateData.current || !ExtensionDependenciesLoaded || !studyInstanceUIDs?.length) { + return; + } + + const setupRouteInit = async () => { + // TODO: For some reason this is running before the Providers + // are calling setServiceImplementation + // TODO -> iterate through services. + + // Extension + + // Add SOPClassHandlers to a new SOPClassManager. + displaySetService.init(extensionManager, sopClassHandlers); + + extensionManager.onModeEnter({ + servicesManager, + extensionManager, + commandsManager, + appConfig, + }); + + // use the URL hangingProtocolId if it exists, otherwise use the one + // defined in the mode configuration + const hangingProtocolIdToUse = hangingProtocolService.getProtocolById( + runTimeHangingProtocolId + ) + ? runTimeHangingProtocolId + : hangingProtocol; + + // Determine the index of the stageId if the hangingProtocolIdToUse is defined + const stageIndex = Array.isArray(hangingProtocolIdToUse) + ? -1 + : hangingProtocolService.getStageIndex(hangingProtocolIdToUse, { + stageId: runTimeStageId || undefined, + }); + // Ensure that the stage index is never negative + // If stageIndex is negative (e.g., if stage wasn't found), use 0 as the default + const stageIndexToUse = Math.max(0, stageIndex); + + // Sets the active hanging protocols - if hangingProtocol is undefined, + // resets to default. Done before the onModeEnter to allow the onModeEnter + // to perform custom hanging protocol actions + hangingProtocolService.setActiveProtocolIds(hangingProtocolIdToUse); + + mode?.onModeEnter({ + servicesManager, + extensionManager, + commandsManager, + appConfig, + }); + + // Move hotkeys setup here, after onModeEnter + const hotkeys = customizationService.getCustomization('ohif.hotkeyBindings'); + hotkeysManager.setDefaultHotKeys(hotkeys); + + /** + * The next line should get all the query parameters provided by the URL + * - except the StudyInstanceUIDs - and create an object called filters + * used to filtering the study as the user wants otherwise it will return + * a empty object. + * + * Example: + * const filters = { + * seriesInstanceUID: 1.2.276.0.7230010.3.1.3.1791068887.5412.1620253993.114611 + * } + */ + const filters = + Array.from(query.keys()).reduce((acc: Record, val: string) => { + const lowerVal = val.toLowerCase(); + // Not sure why the case matters here - it doesn't in the URL + if (lowerVal === 'seriesinstanceuids' || lowerVal === 'seriesinstanceuid') { + const seriesUIDs = getSplitParam(lowerVal, query); + return { + ...acc, + seriesInstanceUID: seriesUIDs, + }; + } + return { ...acc, [val]: getSplitParam(lowerVal, query) }; + }, {}) ?? {}; + + let unsubs; + + if (route.init) { + unsubs = await route.init( + { + servicesManager, + extensionManager, + hotkeysManager, + studyInstanceUIDs, + dataSource, + filters, + }, + hangingProtocolIdToUse, + stageIndexToUse + ); + } + + return defaultRouteInit( + { + servicesManager, + studyInstanceUIDs, + dataSource, + filters, + appConfig, + }, + hangingProtocolIdToUse, + stageIndexToUse + ); + }; + + let unsubscriptions; + setupRouteInit().then(unsubs => { + unsubscriptions = unsubs; + + mode?.onSetupRouteComplete?.({ + servicesManager, + extensionManager, + commandsManager, + }); + }); + + return () => { + // The mode.onModeExit must be done first to allow it to store + // information, and must be in a try/catch to ensure subscriptions + // are unsubscribed. + try { + mode?.onModeExit?.({ + servicesManager, + extensionManager, + appConfig, + }); + } catch (e) { + console.warn('mode exit failure', e); + } + // Clean up hotkeys + hotkeysManager.destroy(); + + // The unsubscriptions must occur before the extension onModeExit + // in order to prevent exceptions during cleanup caused by spurious events + if (unsubscriptions) { + unsubscriptions.forEach(unsub => { + unsub(); + }); + } + // The extension manager must be called after the mode, this is + // expected to cleanup the state to a standard setup. + extensionManager.onModeExit(); + }; + }, [ + mode, + dataSourceName, + location, + ExtensionDependenciesLoaded, + route, + servicesManager, + extensionManager, + hotkeysManager, + studyInstanceUIDs, + refresh, + ]); + + if (!studyInstanceUIDs || !layoutTemplateData.current || !ExtensionDependenciesLoaded) { + return null; + } + + const ViewportGridWithDataSource = props => { + return ViewportGrid({ ...props, dataSource }); + }; + + const CombinedExtensionsContextProvider = createCombinedContextProvider( + extensionManager, + servicesManager, + commandsManager + ); + + const getLayoutComponent = props => { + const layoutTemplateModuleEntry = extensionManager.getModuleEntry( + layoutTemplateData.current.id + ); + const LayoutComponent = layoutTemplateModuleEntry.component; + + return ; + }; + + const LayoutComponent = getLayoutComponent({ + ...layoutTemplateData.current.props, + ViewportGridComp: ViewportGridWithDataSource, + }); + + return ( + + {CombinedExtensionsContextProvider ? ( + + {LayoutComponent} + + ) : ( + {LayoutComponent} + )} + + ); +} + +/** + * Creates a combined context provider using the context modules from the extension manager. + * @param {object} extensionManager - The extension manager instance. + * @param {object} servicesManager - The services manager instance. + * @param {object} commandsManager - The commands manager instance. + * @returns {React.Component} - A React component that provides combined contexts to its children. + */ +function createCombinedContextProvider(extensionManager, servicesManager, commandsManager) { + const extensionsContextModules = extensionManager.getModulesByType( + extensionManager.constructor.MODULE_TYPES.CONTEXT + ); + + if (!extensionsContextModules?.length) { + return; + } + + const contextModuleProviders = extensionsContextModules.flatMap(({ module }) => { + return module.map(aContextModule => { + return aContextModule.provider; + }); + }); + + return ({ children }) => { + return Compose({ components: contextModuleProviders, children }); + }; +} + +ModeRoute.propTypes = { + mode: PropTypes.object.isRequired, + dataSourceName: PropTypes.string, + extensionManager: PropTypes.object, + servicesManager: PropTypes.object, + hotkeysManager: PropTypes.object, + commandsManager: PropTypes.object, +}; diff --git a/platform/app/src/routes/Mode/defaultRouteInit.ts b/platform/app/src/routes/Mode/defaultRouteInit.ts new file mode 100644 index 0000000..fceb757 --- /dev/null +++ b/platform/app/src/routes/Mode/defaultRouteInit.ts @@ -0,0 +1,149 @@ +import getStudies from './studiesList'; +import { DicomMetadataStore, log, utils, Enums } from '@ohif/core'; +import isSeriesFilterUsed from '../../utils/isSeriesFilterUsed'; + +const { getSplitParam } = utils; + +/** + * Initialize the route. + * + * @param props.servicesManager to read services from + * @param props.studyInstanceUIDs for a list of studies to read + * @param props.dataSource to read the data from + * @param props.filters filters from query params to read the data from + * @returns array of subscriptions to cancel + */ +export async function defaultRouteInit( + { servicesManager, studyInstanceUIDs, dataSource, filters, appConfig }: withAppTypes, + hangingProtocolId, + stageIndex +) { + const { displaySetService, hangingProtocolService, uiNotificationService, customizationService } = + servicesManager.services; + /** + * Function to apply the hanging protocol when the minimum number of display sets were + * received or all display sets retrieval were completed + * @returns + */ + function applyHangingProtocol() { + const displaySets = displaySetService.getActiveDisplaySets(); + + if (!displaySets || !displaySets.length) { + return; + } + + // Gets the studies list to use + const studies = getStudies(studyInstanceUIDs, displaySets); + + // study being displayed, and is thus the "active" study. + const activeStudy = studies[0]; + + // run the hanging protocol matching on the displaySets with the predefined + // hanging protocol in the mode configuration + hangingProtocolService.run({ studies, activeStudy, displaySets }, hangingProtocolId, { + stageIndex, + }); + } + + const unsubscriptions = []; + const issuedWarningSeries = []; + const { unsubscribe: instanceAddedUnsubscribe } = DicomMetadataStore.subscribe( + DicomMetadataStore.EVENTS.INSTANCES_ADDED, + function ({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) { + const seriesMetadata = DicomMetadataStore.getSeries(StudyInstanceUID, SeriesInstanceUID); + + // checks if the series filter was used, if it exists + const seriesInstanceUIDs = filters?.seriesInstanceUID; + if ( + seriesInstanceUIDs?.length && + !isSeriesFilterUsed(seriesMetadata.instances, filters) && + !issuedWarningSeries.includes(seriesInstanceUIDs[0]) + ) { + // stores the series instance filter so it shows only once the warning + issuedWarningSeries.push(seriesInstanceUIDs[0]); + uiNotificationService.show({ + title: 'Series filter', + message: `Each of the series in filter: ${seriesInstanceUIDs} are not part of the current study. The entire study is being displayed`, + type: 'error', + duration: 7000, + }); + } + + displaySetService.makeDisplaySets(seriesMetadata.instances, { madeInClient }); + } + ); + + unsubscriptions.push(instanceAddedUnsubscribe); + + log.time(Enums.TimingEnum.STUDY_TO_DISPLAY_SETS); + log.time(Enums.TimingEnum.STUDY_TO_FIRST_IMAGE); + + const allRetrieves = studyInstanceUIDs.map(StudyInstanceUID => + dataSource.retrieve.series.metadata({ + StudyInstanceUID, + filters, + returnPromises: true, + sortCriteria: customizationService.getCustomization('sortingCriteria'), + }) + ); + + // log the error if this fails, otherwise it's so difficult to tell what went wrong... + allRetrieves.forEach(retrieve => { + retrieve.catch(error => { + console.error(error); + }); + }); + + // is displaysets from URL and has initialSOPInstanceUID or initialSeriesInstanceUID + // then we need to wait for all display sets to be retrieved before applying the hanging protocol + const params = new URLSearchParams(window.location.search); + + const initialSeriesInstanceUID = getSplitParam('initialseriesinstanceuid', params); + const initialSOPInstanceUID = getSplitParam('initialsopinstanceuid', params); + + let displaySetFromUrl = false; + if (initialSeriesInstanceUID || initialSOPInstanceUID) { + displaySetFromUrl = true; + } + + await Promise.allSettled(allRetrieves).then(async promises => { + log.timeEnd(Enums.TimingEnum.STUDY_TO_DISPLAY_SETS); + log.time(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE); + log.time(Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES); + + const allPromises = []; + const remainingPromises = []; + + function startRemainingPromises(remainingPromises) { + remainingPromises.forEach(p => p.forEach(p => p.start())); + } + + promises.forEach(promise => { + const retrieveSeriesMetadataPromise = promise.value; + if (!Array.isArray(retrieveSeriesMetadataPromise)) { + return; + } + + if (displaySetFromUrl) { + const requiredSeriesPromises = retrieveSeriesMetadataPromise.map(promise => + promise.start() + ); + allPromises.push(Promise.allSettled(requiredSeriesPromises)); + } else { + const { requiredSeries, remaining } = hangingProtocolService.filterSeriesRequiredForRun( + hangingProtocolId, + retrieveSeriesMetadataPromise + ); + const requiredSeriesPromises = requiredSeries.map(promise => promise.start()); + allPromises.push(Promise.allSettled(requiredSeriesPromises)); + remainingPromises.push(remaining); + } + }); + + await Promise.allSettled(allPromises).then(applyHangingProtocol); + startRemainingPromises(remainingPromises); + applyHangingProtocol(); + }); + + return unsubscriptions; +} diff --git a/platform/app/src/routes/Mode/index.js b/platform/app/src/routes/Mode/index.js new file mode 100644 index 0000000..d21dfed --- /dev/null +++ b/platform/app/src/routes/Mode/index.js @@ -0,0 +1 @@ +export { default } from './Mode'; diff --git a/platform/app/src/routes/Mode/studiesList.ts b/platform/app/src/routes/Mode/studiesList.ts new file mode 100644 index 0000000..cacb31d --- /dev/null +++ b/platform/app/src/routes/Mode/studiesList.ts @@ -0,0 +1,65 @@ +import { DicomMetadataStore, Types } from '@ohif/core'; + +type StudyMetadata = Types.StudyMetadata; + +/** + * Compare function for sorting + * + * @param a - some simple value (string, number, timestamp) + * @param b - some simple value + * @param defaultCompare - default return value as a fallback when a===b + * @returns - compare a and b, returning 1 if ab and defaultCompare otherwise + */ +const compare = (a, b, defaultCompare = 0): number => { + if (a === b) { + return defaultCompare; + } + if (a < b) { + return 1; + } + return -1; +}; + +/** + * The studies from display sets gets the studies in study date + * order or in study instance UID order - not very useful, but + * if not specifically specified then at least making it consistent is useful. + */ +const getStudiesfromDisplaySets = (displaysets): StudyMetadata[] => { + const studyMap = {}; + + const ret = displaySets.reduce((prev, curr) => { + const { StudyInstanceUID } = curr; + if (!studyMap[StudyInstanceUID]) { + const study = DicomMetadataStore.getStudy(StudyInstanceUID); + studyMap[StudyInstanceUID] = study; + prev.push(study); + } + return prev; + }, []); + // Return the sorted studies, first on study date and second on study instance UID + ret.sort((a, b) => { + return compare(a.StudyDate, b.StudyDate, compare(a.StudyInstanceUID, b.StudyInstanceUID)); + }); + return ret; +}; + +/** + * The studies retrieve from the Uids is faster and gets the studies + * in the original order, as specified. + */ +const getStudiesFromUIDs = (studyUids: string[]): StudyMetadata[] => { + if (!studyUids?.length) { + return; + } + return studyUids.map(uid => DicomMetadataStore.getStudy(uid)); +}; + +/** Gets the array of studies */ +const getStudies = (studyUids?: string[], displaySets): StudyMetadata[] => { + return getStudiesFromUIDs(studyUids) || getStudiesfromDisplaySets(displaySets); +}; + +export default getStudies; + +export { getStudies, getStudiesFromUIDs, getStudiesfromDisplaySets, compare }; diff --git a/platform/app/src/routes/Mode/updateAuthServiceAndCleanUrl.ts b/platform/app/src/routes/Mode/updateAuthServiceAndCleanUrl.ts new file mode 100644 index 0000000..c387d69 --- /dev/null +++ b/platform/app/src/routes/Mode/updateAuthServiceAndCleanUrl.ts @@ -0,0 +1,35 @@ +/** + * Updates the user authentication service with the provided token and cleans the token from the URL. + * @param token - The token to set in the user authentication service. + * @param location - The location object from the router. + * @param userAuthenticationService - The user authentication service instance. + */ +export function updateAuthServiceAndCleanUrl( + token: string, + location: any, + userAuthenticationService: any +): void { + if (!token) { + return; + } + + // if a token is passed in, set the userAuthenticationService to use it + // for the Authorization header for all requests + userAuthenticationService.setServiceImplementation({ + getAuthorizationHeader: () => ({ + Authorization: 'Bearer ' + token, + }), + }); + + // Create a URL object with the current location + const urlObj = new URL(window.location.origin + window.location.pathname + location.search); + + // Remove the token from the URL object + urlObj.searchParams.delete('token'); + const cleanUrl = urlObj.toString(); + + // Update the browser's history without the token + if (window.history && window.history.replaceState) { + window.history.replaceState(null, '', cleanUrl); + } +} diff --git a/platform/app/src/routes/NotFound/NotFound.tsx b/platform/app/src/routes/NotFound/NotFound.tsx new file mode 100644 index 0000000..4672b97 --- /dev/null +++ b/platform/app/src/routes/NotFound/NotFound.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; + +import { useAppConfig } from '@state'; + +const NotFound = ({ message = 'Sorry, this page does not exist.', showGoBackButton = true }) => { + const [appConfig] = useAppConfig(); + const { showStudyList } = appConfig; + + return ( +
+
+

{message}

+ {showGoBackButton && showStudyList && ( +
+ Go back to the Study List +
+ )} +
+
+ ); +}; + +NotFound.propTypes = { + message: PropTypes.string, + showGoBackButton: PropTypes.bool, +}; + +export default NotFound; diff --git a/platform/app/src/routes/NotFound/index.js b/platform/app/src/routes/NotFound/index.js new file mode 100644 index 0000000..106131b --- /dev/null +++ b/platform/app/src/routes/NotFound/index.js @@ -0,0 +1 @@ +export { default } from './NotFound'; diff --git a/platform/app/src/routes/PrivateRoute.tsx b/platform/app/src/routes/PrivateRoute.tsx new file mode 100644 index 0000000..0e9f927 --- /dev/null +++ b/platform/app/src/routes/PrivateRoute.tsx @@ -0,0 +1,13 @@ +import { useUserAuthentication } from '@ohif/ui'; + +export const PrivateRoute = ({ children, handleUnauthenticated }) => { + const [{ user, enabled }] = useUserAuthentication(); + + if (enabled && !user) { + return handleUnauthenticated(); + } + + return children; +}; + +export default PrivateRoute; diff --git a/platform/app/src/routes/SignoutCallbackComponent.tsx b/platform/app/src/routes/SignoutCallbackComponent.tsx new file mode 100644 index 0000000..f01a89a --- /dev/null +++ b/platform/app/src/routes/SignoutCallbackComponent.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import PropTypes from 'prop-types'; + +function SignoutCallbackComponent({ userManager }) { + const navigate = useNavigate(); + + const onRedirectSuccess = (/* user */) => { + const { pathname, search = '' } = JSON.parse(sessionStorage.getItem('ohif-redirect-to')); + + navigate(`${pathname}?${search}`); + }; + + const onRedirectError = error => { + throw new Error(error); + }; + + userManager + .signoutRedirectCallback() + .then(user => onRedirectSuccess(user)) + .catch(error => onRedirectError(error)); + + return null; +} + +SignoutCallbackComponent.propTypes = { + userManager: PropTypes.object.isRequired, +}; + +export default SignoutCallbackComponent; diff --git a/platform/app/src/routes/WorkList/WorkList.tsx b/platform/app/src/routes/WorkList/WorkList.tsx new file mode 100644 index 0000000..a63a96e --- /dev/null +++ b/platform/app/src/routes/WorkList/WorkList.tsx @@ -0,0 +1,707 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import classnames from 'classnames'; +import PropTypes from 'prop-types'; +import { Link, useNavigate } from 'react-router-dom'; +import moment from 'moment'; +import qs from 'query-string'; +import isEqual from 'lodash.isequal'; +import { useTranslation } from 'react-i18next'; +// +import filtersMeta from './filtersMeta.js'; +import { useAppConfig } from '@state'; +import { useDebounce, useSearchParams } from '@hooks'; +import { utils, hotkeys } from '@ohif/core'; +import publicUrl from '../../utils/publicUrl'; + +import { + StudyListExpandedRow, + EmptyStudies, + StudyListTable, + StudyListPagination, + StudyListFilter, + useModal, + AboutModal, + UserPreferences, + useSessionStorage, + InvestigationalUseDialog, + Button, + ButtonEnums, +} from '@ohif/ui'; + +import { + Header, + Icons, + Tooltip, + TooltipTrigger, + TooltipContent, + Clipboard, + Onboarding, + ScrollArea, +} from '@ohif/ui-next'; + +import { Types } from '@ohif/ui'; + +import i18n from '@ohif/i18n'; +import { preserveQueryParameters, preserveQueryStrings } from '../../utils/preserveQueryParameters'; + +const PatientInfoVisibility = Types.PatientInfoVisibility; + +const { sortBySeriesDate } = utils; + +const { availableLanguages, defaultLanguage, currentLanguage } = i18n; + +const seriesInStudiesMap = new Map(); + +/** + * TODO: + * - debounce `setFilterValues` (150ms?) + */ +function WorkList({ + data: studies, + dataTotal: studiesTotal, + isLoadingData, + dataSource, + hotkeysManager, + dataPath, + onRefresh, + servicesManager, +}: withAppTypes) { + const { hotkeyDefinitions, hotkeyDefaults } = hotkeysManager; + const { show, hide } = useModal(); + const { t } = useTranslation(); + // ~ Modes + const [appConfig] = useAppConfig(); + // ~ Filters + const searchParams = useSearchParams(); + const navigate = useNavigate(); + const STUDIES_LIMIT = 101; + const queryFilterValues = _getQueryFilterValues(searchParams); + const [sessionQueryFilterValues, updateSessionQueryFilterValues] = useSessionStorage({ + key: 'queryFilterValues', + defaultValue: queryFilterValues, + // ToDo: useSessionStorage currently uses an unload listener to clear the filters from session storage + // so on systems that do not support unload events a user will NOT be able to alter any existing filter + // in the URL, load the page and have it apply. + clearOnUnload: true, + }); + const [filterValues, _setFilterValues] = useState({ + ...defaultFilterValues, + ...sessionQueryFilterValues, + }); + + const debouncedFilterValues = useDebounce(filterValues, 200); + const { resultsPerPage, pageNumber, sortBy, sortDirection } = filterValues; + + /* + * The default sort value keep the filters synchronized with runtime conditional sorting + * Only applied if no other sorting is specified and there are less than 101 studies + */ + + const canSort = studiesTotal < STUDIES_LIMIT; + const shouldUseDefaultSort = sortBy === '' || !sortBy; + const sortModifier = sortDirection === 'descending' ? 1 : -1; + const defaultSortValues = + shouldUseDefaultSort && canSort ? { sortBy: 'studyDate', sortDirection: 'ascending' } : {}; + + const sortedStudies = useMemo(() => { + if (!canSort) { + return studies; + } + + return [...studies].sort((s1, s2) => { + if (shouldUseDefaultSort) { + const ascendingSortModifier = -1; + return _sortStringDates(s1, s2, ascendingSortModifier); + } + + const s1Prop = s1[sortBy]; + const s2Prop = s2[sortBy]; + + if (typeof s1Prop === 'string' && typeof s2Prop === 'string') { + return s1Prop.localeCompare(s2Prop) * sortModifier; + } else if (typeof s1Prop === 'number' && typeof s2Prop === 'number') { + return (s1Prop > s2Prop ? 1 : -1) * sortModifier; + } else if (!s1Prop && s2Prop) { + return -1 * sortModifier; + } else if (!s2Prop && s1Prop) { + return 1 * sortModifier; + } else if (sortBy === 'studyDate') { + return _sortStringDates(s1, s2, sortModifier); + } + + return 0; + }); + }, [canSort, studies, shouldUseDefaultSort, sortBy, sortModifier]); + + // ~ Rows & Studies + const [expandedRows, setExpandedRows] = useState([]); + const [studiesWithSeriesData, setStudiesWithSeriesData] = useState([]); + const numOfStudies = studiesTotal; + const querying = useMemo(() => { + return isLoadingData || expandedRows.length > 0; + }, [isLoadingData, expandedRows]); + + const setFilterValues = val => { + if (filterValues.pageNumber === val.pageNumber) { + val.pageNumber = 1; + } + _setFilterValues(val); + updateSessionQueryFilterValues(val); + setExpandedRows([]); + }; + + const onPageNumberChange = newPageNumber => { + const oldPageNumber = filterValues.pageNumber; + const rollingPageNumberMod = Math.floor(101 / filterValues.resultsPerPage); + const rollingPageNumber = oldPageNumber % rollingPageNumberMod; + const isNextPage = newPageNumber > oldPageNumber; + const hasNextPage = Math.max(rollingPageNumber, 1) * resultsPerPage < numOfStudies; + + if (isNextPage && !hasNextPage) { + return; + } + + setFilterValues({ ...filterValues, pageNumber: newPageNumber }); + }; + + const onResultsPerPageChange = newResultsPerPage => { + setFilterValues({ + ...filterValues, + pageNumber: 1, + resultsPerPage: Number(newResultsPerPage), + }); + }; + + // Set body style + useEffect(() => { + document.body.classList.add('bg-black'); + return () => { + document.body.classList.remove('bg-black'); + }; + }, []); + + // Sync URL query parameters with filters + useEffect(() => { + if (!debouncedFilterValues) { + return; + } + + const queryString = {}; + Object.keys(defaultFilterValues).forEach(key => { + const defaultValue = defaultFilterValues[key]; + const currValue = debouncedFilterValues[key]; + + // TODO: nesting/recursion? + if (key === 'studyDate') { + if (currValue.startDate && defaultValue.startDate !== currValue.startDate) { + queryString.startDate = currValue.startDate; + } + if (currValue.endDate && defaultValue.endDate !== currValue.endDate) { + queryString.endDate = currValue.endDate; + } + } else if (key === 'modalities' && currValue.length) { + queryString.modalities = currValue.join(','); + } else if (currValue !== defaultValue) { + queryString[key] = currValue; + } + }); + + preserveQueryStrings(queryString); + + const search = qs.stringify(queryString, { + skipNull: true, + skipEmptyString: true, + }); + navigate({ + pathname: publicUrl, + search: search ? `?${search}` : undefined, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [debouncedFilterValues]); + + // Query for series information + useEffect(() => { + const fetchSeries = async studyInstanceUid => { + try { + const series = await dataSource.query.series.search(studyInstanceUid); + seriesInStudiesMap.set(studyInstanceUid, sortBySeriesDate(series)); + setStudiesWithSeriesData([...studiesWithSeriesData, studyInstanceUid]); + } catch (ex) { + // TODO: UI Notification Service + console.warn(ex); + } + }; + + // TODO: WHY WOULD YOU USE AN INDEX OF 1?! + // Note: expanded rows index begins at 1 + for (let z = 0; z < expandedRows.length; z++) { + const expandedRowIndex = expandedRows[z] - 1; + const studyInstanceUid = sortedStudies[expandedRowIndex].studyInstanceUid; + + if (studiesWithSeriesData.includes(studyInstanceUid)) { + continue; + } + + fetchSeries(studyInstanceUid); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [expandedRows, studies]); + + const isFiltering = (filterValues, defaultFilterValues) => { + return !isEqual(filterValues, defaultFilterValues); + }; + + const rollingPageNumberMod = Math.floor(101 / resultsPerPage); + const rollingPageNumber = (pageNumber - 1) % rollingPageNumberMod; + const offset = resultsPerPage * rollingPageNumber; + const offsetAndTake = offset + resultsPerPage; + const tableDataSource = sortedStudies.map((study, key) => { + const rowKey = key + 1; + const isExpanded = expandedRows.some(k => k === rowKey); + const { + studyInstanceUid, + accession, + modalities, + instances, + description, + mrn, + patientName, + date, + time, + } = study; + const studyDate = + date && + moment(date, ['YYYYMMDD', 'YYYY.MM.DD'], true).isValid() && + moment(date, ['YYYYMMDD', 'YYYY.MM.DD']).format(t('Common:localDateFormat', 'MMM-DD-YYYY')); + const studyTime = + time && + moment(time, ['HH', 'HHmm', 'HHmmss', 'HHmmss.SSS']).isValid() && + moment(time, ['HH', 'HHmm', 'HHmmss', 'HHmmss.SSS']).format( + t('Common:localTimeFormat', 'hh:mm A') + ); + + const makeCopyTooltipCell = textValue => { + if (!textValue) { + return ''; + } + return ( + + + {textValue} + + +
+ {textValue} + {textValue} +
+
+
+ ); + }; + + return { + dataCY: `studyRow-${studyInstanceUid}`, + clickableCY: studyInstanceUid, + row: [ + { + key: 'patientName', + content: patientName ? makeCopyTooltipCell(patientName) : null, + gridCol: 4, + }, + { + key: 'mrn', + content: makeCopyTooltipCell(mrn), + gridCol: 3, + }, + { + key: 'studyDate', + content: ( + <> + {studyDate && {studyDate}} + {studyTime && {studyTime}} + + ), + title: `${studyDate || ''} ${studyTime || ''}`, + gridCol: 5, + }, + { + key: 'description', + content: makeCopyTooltipCell(description), + gridCol: 4, + }, + { + key: 'modality', + content: modalities, + title: modalities, + gridCol: 3, + }, + { + key: 'accession', + content: makeCopyTooltipCell(accession), + gridCol: 3, + }, + { + key: 'instances', + content: ( + <> + + {instances} + + ), + title: (instances || 0).toString(), + gridCol: 2, + }, + ], + // Todo: This is actually running for all rows, even if they are + // not clicked on. + expandedContent: ( + { + return { + description: s.description || '(empty)', + seriesNumber: s.seriesNumber ?? '', + modality: s.modality || '', + instances: s.numSeriesInstances || '', + }; + }) + : [] + } + > +
+ {(appConfig.groupEnabledModesFirst + ? appConfig.loadedModes.sort((a, b) => { + const isValidA = a.isValidMode({ + modalities: modalities.replaceAll('/', '\\'), + study, + }).valid; + const isValidB = b.isValidMode({ + modalities: modalities.replaceAll('/', '\\'), + study, + }).valid; + + return isValidB - isValidA; + }) + : appConfig.loadedModes + ).map((mode, i) => { + const modalitiesToCheck = modalities.replaceAll('/', '\\'); + + const { valid: isValidMode, description: invalidModeDescription } = mode.isValidMode({ + modalities: modalitiesToCheck, + study, + }); + // TODO: Modes need a default/target route? We mostly support a single one for now. + // We should also be using the route path, but currently are not + // mode.routeName + // mode.routes[x].path + // Don't specify default data source, and it should just be picked up... (this may not currently be the case) + // How do we know which params to pass? Today, it's just StudyInstanceUIDs and configUrl if exists + const query = new URLSearchParams(); + if (filterValues.configUrl) { + query.append('configUrl', filterValues.configUrl); + } + query.append('StudyInstanceUIDs', studyInstanceUid); + preserveQueryParameters(query); + + return ( + mode.displayName && ( + { + // In case any event bubbles up for an invalid mode, prevent the navigation. + // For example, the event bubbles up when the icon embedded in the disabled button is clicked. + if (!isValidMode) { + event.preventDefault(); + } + }} + // to={`${mode.routeName}/dicomweb?StudyInstanceUIDs=${studyInstanceUid}`} + > + {/* TODO revisit the completely rounded style of buttons used for launching a mode from the worklist later - for now use LegacyButton*/} +
+ ) : null + } + startIcon={ + isValidMode ? ( + + ) : ( + + ) + } + onClick={() => {}} + dataCY={`mode-${mode.routeName}-${studyInstanceUid}`} + className={isValidMode ? 'text-[13px]' : 'bg-[#222d44] text-[13px]'} + > + {mode.displayName} + + + ) + ); + })} +
+ + ), + onClickRow: () => + setExpandedRows(s => (isExpanded ? s.filter(n => rowKey !== n) : [...s, rowKey])), + isExpanded, + }; + }); + + const hasStudies = numOfStudies > 0; + const versionNumber = process.env.VERSION_NUMBER; + const commitHash = process.env.COMMIT_HASH; + + const menuOptions = [ + { + title: t('Header:About'), + icon: 'info', + onClick: () => + show({ + content: AboutModal, + title: t('AboutModal:About OHIF Viewer'), + contentProps: { versionNumber, commitHash }, + containerDimensions: 'max-w-4xl max-h-4xl', + }), + }, + { + title: t('Header:Preferences'), + icon: 'settings', + onClick: () => + show({ + title: t('UserPreferencesModal:User preferences'), + content: UserPreferences, + contentProps: { + hotkeyDefaults: hotkeysManager.getValidHotkeyDefinitions(hotkeyDefaults), + hotkeyDefinitions, + onCancel: hide, + currentLanguage: currentLanguage(), + availableLanguages, + defaultLanguage, + onSubmit: state => { + if (state.language.value !== currentLanguage().value) { + i18n.changeLanguage(state.language.value); + } + hotkeysManager.setHotkeys(state.hotkeyDefinitions); + hide(); + }, + onReset: () => hotkeysManager.restoreDefaultBindings(), + hotkeysModule: hotkeys, + }, + }), + }, + ]; + + if (appConfig.oidc) { + menuOptions.push({ + icon: 'power-off', + title: t('Header:Logout'), + onClick: () => { + navigate(`/logout?redirect_uri=${encodeURIComponent(window.location.href)}`); + }, + }); + } + + const { customizationService } = servicesManager.services; + const LoadingIndicatorProgress = customizationService.getCustomization( + 'ui.loadingIndicatorProgress' + ); + const DicomUploadComponent = customizationService.getCustomization('dicomUploadComponent'); + + const uploadProps = + DicomUploadComponent && dataSource.getConfig()?.dicomUploadEnabled + ? { + title: 'Upload files', + closeButton: true, + shouldCloseOnEsc: false, + shouldCloseOnOverlayClick: false, + content: () => ( + { + hide(); + onRefresh(); + }} + onStarted={() => { + show({ + ...uploadProps, + // when upload starts, hide the default close button as closing the dialogue must be handled by the upload dialogue itself + closeButton: false, + }); + }} + /> + ), + } + : undefined; + + const dataSourceConfigurationComponent = customizationService.getCustomization( + 'ohif.dataSourceConfigurationComponent' + ); + + return ( +
+
+ + +
+ +
+ 100 ? 101 : numOfStudies} + filtersMeta={filtersMeta} + filterValues={{ ...filterValues, ...defaultSortValues }} + onChange={setFilterValues} + clearFilters={() => setFilterValues(defaultFilterValues)} + isFiltering={isFiltering(filterValues, defaultFilterValues)} + onUploadClick={uploadProps ? () => show(uploadProps) : undefined} + getDataSourceConfigurationComponent={ + dataSourceConfigurationComponent + ? () => dataSourceConfigurationComponent() + : undefined + } + /> +
+ {hasStudies ? ( +
+ +
+ +
+
+ ) : ( +
+ {appConfig.showLoadingIndicator && isLoadingData ? ( + + ) : ( + + )} +
+ )} +
+
+
+ ); +} + +WorkList.propTypes = { + data: PropTypes.array.isRequired, + dataSource: PropTypes.shape({ + query: PropTypes.object.isRequired, + getConfig: PropTypes.func, + }).isRequired, + isLoadingData: PropTypes.bool.isRequired, + servicesManager: PropTypes.object.isRequired, +}; + +const defaultFilterValues = { + patientName: '', + mrn: '', + studyDate: { + startDate: null, + endDate: null, + }, + description: '', + modalities: [], + accession: '', + sortBy: '', + sortDirection: 'none', + pageNumber: 1, + resultsPerPage: 25, + datasources: '', +}; + +function _tryParseInt(str, defaultValue) { + let retValue = defaultValue; + if (str && str.length > 0) { + if (!isNaN(str)) { + retValue = parseInt(str); + } + } + return retValue; +} + +function _getQueryFilterValues(params) { + const newParams = new URLSearchParams(); + for (const [key, value] of params) { + newParams.set(key.toLowerCase(), value); + } + params = newParams; + + const queryFilterValues = { + patientName: params.get('patientname'), + mrn: params.get('mrn'), + studyDate: { + startDate: params.get('startdate') || null, + endDate: params.get('enddate') || null, + }, + description: params.get('description'), + modalities: params.get('modalities') ? params.get('modalities').split(',') : [], + accession: params.get('accession'), + sortBy: params.get('sortby'), + sortDirection: params.get('sortdirection'), + pageNumber: _tryParseInt(params.get('pagenumber'), undefined), + resultsPerPage: _tryParseInt(params.get('resultsperpage'), undefined), + datasources: params.get('datasources'), + configUrl: params.get('configurl'), + }; + + // Delete null/undefined keys + Object.keys(queryFilterValues).forEach( + key => queryFilterValues[key] == null && delete queryFilterValues[key] + ); + + return queryFilterValues; +} + +function _sortStringDates(s1, s2, sortModifier) { + // TODO: Delimiters are non-standard. Should we support them? + const s1Date = moment(s1.date, ['YYYYMMDD', 'YYYY.MM.DD'], true); + const s2Date = moment(s2.date, ['YYYYMMDD', 'YYYY.MM.DD'], true); + + if (s1Date.isValid() && s2Date.isValid()) { + return (s1Date.toISOString() > s2Date.toISOString() ? 1 : -1) * sortModifier; + } else if (s1Date.isValid()) { + return sortModifier; + } else if (s2Date.isValid()) { + return -1 * sortModifier; + } +} + +export default WorkList; diff --git a/platform/app/src/routes/WorkList/filtersMeta.js b/platform/app/src/routes/WorkList/filtersMeta.js new file mode 100644 index 0000000..7057c8b --- /dev/null +++ b/platform/app/src/routes/WorkList/filtersMeta.js @@ -0,0 +1,129 @@ +import i18n from 'i18next'; + +const filtersMeta = [ + { + name: 'patientName', + displayName: i18n.t('StudyList:PatientName'), + inputType: 'Text', + isSortable: true, + gridCol: 4, + }, + { + name: 'mrn', + displayName: i18n.t('StudyList:MRN'), + inputType: 'Text', + isSortable: true, + gridCol: 3, + }, + { + name: 'studyDate', + displayName: i18n.t('StudyList:StudyDate'), + inputType: 'DateRange', + isSortable: true, + gridCol: 5, + }, + { + name: 'description', + displayName: i18n.t('StudyList:Description'), + inputType: 'Text', + isSortable: true, + gridCol: 4, + }, + { + name: 'modalities', + displayName: i18n.t('StudyList:Modality'), + inputType: 'MultiSelect', + inputProps: { + options: [ + { value: 'AR', label: 'AR' }, + { value: 'ASMT', label: 'ASMT' }, + { value: 'AU', label: 'AU' }, + { value: 'BDUS', label: 'BDUS' }, + { value: 'BI', label: 'BI' }, + { value: 'BMD', label: 'BMD' }, + { value: 'CR', label: 'CR' }, + { value: 'CT', label: 'CT' }, + { value: 'CTPROTOCOL', label: 'CTPROTOCOL' }, + { value: 'DG', label: 'DG' }, + { value: 'DOC', label: 'DOC' }, + { value: 'DX', label: 'DX' }, + { value: 'ECG', label: 'ECG' }, + { value: 'EPS', label: 'EPS' }, + { value: 'ES', label: 'ES' }, + { value: 'FID', label: 'FID' }, + { value: 'GM', label: 'GM' }, + { value: 'HC', label: 'HC' }, + { value: 'HD', label: 'HD' }, + { value: 'IO', label: 'IO' }, + { value: 'IOL', label: 'IOL' }, + { value: 'IVOCT', label: 'IVOCT' }, + { value: 'IVUS', label: 'IVUS' }, + { value: 'KER', label: 'KER' }, + { value: 'KO', label: 'KO' }, + { value: 'LEN', label: 'LEN' }, + { value: 'LS', label: 'LS' }, + { value: 'MG', label: 'MG' }, + { value: 'MR', label: 'MR' }, + { value: 'M3D', label: 'M3D' }, + { value: 'NM', label: 'NM' }, + { value: 'OAM', label: 'OAM' }, + { value: 'OCT', label: 'OCT' }, + { value: 'OP', label: 'OP' }, + { value: 'OPM', label: 'OPM' }, + { value: 'OPT', label: 'OPT' }, + { value: 'OPTBSV', label: 'OPTBSV' }, + { value: 'OPTENF', label: 'OPTENF' }, + { value: 'OPV', label: 'OPV' }, + { value: 'OSS', label: 'OSS' }, + { value: 'OT', label: 'OT' }, + { value: 'PLAN', label: 'PLAN' }, + { value: 'PR', label: 'PR' }, + { value: 'PT', label: 'PT' }, + { value: 'PX', label: 'PX' }, + { value: 'REG', label: 'REG' }, + { value: 'RESP', label: 'RESP' }, + { value: 'RF', label: 'RF' }, + { value: 'RG', label: 'RG' }, + { value: 'RTDOSE', label: 'RTDOSE' }, + { value: 'RTIMAGE', label: 'RTIMAGE' }, + { value: 'RTINTENT', label: 'RTINTENT' }, + { value: 'RTPLAN', label: 'RTPLAN' }, + { value: 'RTRAD', label: 'RTRAD' }, + { value: 'RTRECORD', label: 'RTRECORD' }, + { value: 'RTSEGANN', label: 'RTSEGANN' }, + { value: 'RTSTRUCT', label: 'RTSTRUCT' }, + { value: 'RWV', label: 'RWV' }, + { value: 'SEG', label: 'SEG' }, + { value: 'SM', label: 'SM' }, + { value: 'SMR', label: 'SMR' }, + { value: 'SR', label: 'SR' }, + { value: 'SRF', label: 'SRF' }, + { value: 'STAIN', label: 'STAIN' }, + { value: 'TEXTUREMAP', label: 'TEXTUREMAP' }, + { value: 'TG', label: 'TG' }, + { value: 'US', label: 'US' }, + { value: 'VA', label: 'VA' }, + { value: 'XA', label: 'XA' }, + { value: 'XC', label: 'XC' }, + ], + }, + isSortable: true, + gridCol: 3, + }, + { + name: 'accession', + displayName: i18n.t('StudyList:AccessionNumber'), + inputType: 'Text', + isSortable: true, + gridCol: 3, + }, + { + name: 'instances', + displayName: i18n.t('StudyList:Instances'), + inputType: 'None', + isSortable: false, + gridCol: 2, + }, +]; + +export default filtersMeta; diff --git a/platform/app/src/routes/WorkList/index.js b/platform/app/src/routes/WorkList/index.js new file mode 100644 index 0000000..83a6650 --- /dev/null +++ b/platform/app/src/routes/WorkList/index.js @@ -0,0 +1 @@ +export { default } from './WorkList'; diff --git a/platform/app/src/routes/buildModeRoutes.tsx b/platform/app/src/routes/buildModeRoutes.tsx new file mode 100644 index 0000000..2e60d42 --- /dev/null +++ b/platform/app/src/routes/buildModeRoutes.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import ModeRoute from '@routes/Mode'; +import publicUrl from '../utils/publicUrl'; + +/* + Routes uniquely define an entry point to: + - A mode + - Linked to a data source + - With a specified data set. + + The full route template is: + + /:modeId/:modeRoute/:sourceType/?queryParameters=example + + Where: + :modeId - Is the mode selected. + :modeRoute - Is the route within the mode to select. + :sourceType - Is the data source identifier, which specifies which DataSource to use. + ?queryParameters - Are query parameters as defined by data source. + + A default source can be specified at the app level configuration, and then that source is used if :sourceType is omitted: + + /:modeId/:modeRoute/?queryParameters=example + */ +export default function buildModeRoutes({ + modes, + dataSources, + extensionManager, + servicesManager, + commandsManager, + hotkeysManager, +}: withAppTypes) { + const routes = []; + const dataSourceNames = []; + + dataSources.forEach(dataSource => { + const { sourceName } = dataSource; + if (!dataSourceNames.includes(sourceName)) { + dataSourceNames.push(sourceName); + } + }); + + modes.forEach(mode => { + // todo: for each route. add route to path. + dataSourceNames.forEach(dataSourceName => { + const path = `${publicUrl}${mode.routeName}/${dataSourceName}`; + + // TODO move up. + const children = () => ( + + ); + + routes.push({ + path, + children, + private: true, + }); + }); + + // Add active DataSource route. + // This is the DataSource route for the active data source defined in ExtensionManager.getActiveDataSource + const path = `${publicUrl}${mode.routeName}`; + + // TODO move up. + const children = () => ( + + ); + + routes.push({ + path, + children, + private: true, + }); + }); + + return routes; +} diff --git a/platform/app/src/routes/index.tsx b/platform/app/src/routes/index.tsx new file mode 100644 index 0000000..1dd0c63 --- /dev/null +++ b/platform/app/src/routes/index.tsx @@ -0,0 +1,170 @@ +import React from 'react'; +import { Routes, Route, Link } from 'react-router-dom'; +import { ErrorBoundary } from '@ohif/ui-next'; + +// Route Components +import DataSourceWrapper from './DataSourceWrapper'; +import WorkList from './WorkList'; +import Local from './Local'; +import Debug from './Debug'; +import NotFound from './NotFound'; +import buildModeRoutes from './buildModeRoutes'; +import PrivateRoute from './PrivateRoute'; +import PropTypes from 'prop-types'; +import publicUrl from '../utils/publicUrl'; + +const NotFoundServer = ({ + message = 'Unable to query for studies at this time. Check your data source configuration or network connection', +}) => { + return ( +
+
+

{message}

+
+
+ ); +}; + +NotFoundServer.propTypes = { + message: PropTypes.string, +}; + +const NotFoundStudy = () => { + return ( +
+
+

+ One or more of the requested studies are not available at this time. Return to the{' '} + + study list + {' '} + to select a different study to view. +

+
+
+ ); +}; + +NotFoundStudy.propTypes = { + message: PropTypes.string, +}; + +// TODO: Include "routes" debug route if dev build +const bakedInRoutes = [ + { + path: `${publicUrl}notfoundserver`, + children: NotFoundServer, + }, + { + path: `${publicUrl}notfoundstudy`, + children: NotFoundStudy, + }, + { + path: `${publicUrl}debug`, + children: Debug, + }, + { + path: `${publicUrl}local`, + children: Local.bind(null, { modePath: '' }), // navigate to the worklist + }, + { + path: `${publicUrl}localbasic`, + children: Local.bind(null, { modePath: 'viewer/dicomlocal' }), + }, +]; + +// NOT FOUND (404) +const notFoundRoute = { component: NotFound }; + +const createRoutes = ({ + modes, + dataSources, + extensionManager, + servicesManager, + commandsManager, + hotkeysManager, + routerBasename, + showStudyList, +}: withAppTypes) => { + const routes = + buildModeRoutes({ + modes, + dataSources, + extensionManager, + servicesManager, + commandsManager, + hotkeysManager, + }) || []; + + const { customizationService } = servicesManager.services; + + const WorkListRoute = { + path: publicUrl, + children: DataSourceWrapper, + private: true, + props: { children: WorkList, servicesManager, extensionManager }, + }; + + const customRoutes = customizationService.getCustomization('routes.customRoutes'); + + const allRoutes = [ + ...routes, + ...(showStudyList ? [WorkListRoute] : []), + ...(publicUrl !== '/' && showStudyList ? [{ ...WorkListRoute, path: publicUrl }] : []), + ...(customRoutes?.routes || []), + ...bakedInRoutes, + customRoutes?.notFoundRoute || notFoundRoute, + ]; + + function RouteWithErrorBoundary({ route, ...rest }) { + // eslint-disable-next-line react/jsx-props-no-spreading + return ( + + + + ); + } + + const { userAuthenticationService } = servicesManager.services; + + // All routes are private by default and then we let the user auth service + // to check if it is enabled or not + // Todo: I think we can remove the second public return below + return ( + + {allRoutes.map((route, i) => { + return route.private === true ? ( + userAuthenticationService.handleUnauthenticated()} + > + + + } + > + ) : ( + } + /> + ); + })} + + ); +}; + +export default createRoutes; diff --git a/platform/app/src/sanity.test.js b/platform/app/src/sanity.test.js new file mode 100644 index 0000000..6daeb00 --- /dev/null +++ b/platform/app/src/sanity.test.js @@ -0,0 +1,8 @@ +describe('Sanity Test', () => { + test('how many marbles?', () => { + const expectedMarbles = 4; + const actualMarbles = 2 + 2; + + expect(actualMarbles).toEqual(expectedMarbles); + }); +}); diff --git a/platform/app/src/service-worker.js b/platform/app/src/service-worker.js new file mode 100644 index 0000000..a235b8c --- /dev/null +++ b/platform/app/src/service-worker.js @@ -0,0 +1,71 @@ +navigator.serviceWorker.getRegistrations().then(function (registrations) { + for (let registration of registrations) { + registration.unregister(); + } +}); + +// https://developers.google.com/web/tools/workbox/guides/troubleshoot-and-debug +importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.0.0-beta.1/workbox-sw.js'); + +// Install newest +// https://developers.google.com/web/tools/workbox/modules/workbox-core +workbox.core.skipWaiting(); +workbox.core.clientsClaim(); + +// Cache static assets that aren't precached +workbox.routing.registerRoute( + /\.(?:js|css|json5)$/, + new workbox.strategies.StaleWhileRevalidate({ + cacheName: 'static-resources', + }) +); + +// Cache the Google Fonts stylesheets with a stale-while-revalidate strategy. +workbox.routing.registerRoute( + /^https:\/\/fonts\.googleapis\.com/, + new workbox.strategies.StaleWhileRevalidate({ + cacheName: 'google-fonts-stylesheets', + }) +); + +// Cache the underlying font files with a cache-first strategy for 1 year. +workbox.routing.registerRoute( + /^https:\/\/fonts\.gstatic\.com/, + new workbox.strategies.CacheFirst({ + cacheName: 'google-fonts-webfonts', + plugins: [ + new workbox.cacheableResponse.CacheableResponsePlugin({ + statuses: [0, 200], + }), + new workbox.expiration.ExpirationPlugin({ + maxAgeSeconds: 60 * 60 * 24 * 365, // 1 Year + maxEntries: 30, + }), + ], + }) +); + +// MESSAGE HANDLER +self.addEventListener('message', event => { + if (event.data && event.data.type === 'SKIP_WAITING') { + switch (event.data.type) { + case 'SKIP_WAITING': + // TODO: We'll eventually want this to be user prompted + // workbox.core.skipWaiting(); + // workbox.core.clientsClaim(); + // TODO: Global notification to indicate incoming reload + break; + + default: + console.warn(`SW: Invalid message type: ${event.data.type}`); + } + } +}); + +workbox.precaching.precacheAndRoute(self.__WB_MANIFEST); + +// TODO: Cache API +// https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api +// Store DICOMs? +// Clear Service Worker cache? +// navigator.storage.estimate().then(est => console.log(est)); (2GB?) diff --git a/platform/app/src/state/appConfig.tsx b/platform/app/src/state/appConfig.tsx new file mode 100644 index 0000000..ce7eaf7 --- /dev/null +++ b/platform/app/src/state/appConfig.tsx @@ -0,0 +1,20 @@ +import React, { useState, createContext, useContext } from 'react'; +import PropTypes from 'prop-types'; + +const appConfigContext = createContext(null); +const { Provider } = appConfigContext; + +export const useAppConfig = () => useContext(appConfigContext); + +export function AppConfigProvider({ children, value: initAppConfig }) { + const [appConfig, setAppConfig] = useState(initAppConfig); + + return {children}; +} + +AppConfigProvider.propTypes = { + children: PropTypes.any, + value: PropTypes.any, +}; + +export default AppConfigProvider; diff --git a/platform/app/src/state/index.js b/platform/app/src/state/index.js new file mode 100644 index 0000000..7fd53f7 --- /dev/null +++ b/platform/app/src/state/index.js @@ -0,0 +1,3 @@ +import { AppConfigProvider, useAppConfig } from './appConfig.tsx'; + +export { AppConfigProvider, useAppConfig }; diff --git a/platform/app/src/utils/OpenIdConnectRoutes.tsx b/platform/app/src/utils/OpenIdConnectRoutes.tsx new file mode 100644 index 0000000..9df270a --- /dev/null +++ b/platform/app/src/utils/OpenIdConnectRoutes.tsx @@ -0,0 +1,230 @@ +import React from 'react'; +import { useEffect } from 'react'; +import { Route, Routes, useLocation, useNavigate } from 'react-router'; +import CallbackPage from '../routes/CallbackPage'; +import SignoutCallbackComponent from '../routes/SignoutCallbackComponent'; +import LegacyClient from './legacyOIDCClient'; +import NextClient from './nextOIDCClient'; + +function _isAbsoluteUrl(url) { + return url.includes('http://') || url.includes('https://'); +} + +function _makeAbsoluteIfNecessary(url, base_url) { + if (_isAbsoluteUrl(url)) { + return url; + } + + /* + * Make sure base_url and url are not duplicating slashes. + */ + if (base_url[base_url.length - 1] === '/') { + base_url = base_url.slice(0, base_url.length - 1); + } + + return base_url + url; +} + +const initUserManager = (oidc, routerBasename) => { + if (!oidc || !oidc.length) { + return; + } + + const firstOpenIdClient = oidc[0]; + const { protocol, host } = window.location; + const baseUri = `${protocol}//${host}${routerBasename}`; + + const redirect_uri = firstOpenIdClient.redirect_uri || '/callback'; + const silent_redirect_uri = firstOpenIdClient.silent_redirect_uri || '/silent-refresh.html'; + const post_logout_redirect_uri = firstOpenIdClient.post_logout_redirect_uri || '/'; + + const openIdConnectConfiguration = Object.assign({}, firstOpenIdClient, { + redirect_uri: _makeAbsoluteIfNecessary(redirect_uri, baseUri), + silent_redirect_uri: _makeAbsoluteIfNecessary(silent_redirect_uri, baseUri), + post_logout_redirect_uri: _makeAbsoluteIfNecessary(post_logout_redirect_uri, baseUri), + }); + + const client = firstOpenIdClient.response_type === 'code' ? NextClient : LegacyClient; + + return client(openIdConnectConfiguration); +}; + +function LogoutComponent(props) { + const { userManager } = props; + localStorage.setItem('signoutEvent', 'true'); + const location = useLocation(); + const query = new URLSearchParams(location.search); + userManager.signoutRedirect({ + post_logout_redirect_uri: query.get('redirect_uri'), + }); + return null; +} + +function LoginComponent(userManager) { + const queryParams = new URLSearchParams(location.search); + const iss = queryParams.get('iss'); + const loginHint = queryParams.get('login_hint'); + const targetLinkUri = queryParams.get('target_link_uri'); + if (iss !== oidcAuthority) { + console.error('iss of /login does not match the oidc authority'); + return null; + } + + userManager.removeUser().then(() => { + if (targetLinkUri !== null) { + const ohifRedirectTo = { + pathname: new URL(targetLinkUri).pathname, + }; + sessionStorage.setItem('ohif-redirect-to', JSON.stringify(ohifRedirectTo)); + } else { + const ohifRedirectTo = { + pathname: '/', + }; + sessionStorage.setItem('ohif-redirect-to', JSON.stringify(ohifRedirectTo)); + } + + if (loginHint !== null) { + userManager.signinRedirect({ login_hint: loginHint }); + } else { + userManager.signinRedirect(); + } + }); + + return null; +} + +function OpenIdConnectRoutes({ oidc, routerBasename, userAuthenticationService }) { + const userManager = initUserManager(oidc, routerBasename); + + const getAuthorizationHeader = () => { + const user = userAuthenticationService.getUser(); + + // if the user is null return early, next time + // we hit this function we will have a user + if (!user) { + return; + } + + return { + Authorization: `Bearer ${user.access_token}`, + }; + }; + + const handleUnauthenticated = () => { + // Note: Don't await the redirect. If you make this component async it + // causes a react error before redirect as it returns a promise of a component rather than a component. + userManager.signinRedirect(); + + // return null because this is used in a react component + return null; + }; + + const navigate = useNavigate(); + + //for multi-tab logout + useEffect(() => { + localStorage.removeItem('signoutEvent'); + const storageEventListener = event => { + const signOutEvent = localStorage.getItem('signoutEvent'); + if (signOutEvent) { + navigate(`/logout?redirect_uri=${encodeURIComponent(window.location.href)}`); + } + }; + + window.addEventListener('storage', storageEventListener); + + return () => { + window.removeEventListener('storage', storageEventListener); + }; + }, []); + + useEffect(() => { + userAuthenticationService.set({ enabled: true }); + + userAuthenticationService.setServiceImplementation({ + getAuthorizationHeader, + handleUnauthenticated, + }); + }, []); + + const oidcAuthority = oidc[0].authority; + + const location = useLocation(); + const { pathname, search } = location; + + const redirectURI = userManager.settings._redirect_uri ?? userManager.settings.redirect_uri; + const silentRedirectURI = + userManager.settings._silent_redirect_uri ?? userManager.settings.silent_redirect_uri; + const postLogoutRedirectURI = + userManager.settings._post_logout_redirect_uri ?? userManager.settings.post_logout_redirect_uri; + + const redirect_uri = new URL(redirectURI).pathname.replace( + routerBasename !== '/' ? routerBasename : '', + '' + ); + const silent_refresh_uri = new URL(silentRedirectURI).pathname; //.replace(routerBasename,'') + const post_logout_redirect_uri = new URL(postLogoutRedirectURI).pathname; //.replace(routerBasename,''); + + // const pathnameRelative = pathname.replace(routerBasename,''); + + if (pathname !== redirect_uri) { + sessionStorage.setItem('ohif-redirect-to', JSON.stringify({ pathname, search })); + } + + return ( + + + console.log('Signout successful')} + errorCallback={error => { + console.warn(error); + console.warn('Signout failed'); + }} + /> + } + /> + { + const { pathname, search = '' } = JSON.parse( + sessionStorage.getItem('ohif-redirect-to') + ); + + userAuthenticationService.setUser(user); + + navigate({ + pathname, + search, + }); + }} + /> + } + /> + + } + /> + } + /> + + ); +} + +export default OpenIdConnectRoutes; diff --git a/platform/app/src/utils/history.ts b/platform/app/src/utils/history.ts new file mode 100644 index 0000000..57cdf7d --- /dev/null +++ b/platform/app/src/utils/history.ts @@ -0,0 +1,9 @@ +import { NavigateFunction } from 'react-router'; + +type History = { + navigate: NavigateFunction; +}; + +export const history: History = { + navigate: null, +}; diff --git a/platform/app/src/utils/isSeriesFilterUsed.ts b/platform/app/src/utils/isSeriesFilterUsed.ts new file mode 100644 index 0000000..dca8f47 --- /dev/null +++ b/platform/app/src/utils/isSeriesFilterUsed.ts @@ -0,0 +1,16 @@ +/** + * This function is used to check if the filter is used. Its intend is to + * warn the user in case of link with a SeriesInstanceUID was called + * @param instances + * @returns + */ +export default function isSeriesFilterUsed(instances, filters) { + const seriesInstanceUIDs = filters?.seriesInstanceUID; + if (!seriesInstanceUIDs) { + return true; + } + if (!instances.length) { + return false; + } + return seriesInstanceUIDs.includes(instances[0].SeriesInstanceUID); +} diff --git a/platform/app/src/utils/legacyOIDCClient.ts b/platform/app/src/utils/legacyOIDCClient.ts new file mode 100644 index 0000000..73a4245 --- /dev/null +++ b/platform/app/src/utils/legacyOIDCClient.ts @@ -0,0 +1,30 @@ +import { UserManager } from 'oidc-client'; + +/** + * Creates a userManager from oidcSettings + * LINK: https://github.com/IdentityModel/oidc-client-js/wiki#configuration + * + * @param {Object} oidcSettings + * @param {string} oidcSettings.authServerUrl, + * @param {string} oidcSettings.clientId, + * @param {string} oidcSettings.authRedirectUri, + * @param {string} oidcSettings.postLogoutRedirectUri, + * @param {string} oidcSettings.responseType, + * @param {string} oidcSettings.extraQueryParams, + */ +export default function getUserManagerForOpenIdConnectClient(oidcSettings) { + if (!oidcSettings) { + return; + } + + const settings = { + ...oidcSettings, + automaticSilentRenew: true, + revokeAccessTokenOnSignout: true, + filterProtocolClaims: true, + }; + + const userManager = new UserManager(settings); + + return userManager; +} diff --git a/platform/app/src/utils/nextOIDCClient.ts b/platform/app/src/utils/nextOIDCClient.ts new file mode 100644 index 0000000..8f42aa9 --- /dev/null +++ b/platform/app/src/utils/nextOIDCClient.ts @@ -0,0 +1,38 @@ +import { UserManager } from 'oidc-client-ts'; + +/** + * Creates a userManager from oidcSettings + * LINK: https://github.com/IdentityModel/oidc-client-js/wiki#configuration + * + * @param {Object} oidcSettings + * @param {string} oidcSettings.authServerUrl, + * @param {string} oidcSettings.clientId, + * @param {string} oidcSettings.authRedirectUri, + * @param {string} oidcSettings.postLogoutRedirectUri, + * @param {string} oidcSettings.responseType, + * @param {string} oidcSettings.extraQueryParams, + */ +export default function getUserManagerForOpenIdConnectClient(oidcSettings) { + if (!oidcSettings) { + return; + } + + if (!oidcSettings.authority || !oidcSettings.client_id || !oidcSettings.redirect_uri) { + console.error('Missing required oidc settings: authority, client_id, redirect_uri'); + return; + } + + const settings = { + ...oidcSettings, + // The next client always use the code flow with PKCE + response_type: 'code', + revokeTokensOnSignout: oidcSettings.revokeAccessTokenOnSignout ?? true, + filterProtocolClaims: true, + // the followings are default values in the lib so no need to set them + // automaticSilentRenew: true, + }; + + const userManager = new UserManager(settings); + + return userManager; +} diff --git a/platform/app/src/utils/preserveQueryParameters.ts b/platform/app/src/utils/preserveQueryParameters.ts new file mode 100644 index 0000000..405e9b9 --- /dev/null +++ b/platform/app/src/utils/preserveQueryParameters.ts @@ -0,0 +1,26 @@ +function preserve(query, current, key) { + const value = current.get(key); + if (value) { + query.append(key, value); + } +} + +export const preserveKeys = ['configUrl', 'multimonitor', 'screenNumber']; + +export function preserveQueryParameters( + query, + current = new URLSearchParams(window.location.search) +) { + for (const key of preserveKeys) { + preserve(query, current, key); + } +} + +export function preserveQueryStrings(query, current = new URLSearchParams(window.location.search)) { + for (const key of preserveKeys) { + const value = current.get(key); + if (value) { + query[key] = value; + } + } +} diff --git a/platform/app/src/utils/publicUrl.ts b/platform/app/src/utils/publicUrl.ts new file mode 100644 index 0000000..47ed7c6 --- /dev/null +++ b/platform/app/src/utils/publicUrl.ts @@ -0,0 +1,4 @@ +const publicUrl = (window as any).PUBLIC_URL || '/'; + +export default publicUrl; +export { publicUrl }; diff --git a/platform/app/tailwind.config.js b/platform/app/tailwind.config.js new file mode 100644 index 0000000..1ba3239 --- /dev/null +++ b/platform/app/tailwind.config.js @@ -0,0 +1,56 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + // Note: in Tailwind 3.0, JIT will purge unused styles by default + // but in development, it is often useful to disable this to see + // and try out all the styles that are available. + // ...(process.env.NODE_ENV === 'development' && { + // safelist: [{ pattern: /.*/ }], + // }), + presets: [require('../ui/tailwind.config.js'), require('../ui-next/tailwind.config.js')], + content: [ + './src/**/*.{jsx,js,ts,tsx, css}', + '../../extensions/**/*.{jsx,js,ts,tsx, css}', + '../ui/src/**/*.{jsx,js,ts,tsx, css}', + '../../modes/**/*.{jsx,js,ts,tsx, css}', + './node_modules/@ohif/ui/src/**/*.{js,jsx,ts,tsx, css}', + '../../node_modules/@ohif/ui/src/**/*.{js,jsx,ts,tsx,css}', + '../../node_modules/@ohif/ui-next/src/**/*.{js,jsx,ts,tsx,css}', + '../../node_modules/@ohif/extension-*/src/**/*.{js,jsx,css, ts,tsx}', + ], + theme: { + fontFamily: { + sans: [ + 'Inter', + 'system-ui', + '-apple-system', + 'BlinkMacSystemFont', + '"Segoe UI"', + 'Roboto', + '"Helvetica Neue"', + 'Arial', + '"Noto Sans"', + 'sans-serif', + '"Apple Color Emoji"', + '"Segoe UI Emoji"', + '"Segoe UI Symbol"', + '"Noto Color Emoji"', + ], + serif: ['Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'], + mono: ['Menlo', 'Monaco', 'Consolas', '"Liberation Mono"', '"Courier New"', 'monospace'], + }, + fontSize: { + xxs: '0.625rem', // 10px + xs: '0.6875rem', // 11px + sm: '0.75rem', // 12px + base: '0.8125rem', // 13px + lg: '0.875rem', // 14px + xl: '1rem', // 16px + // 2xl and above will be updated in an upcoming version + '2xl': '1.5rem', + '3xl': '1.875rem', + '4xl': '2.25rem', + '5xl': '3rem', + '6xl': '4rem', + }, + }, +}; diff --git a/platform/app/tailwind.css b/platform/app/tailwind.css new file mode 100644 index 0000000..4923497 --- /dev/null +++ b/platform/app/tailwind.css @@ -0,0 +1,4 @@ +/* IMPORT CUSTOM FONT */ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/platform/cli/CHANGELOG.md b/platform/cli/CHANGELOG.md new file mode 100644 index 0000000..f8de638 --- /dev/null +++ b/platform/cli/CHANGELOG.md @@ -0,0 +1,3044 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + + +### Features + +* **hotkeys:** Migrate hotkeys to customization service and fix issues with overrides ([#4777](https://github.com/OHIF/Viewers/issues/4777)) ([3e6913b](https://github.com/OHIF/Viewers/commit/3e6913b097569280a5cc2fa5bbe4add52f149305)) + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + + +### Bug Fixes + +* right panel for the create mode cli command ([#4788](https://github.com/OHIF/Viewers/issues/4788)) ([5712e91](https://github.com/OHIF/Viewers/commit/5712e91ca1d939ff3c36615d3cf1a1f6f0051c4f)) + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + + +### Features + +* **dev:** move to rsbuild for dev - faster ([#4674](https://github.com/OHIF/Viewers/issues/4674)) ([d4a4267](https://github.com/OHIF/Viewers/commit/d4a4267429c02916dd51f6aefb290d96dd1c3b04)) + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + + +### Bug Fixes + +* **cli:** missing js ([#4268](https://github.com/OHIF/Viewers/issues/4268)) ([f660f8e](https://github.com/OHIF/Viewers/commit/f660f8e970c0226b34a9de10e2c57429dcce6763)) + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + + +### Bug Fixes + +* **cli:** Fix the cli utilities which require full paths ([d09f8b5](https://github.com/OHIF/Viewers/commit/d09f8b5ba2dcc0c02beb405b8cfa79fbae5bdde8)), closes [#4267](https://github.com/OHIF/Viewers/issues/4267) + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + + +### Bug Fixes + +* **cli:** version txt had a new line which it should not ([#4233](https://github.com/OHIF/Viewers/issues/4233)) ([097ef76](https://github.com/OHIF/Viewers/commit/097ef7665559a672d73e1babfc42afccc3cdd41d)) + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + + +### Bug Fixes + +* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025)) + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + + +### Features + +* **layout:** new layout selector with 3D volume rendering ([#3923](https://github.com/OHIF/Viewers/issues/3923)) ([617043f](https://github.com/OHIF/Viewers/commit/617043fe0da5de91fbea4ac33a27f1df16ae1ca6)) + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + + +### Features + +* **worklist:** New worklist buttons and tooltips ([#3989](https://github.com/OHIF/Viewers/issues/3989)) ([9bcd1ae](https://github.com/OHIF/Viewers/commit/9bcd1ae6f51d61786cc1e99624f396b56a47cd69)) + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + + +### Bug Fixes + +* **cli:** mode creation template ([#3876](https://github.com/OHIF/Viewers/issues/3876)) ([#3981](https://github.com/OHIF/Viewers/issues/3981)) ([e485d68](https://github.com/OHIF/Viewers/commit/e485d68fd4619ce7187113cbe59e47f9523dbcc8)) + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + + +### Bug Fixes + +* address and improve system vulnerabilities ([#3851](https://github.com/OHIF/Viewers/issues/3851)) ([805c532](https://github.com/OHIF/Viewers/commit/805c53270f243ec61f142a3ffa0af500021cd5ec)) + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + + +### Bug Fixes + +* **measurement and microscopy:** various small fixes for measurement and microscopy side panel ([#3696](https://github.com/OHIF/Viewers/issues/3696)) ([c1d5ee7](https://github.com/OHIF/Viewers/commit/c1d5ee7e3f7f4c0c6bed9ae81eba5519741c5155)) + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + + +### Bug Fixes + +* **cli:** Add npm packaged mode not working ([#3689](https://github.com/OHIF/Viewers/issues/3689)) ([28cec04](https://github.com/OHIF/Viewers/commit/28cec04ff43b81e218c3e9addef4665b3833a6fe)) + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + + +### Bug Fixes + +* **cli:** various fixes for adding custom modes and extensions ([#3683](https://github.com/OHIF/Viewers/issues/3683)) ([dc73b18](https://github.com/OHIF/Viewers/commit/dc73b187484da029a2664bb1302f30137c973b8c)) + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + +**Note:** Version bump only for package @ohif/cli + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + +**Note:** Version bump only for package @ohif/cli diff --git a/platform/cli/package.json b/platform/cli/package.json new file mode 100644 index 0000000..86b1f7f --- /dev/null +++ b/platform/cli/package.json @@ -0,0 +1,44 @@ +{ + "name": "@ohif/cli", + "version": "3.10.0-beta.111", + "description": "A CLI to bootstrap new OHIF extension or mode", + "type": "module", + "main": "src/index.js", + "private": true, + "bin": { + "ohif-cli": "src/index.js" + }, + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "cli", + "ohif" + ], + "author": "OHIF Contributors", + "license": "MIT", + "dependencies": { + "@babel/core": "7.24.7", + "axios": "^0.28.0", + "chalk": "^5.0.0", + "execa": "^8.0.1", + "gitignore": "^0.7.0", + "inquirer": "^8.2.0", + "listr": "^0.14.3", + "mustache": "^4.2.0", + "ncp": "^2.0.0", + "node-fetch": "^3.1.1", + "pkg-install": "^1.0.0", + "registry-url": "^6.0.0", + "spdx-license-list": "^6.4.0", + "util": "^0.12.4", + "yarn-programmatic": "^0.1.2" + }, + "files": [ + "bin/", + "src/", + "templates/" + ] +} diff --git a/platform/cli/src/commands/addExtension.js b/platform/cli/src/commands/addExtension.js new file mode 100644 index 0000000..37e2c3d --- /dev/null +++ b/platform/cli/src/commands/addExtension.js @@ -0,0 +1,53 @@ +import Listr from 'listr'; +import chalk from 'chalk'; + +import { + installNPMPackage, + getYarnInfo, + validateExtension, + getVersionedPackageName, + addExtensionToConfig, +} from './utils/index.js'; + +export default async function addExtension(packageName, version) { + console.log(chalk.green.bold(`Adding ohif-extension ${packageName}...`)); + + const versionedPackageName = getVersionedPackageName(packageName, version); + + const tasks = new Listr( + [ + { + title: `Searching for extension: ${versionedPackageName}`, + task: async () => await validateExtension(packageName, version), + }, + { + title: `Installing npm package: ${versionedPackageName}`, + task: async () => await installNPMPackage(packageName, version), + }, + { + title: 'Adding ohif-extension to the configuration file', + task: async ctx => { + const yarnInfo = await getYarnInfo(packageName); + + addExtensionToConfig(packageName, yarnInfo); + + ctx.yarnInfo = yarnInfo; + }, + }, + ], + { + exitOnError: true, + } + ); + + await tasks + .run() + .then(ctx => { + console.log( + `${chalk.green.bold(`Added ohif-extension ${packageName}@${ctx.yarnInfo.version}`)} ` + ); + }) + .catch(error => { + console.log(error.message); + }); +} diff --git a/platform/cli/src/commands/addExtensions.js b/platform/cli/src/commands/addExtensions.js new file mode 100644 index 0000000..cf5252d --- /dev/null +++ b/platform/cli/src/commands/addExtensions.js @@ -0,0 +1,36 @@ +import Listr from 'listr'; +import chalk from 'chalk'; +import addExtension from './addExtension.js'; + +export default async function addExtensions(ohifExtensions) { + // Auto generate Listr tasks... + const taskEntries = []; + + ohifExtensions.forEach(({ packageName, version }) => { + const title = `Adding ohif-extension ${packageName}`; + + taskEntries.push({ + title, + task: async () => await addExtension(packageName, version), + }); + }); + + const tasks = new Listr(taskEntries, { + exitOnError: true, + }); + + await tasks + .run() + .then(() => { + let extensonsString = ''; + + ohifExtensions.forEach(({ packageName, version }) => { + extensonsString += ` ${packageName}@${version}`; + }); + + console.log(`${chalk.green.bold(`Extensions added:${extensonsString}`)} `); + }) + .catch(error => { + console.log(error.message); + }); +} diff --git a/platform/cli/src/commands/addMode.js b/platform/cli/src/commands/addMode.js new file mode 100644 index 0000000..827f2e8 --- /dev/null +++ b/platform/cli/src/commands/addMode.js @@ -0,0 +1,66 @@ +import Listr from 'listr'; +import chalk from 'chalk'; + +import { + installNPMPackage, + getYarnInfo, + getVersionedPackageName, + validateMode, + addModeToConfig, + findRequiredOhifExtensionsForMode, +} from './utils/index.js'; +import addExtensions from './addExtensions.js'; + +export default async function addMode(packageName, version) { + console.log(chalk.green.bold(`Adding ohif-mode ${packageName}...`)); + + const versionedPackageName = getVersionedPackageName(packageName, version); + + const tasks = new Listr( + [ + { + title: `Searching for mode: ${versionedPackageName}`, + task: async () => await validateMode(packageName, version), + }, + { + title: `Installing npm package: ${versionedPackageName}`, + task: async () => await installNPMPackage(packageName, version), + }, + { + title: 'Adding ohif-mode to the configuration file', + task: async ctx => { + const yarnInfo = await getYarnInfo(packageName); + + addModeToConfig(packageName, yarnInfo); + + ctx.yarnInfo = yarnInfo; + }, + }, + { + title: 'Detecting required ohif-extensions...', + task: async ctx => { + ctx.ohifExtensions = await findRequiredOhifExtensionsForMode(ctx.yarnInfo); + }, + }, + ], + { + exitOnError: true, + } + ); + + await tasks + .run() + .then(async ctx => { + console.log(`${chalk.green.bold(`Added ohif-mode ${packageName}@${ctx.yarnInfo.version}`)} `); + + const ohifExtensions = ctx.ohifExtensions; + + if (ohifExtensions.length) { + console.log(`${chalk.green.bold(`Installing dependent extensions`)} `); + await addExtensions(ohifExtensions); + } + }) + .catch(error => { + console.log(error.message); + }); +} diff --git a/platform/cli/src/commands/constants/notFound.js b/platform/cli/src/commands/constants/notFound.js new file mode 100644 index 0000000..30bb85e --- /dev/null +++ b/platform/cli/src/commands/constants/notFound.js @@ -0,0 +1 @@ +export default 'Not found'; diff --git a/platform/cli/src/commands/createPackage.js b/platform/cli/src/commands/createPackage.js new file mode 100644 index 0000000..3b1f20a --- /dev/null +++ b/platform/cli/src/commands/createPackage.js @@ -0,0 +1,77 @@ +import Listr from 'listr'; +import chalk from 'chalk'; +import fs from 'fs'; + +import { + createDirectoryContents, + editPackageJson, + createLicense, + createReadme, + initGit, +} from './utils/index.js'; + +const createPackage = async options => { + const { packageType } = options; // extension or mode + + if (fs.existsSync(options.targetDir)) { + console.error( + `%s ${packageType} with the same name already exists in this directory, either delete it or choose a different name`, + chalk.red.bold('ERROR') + ); + process.exit(1); + } + + fs.mkdirSync(options.targetDir); + + const tasks = new Listr( + [ + { + title: 'Copying template files', + task: () => + createDirectoryContents(options.templateDir, options.targetDir, options.prettier), + }, + { + title: 'Editing Package.json with provided information', + task: () => editPackageJson(options), + }, + { + title: 'Creating a License file', + task: () => createLicense(options), + }, + { + title: 'Creating a Readme file', + task: () => createReadme(options), + }, + { + title: 'Initializing a Git Repository', + enabled: () => options.gitRepository, + task: () => initGit(options), + }, + ], + { + exitOnError: true, + } + ); + + await tasks.run(); + console.log(); + console.log(chalk.green(`Done: ${packageType} is ready at`, options.targetDir)); + console.log(); + + console.log(chalk.green(`NOTE: In order to use this ${packageType} for development,`)); + console.log(chalk.green(`run the following command inside the root of the OHIF monorepo`)); + + console.log(); + console.log(chalk.green.bold(` yarn run cli link-${packageType} ${options.targetDir}`)); + console.log(); + console.log( + chalk.yellow("and when you don't need it anymore, run the following command to unlink it") + ); + console.log(); + console.log(chalk.yellow(` yarn run cli unlink-${packageType} ${options.name}`)); + console.log(); + + return true; +}; + +export default createPackage; diff --git a/platform/cli/src/commands/enums/colors.js b/platform/cli/src/commands/enums/colors.js new file mode 100644 index 0000000..566da78 --- /dev/null +++ b/platform/cli/src/commands/enums/colors.js @@ -0,0 +1,8 @@ +const colors = { + LIGHT: '#5acce6', + MAIN: '#0944b3', + DARK: '#090c29', + ACTIVE: '#348cfd', +}; + +export default colors; diff --git a/platform/cli/src/commands/enums/endPoints.js b/platform/cli/src/commands/enums/endPoints.js new file mode 100644 index 0000000..edc7349 --- /dev/null +++ b/platform/cli/src/commands/enums/endPoints.js @@ -0,0 +1,5 @@ +const endPoints = { + NPM_KEYWORD: 'https://registry.npmjs.com/-/v1/search?text=keywords:', +}; + +export default endPoints; diff --git a/platform/cli/src/commands/enums/index.js b/platform/cli/src/commands/enums/index.js new file mode 100644 index 0000000..b586289 --- /dev/null +++ b/platform/cli/src/commands/enums/index.js @@ -0,0 +1,5 @@ +import keywords from './keywords.js'; +import colors from './colors.js'; +import endPoints from './endPoints.js'; + +export { keywords, colors, endPoints }; diff --git a/platform/cli/src/commands/enums/keywords.js b/platform/cli/src/commands/enums/keywords.js new file mode 100644 index 0000000..56df480 --- /dev/null +++ b/platform/cli/src/commands/enums/keywords.js @@ -0,0 +1,6 @@ +const keywords = { + MODE: 'ohif-mode', + EXTENSION: 'ohif-extension', +}; + +export default keywords; diff --git a/platform/cli/src/commands/index.js b/platform/cli/src/commands/index.js new file mode 100644 index 0000000..4101a49 --- /dev/null +++ b/platform/cli/src/commands/index.js @@ -0,0 +1,23 @@ +import createPackage from './createPackage.js'; +import addExtension from './addExtension.js'; +import removeExtension from './removeExtension.js'; +import addMode from './addMode.js'; +import removeMode from './removeMode.js'; +import listPlugins from './listPlugins.js'; +import searchPlugins from './searchPlugins.js'; +import { linkExtension, linkMode } from './linkPackage.js'; +import { unlinkExtension, unlinkMode } from './unlinkPackage.js'; + +export { + createPackage, + addExtension, + removeExtension, + addMode, + removeMode, + listPlugins, + searchPlugins, + linkExtension, + linkMode, + unlinkExtension, + unlinkMode, +}; diff --git a/platform/cli/src/commands/linkPackage.js b/platform/cli/src/commands/linkPackage.js new file mode 100644 index 0000000..25aaec8 --- /dev/null +++ b/platform/cli/src/commands/linkPackage.js @@ -0,0 +1,79 @@ +import fs from 'fs'; +import path from 'path'; +import { execa } from 'execa'; +import { keywords } from './enums/index.js'; +import { validateYarn, addExtensionToConfig, addModeToConfig } from './utils/index.js'; + +async function linkPackage(packageDir, options, addToConfig, keyword) { + const { viewerDirectory } = options; + + // read package.json from packageDir + const file = fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8'); + + // name of the package + const packageJSON = JSON.parse(file); + const packageName = packageJSON.name; + const packageKeywords = packageJSON.keywords; + + // check if package is an extension or a mode + if (!packageKeywords.includes(keyword)) { + throw new Error(`${packageName} is not ${keyword}`); + } + + const version = packageJSON.version; + + // make sure yarn is installed + await validateYarn(); + + // change directory to packageDir and execute yarn link + process.chdir(packageDir); + + let results; + results = await execa(`yarn`, ['link']); + + // change directory to OHIF Platform root and execute yarn link + process.chdir(`${viewerDirectory}/../..`); + + results = await execa(`yarn`, ['link', packageName]); + console.log(results.stdout); + + // Add the node_modules of the linked package so that webpack + // can find the linked package externals if there are + const webpackPwaPath = path.join(viewerDirectory, '.webpack', 'webpack.pwa.js'); + + async function updateWebpackConfig(webpackConfigPath, packageDir) { + const packageNodeModules = path.join(packageDir, 'node_modules'); + const fileContent = await fs.promises.readFile(webpackConfigPath, 'utf8'); + + const newLine = `path.resolve(__dirname, '${packageNodeModules}'),`; + const modifiedFileContent = fileContent.replace( + /(modules:\s*\[)([\s\S]*?)(\])/, + `$1$2 ${newLine.replace(/\\/g, '/')}$3` + ); + + await fs.promises.writeFile(webpackConfigPath, modifiedFileContent); + } + + await updateWebpackConfig(webpackPwaPath, packageDir); + + // change directory to viewer packages and add the config item + process.chdir(viewerDirectory); + addToConfig(packageName, { + version, + }); + + // run prettier on the webpack config + results = await execa(`yarn`, ['prettier', '--write', webpackPwaPath]); +} + +function linkExtension(packageDir, options) { + const keyword = keywords.EXTENSION; + linkPackage(packageDir, options, addExtensionToConfig, keyword); +} + +function linkMode(packageDir, options) { + const keyword = keywords.MODE; + linkPackage(packageDir, options, addModeToConfig, keyword); +} + +export { linkExtension, linkMode }; diff --git a/platform/cli/src/commands/listPlugins.js b/platform/cli/src/commands/listPlugins.js new file mode 100644 index 0000000..039f2ad --- /dev/null +++ b/platform/cli/src/commands/listPlugins.js @@ -0,0 +1,23 @@ +import fs from 'fs'; +import { prettyPrint } from './utils/index.js'; +import { colors } from './enums/index.js'; + +const listPlugins = async configPath => { + const pluginConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); + + const { extensions, modes } = pluginConfig; + + const titleOptions = { color: colors.LIGHT, bold: true }; + const itemsOptions = { color: colors.ACTIVE, bold: true }; + + const extensionsItems = extensions.map( + extension => `${extension.packageName} @ ${extension.version}` + ); + + const modesItems = modes.map(mode => `${mode.packageName} @ ${mode.version}`); + + prettyPrint('Extensions', titleOptions, extensionsItems, itemsOptions); + prettyPrint('Modes', titleOptions, modesItems, itemsOptions); +}; + +export default listPlugins; diff --git a/platform/cli/src/commands/removeExtension.js b/platform/cli/src/commands/removeExtension.js new file mode 100644 index 0000000..e5345b2 --- /dev/null +++ b/platform/cli/src/commands/removeExtension.js @@ -0,0 +1,46 @@ +import chalk from 'chalk'; +import Listr from 'listr'; + +import { + uninstallNPMPackage, + throwIfExtensionUsedByInstalledMode, + removeExtensionFromConfig, + validateExtensionYarnInfo, +} from './utils/index.js'; + +export default async function removeExtension(packageName) { + console.log(chalk.green.bold(`Removing ohif-extension ${packageName}...`)); + + const tasks = new Listr( + [ + { + title: `Searching for installed extension: ${packageName}`, + task: async () => await validateExtensionYarnInfo(packageName), + }, + { + title: `Checking if ${packageName} is in use by an installed mode`, + task: async () => await throwIfExtensionUsedByInstalledMode(packageName), + }, + { + title: `Uninstalling npm package: ${packageName}`, + task: async () => await uninstallNPMPackage(packageName), + }, + { + title: 'Removing ohif-extension from the configuration file', + task: async () => removeExtensionFromConfig(packageName), + }, + ], + { + exitOnError: true, + } + ); + + await tasks + .run() + .then(() => { + console.log(`${chalk.green.bold(`Removed ohif-extension ${packageName}`)} `); + }) + .catch(error => { + console.log(error.message); + }); +} diff --git a/platform/cli/src/commands/removeExtensions.js b/platform/cli/src/commands/removeExtensions.js new file mode 100644 index 0000000..951af30 --- /dev/null +++ b/platform/cli/src/commands/removeExtensions.js @@ -0,0 +1,36 @@ +import Listr from 'listr'; +import chalk from 'chalk'; +import removeExtension from './removeExtension.js'; + +export default async function removeExtensions(ohifExtensionsToRemove) { + // Auto generate Listr tasks... + const taskEntries = []; + + ohifExtensionsToRemove.forEach(packageName => { + const title = `Removing ohif-extension ${packageName}`; + + taskEntries.push({ + title, + task: async () => await removeExtension(packageName), + }); + }); + + const tasks = new Listr(taskEntries, { + exitOnError: true, + }); + + await tasks + .run() + .then(() => { + let extensonsString = ''; + + ohifExtensionsToRemove.forEach(packageName => { + extensonsString += ` ${packageName}`; + }); + + console.log(`${chalk.green.bold(`Extensions removed:${extensonsString}`)} `); + }) + .catch(error => { + console.log(error.message); + }); +} diff --git a/platform/cli/src/commands/removeMode.js b/platform/cli/src/commands/removeMode.js new file mode 100644 index 0000000..7545583 --- /dev/null +++ b/platform/cli/src/commands/removeMode.js @@ -0,0 +1,68 @@ +import Listr from 'listr'; +import chalk from 'chalk'; + +import { + uninstallNPMPackage, + findOhifExtensionsToRemoveAfterRemovingMode, + removeModeFromConfig, + validateModeYarnInfo, + getYarnInfo, +} from './utils/index.js'; +import removeExtensions from './removeExtensions.js'; + +export default async function removeMode(packageName) { + console.log(chalk.green.bold(`Removing ohif-mode ${packageName}...`)); + + const tasks = new Listr( + [ + { + title: `Searching for installed mode: ${packageName}`, + task: async ctx => { + ctx.yarnInfo = await getYarnInfo(packageName); + await validateModeYarnInfo(packageName); + }, + }, + { + title: `Uninstalling npm package: ${packageName}`, + task: async () => await uninstallNPMPackage(packageName), + }, + { + title: 'Removing ohif-mode from the configuration file', + task: async () => await removeModeFromConfig(packageName), + }, + { + title: 'Detecting extensions that can be removed...', + task: async ctx => { + ctx.ohifExtensionsToRemove = await findOhifExtensionsToRemoveAfterRemovingMode( + ctx.yarnInfo + ); + }, + }, + ], + { + exitOnError: true, + } + ); + + await tasks + .run() + .then(async ctx => { + // Remove extensions if they aren't used by any other mode. + console.log(`${chalk.green.bold(`Removed ohif-mode ${packageName}`)} `); + + const ohifExtensionsToRemove = ctx.ohifExtensionsToRemove; + + if (ohifExtensionsToRemove.length) { + console.log( + `${chalk.green.bold( + `Removing ${ohifExtensionsToRemove.length} extensions no longer used by any installed mode` + )}` + ); + + await removeExtensions(ohifExtensionsToRemove); + } + }) + .catch(error => { + console.log(error.message); + }); +} diff --git a/platform/cli/src/commands/searchPlugins.js b/platform/cli/src/commands/searchPlugins.js new file mode 100644 index 0000000..ef5e3af --- /dev/null +++ b/platform/cli/src/commands/searchPlugins.js @@ -0,0 +1,57 @@ +import axios from 'axios'; + +import { prettyPrint } from './utils/index.js'; +import { keywords, colors, endPoints } from './enums/index.js'; + +async function searchRegistry(keyword) { + const url = `${endPoints.NPM_KEYWORD}${keyword}`; + + try { + const response = await axios.get(url); + const { objects } = response.data; + return objects; + } catch (error) { + console.log(error); + } +} + +async function searchPlugins(options) { + const { verbose } = options; + + const extensions = await searchRegistry(keywords.EXTENSION); + const modes = await searchRegistry(keywords.MODE); + + const titleOptions = { color: colors.LIGHT, bold: true }; + const itemsOptions = {}; + + const extensionsItems = extensions.map(extension => { + const item = [ + `${extension.package.name} @ ${extension.package.version}`, + [`Description: ${extension.package.description}`], + ]; + + if (verbose) { + item[1].push(`Repository: ${extension.package.links.repository}`); + } + + return item; + }); + + const modesItems = modes.map(mode => { + const item = [ + `${mode.package.name} @ ${mode.package.version}`, + [`Description: ${mode.package.description}`], + ]; + + if (verbose) { + item[1].push(`Repository: ${mode.package.links.repository}`); + } + + return item; + }); + + prettyPrint('Extensions', titleOptions, extensionsItems, itemsOptions); + prettyPrint('Modes', titleOptions, modesItems, itemsOptions); +} + +export default searchPlugins; diff --git a/platform/cli/src/commands/unlinkPackage.js b/platform/cli/src/commands/unlinkPackage.js new file mode 100644 index 0000000..8aaa9f8 --- /dev/null +++ b/platform/cli/src/commands/unlinkPackage.js @@ -0,0 +1,66 @@ +import { execa } from 'execa'; +import fs from 'fs'; +import path from 'path'; +import { validateYarn, removeExtensionFromConfig, removeModeFromConfig } from './utils/index.js'; + +const linkPackage = async (packageName, options, removeFromConfig) => { + const { viewerDirectory } = options; + + // make sure yarn is installed + await validateYarn(); + + // change directory to OHIF Platform root and execute yarn link + process.chdir(viewerDirectory); + + const results = await execa(`yarn`, ['unlink', packageName]); + console.log(results.stdout); + + const webpackPwaPath = path.join(viewerDirectory, '.webpack', 'webpack.pwa.js'); + + await removePathFromWebpackConfig(webpackPwaPath, packageName); + + //update the plugin.json file + removeFromConfig(packageName); + + // run prettier on the webpack config + await execa(`yarn`, ['prettier', '--write', webpackPwaPath]); +}; + +async function removePathFromWebpackConfig(webpackConfigPath, packageName) { + const fileContent = await fs.promises.readFile(webpackConfigPath, 'utf8'); + + const packageNameSubstring = `${packageName}/node_modules`; + const pathResolveStart = 'path.resolve('; + const closingParenthesis = ')'; + + let startIndex = fileContent.indexOf(packageNameSubstring); + + if (startIndex === -1) { + return; + } + + // Find the start of the "path.resolve" line. + startIndex = fileContent.lastIndexOf(pathResolveStart, startIndex); + + // Find the end of the line with the closing parenthesis. + let endIndex = fileContent.indexOf(closingParenthesis, startIndex) + 1; + + // Check if there's a comma after the closing parenthesis and remove it as well. + if (fileContent[endIndex] === ',') { + endIndex++; + } + + const modifiedFileContent = fileContent.slice(0, startIndex) + fileContent.slice(endIndex); + + await fs.promises.writeFile(webpackConfigPath, modifiedFileContent); +} + +function unlinkExtension(extensionName, options) { + linkPackage(extensionName, options, removeExtensionFromConfig); +} + +function unlinkMode(modeName, options) { + linkPackage(modeName, options, removeModeFromConfig); +} + +export { unlinkExtension, unlinkMode }; diff --git a/platform/cli/src/commands/utils/addToConfig.js b/platform/cli/src/commands/utils/addToConfig.js new file mode 100644 index 0000000..45b4f34 --- /dev/null +++ b/platform/cli/src/commands/utils/addToConfig.js @@ -0,0 +1,34 @@ +import { + addExtensionToConfigJson, + addModeToConfigJson, + readPluginConfigFile, + writePluginConfigFile, +} from './private/index.js'; + +function addToAndOverwriteConfig(packageName, options, augmentConfigFunction) { + const installedVersion = options.version; + let pluginConfig = readPluginConfigFile(); + + if (!pluginConfig) { + pluginConfig = { + extensions: [], + modes: [], + }; + } + + augmentConfigFunction(pluginConfig, { + packageName, + version: installedVersion, + }); + writePluginConfigFile(pluginConfig); +} + +function addExtensionToConfig(packageName, options) { + addToAndOverwriteConfig(packageName, options, addExtensionToConfigJson); +} + +function addModeToConfig(packageName, options) { + addToAndOverwriteConfig(packageName, options, addModeToConfigJson); +} + +export { addExtensionToConfig, addModeToConfig }; diff --git a/platform/cli/src/commands/utils/createDirectoryContents.js b/platform/cli/src/commands/utils/createDirectoryContents.js new file mode 100644 index 0000000..4ea3d65 --- /dev/null +++ b/platform/cli/src/commands/utils/createDirectoryContents.js @@ -0,0 +1,40 @@ +import fs from 'fs'; + +// https://github.dev/leoroese/template-cli/blob/628dd24db7df399ebb520edd0bc301bc7b5e8b66/index.js#L19 +const createDirectoryContents = (templatePath, targetDirPath, copyPrettierRules) => { + const filesToCreate = fs.readdirSync(templatePath); + + filesToCreate.forEach(file => { + if (!copyPrettierRules && file === '.prettierrc') { + return; + } + + const origFilePath = `${templatePath}/${file}`; + + // get stats about the current file + const stats = fs.statSync(origFilePath); + + if (stats.isFile()) { + const contents = fs.readFileSync(origFilePath, 'utf8'); + + // Rename + if (file === '.npmignore') { + file = '.gitignore'; + } + + const writePath = `${targetDirPath}/${file}`; + fs.writeFileSync(writePath, contents, 'utf8'); + } else if (stats.isDirectory()) { + fs.mkdirSync(`${targetDirPath}/${file}`); + + // recursive call + createDirectoryContents( + `${templatePath}/${file}`, + `${targetDirPath}/${file}`, + copyPrettierRules + ); + } + }); +}; + +export default createDirectoryContents; diff --git a/platform/cli/src/commands/utils/createLicense.js b/platform/cli/src/commands/utils/createLicense.js new file mode 100644 index 0000000..b48bde8 --- /dev/null +++ b/platform/cli/src/commands/utils/createLicense.js @@ -0,0 +1,31 @@ +import chalk from 'chalk'; +import fs from 'fs'; +import path from 'path'; +import { promisify } from 'util'; +import spdxLicenseList from 'spdx-license-list/full.js'; + +const writeFile = promisify(fs.writeFile); + +async function createLicense(options) { + const { targetDir, name, email } = options; + const targetPath = path.join(targetDir, 'LICENSE'); + + let license; + try { + license = spdxLicenseList[options.license]; + } catch (err) { + console.error( + '%s License %s not found in the list of licenses', + chalk.red.bold('ERROR'), + options.license + ); + process.exit(1); + } + + const licenseContent = license.licenseText + .replace('', new Date().getFullYear()) + .replace('', `${name} (${email})`); + return writeFile(targetPath, licenseContent, 'utf8'); +} + +export default createLicense; diff --git a/platform/cli/src/commands/utils/createReadme.js b/platform/cli/src/commands/utils/createReadme.js new file mode 100644 index 0000000..ab4a72d --- /dev/null +++ b/platform/cli/src/commands/utils/createReadme.js @@ -0,0 +1,23 @@ +import fs from 'fs'; +import path from 'path'; +import { promisify } from 'util'; +import mustache from 'mustache'; + +const writeFile = promisify(fs.writeFile); + +async function createReadme(options) { + let template = `# {{name}} \n## Description \n{{description}} \n## Author \n{{author}} \n## License \n{{license}}`; + const { name, description, author, license, targetDir } = options; + const targetPath = path.join(targetDir, 'README.md'); + + const readmeContent = mustache.render(template, { + name, + description, + author, + license, + }); + + return writeFile(targetPath, readmeContent, 'utf8'); +} + +export default createReadme; diff --git a/platform/cli/src/commands/utils/editPackageJson.js b/platform/cli/src/commands/utils/editPackageJson.js new file mode 100644 index 0000000..e29da6a --- /dev/null +++ b/platform/cli/src/commands/utils/editPackageJson.js @@ -0,0 +1,38 @@ +import fs from 'fs'; +import path from 'path'; + +async function editPackageJson(options) { + const { name, version, description, author, license, targetDir } = options; + + const ohifVersion = fs.readFileSync('./version.txt', 'utf8').trim(); + + // read package.json from targetDir + const dependenciesPath = path.join(targetDir, 'dependencies.json'); + const rawData = fs.readFileSync(dependenciesPath, 'utf8'); + + const dataWithOHIFVersion = rawData.replace(/\{LATEST_OHIF_VERSION\}/g, ohifVersion); + const packageJson = JSON.parse(dataWithOHIFVersion); + + // edit package.json + const mergedObj = Object.assign( + { + name, + version, + description, + author, + license, + main: `dist/umd/${name}/index.umd.js`, + files: ['dist/**', 'public/**', 'README.md'], + }, + packageJson + ); + + // write package.json back to targetDir + const writePath = path.join(targetDir, 'package.json'); + fs.writeFileSync(writePath, JSON.stringify(mergedObj, null, 2)); + + // remove the dependencies.json file + fs.unlinkSync(dependenciesPath); +} + +export default editPackageJson; diff --git a/platform/cli/src/commands/utils/findOhifExtensionsToRemoveAfterRemovingMode.js b/platform/cli/src/commands/utils/findOhifExtensionsToRemoveAfterRemovingMode.js new file mode 100644 index 0000000..afb295c --- /dev/null +++ b/platform/cli/src/commands/utils/findOhifExtensionsToRemoveAfterRemovingMode.js @@ -0,0 +1,59 @@ +import { readPluginConfigFile } from './private/index.js'; +import getYarnInfo from './getYarnInfo.js'; + +export default async function findOhifExtensionsToRemoveAfterRemovingMode(removedModeYarnInfo) { + const pluginConfig = readPluginConfigFile(); + + if (!pluginConfig) { + // No other modes or extensions, no action item. + return []; + } + + const { modes, extensions } = pluginConfig; + + const registeredExtensions = extensions.map(extension => extension.packageName); + // TODO this is not a function + const ohifExtensionsOfMode = Object.keys(removedModeYarnInfo.peerDependencies).filter( + peerDependency => registeredExtensions.includes(peerDependency) + ); + + const ohifExtensionsUsedInOtherModes = ohifExtensionsOfMode.map(packageName => { + return { + packageName, + used: false, + }; + }); + + // Check if other modes use each extension used by this mode + const otherModes = modes.filter(mode => mode.packageName !== removedModeYarnInfo.name); + + for (let i = 0; i < otherModes.length; i++) { + const mode = otherModes[i]; + const yarnInfo = await getYarnInfo(mode.packageName); + + const peerDependencies = yarnInfo.peerDependencies; + + if (!peerDependencies) { + continue; + } + + for (let j = 0; j < ohifExtensionsUsedInOtherModes.length; j++) { + const ohifExtension = ohifExtensionsUsedInOtherModes[j]; + if (ohifExtension.used) { + // Already accounted that we can't delete this, so don't waste effort + return; + } + + if (Object.keys(peerDependencies).includes(ohifExtension.packageName)) { + ohifExtension.used = true; + } + } + } + + // Return list of now unused extensions + const ohifExtensionsToRemove = ohifExtensionsUsedInOtherModes + .filter(ohifExtension => !ohifExtension.used) + .map(ohifExtension => ohifExtension.packageName); + + return ohifExtensionsToRemove; +} diff --git a/platform/cli/src/commands/utils/findRequiredOhifExtensionsForMode.js b/platform/cli/src/commands/utils/findRequiredOhifExtensionsForMode.js new file mode 100644 index 0000000..f895abd --- /dev/null +++ b/platform/cli/src/commands/utils/findRequiredOhifExtensionsForMode.js @@ -0,0 +1,41 @@ +import { validateExtension } from './validate.js'; + +export default async function findRequiredOhifExtensionsForMode(yarnInfo) { + // Get yarn info file and get peer dependencies + if (!yarnInfo.peerDependencies) { + // No ohif-extension dependencies + return; + } + + const peerDependencies = yarnInfo.peerDependencies; + const dependencies = []; + const ohifExtensions = []; + + Object.keys(peerDependencies).forEach(packageName => { + dependencies.push({ + packageName, + version: peerDependencies[packageName], + }); + }); + + const promises = []; + + // Fetch each npm json and check which are ohif extensions + for (let i = 0; i < dependencies.length; i++) { + const dependency = dependencies[i]; + const { packageName, version } = dependency; + const promise = validateExtension(packageName, version) + .then(() => { + ohifExtensions.push({ packageName, version }); + }) + .catch(() => {}); + + promises.push(promise); + } + + // Await all the extensions // TODO -> Improve so we async install each + // extension and await all of those promises instead. + await Promise.all(promises); + + return ohifExtensions; +} diff --git a/platform/cli/src/commands/utils/getVersionedPackageName.js b/platform/cli/src/commands/utils/getVersionedPackageName.js new file mode 100644 index 0000000..900d7aa --- /dev/null +++ b/platform/cli/src/commands/utils/getVersionedPackageName.js @@ -0,0 +1,3 @@ +export default function getVersionedPackageName(packageName, version) { + return version === undefined ? packageName : `${packageName}@${version}`; +} diff --git a/platform/cli/src/commands/utils/getYarnInfo.js b/platform/cli/src/commands/utils/getYarnInfo.js new file mode 100644 index 0000000..d5f3ce5 --- /dev/null +++ b/platform/cli/src/commands/utils/getYarnInfo.js @@ -0,0 +1,5 @@ +import { info } from 'yarn-programmatic'; + +export default async function getYarnInfo(packageName) { + return await info(packageName); +} diff --git a/platform/cli/src/commands/utils/index.js b/platform/cli/src/commands/utils/index.js new file mode 100644 index 0000000..ee32c1e --- /dev/null +++ b/platform/cli/src/commands/utils/index.js @@ -0,0 +1,47 @@ +import getVersionedPackageName from './getVersionedPackageName.js'; +import installNPMPackage from './installNPMPackage.js'; +import uninstallNPMPackage from './uninstallNPMPackage.js'; +import { + validateMode, + validateExtension, + validateModeYarnInfo, + validateExtensionYarnInfo, +} from './validate.js'; +import getYarnInfo from './getYarnInfo.js'; +import { addExtensionToConfig, addModeToConfig } from './addToConfig.js'; +import findRequiredOhifExtensionsForMode from './findRequiredOhifExtensionsForMode.js'; +import { removeExtensionFromConfig, removeModeFromConfig } from './removeFromConfig.js'; +import throwIfExtensionUsedByInstalledMode from './throwIfExtensionUsedByInstalledMode.js'; +import findOhifExtensionsToRemoveAfterRemovingMode from './findOhifExtensionsToRemoveAfterRemovingMode.js'; +import initGit from './initGit.js'; +import createDirectoryContents from './createDirectoryContents.js'; +import editPackageJson from './editPackageJson.js'; +import createLicense from './createLicense.js'; +import createReadme from './createReadme.js'; +import prettyPrint from './prettyPrint.js'; +import validateYarn from './validateYarn.js'; + +export { + getYarnInfo, + getVersionedPackageName, + installNPMPackage, + uninstallNPMPackage, + validateMode, + validateExtension, + validateModeYarnInfo, + validateExtensionYarnInfo, + addExtensionToConfig, + addModeToConfig, + findRequiredOhifExtensionsForMode, + removeExtensionFromConfig, + throwIfExtensionUsedByInstalledMode, + removeModeFromConfig, + findOhifExtensionsToRemoveAfterRemovingMode, + initGit, + createDirectoryContents, + editPackageJson, + createLicense, + createReadme, + prettyPrint, + validateYarn, +}; diff --git a/platform/cli/src/commands/utils/initGit.js b/platform/cli/src/commands/utils/initGit.js new file mode 100644 index 0000000..88686a5 --- /dev/null +++ b/platform/cli/src/commands/utils/initGit.js @@ -0,0 +1,35 @@ +import chalk from 'chalk'; +import fs from 'fs'; +import path from 'path'; +import { promisify } from 'util'; +import { execa } from 'execa'; + +const exists = promisify(fs.exists); + +async function initGit(options) { + const { targetDir } = options; + const targetPath = path.join(targetDir, '.git'); + + // Check if git is installed + try { + await execa('git', ['--version']); + } catch (err) { + console.error( + '%s Git is not installed. Please install git and try again.', + chalk.red.bold('ERROR') + ); + process.exit(1); + } + + if (!(await exists(targetPath))) { + try { + await execa('git', ['init'], { cwd: targetDir }); + } catch (err) { + console.error('%s Failed to initialize git', chalk.red.bold('ERROR')); + console.error(err); + process.exit(1); + } + } +} + +export default initGit; diff --git a/platform/cli/src/commands/utils/installNPMPackage.js b/platform/cli/src/commands/utils/installNPMPackage.js new file mode 100644 index 0000000..fd125a1 --- /dev/null +++ b/platform/cli/src/commands/utils/installNPMPackage.js @@ -0,0 +1,14 @@ +import { install } from 'pkg-install'; + +const installNPMPackage = async (packageName, version) => { + let installObject = {}; + + installObject[packageName] = version; + + await install(installObject, { + prefer: 'yarn', + cwd: process.cwd(), + }); +}; + +export default installNPMPackage; diff --git a/platform/cli/src/commands/utils/prettyPrint.js b/platform/cli/src/commands/utils/prettyPrint.js new file mode 100644 index 0000000..ad7d9a1 --- /dev/null +++ b/platform/cli/src/commands/utils/prettyPrint.js @@ -0,0 +1,79 @@ +import chalk from 'chalk'; +import { colors } from '../enums/index.js'; + +function getStyle({ color, bold }) { + return bold ? chalk.hex(color).bold : chalk.hex(color); +} + +function levelOnePrint(items) { + let output = ''; + if (Array.isArray(items)) { + items.forEach(item => { + output += ` |- ${item}\n`; + }); + return output; + } + + return ` |- ${items}\n`; +} + +function levelTwoPrint(items) { + let output = ''; + items.forEach(item => { + output += ` | |- ${item}\n`; + }); + return output; +} + +/** + * + * @param {string} title Title of the section + * @param {object} titleOptions Options for the title includes color and bold + * @param { [] | [][] } items Array of items to display, OR a list of lists + * @param {object} itemOptions Options for the items includes color and bold + * + * + * items= ['Mode-A', 'Mode-B', 'Mode-C'] + * + * |- Mode-A + * |- Mode-B + * |- Mode-C + * + * items = [['Mode-A', ['Description-A', 'Authors-A', 'Repository-A]], ['Mode-B', ['Description-B', 'Authors-B', 'Repository-B]], ['Mode-C', ['Description-C', 'Authors-C', 'Repository-C]]] + * + * |- Mode-A + * | |- Description-A + * | |- Authors-A + * | |- Repository-A + * | + * |- Mode-B + * | |- Description-B + * | |- Authors-B + * | |- Repository-B + * + * + */ +function prettyPrint( + title, + titleOptions = { color: colors.MAIN, bold: true }, + itemsArray = [[]], + itemOptions = {} +) { + console.log(''); + console.log(getStyle(titleOptions)(title)); + + let output = ''; + itemsArray.forEach(items => { + if (!Array.isArray(items)) { + output += levelOnePrint(items); + } else { + output += levelOnePrint(items[0]); + output += levelTwoPrint(items[1]); + } + }); + + const itmeStyle = itemOptions.color ? getStyle(itemOptions)(output) : output; + console.log(itmeStyle); +} + +export default prettyPrint; diff --git a/platform/cli/src/commands/utils/private/getPackageNameAndScope.js b/platform/cli/src/commands/utils/private/getPackageNameAndScope.js new file mode 100644 index 0000000..14c05f6 --- /dev/null +++ b/platform/cli/src/commands/utils/private/getPackageNameAndScope.js @@ -0,0 +1,15 @@ +export default function getPackageNameAndScope(packageName) { + let scope; + let packageNameLessScope; + + if (packageName.includes('@')) { + [scope, packageNameLessScope] = packageName.split('/'); + } else { + packageNameLessScope = packageName; + } + + return { + scope, + packageNameLessScope, + }; +} diff --git a/platform/cli/src/commands/utils/private/index.js b/platform/cli/src/commands/utils/private/index.js new file mode 100644 index 0000000..907418e --- /dev/null +++ b/platform/cli/src/commands/utils/private/index.js @@ -0,0 +1,19 @@ +import getPackageNameAndScope from './getPackageNameAndScope.js'; +import { + addExtensionToConfigJson, + removeExtensionFromConfigJson, + addModeToConfigJson, + removeModeFromConfigJson, +} from './manipulatePluginConfigFile.js'; +import writePluginConfigFile from './writePluginConfigFile.js'; +import readPluginConfigFile from './readPluginConfigFile.js'; + +export { + getPackageNameAndScope, + addExtensionToConfigJson, + removeExtensionFromConfigJson, + addModeToConfigJson, + removeModeFromConfigJson, + readPluginConfigFile, + writePluginConfigFile, +}; diff --git a/platform/cli/src/commands/utils/private/manipulatePluginConfigFile.js b/platform/cli/src/commands/utils/private/manipulatePluginConfigFile.js new file mode 100644 index 0000000..119314d --- /dev/null +++ b/platform/cli/src/commands/utils/private/manipulatePluginConfigFile.js @@ -0,0 +1,38 @@ +function addExtensionToConfigJson(pluginConfig, { packageName, version }) { + addToList('extensions', pluginConfig, { packageName, version }); +} + +function addModeToConfigJson(pluginConfig, { packageName, version }) { + addToList('modes', pluginConfig, { packageName, version }); +} + +function removeExtensionFromConfigJson(pluginConfig, { packageName }) { + removeFromList('extensions', pluginConfig, { packageName }); +} + +function removeModeFromConfigJson(pluginConfig, { packageName }) { + removeFromList('modes', pluginConfig, { packageName }); +} + +function removeFromList(listName, pluginConfig, { packageName }) { + const list = pluginConfig[listName]; + + const indexOfExistingEntry = list.findIndex(entry => entry.packageName === packageName); + + if (indexOfExistingEntry !== -1) { + pluginConfig[listName].splice(indexOfExistingEntry, 1); + } +} + +function addToList(listName, pluginConfig, { packageName, version }) { + removeFromList(listName, pluginConfig, { packageName }); + + pluginConfig[listName].push({ packageName, version }); +} + +export { + addExtensionToConfigJson, + addModeToConfigJson, + removeExtensionFromConfigJson, + removeModeFromConfigJson, +}; diff --git a/platform/cli/src/commands/utils/private/readPluginConfigFile.js b/platform/cli/src/commands/utils/private/readPluginConfigFile.js new file mode 100644 index 0000000..07193fe --- /dev/null +++ b/platform/cli/src/commands/utils/private/readPluginConfigFile.js @@ -0,0 +1,17 @@ +import fs from 'fs'; + +export default function readPluginConfigFile() { + let fileContents; + + try { + fileContents = fs.readFileSync('./pluginConfig.json', { flag: 'r' }); + } catch (err) { + return; // File doesn't exist yet. + } + + if (fileContents) { + fileContents = JSON.parse(fileContents); + } + + return fileContents; +} diff --git a/platform/cli/src/commands/utils/private/writePluginConfigFile.js b/platform/cli/src/commands/utils/private/writePluginConfigFile.js new file mode 100644 index 0000000..9ff9a4b --- /dev/null +++ b/platform/cli/src/commands/utils/private/writePluginConfigFile.js @@ -0,0 +1,18 @@ +import fs from 'fs'; + +export default function writePluginConfigFile(pluginConfig) { + // Note: Second 2 arguments are to pretty print the JSON so its human readable. + const jsonStringOfFileContents = JSON.stringify(pluginConfig, null, 2); + + fs.writeFileSync( + `./pluginConfig.json`, + jsonStringOfFileContents + '\n', // Add a newline character at the end + { flag: 'w+' }, + err => { + if (err) { + console.error(err); + return; + } + } + ); +} diff --git a/platform/cli/src/commands/utils/removeFromConfig.js b/platform/cli/src/commands/utils/removeFromConfig.js new file mode 100644 index 0000000..820cd3e --- /dev/null +++ b/platform/cli/src/commands/utils/removeFromConfig.js @@ -0,0 +1,26 @@ +import { + removeExtensionFromConfigJson, + removeModeFromConfigJson, + writePluginConfigFile, + readPluginConfigFile, +} from './private/index.js'; + +function removeFromAndOverwriteConfig(packageName, augmentConfigFunction) { + const pluginConfig = readPluginConfigFile(); + + // Note: if file is not found, nothing to remove. + if (pluginConfig) { + augmentConfigFunction(pluginConfig, { packageName }); + writePluginConfigFile(pluginConfig); + } +} + +function removeExtensionFromConfig(packageName) { + removeFromAndOverwriteConfig(packageName, removeExtensionFromConfigJson); +} + +function removeModeFromConfig(packageName) { + removeFromAndOverwriteConfig(packageName, removeModeFromConfigJson); +} + +export { removeExtensionFromConfig, removeModeFromConfig }; diff --git a/platform/cli/src/commands/utils/throwIfExtensionUsedByInstalledMode.js b/platform/cli/src/commands/utils/throwIfExtensionUsedByInstalledMode.js new file mode 100644 index 0000000..63ce1ba --- /dev/null +++ b/platform/cli/src/commands/utils/throwIfExtensionUsedByInstalledMode.js @@ -0,0 +1,48 @@ +import { readPluginConfigFile } from './private/index.js'; +import getYarnInfo from './getYarnInfo.js'; +import chalk from 'chalk'; + +export default async function throwIfExtensionUsedByInstalledMode(packageName) { + const pluginConfig = readPluginConfigFile(); + + if (!pluginConfig) { + // No other modes, not in use + return false; + } + + const { modes } = pluginConfig; + + const modesUsingExtension = []; + + for (let i = 0; i < modes.length; i++) { + const mode = modes[i]; + const modePackageName = mode.packageName; + const yarnInfo = await getYarnInfo(modePackageName); + + const peerDependencies = yarnInfo.peerDependencies; + + if (!peerDependencies) { + continue; + } + + if (Object.keys(peerDependencies).includes(packageName)) { + modesUsingExtension.push(modePackageName); + } + } + + if (modesUsingExtension.length > 0) { + let modesString = ''; + + modesUsingExtension.forEach(packageName => { + modesString += ` ${packageName}`; + }); + + const error = new Error( + `${chalk.yellow.red( + 'Error' + )} ohif-extension ${packageName} used by installed modes:${modesString}` + ); + + throw error; + } +} diff --git a/platform/cli/src/commands/utils/uninstallNPMPackage.js b/platform/cli/src/commands/utils/uninstallNPMPackage.js new file mode 100644 index 0000000..e8b3271 --- /dev/null +++ b/platform/cli/src/commands/utils/uninstallNPMPackage.js @@ -0,0 +1,12 @@ +import { remove } from 'yarn-programmatic'; + +const uninstallNPMPackage = async packageName => { + // TODO - Anoyingly pkg-install doesn't seem to have uninstall. + // So since we are using yarn we will just use yarn here, but the tool + // is certainly less generic. But its a super minor issue. + await remove(packageName).catch(err => { + console.log(err); + }); +}; + +export default uninstallNPMPackage; diff --git a/platform/cli/src/commands/utils/validate.js b/platform/cli/src/commands/utils/validate.js new file mode 100644 index 0000000..356dd90 --- /dev/null +++ b/platform/cli/src/commands/utils/validate.js @@ -0,0 +1,152 @@ +import registryUrl from 'registry-url'; +import keywords from '../enums/keywords.js'; +import { getPackageNameAndScope } from './private/index.js'; +import chalk from 'chalk'; +import fetch from 'node-fetch'; +import getYarnInfo from './getYarnInfo.js'; +import NOT_FOUND from '../constants/notFound.js'; + +async function validateMode(packageName, version) { + return validate(packageName, version, keywords.MODE); +} + +async function validateExtension(packageName, version) { + return validate(packageName, version, keywords.EXTENSION); +} + +async function validateModeYarnInfo(packageName) { + return validateYarnInfo(packageName, keywords.MODE); +} + +async function validateExtensionYarnInfo(packageName) { + return validateYarnInfo(packageName, keywords.EXTENSION); +} + +function validateYarnInfo(packageName, keyword) { + return new Promise(async (resolve, reject) => { + function rejectIfNotFound() { + const error = new Error(`${chalk.red.bold('Error')} extension ${packageName} not installed`); + reject(error); + } + + const packageInfo = await getYarnInfo(packageName).catch(() => { + rejectIfNotFound(); + }); + + if (!packageInfo) { + rejectIfNotFound(); + return; + } + + const { keywords } = packageInfo; + const isValid = keywords && keywords.includes(keyword); + + if (isValid) { + resolve(true); + } else { + const error = new Error( + `${chalk.red.bold('Error')} package ${packageName} is not an ${keyword}` + ); + reject(error); + } + }); +} + +function getVersion(json, version) { + const versions = Object.keys(json.versions); + // if no version is defined get the latest + if (version === undefined) { + return json['dist-tags'].latest; + } + + // Get and validate version if it is explicitly defined + const allowMinorVersionUpgrade = version.startsWith('^'); + if (!allowMinorVersionUpgrade) { + const isValidVersion = versions.includes(version); + + if (!isValidVersion) { + return; + } + + return version; + } + + // Choose version based on the newer minor/patch versions + const [majorVersion] = version + .split('^')[1] + .split('.') + .map(v => parseInt(v)); + + // Find the version that matches the major version, but is the latest minor version + versions + .filter(version => parseInt(version.split('.')[0]) === majorVersion) + .sort((a, b) => { + const [majorA, minorA, patchA] = a.split('.').map(v => parseInt(v)); + const [majorB, minorB, patchB] = b.split('.').map(v => parseInt(v)); + + if (majorA === majorB) { + if (minorA === minorB) { + return patchB - patchA; + } + + return minorB - minorA; + } + + return majorB - majorA; + }); + + if (versions.length === 0) { + return; + } + + return versions[0]; +} + +function validate(packageName, version, keyword) { + return new Promise(async (resolve, reject) => { + const { scope } = getPackageNameAndScope(packageName); + + // Gets the registry of the package. Scoped packages may not be using the global default. + const registryUrlOfPackage = registryUrl(scope); + let options = {}; + if (process.env.NPM_TOKEN) { + options['headers'] = { + Authorization: `Bearer ${process.env.NPM_TOKEN}`, + }; + } + const response = await fetch(`${registryUrlOfPackage}${packageName}`, options); + const json = await response.json(); + + if (json.error && json.error === NOT_FOUND) { + const error = new Error(`${chalk.red.bold('Error')} package ${packageName} not found`); + reject(error); + return; + } + + const packageVersion = getVersion(json, version); + + if (packageVersion) { + const versionedJson = json.versions[packageVersion]; + const keywords = versionedJson.keywords; + + const isValid = keywords && keywords.includes(keyword); + + if (isValid) { + resolve(true); + } else { + const error = new Error( + `${chalk.red.bold('Error')} package ${packageName} is not an ${keyword}` + ); + reject(error); + } + } else { + // Particular version undefined + const error = new Error( + `${chalk.red.bold('Error')} version ${packageVersion} of package ${packageName} not found` + ); + reject(error); + } + }); +} + +export { validateMode, validateExtension, validateModeYarnInfo, validateExtensionYarnInfo }; diff --git a/platform/cli/src/commands/utils/validateYarn.js b/platform/cli/src/commands/utils/validateYarn.js new file mode 100644 index 0000000..8a74b83 --- /dev/null +++ b/platform/cli/src/commands/utils/validateYarn.js @@ -0,0 +1,14 @@ +import chalk from 'chalk'; +import { execa } from 'execa'; + +export default async function validateYarn() { + try { + await execa('yarn', ['--version']); + } catch (err) { + console.log( + '%s Yarn is not installed, please install it before linking your extension', + chalk.red.bold('ERROR') + ); + process.exit(1); + } +} diff --git a/platform/cli/src/index.js b/platform/cli/src/index.js new file mode 100755 index 0000000..d14445a --- /dev/null +++ b/platform/cli/src/index.js @@ -0,0 +1,203 @@ +#!/usr/bin/env node + +import { Command } from 'commander'; +import inquirer from 'inquirer'; +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; + +import { getPathQuestions, getRepoQuestions } from './questions.js'; +import { + createPackage, + addExtension, + removeExtension, + addMode, + removeMode, + listPlugins, + searchPlugins, + linkExtension, + linkMode, + unlinkExtension, + unlinkMode, +} from './commands/index.js'; +import chalk from 'chalk'; + +const runningDirectory = process.cwd(); +const viewerDirectory = path.resolve(runningDirectory, 'platform/app'); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const packageJsonPath = path.join(runningDirectory, 'package.json'); + +try { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + if (packageJson.name !== 'ohif-monorepo-root') { + console.log(packageJson); + console.log(chalk.red('ohif-cli must run from the root of the OHIF platform')); + process.exit(1); + } +} catch (error) { + console.log(chalk.red('ohif-cli must run from the root of the OHIF platform')); + process.exit(1); +} + +function _createPackage(packageType) { + const pathQuestions = getPathQuestions(packageType); + const repoQuestions = getRepoQuestions(packageType); + + let pathAnswers; + + const askPathQuestions = () => { + inquirer.prompt(pathQuestions).then(answers => { + pathAnswers = answers; + if (pathAnswers.confirm) { + askRepoQuestions(answers.baseDir, answers.name); + } else { + askPathQuestions(); + } + }); + }; + + const askRepoQuestions = () => { + inquirer.prompt(repoQuestions).then(repoAnswers => { + const answers = { + ...pathAnswers, + ...repoAnswers, + }; + + const templateDir = path.join(__dirname, `../templates/${packageType}`); + answers.templateDir = templateDir; + answers.targetDir = path.join(answers.baseDir); + answers.packageType = packageType; + + createPackage(answers); + }); + }; + + askPathQuestions(); +} + +// for now ohif-cli is ran through yarn only. +// see ohif-cli.md section # OHIF Command Line Interface for reference. +const program = new Command('yarn run cli'); +// Todo: inject with webpack +program + .version('2.0.7') + .description('OHIF CLI') + .configureHelp({ sortOptions: true, sortSubcommands: true }) + .showHelpAfterError('(add --help for additional information)'); + +program + .command('create-extension') + .description('Create a new template Extension') + .action(() => { + _createPackage('extension'); + }); + +program + .command('create-mode') + .description('Create a new template Mode') + .action(() => { + _createPackage('mode'); + }); + +program + .command('add-extension [version]') + .description('Adds an OHIF Extension') + .action((packageName, version) => { + // change directory to viewer + process.chdir(viewerDirectory); + addExtension(packageName, version); + }); + +program + .command('remove-extension ') + .description('Removes an OHIF Extension') + .action(packageName => { + // change directory to viewer + process.chdir(viewerDirectory); + removeExtension(packageName); + }); + +program + .command('add-mode [version]') + .description('Add an OHIF Mode') + .action((packageName, version) => { + // change directory to viewer + process.chdir(viewerDirectory); + addMode(packageName, version); + }); + +program + .command('remove-mode ') + .description('Removes an OHIF Mode') + .action(packageName => { + // change directory to viewer + process.chdir(viewerDirectory); + removeMode(packageName); + }); + +program + .command('link-extension ') + .description('Links a local OHIF Extension to the Viewer to be used for development') + .action(packageDir => { + if (!fs.existsSync(packageDir)) { + console.log( + chalk.red('The Extension directory does not exist, please provide a valid directory') + ); + process.exit(1); + } + linkExtension(packageDir, { viewerDirectory }); + }); + +program + .command('unlink-extension ') + .description('Unlinks a local OHIF Extension from the Viewer') + .action(extensionName => { + unlinkExtension(extensionName, { viewerDirectory }); + console.log( + chalk.green( + `Successfully unlinked Extension ${extensionName} from the Viewer, don't forget to run yarn install --force` + ) + ); + }); + +program + .command('link-mode ') + .description('Links a local OHIF Mode to the Viewer to be used for development') + .action(packageDir => { + if (!fs.existsSync(packageDir)) { + console.log(chalk.red('The Mode directory does not exist, please provide a valid directory')); + process.exit(1); + } + linkMode(packageDir, { viewerDirectory }); + }); + +program + .command('unlink-mode ') + .description('Unlinks a local OHIF Mode from the Viewer') + .action(modeName => { + unlinkMode(modeName, { viewerDirectory }); + console.log( + chalk.green( + `Successfully unlinked Mode ${modeName} from the Viewer, don't forget to run yarn install --force` + ) + ); + }); + +program + .command('list') + .description('List Added Extensions and Modes') + .action(() => { + const configPath = path.resolve(viewerDirectory, './pluginConfig.json'); + listPlugins(configPath); + }); + +program + .command('search') + .option('-v, --verbose', 'Verbose output') + .description('Search NPM for the list of Modes and Extensions') + .action(options => { + searchPlugins(options); + }); + +program.parse(process.argv); diff --git a/platform/cli/src/questions.js b/platform/cli/src/questions.js new file mode 100644 index 0000000..03aeec6 --- /dev/null +++ b/platform/cli/src/questions.js @@ -0,0 +1,95 @@ +import path from 'path'; +import os from 'os'; + +function getPathQuestions(packageType) { + return [ + { + type: 'input', + name: 'name', + message: `What is the name of your ${packageType}?`, + validate: input => { + if (!input) { + return 'Please enter a name'; + } + return true; + }, + default: `my-${packageType}`, + }, + { + type: 'input', + name: 'baseDir', + message: `What is the target path to create your ${packageType}?`, + suffix: `\n(we recommend you do not use the OHIF ${packageType} folder (./${packageType}s) unless you are developing a core ${packageType})`, + maxLength: 40, + validate: input => { + if (!input) { + console.log('Please provide a valid target directory path'); + return; + } + return true; + }, + filter: (input, answers) => { + // Replace ~ with the user's home directory + const expandedPath = input.replace(/^~(?=$|\/|\\)/, os.homedir()); + + // Resolve the path to an absolute path + const resolvedPath = path.resolve(expandedPath, answers.name); + + return resolvedPath; + }, + }, + { + type: 'confirm', + name: 'confirm', + message: `Please confirm the above path for generating the ${packageType} folder:`, + }, + ]; +} + +function getRepoQuestions(packageType) { + return [ + { + type: 'confirm', + name: 'gitRepository', + message: 'Should it be a git repository?', + default: false, + }, + { + type: 'confirm', + name: 'prettier', + message: 'Should it follow same prettier rules as OHIF?', + }, + { + type: 'input', + name: 'version', + message: `What is the version of your ${packageType}?`, + default: '0.0.1', + }, + { + type: 'input', + name: 'description', + message: `What is the description of your ${packageType}?`, + default: '', + }, + { + type: 'input', + name: 'author', + message: `Who is the author of your ${packageType}?`, + default: '', + }, + { + type: 'input', + name: 'email', + message: 'What is your email address?', + default: '', + }, + { + type: 'input', + name: 'license', + message: `What is the license of your ${packageType}?`, + default: 'MIT', + }, + ]; +} + +export { getPathQuestions, getRepoQuestions }; diff --git a/platform/cli/templates/extension/.gitignore b/platform/cli/templates/extension/.gitignore new file mode 100644 index 0000000..6704566 --- /dev/null +++ b/platform/cli/templates/extension/.gitignore @@ -0,0 +1,104 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/platform/cli/templates/extension/.prettierrc b/platform/cli/templates/extension/.prettierrc new file mode 100644 index 0000000..ef83baa --- /dev/null +++ b/platform/cli/templates/extension/.prettierrc @@ -0,0 +1,11 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"], + "trailingComma": "es5", + "printWidth": 100, + "proseWrap": "always", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/platform/cli/templates/extension/.webpack/webpack.prod.js b/platform/cli/templates/extension/.webpack/webpack.prod.js new file mode 100644 index 0000000..3a78c48 --- /dev/null +++ b/platform/cli/templates/extension/.webpack/webpack.prod.js @@ -0,0 +1,96 @@ +const path = require('path'); +const pkg = require('../package.json'); + +const outputFile = 'index.umd.js'; +const rootDir = path.resolve(__dirname, '../'); +const outputFolder = path.join(__dirname, `../dist/umd/${pkg.name}/`); + +// Todo: add ESM build for the extension in addition to umd build + +const config = { + mode: 'production', + entry: rootDir + '/' + pkg.module, + devtool: 'source-map', + output: { + path: outputFolder, + filename: outputFile, + library: pkg.name, + libraryTarget: 'umd', + chunkFilename: '[name].chunk.js', + umdNamedDefine: true, + globalObject: "typeof self !== 'undefined' ? self : this", + }, + externals: [ + { + react: { + root: 'React', + commonjs2: 'react', + commonjs: 'react', + amd: 'react', + }, + '@ohif/core': { + commonjs2: '@ohif/core', + commonjs: '@ohif/core', + amd: '@ohif/core', + root: '@ohif/core', + }, + '@ohif/ui': { + commonjs2: '@ohif/ui', + commonjs: '@ohif/ui', + amd: '@ohif/ui', + root: '@ohif/ui', + }, + }, + ], + module: { + + rules: [ + { + test: /\.svg?$/, + oneOf: [ + { + use: [ + { + loader: '@svgr/webpack', + options: { + svgoConfig: { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false + }, + }, + }, + ] + }, + prettier: false, + svgo: true, + titleProp: true, + }, + }, + ], + issuer: { + and: [/\.(ts|tsx|js|jsx|md|mdx)$/], + }, + }, + ], + }, + { + test: /(\.jsx|\.js|\.tsx|\.ts)$/, + loader: 'babel-loader', + exclude: /(node_modules|bower_components)/, + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + }, + ], + }, + resolve: { + modules: [path.resolve('./node_modules'), path.resolve('./src')], + extensions: ['.json', '.js', '.jsx', '.tsx', '.ts'], + }, +}; + +module.exports = config; diff --git a/platform/cli/templates/extension/babel.config.js b/platform/cli/templates/extension/babel.config.js new file mode 100644 index 0000000..8ab9844 --- /dev/null +++ b/platform/cli/templates/extension/babel.config.js @@ -0,0 +1,49 @@ +module.exports = { + plugins: [ + ['@babel/plugin-proposal-class-properties', { loose: true }], + '@babel/plugin-transform-typescript', + ['@babel/plugin-proposal-private-property-in-object', { loose: true }], + ['@babel/plugin-proposal-private-methods', { loose: true }], + ], + env: { + test: { + presets: [ + [ + // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2 + '@babel/preset-env', + { + modules: 'commonjs', + debug: false, + }, + ], + '@babel/preset-react', + '@babel/preset-typescript', + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-regenerator', + '@babel/plugin-transform-runtime', + '@babel/plugin-transform-typescript', + ], + }, + production: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + development: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + }, +}; diff --git a/platform/cli/templates/extension/dependencies.json b/platform/cli/templates/extension/dependencies.json new file mode 100644 index 0000000..df3e928 --- /dev/null +++ b/platform/cli/templates/extension/dependencies.json @@ -0,0 +1,62 @@ +{ + "repository": "OHIF/Viewers", + "keywords": ["ohif-extension"], + "module": "src/index.tsx", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.18.0" + }, + "scripts": { + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:my-extension": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "start": "yarn run dev" + }, + "peerDependencies": { + "@ohif/core": "^{LATEST_OHIF_VERSION}", + "@ohif/extension-default": "^{LATEST_OHIF_VERSION}", + "@ohif/extension-cornerstone": "^{LATEST_OHIF_VERSION}", + "@ohif/i18n": "^1.0.0", + "prop-types": "^15.6.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^12.2.2", + "react-router": "^6.23.1", + "react-router-dom": "^6.23.1", + "webpack": "5.89.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "@babel/runtime": "^7.20.13" + }, + "devDependencies": { + "@babel/core": "7.24.7", + "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-proposal-object-rest-spread": "^7.17.3", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.16.7", + "@babel/plugin-transform-regenerator": "^7.16.7", + "@babel/plugin-transform-runtime": "7.24.7", + "@babel/plugin-transform-typescript": "^7.13.0", + "@babel/preset-env": "7.24.7", + "@babel/preset-react": "^7.16.7", + "@babel/preset-typescript": "^7.13.0", + "@babel/plugin-proposal-private-property-in-object": "7.21.11", + "babel-eslint": "9.x", + "babel-loader": "^8.2.4", + "@svgr/webpack": "^8.1.0", + "babel-plugin-module-resolver": "^5.0.0", + "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^10.2.0", + "cross-env": "^7.0.3", + "dotenv": "^14.1.0", + "eslint": "^8.39.0", + "eslint-loader": "^2.0.0", + "webpack": "5.89.0", + "webpack-merge": "^5.7.3", + "webpack-cli": "^5.0.2" + } +} diff --git a/platform/cli/templates/extension/src/id.js b/platform/cli/templates/extension/src/id.js new file mode 100644 index 0000000..ebe5acd --- /dev/null +++ b/platform/cli/templates/extension/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/platform/cli/templates/extension/src/index.tsx b/platform/cli/templates/extension/src/index.tsx new file mode 100644 index 0000000..24eff3f --- /dev/null +++ b/platform/cli/templates/extension/src/index.tsx @@ -0,0 +1,86 @@ +import { id } from './id'; + +/** + * You can remove any of the following modules if you don't need them. + */ +export default { + /** + * Only required property. Should be a unique value across all extensions. + * You ID can be anything you want, but it should be unique. + */ + id, + + /** + * Perform any pre-registration tasks here. This is called before the extension + * is registered. Usually we run tasks such as: configuring the libraries + * (e.g. cornerstone, cornerstoneTools, ...) or registering any services that + * this extension is providing. + */ + preRegistration: ({ servicesManager, commandsManager, configuration = {} }) => {}, + /** + * PanelModule should provide a list of panels that will be available in OHIF + * for Modes to consume and render. Each panel is defined by a {name, + * iconName, iconLabel, label, component} object. Example of a panel module + * is the StudyBrowserPanel that is provided by the default extension in OHIF. + */ + getPanelModule: ({ servicesManager, commandsManager, extensionManager }) => {}, + /** + * ViewportModule should provide a list of viewports that will be available in OHIF + * for Modes to consume and use in the viewports. Each viewport is defined by + * {name, component} object. Example of a viewport module is the CornerstoneViewport + * that is provided by the Cornerstone extension in OHIF. + */ + getViewportModule: ({ servicesManager, commandsManager, extensionManager }) => {}, + /** + * ToolbarModule should provide a list of tool buttons that will be available in OHIF + * for Modes to consume and use in the toolbar. Each tool button is defined by + * {name, defaultComponent, clickHandler }. Examples include radioGroupIcons and + * splitButton toolButton that the default extension is providing. + */ + getToolbarModule: ({ servicesManager, commandsManager, extensionManager }) => {}, + /** + * LayoutTemplateMOdule should provide a list of layout templates that will be + * available in OHIF for Modes to consume and use to layout the viewer. + * Each layout template is defined by a { name, id, component}. Examples include + * the default layout template provided by the default extension which renders + * a Header, left and right sidebars, and a viewport section in the middle + * of the viewer. + */ + getLayoutTemplateModule: ({ servicesManager, commandsManager, extensionManager }) => {}, + /** + * SopClassHandlerModule should provide a list of sop class handlers that will be + * available in OHIF for Modes to consume and use to create displaySets from Series. + * Each sop class handler is defined by a { name, sopClassUids, getDisplaySetsFromSeries}. + * Examples include the default sop class handler provided by the default extension + */ + getSopClassHandlerModule: ({ servicesManager, commandsManager, extensionManager }) => {}, + /** + * HangingProtocolModule should provide a list of hanging protocols that will be + * available in OHIF for Modes to use to decide on the structure of the viewports + * and also the series that hung in the viewports. Each hanging protocol is defined by + * { name, protocols}. Examples include the default hanging protocol provided by + * the default extension that shows 2x2 viewports. + */ + getHangingProtocolModule: ({ servicesManager, commandsManager, extensionManager }) => {}, + /** + * CommandsModule should provide a list of commands that will be available in OHIF + * for Modes to consume and use in the viewports. Each command is defined by + * an object of { actions, definitions, defaultContext } where actions is an + * object of functions, definitions is an object of available commands, their + * options, and defaultContext is the default context for the command to run against. + */ + getCommandsModule: ({ servicesManager, commandsManager, extensionManager }) => {}, + /** + * ContextModule should provide a list of context that will be available in OHIF + * and will be provided to the Modes. A context is a state that is shared OHIF. + * Context is defined by an object of { name, context, provider }. Examples include + * the measurementTracking context provided by the measurementTracking extension. + */ + getContextModule: ({ servicesManager, commandsManager, extensionManager }) => {}, + /** + * DataSourceModule should provide a list of data sources to be used in OHIF. + * DataSources can be used to map the external data formats to the OHIF's + * native format. DataSources are defined by an object of { name, type, createDataSource }. + */ + getDataSourcesModule: ({ servicesManager, commandsManager, extensionManager }) => {}, +}; diff --git a/platform/cli/templates/mode/.gitignore b/platform/cli/templates/mode/.gitignore new file mode 100644 index 0000000..6704566 --- /dev/null +++ b/platform/cli/templates/mode/.gitignore @@ -0,0 +1,104 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/platform/cli/templates/mode/.prettierrc b/platform/cli/templates/mode/.prettierrc new file mode 100644 index 0000000..ef83baa --- /dev/null +++ b/platform/cli/templates/mode/.prettierrc @@ -0,0 +1,11 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"], + "trailingComma": "es5", + "printWidth": 100, + "proseWrap": "always", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/platform/cli/templates/mode/.webpack/webpack.prod.js b/platform/cli/templates/mode/.webpack/webpack.prod.js new file mode 100644 index 0000000..9f742f9 --- /dev/null +++ b/platform/cli/templates/mode/.webpack/webpack.prod.js @@ -0,0 +1,100 @@ +const path = require('path'); +const pkg = require('../package.json'); + +const outputFile = 'index.umd.js'; +const rootDir = path.resolve(__dirname, '../'); +const outputFolder = path.join(__dirname, `../dist/umd/${pkg.name}/`); + +// Todo: add ESM build for the mode in addition to umd build +const config = { + mode: 'production', + entry: rootDir + '/' + pkg.module, + devtool: 'source-map', + output: { + path: outputFolder, + filename: outputFile, + library: pkg.name, + libraryTarget: 'umd', + chunkFilename: '[name].chunk.js', + umdNamedDefine: true, + globalObject: "typeof self !== 'undefined' ? self : this", + }, + externals: [ + { + react: { + root: 'React', + commonjs2: 'react', + commonjs: 'react', + amd: 'react', + }, + '@ohif/core': { + commonjs2: '@ohif/core', + commonjs: '@ohif/core', + amd: '@ohif/core', + root: '@ohif/core', + }, + '@ohif/ui': { + commonjs2: '@ohif/ui', + commonjs: '@ohif/ui', + amd: '@ohif/ui', + root: '@ohif/ui', + }, + '@ohif/mode-longitudinal': { + commonjs2: '@ohif/mode-longitudinal', + commonjs: '@ohif/mode-longitudinal', + amd: '@ohif/mode-longitudinal', + root: '@ohif/mode-longitudinal', + } + }, + ], + module: { + rules: [ + { + test: /\.svg?$/, + oneOf: [ + { + use: [ + { + loader: '@svgr/webpack', + options: { + svgoConfig: { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false + }, + }, + }, + ] + }, + prettier: false, + svgo: true, + titleProp: true, + }, + }, + ], + issuer: { + and: [/\.(ts|tsx|js|jsx|md|mdx)$/], + }, + }, + ], + }, + { + test: /(\.jsx|\.js|\.tsx|\.ts)$/, + loader: 'babel-loader', + exclude: /(node_modules|bower_components)/, + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + }, + ], + }, + resolve: { + modules: [path.resolve('./node_modules'), path.resolve('./src')], + extensions: ['.json', '.js', '.jsx', '.tsx', '.ts'], + }, +}; + +module.exports = config; diff --git a/platform/cli/templates/mode/babel.config.js b/platform/cli/templates/mode/babel.config.js new file mode 100644 index 0000000..a35080a --- /dev/null +++ b/platform/cli/templates/mode/babel.config.js @@ -0,0 +1,43 @@ +module.exports = { + plugins: ['@babel/plugin-proposal-class-properties'], + env: { + test: { + presets: [ + [ + // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2 + '@babel/preset-env', + { + modules: 'commonjs', + debug: false, + }, + '@babel/preset-typescript', + ], + '@babel/preset-react', + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-regenerator', + '@babel/plugin-transform-runtime', + ], + }, + production: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + development: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + }, +}; diff --git a/platform/cli/templates/mode/dependencies.json b/platform/cli/templates/mode/dependencies.json new file mode 100644 index 0000000..3ef164b --- /dev/null +++ b/platform/cli/templates/mode/dependencies.json @@ -0,0 +1,53 @@ +{ + "repository": "OHIF/Viewers", + "keywords": [ + "ohif-mode" + ], + "module": "src/index.tsx", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "scripts": { + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:cornerstone": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" + }, + "peerDependencies": { + "@ohif/core": "^{LATEST_OHIF_VERSION}" + }, + "dependencies": { + "@babel/runtime": "^7.20.13" + }, + "devDependencies": { + "@babel/core": "7.24.7", + "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-proposal-object-rest-spread": "^7.17.3", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.16.7", + "@babel/plugin-transform-regenerator": "^7.16.7", + "@babel/plugin-transform-runtime": "7.24.7", + "@babel/plugin-transform-typescript": "^7.13.0", + "@babel/preset-env": "7.24.7", + "@babel/preset-react": "^7.16.7", + "@babel/preset-typescript": "^7.13.0", + "babel-eslint": "^8.0.3", + "babel-loader": "^8.0.0-beta.4", + "@svgr/webpack": "^8.1.0", + "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^10.2.0", + "cross-env": "^7.0.3", + "dotenv": "^14.1.0", + "eslint": "^8.39.0", + "eslint-loader": "^2.0.0", + "webpack": "5.89.0", + "webpack-merge": "^5.7.3", + "webpack-cli": "^5.0.2" + } +} diff --git a/platform/cli/templates/mode/src/id.js b/platform/cli/templates/mode/src/id.js new file mode 100644 index 0000000..ebe5acd --- /dev/null +++ b/platform/cli/templates/mode/src/id.js @@ -0,0 +1,5 @@ +import packageJson from '../package.json'; + +const id = packageJson.name; + +export { id }; diff --git a/platform/cli/templates/mode/src/index.tsx b/platform/cli/templates/mode/src/index.tsx new file mode 100644 index 0000000..4f2a57c --- /dev/null +++ b/platform/cli/templates/mode/src/index.tsx @@ -0,0 +1,140 @@ +import { hotkeys } from '@ohif/core'; +import { initToolGroups, toolbarButtons } from '@ohif/mode-longitudinal'; +import { id } from './id'; + +const ohif = { + layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', + sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', + hangingProtocol: '@ohif/extension-default.hangingProtocolModule.default', + leftPanel: '@ohif/extension-default.panelModule.seriesList', + rightPanel: '@ohif/extension-cornerstone.panelModule.panelMeasurement', +}; + +const cornerstone = { + viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', +}; + +/** + * Just two dependencies to be able to render a viewport with panels in order + * to make sure that the mode is working. + */ +const extensionDependencies = { + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', +}; + +function modeFactory({ modeConfiguration }) { + return { + /** + * Mode ID, which should be unique among modes used by the viewer. This ID + * is used to identify the mode in the viewer's state. + */ + id, + routeName: 'template', + /** + * Mode name, which is displayed in the viewer's UI in the workList, for the + * user to select the mode. + */ + displayName: 'Template Mode', + /** + * Runs when the Mode Route is mounted to the DOM. Usually used to initialize + * Services and other resources. + */ + onModeEnter: ({ servicesManager, extensionManager, commandsManager }: withAppTypes) => { + const { measurementService, toolbarService, toolGroupService } = servicesManager.services; + + measurementService.clearMeasurements(); + + // Init Default and SR ToolGroups + initToolGroups(extensionManager, toolGroupService, commandsManager); + + toolbarService.addButtons(toolbarButtons); + toolbarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'WindowLevel', + 'Pan', + 'Capture', + 'Layout', + 'Crosshairs', + 'MoreTools', + ]); + }, + onModeExit: ({ servicesManager }: withAppTypes) => { + const { + toolGroupService, + syncGroupService, + segmentationService, + cornerstoneViewportService, + uiDialogService, + uiModalService, + } = servicesManager.services; + + uiDialogService.dismissAll(); + uiModalService.hide(); + toolGroupService.destroy(); + syncGroupService.destroy(); + segmentationService.destroy(); + cornerstoneViewportService.destroy(); + }, + /** */ + validationTags: { + study: [], + series: [], + }, + /** + * A boolean return value that indicates whether the mode is valid for the + * modalities of the selected studies. For instance a PET/CT mode should be + */ + isValidMode: ({ modalities }) => { + return { valid: true }; + }, + /** + * Mode Routes are used to define the mode's behavior. A list of Mode Route + * that includes the mode's path and the layout to be used. The layout will + * include the components that are used in the layout. For instance, if the + * default layoutTemplate is used (id: '@ohif/extension-default.layoutTemplateModule.viewerLayout') + * it will include the leftPanels, rightPanels, and viewports. However, if + * you define another layoutTemplate that includes a Footer for instance, + * you should provide the Footer component here too. Note: We use Strings + * to reference the component's ID as they are registered in the internal + * ExtensionManager. The template for the string is: + * `${extensionId}.{moduleType}.${componentId}`. + */ + routes: [ + { + path: 'template', + layoutTemplate: ({ location, servicesManager }) => { + return { + id: ohif.layout, + props: { + leftPanels: [ohif.leftPanel], + rightPanels: [ohif.rightPanel], + viewports: [ + { + namespace: cornerstone.viewport, + displaySetsToDisplay: [ohif.sopClassHandler], + }, + ], + }, + }; + }, + }, + ], + /** List of extensions that are used by the mode */ + extensions: extensionDependencies, + /** HangingProtocol used by the mode */ + // hangingProtocol: [''], + /** SopClassHandlers used by the mode */ + sopClassHandlers: [ohif.sopClassHandler], + /** hotkeys for mode */ + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; diff --git a/platform/core/.all-contributorsrc b/platform/core/.all-contributorsrc new file mode 100644 index 0000000..9f6138f --- /dev/null +++ b/platform/core/.all-contributorsrc @@ -0,0 +1,75 @@ +{ + "files": ["README.md"], + "imageSize": 100, + "commit": false, + "contributors": [ + { + "login": "swederik", + "name": "Erik Ziegler", + "avatar_url": "https://avatars3.githubusercontent.com/u/607793?v=4", + "profile": "https://github.com/swederik", + "contributions": ["code"] + }, + { + "login": "evren217", + "name": "Evren Ozkan", + "avatar_url": "https://avatars1.githubusercontent.com/u/4920551?v=4", + "profile": "https://github.com/evren217", + "contributions": ["code"] + }, + { + "login": "galelis", + "name": "Gustavo Andrรฉ Lelis", + "avatar_url": "https://avatars3.githubusercontent.com/u/2378326?v=4", + "profile": "https://github.com/galelis", + "contributions": ["code"] + }, + { + "login": "dannyrb", + "name": "Danny Brown", + "avatar_url": "https://avatars1.githubusercontent.com/u/5797588?v=4", + "profile": "http://dannyrb.com/", + "contributions": ["code"] + }, + { + "login": "allcontributors", + "name": "allcontributors[bot]", + "avatar_url": "https://avatars3.githubusercontent.com/u/46843839?v=4", + "profile": "https://github.com/all-contributors/all-contributors-bot", + "contributions": ["doc"] + }, + { + "login": "ivan-aksamentov", + "name": "Ivan Aksamentov", + "avatar_url": "https://avatars0.githubusercontent.com/u/9403403?v=4", + "profile": "https://github.com/ivan-aksamentov", + "contributions": ["code", "test"] + }, + { + "login": "igoroctaviano", + "name": "Igor Octaviano", + "avatar_url": "https://avatars0.githubusercontent.com/u/13886933?v=4", + "profile": "http://igoroctaviano.com", + "contributions": ["code"] + }, + { + "login": "dlwire", + "name": "David Wire", + "avatar_url": "https://avatars3.githubusercontent.com/u/1167291?v=4", + "profile": "https://github.com/dlwire", + "contributions": ["code", "test"] + }, + { + "login": "pavertomato", + "name": "Egor Lezhnin", + "avatar_url": "https://avatars0.githubusercontent.com/u/878990?v=4", + "profile": "http://egor.lezhn.in", + "contributions": ["code"] + } + ], + "contributorsPerLine": 7, + "projectName": "@ohif/core", + "projectOwner": "OHIF", + "repoType": "github", + "repoHost": "https://github.com" +} diff --git a/platform/core/.webpack/webpack.dev.js b/platform/core/.webpack/webpack.dev.js new file mode 100644 index 0000000..1b8e34c --- /dev/null +++ b/platform/core/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.ts`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/platform/core/.webpack/webpack.prod.js b/platform/core/.webpack/webpack.prod.js new file mode 100644 index 0000000..beff3bb --- /dev/null +++ b/platform/core/.webpack/webpack.prod.js @@ -0,0 +1,41 @@ +const { merge } = require('webpack-merge'); +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); + +const pkg = require('./../package.json'); +const ROOT_DIR = path.join(__dirname, './../'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.ts`, +}; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: false, + }, + output: { + path: ROOT_DIR, + library: 'ohif-core', + libraryTarget: 'umd', + filename: pkg.main, + }, + externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@cornerstonejs/], + }); +}; diff --git a/platform/core/CHANGELOG.md b/platform/core/CHANGELOG.md new file mode 100644 index 0000000..925a5fa --- /dev/null +++ b/platform/core/CHANGELOG.md @@ -0,0 +1,4240 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + + +### Features + +* **hotkeys:** Migrate hotkeys to customization service and fix issues with overrides ([#4777](https://github.com/OHIF/Viewers/issues/4777)) ([3e6913b](https://github.com/OHIF/Viewers/commit/3e6913b097569280a5cc2fa5bbe4add52f149305)) + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + + +### Bug Fixes + +* combine frame instance ([#4792](https://github.com/OHIF/Viewers/issues/4792)) ([55f0b54](https://github.com/OHIF/Viewers/commit/55f0b54db1e81a99f9e2d92b1d6d78dfb02762f0)) + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + + +### Bug Fixes + +* cache thumbnail in display set ([#4782](https://github.com/OHIF/Viewers/issues/4782)) ([2410c6a](https://github.com/OHIF/Viewers/commit/2410c6a50904c1235993900e837876cc26af019b)) + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + + +### Bug Fixes + +* lodash dependencies ([#4791](https://github.com/OHIF/Viewers/issues/4791)) ([4e16099](https://github.com/OHIF/Viewers/commit/4e16099ad3ab777b09f6ac8f181025cfd656ab6b)) + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + + +### Features + +* improve dicom tag browser with nested rows ([#4451](https://github.com/OHIF/Viewers/issues/4451)) ([0b5836c](https://github.com/OHIF/Viewers/commit/0b5836ca1a908e152336752672b196f0d533f4f9)) + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + + +### Features + +* add viewport overlays to microscopy mode ([#4776](https://github.com/OHIF/Viewers/issues/4776)) ([084a10f](https://github.com/OHIF/Viewers/commit/084a10f7835acab6a851922850c474bc9c7b864b)) + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + + +### Bug Fixes + +* **core:** Address 3D reconstruction and Android compatibility issues and clean up 4D data mode ([#4762](https://github.com/OHIF/Viewers/issues/4762)) ([149d6d0](https://github.com/OHIF/Viewers/commit/149d6d049cd333b9e5846576b403ff387558a66f)) + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + + +### Features + +* **ui:** Add support for Custom Modal component in Modal Service ([#4752](https://github.com/OHIF/Viewers/issues/4752)) ([2c183aa](https://github.com/OHIF/Viewers/commit/2c183aa4a777d7b5a0417ebcc8576a0fc2631ad2)) + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + + +### Features + +* Add customization support for more UI components ([#4634](https://github.com/OHIF/Viewers/issues/4634)) ([f15eb44](https://github.com/OHIF/Viewers/commit/f15eb44b4cf49de1b73a22512571cec02effaef3)) + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + + +### Bug Fixes + +* typo in pet_series_module ([#4748](https://github.com/OHIF/Viewers/issues/4748)) ([f10683c](https://github.com/OHIF/Viewers/commit/f10683c667ea8f20c8d3e99ee0fb206522757b71)) + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + + +### Features + +* delete active annotation using backspace/delete key ([#4722](https://github.com/OHIF/Viewers/issues/4722)) ([d6f0092](https://github.com/OHIF/Viewers/commit/d6f0092a3236cecb5d04ec46c8ad01600500831e)) + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + + +### Features + +* **customization:** new customization service api ([#4688](https://github.com/OHIF/Viewers/issues/4688)) ([55ad8ef](https://github.com/OHIF/Viewers/commit/55ad8efbabc3fabd8031fc08927b2f92ae5aec69)) + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + + +### Bug Fixes + +* **seg:** sphere scissor on stack and cpu rendering reset properties was broken ([#4721](https://github.com/OHIF/Viewers/issues/4721)) ([f00d182](https://github.com/OHIF/Viewers/commit/f00d18292f02e8910215d913edfc994850a68d88)) + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + + +### Features + +* **resizable-side-panels:** Make the left and right side panels (optionally) resizable. ([#4672](https://github.com/OHIF/Viewers/issues/4672)) ([d90a4cf](https://github.com/OHIF/Viewers/commit/d90a4cfb16cc0daed9b905de9780f44cca1323f9)) + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + + +### Bug Fixes + +* Inconsistent Handling of Patient Name Tag ([#4703](https://github.com/OHIF/Viewers/issues/4703)) ([8aedb2e](https://github.com/OHIF/Viewers/commit/8aedb2ec54a0ccf2550f745fed6f0b8aa184a860)) + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + + +### Bug Fixes + +* **hp:** Display set should allow remembered updates ([#4707](https://github.com/OHIF/Viewers/issues/4707)) ([464148e](https://github.com/OHIF/Viewers/commit/464148ece66b48b583dc6e998ca4d11c66746f3a)) + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + + +### Bug Fixes + +* **multiframe:** handling proxies properly ([#4693](https://github.com/OHIF/Viewers/issues/4693)) ([ec4b5a6](https://github.com/OHIF/Viewers/commit/ec4b5a6876cea77278e5cffaf4108eeeefdc57dc)) + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + + +### Bug Fixes + +* Having sop instance in a per-frame or shared attribute breaks load ([#4560](https://github.com/OHIF/Viewers/issues/4560)) ([cded082](https://github.com/OHIF/Viewers/commit/cded08261788143e0d5be57a55c927fd96aafb22)) + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + + +### Bug Fixes + +* bugs after multimonitor ([#4680](https://github.com/OHIF/Viewers/issues/4680)) ([c901a84](https://github.com/OHIF/Viewers/commit/c901a847af75d356509366c695ea46ff4f4bcdaf)) + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + + +### Features + +* **multimonitor:** Add simple multi-monitor support to open another study([#4178](https://github.com/OHIF/Viewers/issues/4178)) ([07c628e](https://github.com/OHIF/Viewers/commit/07c628e689b28f831317a7c28d712509b69c6b13)) + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + + +### Bug Fixes + +* **toolbarService:** All header tools are enabled in volume3D viewport ([#4677](https://github.com/OHIF/Viewers/issues/4677)) ([9832dbe](https://github.com/OHIF/Viewers/commit/9832dbe653a196280a0de57460436b6600a6aaa8)) + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + + +### Features + +* Start using group filtering to define measurements table layout ([#4501](https://github.com/OHIF/Viewers/issues/4501)) ([82440e8](https://github.com/OHIF/Viewers/commit/82440e88d5debe808f0b14281b77e430c2489779)) + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + + +### Bug Fixes + +* Execute HP onProtocolEnter callback after HPservice.run( ([#4589](https://github.com/OHIF/Viewers/issues/4589)) ([8e2c607](https://github.com/OHIF/Viewers/commit/8e2c60790437d4df583a236c99e856d21dbc0dfe)) + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + + +### Bug Fixes + +* Make StudyInstanceUID optional to retrieve a Series from DicomMetadataStore ([#4644](https://github.com/OHIF/Viewers/issues/4644)) ([aef68d1](https://github.com/OHIF/Viewers/commit/aef68d18b82455ee485fef70df4ee7ba2c775417)) + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + + +### Bug Fixes + +* **docker:** publish manifest for multiarch and update cs3d ([#4650](https://github.com/OHIF/Viewers/issues/4650)) ([836e67a](https://github.com/OHIF/Viewers/commit/836e67a6ab8de66d8908c75856774318729544f4)) + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + + +### Bug Fixes + +* **toolbars:** Fix error when filtering out duplicate buttons for a button section. ([#4618](https://github.com/OHIF/Viewers/issues/4618)) ([28cf3a1](https://github.com/OHIF/Viewers/commit/28cf3a17fad5070ba00d0d5d27633237b499da7a)) + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + + +### Bug Fixes + +* **context menu:** Implemented closing of context menu on outside click ([#4627](https://github.com/OHIF/Viewers/issues/4627)) ([6b851df](https://github.com/OHIF/Viewers/commit/6b851dfc12f4cf617d02f683e0661feeebfbcf20)) + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + + +### Bug Fixes + +* **tools:** enable additional tools in volume viewport ([#4620](https://github.com/OHIF/Viewers/issues/4620)) ([1992002](https://github.com/OHIF/Viewers/commit/1992002d2dced171c17b9a0163baf707fc551e3d)) + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + + +### Bug Fixes + +* **icons:** Add Clipboard icon and update MetadataProvider for null checks ([#4615](https://github.com/OHIF/Viewers/issues/4615)) ([93d7076](https://github.com/OHIF/Viewers/commit/93d707690104ae099df6e08156e2efd8c1a6e076)) + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + + +### Features + +* **measurements:** Provide for the Load (SR) measurements button to optionally clear existing measurements prior to loading the SR. ([#4586](https://github.com/OHIF/Viewers/issues/4586)) ([4d3d5e7](https://github.com/OHIF/Viewers/commit/4d3d5e794cb99212eba06bf91dbb30a258725efe)) + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + + +### Features + +* **node:** move to node 20 ([#4594](https://github.com/OHIF/Viewers/issues/4594)) ([1f04d6c](https://github.com/OHIF/Viewers/commit/1f04d6c1be729a26fe7bcda923770a1cd461053c)) + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + + +### Bug Fixes + +* **sr:** correct jump to first image via viewRef ([#4576](https://github.com/OHIF/Viewers/issues/4576)) ([6ec04ca](https://github.com/OHIF/Viewers/commit/6ec04ca65ea2f0fe95eaf624652911b87a6f81e6)) + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + + +### Features + +* **extension:** added 'extensionManager' to 'onModeEnter' parameter list ([#4569](https://github.com/OHIF/Viewers/issues/4569)) ([f87c6cd](https://github.com/OHIF/Viewers/commit/f87c6cd2aa83007393302d4437b417150ed26e2e)) + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + + +### Bug Fixes + +* **CinePlayer:** always show cine player for dynamic data ([#4575](https://github.com/OHIF/Viewers/issues/4575)) ([b8e8bbe](https://github.com/OHIF/Viewers/commit/b8e8bbe482b66e8cbe9167d03e9d8dedd2d3b6c5)) + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + + +### Bug Fixes + +* **WorkflowSteps:** fixed how hooks are invoked + added support for 'onExit' hook ([#4568](https://github.com/OHIF/Viewers/issues/4568)) ([bca2022](https://github.com/OHIF/Viewers/commit/bca20223513c15720b4538533c0f6d38b839e045)) + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + + +### Bug Fixes + +* **multiframe:** metadata handling of NM studies and loading order ([#4554](https://github.com/OHIF/Viewers/issues/4554)) ([7624ccb](https://github.com/OHIF/Viewers/commit/7624ccb5e495c0a151227a458d8d5bfb8babb22c)) + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + + +### Bug Fixes + +* **colorlut:** use the correct colorlut index and update vtk ([#4544](https://github.com/OHIF/Viewers/issues/4544)) ([b9c26e7](https://github.com/OHIF/Viewers/commit/b9c26e775a49044673473418dd5bdee2e5562ab9)) + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + + +### Bug Fixes + +* Make the commands ordering the registration order of hte mode ([#4492](https://github.com/OHIF/Viewers/issues/4492)) ([edfaf72](https://github.com/OHIF/Viewers/commit/edfaf7248d217707e90d24642361a40c6f1a03ff)) + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + + +### Bug Fixes + +* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed)) + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + + +### Bug Fixes + +* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30)) + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + + +### Bug Fixes + +* **tmtv:** fix toggle one up weird behaviours ([#4473](https://github.com/OHIF/Viewers/issues/4473)) ([aa2b649](https://github.com/OHIF/Viewers/commit/aa2b649444eb4fe5422e72ea7830a709c4d24a90)) + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + + +### Bug Fixes + +* build ([#4471](https://github.com/OHIF/Viewers/issues/4471)) ([3d11ef2](https://github.com/OHIF/Viewers/commit/3d11ef28f213361ec7586809317bd219fa70e742)) + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + + +### Features + +* **new-study-panel:** default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser ([#4417](https://github.com/OHIF/Viewers/issues/4417)) ([a7fd9fa](https://github.com/OHIF/Viewers/commit/a7fd9fa5bfff7a1b533d99cb96f7147a35fd528f)) + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + + +### Bug Fixes + +* **dicomjson:** Update getUIDsFromImageID to work with json data source + update getDisplaySetImageUIDs to work with mixed sop class json ([#4322](https://github.com/OHIF/Viewers/issues/4322)) ([3dd0666](https://github.com/OHIF/Viewers/commit/3dd0666c0c090cbd66161f24bc9795f96abb3697)) + + +### Features + +* **hangingProtocols:** added selection of the HangingProtocol stage from the url ([#4310](https://github.com/OHIF/Viewers/issues/4310)) ([fa2435d](https://github.com/OHIF/Viewers/commit/fa2435d5e94e5f903404ca94687b086f90f8d1f8)) +* **SR:** SCOORD3D point annotations support for stack viewports ([#4315](https://github.com/OHIF/Viewers/issues/4315)) ([ac1cad2](https://github.com/OHIF/Viewers/commit/ac1cad25af12ee0f7d508647e3134ed724d9b4d3)) + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + + +### Features + +* **tours:** freeze versions and add licensings doc ([#4407](https://github.com/OHIF/Viewers/issues/4407)) ([60a8d51](https://github.com/OHIF/Viewers/commit/60a8d5154a5d6d2b121bd93aeacf12d97ef9f8cb)) + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + + +### Features + +* **toolbar:** enable extensions to change toolbar button sections ([#4367](https://github.com/OHIF/Viewers/issues/4367)) ([1bfce0a](https://github.com/OHIF/Viewers/commit/1bfce0a03cbbb4cc1f69e8b5d1d72244b30d6b46)) + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + + +### Features + +* **studies-panel:** New OHIF study panel - under experimental flag ([#4254](https://github.com/OHIF/Viewers/issues/4254)) ([7a96406](https://github.com/OHIF/Viewers/commit/7a96406a116e46e62c396855fa64f434e2984b58)) + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + + +### Features + +* Add CS3D WSI and Video Viewports and add annotation navigation for MPR ([#4182](https://github.com/OHIF/Viewers/issues/4182)) ([7599ec9](https://github.com/OHIF/Viewers/commit/7599ec9421129dcade94e6fa6ec7908424ab3134)) + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + + +### Bug Fixes + +* **roundNumber:** handle negative numbers properly ([#4336](https://github.com/OHIF/Viewers/issues/4336)) ([7377db8](https://github.com/OHIF/Viewers/commit/7377db8d280a90515fe099cb580607450cb146a5)) + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + + +### Bug Fixes + +* customization types ([#4321](https://github.com/OHIF/Viewers/issues/4321)) ([72bef63](https://github.com/OHIF/Viewers/commit/72bef63ef6e63395ba18ff91a39294913966e9db)) + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + + +### Features + +* **SR:** text structured report (TEXT, CODE, NUM, PNAME, DATE, TIME and DATETIME) ([#4287](https://github.com/OHIF/Viewers/issues/4287)) ([246ebab](https://github.com/OHIF/Viewers/commit/246ebab6ebf5431a704a1861a5804045b9644ba4)) + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + + +### Bug Fixes + +* Cobb angle not working in basic-test mode and open contour ([#4280](https://github.com/OHIF/Viewers/issues/4280)) ([6fd3c7e](https://github.com/OHIF/Viewers/commit/6fd3c7e293fec851dd30e650c1347cc0bc7a99ee)) +* **image-orientation:** Prevent incorrect orientation marker display for single-slice US images ([#4275](https://github.com/OHIF/Viewers/issues/4275)) ([6d11048](https://github.com/OHIF/Viewers/commit/6d11048ca5ea66284948602613a63277083ec6a5)) +* webpack import bugs showing warnings on import ([#4265](https://github.com/OHIF/Viewers/issues/4265)) ([24c511f](https://github.com/OHIF/Viewers/commit/24c511f4bc04c4143bbd3d0d48029f41f7f36014)) + + +### Features + +* Add interleaved HTJ2K and volume progressive loading ([#4276](https://github.com/OHIF/Viewers/issues/4276)) ([a2084f3](https://github.com/OHIF/Viewers/commit/a2084f319b731d98b59485799fb80357094f8c38)) + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + + +### Features + +* **studyPrefetcher:** Study Prefetcher ([#4206](https://github.com/OHIF/Viewers/issues/4206)) ([2048b19](https://github.com/OHIF/Viewers/commit/2048b19484c0b1fae73f993cfaa814f861bbd230)) + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) +* **code:** remove console log ([#4248](https://github.com/OHIF/Viewers/issues/4248)) ([f3bbfff](https://github.com/OHIF/Viewers/commit/f3bbfff09b66ee020daf503656a2b58e763634a3)) +* **CustomViewportOverlay:** pass accurate data to Custom Viewport Functions ([#4224](https://github.com/OHIF/Viewers/issues/4224)) ([aef00e9](https://github.com/OHIF/Viewers/commit/aef00e91d63e9bc2de289cc6f35975e36547fb20)) +* **studybrowser:** Differentiate recent and all in study panel based on a provided time period ([#4242](https://github.com/OHIF/Viewers/issues/4242)) ([6f93449](https://github.com/OHIF/Viewers/commit/6f9344914951c204feaff48aaeb43cd7d727623d)) + + +### Features + +* customization service append and customize functionality should run once ([#4238](https://github.com/OHIF/Viewers/issues/4238)) ([e462fd3](https://github.com/OHIF/Viewers/commit/e462fd31f7944acfee34f08cfbc28cfd9de16169)) +* **sort:** custom series sort in study panel ([#4214](https://github.com/OHIF/Viewers/issues/4214)) ([a433d40](https://github.com/OHIF/Viewers/commit/a433d406e2cac13f644203996c682260b54e8865)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + + +### Features + +* Add customization merge, append or replace functionality ([#3871](https://github.com/OHIF/Viewers/issues/3871)) ([55dcfa1](https://github.com/OHIF/Viewers/commit/55dcfa1f6994a7036e7e594efb23673382a41915)) + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + + +### Bug Fixes + +* **seg:** maintain algorithm name and algorithm type when DICOM seg is exported or downloaded ([#4203](https://github.com/OHIF/Viewers/issues/4203)) ([a29e94d](https://github.com/OHIF/Viewers/commit/a29e94de803f79bbb3372d00ad8eb14b4224edc2)) + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + + +### Bug Fixes + +* **tmtv:** crosshairs should not have viewport indicators ([#4197](https://github.com/OHIF/Viewers/issues/4197)) ([f85da32](https://github.com/OHIF/Viewers/commit/f85da32f34389ef7cecae03c07e0af26468b52a6)) + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + + +### Features + +* **hp:** Add displayArea option for Hanging protocols and example with Mamo([#3808](https://github.com/OHIF/Viewers/issues/3808)) ([18ac08e](https://github.com/OHIF/Viewers/commit/18ac08ed860d119721c52e4ffc270332259100b6)) + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + + +### Bug Fixes + +* **ultrasound:** Upgrade cornerstone3D version to resolve coloring issues ([#4181](https://github.com/OHIF/Viewers/issues/4181)) ([75a71db](https://github.com/OHIF/Viewers/commit/75a71db7f89840250ad1c2b35df5a35aceb8be7d)) + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + + +### Features + +* **measurements:** show untracked measurements in measurement panel under additional findings ([#4160](https://github.com/OHIF/Viewers/issues/4160)) ([18686c2](https://github.com/OHIF/Viewers/commit/18686c2caf13ede3e881303100bd4cc34b8b135f)) + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + + +### Features + +* **types:** typed app config ([#4171](https://github.com/OHIF/Viewers/issues/4171)) ([8960b89](https://github.com/OHIF/Viewers/commit/8960b89911a9342d93bf1a62bec97a696f101fd4)) + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + + +### Bug Fixes + +* **crosshairs:** reset angle, position, and slabthickness for crosshairs when reset viewport tool is used ([#4113](https://github.com/OHIF/Viewers/issues/4113)) ([73d9e99](https://github.com/OHIF/Viewers/commit/73d9e99d5d6f38ab6c36f4471d54f18798feacb4)) + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + + +### Bug Fixes + +* dicom json for orthanc by Update package versions for [@cornerstonejs](https://github.com/cornerstonejs) dependencies ([#4165](https://github.com/OHIF/Viewers/issues/4165)) ([34c7d72](https://github.com/OHIF/Viewers/commit/34c7d72142847486b98c9c52469940083eeaf87e)) + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + + +### Bug Fixes + +* **HangingProtocol:** fix hp when unsupported series load first ([#4145](https://github.com/OHIF/Viewers/issues/4145)) ([b124c91](https://github.com/OHIF/Viewers/commit/b124c91d8fa0def262d1fee8f105295b02864129)) + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + + +### Bug Fixes + +* **segmentation:** Address issue where segmentation creation failed on layout change ([#4153](https://github.com/OHIF/Viewers/issues/4153)) ([29944c8](https://github.com/OHIF/Viewers/commit/29944c8512c35718af03c03ef82bc43675ee1872)) + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + + +### Bug Fixes + +* **rt:** enhanced RT support, utilize SVGs for rendering. ([#4074](https://github.com/OHIF/Viewers/issues/4074)) ([0156bc4](https://github.com/OHIF/Viewers/commit/0156bc426f1840ae0d090223e94a643726e856cb)) + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + + +### Bug Fixes + +* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f)) + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + + +### Bug Fixes + +* **bugs:** fix patient header for doc, track ball rotate resize observer and add segmentation button not being enabled on viewport data change ([#4068](https://github.com/OHIF/Viewers/issues/4068)) ([c09311d](https://github.com/OHIF/Viewers/commit/c09311d3b7df05fcd00a9f36a7233e9d7e5589d0)) + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + + +### Bug Fixes + +* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951)) + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + + +### Bug Fixes + +* **hp:** Fails to display any layouts in the layout selector if first layout has multiple stages ([#4058](https://github.com/OHIF/Viewers/issues/4058)) ([f0ed3fd](https://github.com/OHIF/Viewers/commit/f0ed3fd7b99b0e4e00b261ceb9888ba94726719c)) + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + + +### Features + +* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb)) + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + + +### Bug Fixes + +* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af)) + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + + +### Bug Fixes + +* **bugs:** and replace seriesInstanceUID and seriesInstanceUIDs URL with seriesInstanceUIDs ([#4049](https://github.com/OHIF/Viewers/issues/4049)) ([da7c1a5](https://github.com/OHIF/Viewers/commit/da7c1a5d8c54bfa1d3f97bbc500386bf76e7fd9d)) + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes - final ([#4048](https://github.com/OHIF/Viewers/issues/4048)) ([170bb96](https://github.com/OHIF/Viewers/commit/170bb96983082c39b22b7352e0c54aacf3e73b02)) + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + + +### Bug Fixes + +* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025)) + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + + +### Features + +* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13)) + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + + +### Bug Fixes + +* **general:** enhancements and bug fixes ([#4018](https://github.com/OHIF/Viewers/issues/4018)) ([2b83393](https://github.com/OHIF/Viewers/commit/2b83393f91cb16ea06821d79d14ff60f80c29c90)) + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + + +### Bug Fixes + +* **MetaDataProvider:** Fix tag in GeneralImageModule ([#4000](https://github.com/OHIF/Viewers/issues/4000)) ([e9c30a1](https://github.com/OHIF/Viewers/commit/e9c30a108e2dd14a8b137b81e5b832cc167bc3d1)) + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + + +### Features + +* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2)) + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + + +### Bug Fixes + +* **cornerstone-dicom-sr:** Freehand SR hydration support ([#3996](https://github.com/OHIF/Viewers/issues/3996)) ([5645ac1](https://github.com/OHIF/Viewers/commit/5645ac1b271e1ed8c57f5d71100809362447267e)) + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + + +### Features + +* **segmentation:** Enhanced segmentation panel design for TMTV ([#3988](https://github.com/OHIF/Viewers/issues/3988)) ([9f3235f](https://github.com/OHIF/Viewers/commit/9f3235ff096636aafa88d8a42859e8dc85d9036d)) + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + + +### Features + +* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1)) + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + + +### Bug Fixes + +* **new layout:** address black screen bugs ([#4008](https://github.com/OHIF/Viewers/issues/4008)) ([158a181](https://github.com/OHIF/Viewers/commit/158a1816703e0ad66cae08cb9bd1ffb93bbd8d43)) + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + + +### Features + +* **layout:** new layout selector with 3D volume rendering ([#3923](https://github.com/OHIF/Viewers/issues/3923)) ([617043f](https://github.com/OHIF/Viewers/commit/617043fe0da5de91fbea4ac33a27f1df16ae1ca6)) + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + + +### Bug Fixes + +* **SR display:** and the token based navigation ([#3995](https://github.com/OHIF/Viewers/issues/3995)) ([feed230](https://github.com/OHIF/Viewers/commit/feed2304c124dc2facc7a7371ed9851548c223c5)) + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + + +### Bug Fixes + +* **demo:** Deploy issue ([#3951](https://github.com/OHIF/Viewers/issues/3951)) ([21e8a2b](https://github.com/OHIF/Viewers/commit/21e8a2bd0b7cc72f90a31e472d285d761be15d30)) + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + + +### Bug Fixes + +* is same orientaiton ([#3905](https://github.com/OHIF/Viewers/issues/3905)) ([31b837f](https://github.com/OHIF/Viewers/commit/31b837fa90f631d4984482c6e952373fbb8bdbfc)) + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + + +### Bug Fixes + +* Update CS3D to fix second render ([#3892](https://github.com/OHIF/Viewers/issues/3892)) ([d00a86b](https://github.com/OHIF/Viewers/commit/d00a86b022742ea089d246d06cfd691f43b64412)) + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + + +### Features + +* **hp:** enable OHIF to run with partial metadata for large studies at the cost of less effective hanging protocol ([#3804](https://github.com/OHIF/Viewers/issues/3804)) ([0049f4c](https://github.com/OHIF/Viewers/commit/0049f4c0303f0b6ea995972326fc8784259f5a47)) + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + + +### Features + +* **transferSyntax:** prefer server transcoded transfer syntax for all images ([#3883](https://github.com/OHIF/Viewers/issues/3883)) ([1456a49](https://github.com/OHIF/Viewers/commit/1456a493d66c90c787b022256c9f2846afb115fc)) + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + + +### Bug Fixes + +* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6)) + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + + +### Bug Fixes + +* PDF display request in v3 ([#3878](https://github.com/OHIF/Viewers/issues/3878)) ([9865030](https://github.com/OHIF/Viewers/commit/98650302c7575f0aea386e32cfc4112c378035e6)) + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + + +### Features + +* **customizationService:** Enable saving and loading of private tags in SRs ([#3842](https://github.com/OHIF/Viewers/issues/3842)) ([e1f55e6](https://github.com/OHIF/Viewers/commit/e1f55e65f2d2a34136ad5d0b1ada77d337a0ea23)) + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + + +### Features + +* **i18n:** enhanced i18n support ([#3761](https://github.com/OHIF/Viewers/issues/3761)) ([d14a8f0](https://github.com/OHIF/Viewers/commit/d14a8f0199db95cd9e85866a011b64d6bf830d57)) + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + + +### Bug Fixes + +* **auth:** fix the issue with oauth at a non root path ([#3840](https://github.com/OHIF/Viewers/issues/3840)) ([6651008](https://github.com/OHIF/Viewers/commit/6651008fbb35dabd5991c7f61128e6ef324012df)) + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + + +### Bug Fixes + +* **cine:** Set cine disabled on mode exit. ([#3812](https://github.com/OHIF/Viewers/issues/3812)) ([924affa](https://github.com/OHIF/Viewers/commit/924affa7b5d420c2f91522a075cecbb3c78e8f52)) + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + + +### Bug Fixes + +* Update the CS3D packages to add the most recent HTJ2K TSUIDS ([#3806](https://github.com/OHIF/Viewers/issues/3806)) ([9d1884d](https://github.com/OHIF/Viewers/commit/9d1884d7d8b6b2a1cdc26965a96995838aa72682)) + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + + +### Features + +* Merge Data Source ([#3788](https://github.com/OHIF/Viewers/issues/3788)) ([c4ff2c2](https://github.com/OHIF/Viewers/commit/c4ff2c2f09546ce8b72eab9c5e7beed611e3cab0)) + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + + +### Features + +* **events:** broadcast series summary metadata ([#3798](https://github.com/OHIF/Viewers/issues/3798)) ([404b0a5](https://github.com/OHIF/Viewers/commit/404b0a5d535182d1ae44e33f7232db500a7b2c16)) + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + + +### Bug Fixes + +* **metadata:** to handle cornerstone3D update for htj2k ([#3783](https://github.com/OHIF/Viewers/issues/3783)) ([8c8924a](https://github.com/OHIF/Viewers/commit/8c8924af373d906773f5db20defe38628cacd4a0)) + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + + +### Features + +* **dicomJSON:** Add Loading Other Display Sets and JSON Metadata Generation script ([#3777](https://github.com/OHIF/Viewers/issues/3777)) ([43b1c17](https://github.com/OHIF/Viewers/commit/43b1c17209502e4876ad59bae09ed9442eda8024)) + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + + +### Features + +* **hp callback:** Add viewport ready callback ([#3772](https://github.com/OHIF/Viewers/issues/3772)) ([bf252bc](https://github.com/OHIF/Viewers/commit/bf252bcec2aae3a00479fdcb732110b344bcf2c0)) + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + + +### Bug Fixes + +* **thumbnail:** Avoid multiple promise creations for thumbnails ([#3756](https://github.com/OHIF/Viewers/issues/3756)) ([b23eeff](https://github.com/OHIF/Viewers/commit/b23eeff93745769e67e60c33d75293d6242c5ec9)) + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + + +### Bug Fixes + +* **measurement service:** Implemented correct check of schema keys in _isValidMeasurment. ([#3750](https://github.com/OHIF/Viewers/issues/3750)) ([db39585](https://github.com/OHIF/Viewers/commit/db395852b6fc6cd5c265a9282e5eee5bd6f951b7)) + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + + +### Bug Fixes + +* **toolbar:** allow customizable toolbar for active viewport and allow active tool to be deactivated via a click ([#3608](https://github.com/OHIF/Viewers/issues/3608)) ([dd6d976](https://github.com/OHIF/Viewers/commit/dd6d9768bbca1d3cc472e8c1e6d85822500b96ef)) + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + + +### Bug Fixes + +* **sr:** dcm4chee requires the patient name for an SR to match what is in the original study ([#3739](https://github.com/OHIF/Viewers/issues/3739)) ([d98439f](https://github.com/OHIF/Viewers/commit/d98439fe7f3825076dbc87b664a1d1480ff414d3)) + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + + +### Bug Fixes + +* **export:** wrong export for the tmtv RT function ([#3715](https://github.com/OHIF/Viewers/issues/3715)) ([a3f2a1a](https://github.com/OHIF/Viewers/commit/a3f2a1a7b0d16bfcc0ecddc2ab731e54c5e377c8)) + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + + +### Bug Fixes + +* **segmentation:** Various fixes for segmentation mode and other ([#3709](https://github.com/OHIF/Viewers/issues/3709)) ([a9a6ad5](https://github.com/OHIF/Viewers/commit/a9a6ad50eae67b43b8b34efc07182d788cacdcfe)) + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + + +### Bug Fixes + +* **voi:** should publish voi change event on reset ([#3707](https://github.com/OHIF/Viewers/issues/3707)) ([52f34c6](https://github.com/OHIF/Viewers/commit/52f34c64d014f433ec1661a39b47e7fb27f15332)) + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + + +### Bug Fixes + +* **modality unit:** fix the modality unit per target via upgrade of cs3d ([#3706](https://github.com/OHIF/Viewers/issues/3706)) ([0a42d57](https://github.com/OHIF/Viewers/commit/0a42d573bbca7f2551a831a46d3aa6b56674a580)) + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + + +### Features + +* **Segmentation:** download RTSS from Labelmap([#3692](https://github.com/OHIF/Viewers/issues/3692)) ([40673f6](https://github.com/OHIF/Viewers/commit/40673f64b36b1150149c55632aa1825178a39e65)) + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + + +### Bug Fixes + +* **bugs:** fixing lots of bugs regarding release candidate ([#3700](https://github.com/OHIF/Viewers/issues/3700)) ([8bc12a3](https://github.com/OHIF/Viewers/commit/8bc12a37d0353160ae5ea4624dc0b244b7d59c07)) + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + + +### Bug Fixes + +* **measurement and microscopy:** various small fixes for measurement and microscopy side panel ([#3696](https://github.com/OHIF/Viewers/issues/3696)) ([c1d5ee7](https://github.com/OHIF/Viewers/commit/c1d5ee7e3f7f4c0c6bed9ae81eba5519741c5155)) + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + + +### Features + +* **debug:** Add timing information about time to first image/all images, and query time ([#3681](https://github.com/OHIF/Viewers/issues/3681)) ([108383b](https://github.com/OHIF/Viewers/commit/108383b9ef51e4bef82d9c932b9bc7aa5354e799)) + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + + +### Bug Fixes + +* **typescript error:** Change pubSubServiceInterface file type to typescript ([#3546](https://github.com/OHIF/Viewers/issues/3546)) ([eb22328](https://github.com/OHIF/Viewers/commit/eb22328fc05d06fc4411805e7a30f826659d796a)) + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + + +### Bug Fixes + +* **dicom overlay:** Handle special cases of ArrayBuffer for various DICOM overlay attributes. ([#3684](https://github.com/OHIF/Viewers/issues/3684)) ([e36a604](https://github.com/OHIF/Viewers/commit/e36a6043315e900eeb6ce183772c7f852f478e96)) +* **StackSync:** Miscellaneous fixes for stack image sync ([#3663](https://github.com/OHIF/Viewers/issues/3663)) ([8a335bd](https://github.com/OHIF/Viewers/commit/8a335bd03d14ba87d65d7468d93f74040aa828d9)) + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + + +### Bug Fixes + +* **config:** support more values for the useSharedArrayBuffer ([#3688](https://github.com/OHIF/Viewers/issues/3688)) ([1129c15](https://github.com/OHIF/Viewers/commit/1129c155d2c7d46c98a5df7c09879aa3d459fa7e)) + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + + +### Performance Improvements + +* **memory:** add 16 bit texture via configuration - reduces memory by half ([#3662](https://github.com/OHIF/Viewers/issues/3662)) ([2bd3b26](https://github.com/OHIF/Viewers/commit/2bd3b26a6aa54b211ef988f3ad64ef1fe5648bab)) + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + + +### Bug Fixes + +* **mpr:** Return the original/raw hanging protocol when fetching and preserving the current active protocol. ([#3670](https://github.com/OHIF/Viewers/issues/3670)) ([221dedd](https://github.com/OHIF/Viewers/commit/221dedde5dd4df086276406a9fa2da1cc23b4eb1)) + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + + +### Bug Fixes + +* **measurements:** Update the calibration tool to match changes in CS3D ([#3505](https://github.com/OHIF/Viewers/issues/3505)) ([38af311](https://github.com/OHIF/Viewers/commit/38af3112ec1f94f36c0ef64ff1cf9d21c0981c81)) + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + + +### Features + +* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f)) + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + + +### Features + +* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd)) + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + + +### Features + +* **data source UI config:** Popup the configuration dialogue whenever a data source is not fully configured ([#3620](https://github.com/OHIF/Viewers/issues/3620)) ([adedc8c](https://github.com/OHIF/Viewers/commit/adedc8c382e18a2e86a569e3d023cc55a157363f)) + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/core + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + + +### Features + +* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1)) + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + + +### Bug Fixes + +* **memory leak:** array buffer was sticking around in volume viewports ([#3611](https://github.com/OHIF/Viewers/issues/3611)) ([65b49ae](https://github.com/OHIF/Viewers/commit/65b49aeb1b5f38224e4892bdf32453500ee351f8)) + + + + + +## [2.9.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.5...@ohif/core@2.9.6) (2020-05-14) + + +### Bug Fixes + +* ๐Ÿ› Load default display set when no time metadata ([#1684](https://github.com/OHIF/Viewers/issues/1684)) ([f7b8b6a](https://github.com/OHIF/Viewers/commit/f7b8b6a41c4626084ef56b0fdf7363e914b143c4)), closes [#1683](https://github.com/OHIF/Viewers/issues/1683) + + + + + +## [2.9.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.4...@ohif/core@2.9.5) (2020-05-12) + + +### Bug Fixes + +* ๐Ÿ› Fix seg color load ([#1724](https://github.com/OHIF/Viewers/issues/1724)) ([c4f84b1](https://github.com/OHIF/Viewers/commit/c4f84b1174d04ba84d37ed89b6d7ab541be28181)) + + + + + +## [2.9.4](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.3...@ohif/core@2.9.4) (2020-05-06) + +**Note:** Version bump only for package @ohif/core + + + + + +## [2.9.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.2...@ohif/core@2.9.3) (2020-05-04) + + +### Bug Fixes + +* ๐Ÿ› Proper error handling for derived display sets ([#1708](https://github.com/OHIF/Viewers/issues/1708)) ([5b20d8f](https://github.com/OHIF/Viewers/commit/5b20d8f323e4b3ef9988f2f2ab672d697b6da409)) + + + + + +## [2.9.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.1...@ohif/core@2.9.2) (2020-05-04) + + +### Bug Fixes + +* use bit-appropriate array for palette lookup tables ([#1698](https://github.com/OHIF/Viewers/issues/1698)) ([7033886](https://github.com/OHIF/Viewers/commit/70338866978a76fa026c18d7c3c05257c5ece762)) + + + + + +## [2.9.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.9.0...@ohif/core@2.9.1) (2020-04-28) + +**Note:** Version bump only for package @ohif/core + + + + + +# [2.9.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.8.1...@ohif/core@2.9.0) (2020-04-24) + + +### Features + +* ๐ŸŽธ Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f)) + + + + + +## [2.8.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.8.0...@ohif/core@2.8.1) (2020-04-23) + + +### Bug Fixes + +* ๐Ÿ› Multiframe fix ([#1661](https://github.com/OHIF/Viewers/issues/1661)) ([7120561](https://github.com/OHIF/Viewers/commit/71205618ecb8b592247c5acb32284bfe7e18fce5)) + + + + + +# [2.8.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.7.1...@ohif/core@2.8.0) (2020-04-23) + + +### Features + +* configuration to hook into XHR Error handling ([e96205d](https://github.com/OHIF/Viewers/commit/e96205de35e5bec14dc8a9a8509db3dd4e6ecdb6)) + + + + + +## [2.7.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.7.0...@ohif/core@2.7.1) (2020-04-22) + + +### Bug Fixes + +* whiteLabeling should support component creation by passing React to defined fn ([#1659](https://github.com/OHIF/Viewers/issues/1659)) ([2093a00](https://github.com/OHIF/Viewers/commit/2093a0036584b2cc698c8f06fe62b334523b1029)) + + + + + +# [2.7.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.11...@ohif/core@2.7.0) (2020-04-17) + + +### Features + +* set the authorization header for DICOMWeb requests if provided in query string ([#1646](https://github.com/OHIF/Viewers/issues/1646)) ([450c80b](https://github.com/OHIF/Viewers/commit/450c80b9d5f172be8b5713b422370360325a0afc)) + + + + + +## [2.6.11](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.10...@ohif/core@2.6.11) (2020-04-15) + +**Note:** Version bump only for package @ohif/core + + + + + +## [2.6.10](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.9...@ohif/core@2.6.10) (2020-04-09) + + +### Bug Fixes + +* Revert "refactor: Reduce bundle size ([#1575](https://github.com/OHIF/Viewers/issues/1575))" ([#1622](https://github.com/OHIF/Viewers/issues/1622)) ([d21af3f](https://github.com/OHIF/Viewers/commit/d21af3f133492fa31492413b8782936c9ff18b44)) + + + + + +## [2.6.9](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.8...@ohif/core@2.6.9) (2020-04-09) + +**Note:** Version bump only for package @ohif/core + + + + + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.6.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.7...@ohif/core@2.6.8) (2020-04-06) + +**Note:** Version bump only for package @ohif/core + + + + + +## [2.6.7](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.6...@ohif/core@2.6.7) (2020-04-02) + + +### Bug Fixes + +* ๐Ÿ› Fix multiframe images ([#1595](https://github.com/OHIF/Viewers/issues/1595)) ([9e0bd52](https://github.com/OHIF/Viewers/commit/9e0bd52c6a86648eb6673344a8555ad787043e5c)) + + + + + +## [2.6.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.5...@ohif/core@2.6.6) (2020-04-02) + +**Note:** Version bump only for package @ohif/core + + + + + +## [2.6.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.4...@ohif/core@2.6.5) (2020-04-01) + + +### Bug Fixes + +* segmentation not loading ([#1566](https://github.com/OHIF/Viewers/issues/1566)) ([4a7ce1c](https://github.com/OHIF/Viewers/commit/4a7ce1c09324d74c61048393e3a2427757e4001a)) + + + + + +## [2.6.4](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.3...@ohif/core@2.6.4) (2020-03-25) + + +### Bug Fixes + +* Add support for single entries in SequenceOfUltrasoundRegions. Mโ€ฆ ([#1559](https://github.com/OHIF/Viewers/issues/1559)) ([c1a0d3c](https://github.com/OHIF/Viewers/commit/c1a0d3c662d143b62dfbf1c01f6ce394af3756ca)) +* disable autoFreeze of immer, even in dev mode ([#1560](https://github.com/OHIF/Viewers/issues/1560)) ([d604eba](https://github.com/OHIF/Viewers/commit/d604ebaffd93f688eadd0081e402f27074dd226b)) + + + + + +## [2.6.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.2...@ohif/core@2.6.3) (2020-03-24) + + +### Bug Fixes + +* Ensure we take into account pixel spacing fields properly ([#1555](https://github.com/OHIF/Viewers/issues/1555)) ([77ab0ad](https://github.com/OHIF/Viewers/commit/77ab0ad9a14a135b5560741fc1600704df08c141)) + + + + + +## [2.6.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.1...@ohif/core@2.6.2) (2020-03-24) + + +### Bug Fixes + +* OverlayPlane module usage for ArrayBuffer, BulkDataURI, and InlineBinary cases, as well as PaletteColor LUTs for ArrayBuffer (i.e. local drag/drop) case ([#1546](https://github.com/OHIF/Viewers/issues/1546)) ([404d52f](https://github.com/OHIF/Viewers/commit/404d52fe5c0442dd13e4d407bb0687d72fa5f32c)) + + + + + +## [2.6.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.0...@ohif/core@2.6.1) (2020-03-23) + + +### Bug Fixes + +* avoid-wasteful-renders ([#1544](https://github.com/OHIF/Viewers/issues/1544)) ([e41d339](https://github.com/OHIF/Viewers/commit/e41d339f5faef6b93700bc860f37f29f32ad5ed6)) + + + + + +# [2.6.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.5.3...@ohif/core@2.6.0) (2020-03-13) + + +### Features + +* Segmentations Settings UI - Phase 1 [#1391](https://github.com/OHIF/Viewers/issues/1391) ([#1392](https://github.com/OHIF/Viewers/issues/1392)) ([e8842cf](https://github.com/OHIF/Viewers/commit/e8842cf8aebde98db7fc123e4867c8288552331f)), closes [#1423](https://github.com/OHIF/Viewers/issues/1423) + + + + + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [2.5.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.5.2...@ohif/core@2.5.3) (2020-03-09) + +**Note:** Version bump only for package @ohif/core + + + + + +## [2.5.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.5.1...@ohif/core@2.5.2) (2020-03-06) + +**Note:** Version bump only for package @ohif/core + + + + + +## [2.5.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.5.0...@ohif/core@2.5.1) (2020-02-21) + + +### Bug Fixes + +* ๐Ÿ’ก Update log to warn ([#1454](https://github.com/OHIF/Viewers/issues/1454)) ([509a7b7](https://github.com/OHIF/Viewers/commit/509a7b7e57834a0653add98cc320b2a5dbefd51d)), closes [#1451](https://github.com/OHIF/Viewers/issues/1451) + + + + + +# [2.5.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.4.1...@ohif/core@2.5.0) (2020-02-20) + + +### Features + +* [#1342](https://github.com/OHIF/Viewers/issues/1342) - Window level tab ([#1429](https://github.com/OHIF/Viewers/issues/1429)) ([ebc01a8](https://github.com/OHIF/Viewers/commit/ebc01a8ca238d5a3437b44d81f75aa8a5e8d0574)) + + + + + +## [2.4.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.4.0...@ohif/core@2.4.1) (2020-02-12) + + +### Bug Fixes + +* Combined Hotkeys for special characters ([#1233](https://github.com/OHIF/Viewers/issues/1233)) ([2f30e7a](https://github.com/OHIF/Viewers/commit/2f30e7a821a238144c49c56f37d8e5565540b4bd)) + + + + + +# [2.4.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.9...@ohif/core@2.4.0) (2020-02-10) + + +### Features + +* ๐ŸŽธ MeasurementService ([#1314](https://github.com/OHIF/Viewers/issues/1314)) ([0c37a40](https://github.com/OHIF/Viewers/commit/0c37a406d963569af8c3be24c697dafd42712dfc)) + + + + + +## [2.3.9](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.8...@ohif/core@2.3.9) (2020-02-07) + +**Note:** Version bump only for package @ohif/core + + + + + +## [2.3.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.7...@ohif/core@2.3.8) (2020-02-06) + + +### Bug Fixes + +* Remove trash data from redux storage after updates ([#1358](https://github.com/OHIF/Viewers/issues/1358)) ([7b2d44f](https://github.com/OHIF/Viewers/commit/7b2d44f2c18241ea521b8d3652aee32e36eaddb8)) + + + + + +## [2.3.7](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.6...@ohif/core@2.3.7) (2020-01-30) + + +### Bug Fixes + +* Set VTK viewport as active by interaction ([#1139](https://github.com/OHIF/Viewers/issues/1139)) ([686d12d](https://github.com/OHIF/Viewers/commit/686d12da5c9d3d435b1e326c2a5caee36e2ed27c)) + + + + + +## [2.3.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.5...@ohif/core@2.3.6) (2020-01-28) + +**Note:** Version bump only for package @ohif/core + + + + + +## [2.3.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.4...@ohif/core@2.3.5) (2020-01-28) + +**Note:** Version bump only for package @ohif/core + + + + + +## [2.3.4](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.3...@ohif/core@2.3.4) (2020-01-27) + +**Note:** Version bump only for package @ohif/core + + + + + +## [2.3.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.2...@ohif/core@2.3.3) (2020-01-24) + +**Note:** Version bump only for package @ohif/core + + + + + +## [2.3.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.1...@ohif/core@2.3.2) (2020-01-06) + +**Note:** Version bump only for package @ohif/core + + + + + +## [2.3.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.0...@ohif/core@2.3.1) (2019-12-30) + + +### Bug Fixes + +* ๐Ÿ› 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241) + + + + + +# [2.3.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.2.1...@ohif/core@2.3.0) (2019-12-20) + + +### Features + +* ๐ŸŽธ Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223) + + + + + +## [2.2.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.2.0...@ohif/core@2.2.1) (2019-12-18) + +**Note:** Version bump only for package @ohif/core + + + + + +# [2.2.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.1.1...@ohif/core@2.2.0) (2019-12-16) + + +### Features + +* ๐ŸŽธ Expose extension config to modules ([#1279](https://github.com/OHIF/Viewers/issues/1279)) ([4ea239a](https://github.com/OHIF/Viewers/commit/4ea239a9535ef297e23387c186e537ab273744ea)), closes [#1268](https://github.com/OHIF/Viewers/issues/1268) + + + + + +## [2.1.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.1.0...@ohif/core@2.1.1) (2019-12-16) + +**Note:** Version bump only for package @ohif/core + + + + + +# [2.1.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.2...@ohif/core@2.1.0) (2019-12-11) + + +### Features + +* ๐ŸŽธ DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758) + + + + + +## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.1...@ohif/core@2.0.2) (2019-12-11) + +**Note:** Version bump only for package @ohif/core + + + + + +## [2.0.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.0...@ohif/core@2.0.1) (2019-12-09) + +**Note:** Version bump only for package @ohif/core + + + + + +# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.3...@ohif/core@2.0.0) (2019-12-09) + + +* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229) + + +### BREAKING CHANGES + +* modifies the exposed react components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance. + + + + + +## [1.13.3](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.2...@ohif/core@1.13.3) (2019-12-06) + +**Note:** Version bump only for package @ohif/core + + + + + +## [1.13.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.1...@ohif/core@1.13.2) (2019-12-02) + +**Note:** Version bump only for package @ohif/core + + + + + +## [1.13.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.0...@ohif/core@1.13.1) (2019-11-28) + + +### Bug Fixes + +* User Preferences Issues ([#1207](https://github.com/OHIF/Viewers/issues/1207)) ([1df21a9](https://github.com/OHIF/Viewers/commit/1df21a9e075b5e6dfc10a429ae825826f46c71b8)), closes [#1161](https://github.com/OHIF/Viewers/issues/1161) [#1164](https://github.com/OHIF/Viewers/issues/1164) [#1177](https://github.com/OHIF/Viewers/issues/1177) [#1179](https://github.com/OHIF/Viewers/issues/1179) [#1180](https://github.com/OHIF/Viewers/issues/1180) [#1181](https://github.com/OHIF/Viewers/issues/1181) [#1182](https://github.com/OHIF/Viewers/issues/1182) [#1183](https://github.com/OHIF/Viewers/issues/1183) [#1184](https://github.com/OHIF/Viewers/issues/1184) [#1185](https://github.com/OHIF/Viewers/issues/1185) + + + + + +# [1.13.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.12.0...@ohif/core@1.13.0) (2019-11-25) + + +### Features + +* Add new annotate tool using new dialog service ([#1211](https://github.com/OHIF/Viewers/issues/1211)) ([8fd3af1](https://github.com/OHIF/Viewers/commit/8fd3af1e137e793f1b482760a22591c64a072047)) + + + + + +# [1.12.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.11.0...@ohif/core@1.12.0) (2019-11-19) + + +### Features + +* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37)) + + + + + +# [1.11.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.10.0...@ohif/core@1.11.0) (2019-11-19) + + +### Features + +* Issue 879 viewer route query param not filtering but promoting ([#1141](https://github.com/OHIF/Viewers/issues/1141)) ([b17f753](https://github.com/OHIF/Viewers/commit/b17f753e6222045252ef885e40233681541a32e1)), closes [#1118](https://github.com/OHIF/Viewers/issues/1118) + + + + + +# [1.10.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.9.1...@ohif/core@1.10.0) (2019-11-15) + + +### Features + +* Inject into Extension Modules / improve tests ([f63d8a7](https://github.com/OHIF/Viewers/commit/f63d8a73d867ad9dfd8ee0cad74edce180eb34f0)) + + + + + +## [1.9.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.9.0...@ohif/core@1.9.1) (2019-11-15) + +**Note:** Version bump only for package @ohif/core + + + + + +# [1.9.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.8.0...@ohif/core@1.9.0) (2019-11-13) + +### Features + +- expose UiNotifications service + ([#1172](https://github.com/OHIF/Viewers/issues/1172)) + ([5c04e34](https://github.com/OHIF/Viewers/commit/5c04e34c8fb2394ab7acd9eb4f2ab12afeb2f255)) + +# [1.8.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.7.1...@ohif/core@1.8.0) (2019-11-12) + +### Features + +- ๐ŸŽธ Update hotkeys and user preferences modal + ([#1135](https://github.com/OHIF/Viewers/issues/1135)) + ([e62f5f8](https://github.com/OHIF/Viewers/commit/e62f5f8dd28ab363f23671cd21cee115abb870ff)), + closes [#923](https://github.com/OHIF/Viewers/issues/923) + +## [1.7.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.7.0...@ohif/core@1.7.1) (2019-11-08) + +### Bug Fixes + +- Add a fallback metadata provider which pulls metadata from WADO-โ€ฆ + ([#1158](https://github.com/OHIF/Viewers/issues/1158)) + ([31b1adf](https://github.com/OHIF/Viewers/commit/31b1adfa5993d6c8e3e9c8b03fa9856f2621b037)) + +# [1.7.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.6.2...@ohif/core@1.7.0) (2019-11-05) + +### Features + +- ๐ŸŽธ Filter by url query param for seriesInstnaceUID + ([#1117](https://github.com/OHIF/Viewers/issues/1117)) + ([e208f2e](https://github.com/OHIF/Viewers/commit/e208f2e6a9c49b16dadead0a917f657cf023929a)), + closes [#1118](https://github.com/OHIF/Viewers/issues/1118) + +## [1.6.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.6.1...@ohif/core@1.6.2) (2019-11-05) + +### Bug Fixes + +- [#1075](https://github.com/OHIF/Viewers/issues/1075) Returning to the Study + List before all series have finisheโ€ฆ + ([#1090](https://github.com/OHIF/Viewers/issues/1090)) + ([ecaf578](https://github.com/OHIF/Viewers/commit/ecaf578f92dc40294cec7ff9b272fb432dec4125)) + +## [1.6.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.6.0...@ohif/core@1.6.1) (2019-10-31) + +### Bug Fixes + +- application crash if patientName is an object + ([#1138](https://github.com/OHIF/Viewers/issues/1138)) + ([64cf3b3](https://github.com/OHIF/Viewers/commit/64cf3b324da2383a927af1df2d46db2fca5318aa)) + +# [1.6.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.5.2...@ohif/core@1.6.0) (2019-10-26) + +### Features + +- Snapshot Download Tool ([#840](https://github.com/OHIF/Viewers/issues/840)) + ([450e098](https://github.com/OHIF/Viewers/commit/450e0981a5ba054fcfcb85eeaeb18371af9088f8)) + +## [1.5.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.5.1...@ohif/core@1.5.2) (2019-10-25) + +### Bug Fixes + +- set SR in ActiveViewport by clicking thumb + ([#1091](https://github.com/OHIF/Viewers/issues/1091)) + ([986b7ae](https://github.com/OHIF/Viewers/commit/986b7ae2bf4f7d27f326e62f93285ce20eaf0a79)) + +## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.5.0...@ohif/core@1.5.1) (2019-10-25) + +### Bug Fixes + +- ๐Ÿ› Orthographic MPR fix ([#1092](https://github.com/OHIF/Viewers/issues/1092)) + ([460e375](https://github.com/OHIF/Viewers/commit/460e375f0aa75d35f7a46b4d48e6cc706019956d)) + +# [1.5.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.4.0...@ohif/core@1.5.0) (2019-10-25) + +### Features + +- ๐ŸŽธ Allow routes to load Google Cloud DICOM Stores in the Study List + ([#1069](https://github.com/OHIF/Viewers/issues/1069)) + ([21b586b](https://github.com/OHIF/Viewers/commit/21b586b08f3dde6613859712a9e0577dece564db)) + +# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.3.2...@ohif/core@1.4.0) (2019-10-15) + +### Features + +- ๐ŸŽธ Only allow reconstruction of datasets that make sense + ([#1010](https://github.com/OHIF/Viewers/issues/1010)) + ([2d75e01](https://github.com/OHIF/Viewers/commit/2d75e01)), closes + [#561](https://github.com/OHIF/Viewers/issues/561) + +## [1.3.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.3.1...@ohif/core@1.3.2) (2019-10-14) + +### Bug Fixes + +- Return display sets in StudyMetadata.\_createDisplaySetsForSeries + ([#1042](https://github.com/OHIF/Viewers/issues/1042)) + ([fc01532](https://github.com/OHIF/Viewers/commit/fc01532)) + +## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.3.0...@ohif/core@1.3.1) (2019-10-11) + +**Note:** Version bump only for package @ohif/core + +# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.2.0...@ohif/core@1.3.0) (2019-10-11) + +### Features + +- ๐ŸŽธ Improve usability of Google Cloud adapter, including direct routes to + studies ([#989](https://github.com/OHIF/Viewers/issues/989)) + ([2bc361c](https://github.com/OHIF/Viewers/commit/2bc361c)) + +# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.1.0...@ohif/core@1.2.0) (2019-10-09) + +### Bug Fixes + +- OHIF-1002 Study lazy load should be true by default + ([#1004](https://github.com/OHIF/Viewers/issues/1004)) + ([66d8bc6](https://github.com/OHIF/Viewers/commit/66d8bc6)) + +### Features + +- Allow a server requestOptions.auth to be a function that returns the + Authorization header. ([#928](https://github.com/OHIF/Viewers/issues/928)) + ([0064a4b](https://github.com/OHIF/Viewers/commit/0064a4b)) + +# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.2...@ohif/core@1.1.0) (2019-10-03) + +### Features + +- Use QIDO + WADO to load series metadata individually rather than the entire + study metadata at once ([#953](https://github.com/OHIF/Viewers/issues/953)) + ([9e10c2b](https://github.com/OHIF/Viewers/commit/9e10c2b)) + +## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.1...@ohif/core@1.0.2) (2019-10-02) + +### Bug Fixes + +- Temporarily sort SEG files to the end of the display set list as a workaround + for several metadata issues + ([#987](https://github.com/OHIF/Viewers/issues/987)) + ([b3b4c10](https://github.com/OHIF/Viewers/commit/b3b4c10)) + +## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.0...@ohif/core@1.0.1) (2019-09-27) + +### Bug Fixes + +- Check for Value in 00081155 sequence (Few patient protocol images doesn't have + this value) and removed a duplicate declaration + ([#921](https://github.com/OHIF/Viewers/issues/921)) + ([d0ec9cf](https://github.com/OHIF/Viewers/commit/d0ec9cf)) + +# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.10...@ohif/core@1.0.0) (2019-09-27) + +### Bug Fixes + +- ๐Ÿ› Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support + in local file and WADO-RS-only use cases + ([#862](https://github.com/OHIF/Viewers/issues/862)) + ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes + [#838](https://github.com/OHIF/Viewers/issues/838) + +### BREAKING CHANGES + +- DICOM Seg + +## [0.50.10](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.9...@ohif/core@0.50.10) (2019-09-23) + +### Bug Fixes + +- Avoid using variable name "module" + ([#942](https://github.com/OHIF/Viewers/issues/942)) + ([72427fe](https://github.com/OHIF/Viewers/commit/72427fe)), closes + [#940](https://github.com/OHIF/Viewers/issues/940) + +## [0.50.9](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.8...@ohif/core@0.50.9) (2019-09-17) + +### Bug Fixes + +- bump cornerstone-tools version in peerDeps + ([4afc88c](https://github.com/OHIF/Viewers/commit/4afc88c)) + +## [0.50.8](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.7...@ohif/core@0.50.8) (2019-09-10) + +**Note:** Version bump only for package @ohif/core + +## [0.50.7](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.6...@ohif/core@0.50.7) (2019-09-10) + +### Bug Fixes + +- remove requestOptions when key is not needed + ([32bc47d](https://github.com/OHIF/Viewers/commit/32bc47d)) + +## [0.50.6](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.5...@ohif/core@0.50.6) (2019-09-09) + +**Note:** Version bump only for package @ohif/core + +## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.4...@ohif/core@0.50.5) (2019-09-04) + +**Note:** Version bump only for package @ohif/core + +## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.3...@ohif/core@0.50.4) (2019-09-04) + +### Bug Fixes + +- measurementsAPI issue caused by production build + ([#842](https://github.com/OHIF/Viewers/issues/842)) + ([49d3439](https://github.com/OHIF/Viewers/commit/49d3439)) + +## [0.50.3](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.2...@ohif/core@0.50.3) (2019-08-26) + +### Bug Fixes + +- **Studies:** Qidosupportsincludefield should be true by default + ([#801](https://github.com/OHIF/Viewers/issues/801)) + ([a88d865](https://github.com/OHIF/Viewers/commit/a88d865)) + +## [0.50.2](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.1...@ohif/core@0.50.2) (2019-08-22) + +**Note:** Version bump only for package @ohif/core + +## [0.50.1](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.0-alpha.10...@ohif/core@0.50.1) (2019-08-14) + +**Note:** Version bump only for package @ohif/core + +# [0.50.0-alpha.10](https://github.com/OHIF/Viewers/compare/@ohif/core@0.11.1-alpha.9...@ohif/core@0.50.0-alpha.10) (2019-08-14) + +**Note:** Version bump only for package @ohif/core + +## [0.11.1-alpha.9](https://github.com/OHIF/Viewers/compare/@ohif/core@0.11.1-alpha.8...@ohif/core@0.11.1-alpha.9) (2019-08-14) + +**Note:** Version bump only for package @ohif/core + +## 0.11.1-alpha.8 (2019-08-14) + +**Note:** Version bump only for package @ohif/core + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.11.1-alpha.7](https://github.com/OHIF/Viewers/compare/@ohif/core@0.11.1-alpha.6...@ohif/core@0.11.1-alpha.7) (2019-08-08) + +**Note:** Version bump only for package @ohif/core + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.11.1-alpha.6](https://github.com/OHIF/Viewers/compare/@ohif/core@0.11.1-alpha.5...@ohif/core@0.11.1-alpha.6) (2019-08-08) + +**Note:** Version bump only for package @ohif/core + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.11.1-alpha.5](https://github.com/OHIF/Viewers/compare/@ohif/core@0.11.1-alpha.4...@ohif/core@0.11.1-alpha.5) (2019-08-08) + +**Note:** Version bump only for package @ohif/core + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.11.1-alpha.4](https://github.com/OHIF/Viewers/compare/@ohif/core@0.11.1-alpha.3...@ohif/core@0.11.1-alpha.4) (2019-08-08) + +**Note:** Version bump only for package @ohif/core + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.11.1-alpha.3](https://github.com/OHIF/Viewers/compare/@ohif/core@0.11.1-alpha.2...@ohif/core@0.11.1-alpha.3) (2019-08-08) + +**Note:** Version bump only for package @ohif/core + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.11.1-alpha.2](https://github.com/OHIF/Viewers/compare/@ohif/core@0.11.1-alpha.1...@ohif/core@0.11.1-alpha.2) (2019-08-07) + +**Note:** Version bump only for package @ohif/core + +## [0.11.1-alpha.1](https://github.com/OHIF/Viewers/compare/@ohif/core@0.11.1-alpha.0...@ohif/core@0.11.1-alpha.1) (2019-08-07) + +**Note:** Version bump only for package @ohif/core + +## 0.11.1-alpha.0 (2019-08-05) + +**Note:** Version bump only for package @ohif/core diff --git a/platform/core/LICENSE b/platform/core/LICENSE new file mode 100644 index 0000000..19e20dd --- /dev/null +++ b/platform/core/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/platform/core/README.md b/platform/core/README.md new file mode 100644 index 0000000..ff8f555 --- /dev/null +++ b/platform/core/README.md @@ -0,0 +1,136 @@ + + +
+

@ohif/core

+

@ohif/core is a collection of useful functions and classes for building web-based medical imaging applications. This library helps power OHIF's zero-footprint DICOM viewer.

+
+ +
+ +[![NPM version][npm-version-image]][npm-url] +[![NPM downloads][npm-downloads-image]][npm-url] +[![All Contributors](https://img.shields.io/badge/all_contributors-6-orange.svg?style=flat-square)](#contributors) +[![MIT License][license-image]][license-url] + + + +## Why? + +This library offers pre-packaged solutions for features common to Web-based +medical imaging viewers. For example: + +- Hotkeys +- DICOM Web +- Hanging Protocols +- Managing a study's measurements +- Managing a study's DICOM metadata +- A flexible pattern for extensions +- And many others + +It does this while remaining decoupled from any particular view library or +rendering logic. While we use it to power our [React Viewer][react-viewer], it +can be used with Vue, React, Vanilla JS, or any number of other frameworks. + +## Getting Started + +The documentation for this library is sparse. The best way to get started is to +look at its +[top level exports](https://github.com/OHIF/Viewers/blob/master/platform/core/src/index.js), +and explore the source code of features that interest you. If you want to see +how we use this library, you can check out [our viewer +implementation][react-viewer]. + +### Install + +> This library is pre- v1.0. All releases until a v1.0 have the possibility of +> introducing breaking changes. Please depend on an "exact" version in your +> projects to prevent issues caused by loose versioning. + +``` +// with npm +npm i @ohif/core --save-exact + +// with yarn +yarn add @ohif/core --exact +``` + +### Usage + +Usage is dependent on the feature(s) you want to leverage. The bulk of +`@ohif/core`'s features are "pure" and can be imported and used in place. + +_Example: retrieving study metadata from a server_ + +```js +import { studies } from '@ohif/core'; + +const studiesMetadata = await studies.retrieveStudiesMetadata( + server, // Object + studyInstanceUIDs, // Array + seriesInstanceUIDs // Array (optional) +); +``` + +### Contributing + +It is notoriously difficult to setup multiple dependent repositories for +end-to-end testing and development. That's why we recommend writing and running +unit tests when adding and modifying features for this library. This allows us +to program in isolation without a complex setup, and has the added benefit of +producing well-tested business logic. + +1. Clone this repository +2. Navigate to the project directory, and `yarn install` +3. To begin making changes, `yarn run dev` +4. To commit changes, run `yarn run cm` + +When creating tests, place the test file "next to" the file you're testing. +[For example](https://github.com/OHIF/ohif-core/blob/master/src/index.test.js): + +```js +// File +index.js; + +// Test for file +index.test.js; +``` + +As you add and modify code, `jest` will watch for uncommitted changes and run +your tests, reporting the results to your terminal. Make a pull request with +your changes to `master`, and a core team member will review your work. If you +have any questions, please don't hesitate to reach out via a GitHub issue. + +## Contributors + +Thanks goes to these wonderful people +([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + +
Erik Ziegler
Erik Ziegler

๐Ÿ’ป
Evren Ozkan
Evren Ozkan

๐Ÿ’ป
Gustavo Andrรฉ Lelis
Gustavo Andrรฉ Lelis

๐Ÿ’ป
Danny Brown
Danny Brown

๐Ÿ’ป
allcontributors[bot]
allcontributors[bot]

๐Ÿ“–
Ivan Aksamentov
Ivan Aksamentov

๐Ÿ’ป โš ๏ธ
+ + + +This project follows the +[all-contributors](https://github.com/all-contributors/all-contributors) +specification. Contributions of any kind welcome! + +## License + +MIT ยฉ [OHIF](https://github.com/OHIF) + + + + + +[npm-url]: https://npmjs.org/package/@ohif/core +[npm-downloads-image]: https://img.shields.io/npm/dm/@ohif/core.svg?style=flat-square +[npm-version-image]: https://img.shields.io/npm/v/@ohif/core.svg?style=flat-square +[all-contributors-image]: https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square +[license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square +[license-url]: LICENSE + +[react-viewer]: https://github.com/OHIF/Viewers/tree/react + diff --git a/platform/core/babel.config.js b/platform/core/babel.config.js new file mode 100644 index 0000000..325ca2a --- /dev/null +++ b/platform/core/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/platform/core/jest.config.js b/platform/core/jest.config.js new file mode 100644 index 0000000..f57711b --- /dev/null +++ b/platform/core/jest.config.js @@ -0,0 +1,12 @@ +const base = require('../../jest.config.base.js'); +const pkg = require('./package'); + +module.exports = { + ...base, + displayName: pkg.name, + // rootDir: "../.." + // testMatch: [ + // //`/platform/${pack.name}/**/*.spec.js` + // "/platform/app/**/*.test.js" + // ] +}; diff --git a/platform/core/package.json b/platform/core/package.json new file mode 100644 index 0000000..6f1d9b2 --- /dev/null +++ b/platform/core/package.json @@ -0,0 +1,64 @@ +{ + "name": "@ohif/core", + "version": "3.10.0-beta.111", + "description": "Generic business logic for web-based medical imaging applications", + "author": "OHIF Core Team", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/ohif-core.umd.js", + "module": "src/index.ts", + "types": "src/types/index.ts", + "sideEffects": "false", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist", + "README.md" + ], + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "jest --watchAll", + "dev:core": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "start": "yarn run dev", + "test:unit": "jest --watchAll", + "test:unit:ci": "jest --ci --runInBand --collectCoverage" + }, + "peerDependencies": { + "@cornerstonejs/codec-charls": "^1.2.3", + "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", + "@cornerstonejs/codec-openjpeg": "^1.2.4", + "@cornerstonejs/codec-openjph": "^2.4.5", + "@cornerstonejs/dicom-image-loader": "^2.19.14", + "@ohif/ui": "3.10.0-beta.111", + "cornerstone-math": "0.1.9", + "dicom-parser": "^1.8.21" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "dcmjs": "*", + "dicomweb-client": "^0.10.4", + "gl-matrix": "^3.4.3", + "immutability-helper": "^3.1.1", + "isomorphic-base64": "^1.0.2", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0", + "moment": "*", + "object-hash": "2.1.1", + "query-string": "^6.14.0", + "react-shepherd": "6.1.1", + "shepherd.js": "13.0.3", + "validate.js": "^0.12.0" + }, + "devDependencies": { + "webpack-merge": "*" + } +} diff --git a/platform/core/src/DICOMWeb/getAttribute.js b/platform/core/src/DICOMWeb/getAttribute.js new file mode 100644 index 0000000..b07f3cd --- /dev/null +++ b/platform/core/src/DICOMWeb/getAttribute.js @@ -0,0 +1,52 @@ +/** + * Returns the specified element as a dicom attribute group/element. + * + * @param element - The group/element of the element (e.g. '00280009') + * @param [defaultValue] - The value to return if the element is not present + * @returns {*} + */ +export default function getAttribute(element, defaultValue) { + if (!element) { + return defaultValue; + } + // Value is not present if the attribute has a zero length value + if (!element.Value) { + return defaultValue; + } + // Sanity check to make sure we have at least one entry in the array. + if (!element.Value.length) { + return defaultValue; + } + + return convertToInt(element.Value); +} + +function convertToInt(input) { + function padFour(input) { + const l = input.length; + + if (l === 0) { + return '0000'; + } + if (l === 1) { + return '000' + input; + } + if (l === 2) { + return '00' + input; + } + if (l === 3) { + return '0' + input; + } + + return input; + } + + let output = ''; + for (let i = 0; i < input.length; i++) { + for (let j = 0; j < input[i].length; j++) { + output += padFour(input[i].charCodeAt(j).toString(16)); + } + } + + return parseInt(output, 16); +} diff --git a/platform/core/src/DICOMWeb/getAttribute.test.js b/platform/core/src/DICOMWeb/getAttribute.test.js new file mode 100644 index 0000000..a0dcc6d --- /dev/null +++ b/platform/core/src/DICOMWeb/getAttribute.test.js @@ -0,0 +1,65 @@ +import getAttribute from './getAttribute'; + +describe('getAttribute', () => { + it('should return a default value if element is null or undefined', () => { + const defaultValue = '0000'; + const nullElement = null; + const undefinedElement = undefined; + + expect(getAttribute(nullElement, defaultValue)).toEqual(defaultValue); + expect(getAttribute(undefinedElement, defaultValue)).toEqual(defaultValue); + }); + + it('should return a default value if element.Value is null, undefined or not present', () => { + const defaultValue = '0000'; + const nullElement = { + id: 0, + Value: null, + }; + const undefinedElement = { + id: 0, + Value: undefined, + }; + const noValuePresentElement = { + id: 0, + }; + + expect(getAttribute(nullElement, defaultValue)).toEqual(defaultValue); + expect(getAttribute(undefinedElement, defaultValue)).toEqual(defaultValue); + expect(getAttribute(noValuePresentElement, defaultValue)).toEqual(defaultValue); + }); + + it('should return 48 for element with value 0', () => { + const returnValue = 48; + const element = { + Value: '0', + }; + expect(getAttribute(element, null)).toEqual(returnValue); + }); + + it('should return 3211313 for element with value 11', () => { + const returnValue = 3211313; + const element = { + Value: '11', + }; + expect(getAttribute(element, null)).toEqual(returnValue); + }); + + it('should return 2.4923405222191973e+35 for element with value 00280009', () => { + const returnValue = 2.4923405222191973e35; + const element = { + id: 0, + Value: '00280009', + }; + expect(getAttribute(element, null)).toEqual(returnValue); + }); + + it('should return 2949169 for element with value -1', () => { + const returnValue = 2949169; + const element = { + id: 0, + Value: '-1', + }; + expect(getAttribute(element, null)).toEqual(returnValue); + }); +}); diff --git a/platform/core/src/DICOMWeb/getAuthorizationHeader.js b/platform/core/src/DICOMWeb/getAuthorizationHeader.js new file mode 100644 index 0000000..4d996bb --- /dev/null +++ b/platform/core/src/DICOMWeb/getAuthorizationHeader.js @@ -0,0 +1,35 @@ +import 'isomorphic-base64'; +import user from '../user'; + +/** + * Returns the Authorization header as part of an Object. + * + * @export + * @param {Object} [server={}] + * @param {Object} [server.requestOptions] + * @param {string|function} [server.requestOptions.auth] + * @returns {Object} { Authorization } + */ +export default function getAuthorizationHeader({ requestOptions } = {}, user) { + const headers = {}; + + // Check for OHIF.user since this can also be run on the server + const accessToken = user && user.getAccessToken && user.getAccessToken(); + + // Auth for a specific server + if (requestOptions && requestOptions.auth) { + if (typeof requestOptions.auth === 'function') { + // Custom Auth Header + headers.Authorization = requestOptions.auth(requestOptions); + } else { + // HTTP Basic Auth (user:password) + headers.Authorization = `Basic ${btoa(requestOptions.auth)}`; + } + } + // Auth for the user's default + else if (accessToken) { + headers.Authorization = `Bearer ${accessToken}`; + } + + return headers; +} diff --git a/platform/core/src/DICOMWeb/getAuthorizationHeader.test.js b/platform/core/src/DICOMWeb/getAuthorizationHeader.test.js new file mode 100644 index 0000000..9bf0b46 --- /dev/null +++ b/platform/core/src/DICOMWeb/getAuthorizationHeader.test.js @@ -0,0 +1,72 @@ +import getAuthorizationHeader from './getAuthorizationHeader'; +import user from './../user'; + +jest.mock('./../user.js'); + +describe('getAuthorizationHeader', () => { + it('should return a HTTP Basic Auth when server contains requestOptions.auth', () => { + const validServer = { + requestOptions: { + auth: 'dummy_user:dummy_password', + }, + }; + + const expectedAuthorizationHeader = { + Authorization: `Basic ${btoa(validServer.requestOptions.auth)}`, + }; + + const authentication = getAuthorizationHeader(validServer); + + expect(authentication).toEqual(expectedAuthorizationHeader); + }); + + it('should return a HTTP Basic Auth when server contains requestOptions.auth even though there is no password', () => { + const validServerWithoutPassword = { + requestOptions: { + auth: 'dummy_user', + }, + }; + + const expectedAuthorizationHeader = { + Authorization: `Basic ${btoa(validServerWithoutPassword.requestOptions.auth)}`, + }; + + const authentication = getAuthorizationHeader(validServerWithoutPassword); + + expect(authentication).toEqual(expectedAuthorizationHeader); + }); + + it('should return a HTTP Basic Auth when server contains requestOptions.auth custom function', () => { + const validServerCustomAuth = { + requestOptions: { + auth: options => `Basic ${options.token}`, + token: 'ZHVtbXlfdXNlcjpkdW1teV9wYXNzd29yZA==', + }, + }; + + const expectedAuthorizationHeader = { + Authorization: `Basic ${validServerCustomAuth.requestOptions.token}`, + }; + + const authentication = getAuthorizationHeader(validServerCustomAuth); + + expect(authentication).toEqual(expectedAuthorizationHeader); + }); + + it('should return an empty object when there is no either server.requestOptions.auth or accessToken', () => { + const authentication = getAuthorizationHeader({}); + + expect(authentication).toEqual({}); + }); + + it('should return an Authorization with accessToken when server is not defined and there is an accessToken', () => { + user.getAccessToken.mockImplementationOnce(() => 'MOCKED_TOKEN'); + + const authentication = getAuthorizationHeader({}, user); + const expectedHeaderBasedOnUserAccessToken = { + Authorization: 'Bearer MOCKED_TOKEN', + }; + + expect(authentication).toEqual(expectedHeaderBasedOnUserAccessToken); + }); +}); diff --git a/platform/core/src/DICOMWeb/getModalities.js b/platform/core/src/DICOMWeb/getModalities.js new file mode 100644 index 0000000..6cde6f2 --- /dev/null +++ b/platform/core/src/DICOMWeb/getModalities.js @@ -0,0 +1,29 @@ +export default function getModalities(Modality, ModalitiesInStudy) { + if (!Modality && !ModalitiesInStudy) { + return {}; + } + + const modalities = Modality || { + vr: 'CS', + Value: [], + }; + + // Rare case, depending on the DICOM server we are using, but sometimes, + // modalities.Value is undefined or null. + modalities.Value = modalities.Value || []; + + if (ModalitiesInStudy) { + if (modalities.vr && modalities.vr === ModalitiesInStudy.vr) { + for (let i = 0; i < ModalitiesInStudy.Value.length; i++) { + const value = ModalitiesInStudy.Value[i]; + if (modalities.Value.indexOf(value) === -1) { + modalities.Value.push(value); + } + } + } else { + return ModalitiesInStudy; + } + } + + return modalities; +} diff --git a/platform/core/src/DICOMWeb/getModalities.test.js b/platform/core/src/DICOMWeb/getModalities.test.js new file mode 100644 index 0000000..1ce13bb --- /dev/null +++ b/platform/core/src/DICOMWeb/getModalities.test.js @@ -0,0 +1,70 @@ +import getModalities from './getModalities'; + +describe('getModalities', () => { + test('should return an empty object when Modality and ModalitiesInStudy are not present', () => { + const Modality = null; + const ModalitiesInStudy = null; + + expect(getModalities(Modality, ModalitiesInStudy)).toEqual({}); + }); + + test('should return an empty object when Modality and ModalitiesInStudy are not present', () => { + const Modality = null; + const ModalitiesInStudy = null; + + expect(getModalities(Modality, ModalitiesInStudy)).toEqual({}); + }); + + test('should return modalities in Study when Modality is not defined', () => { + const Modality = null; + + const ModalitiesInStudy = { + Value: ['MOCKED_VALUE'], + vr: 'MOCKED_VALUE', + }; + + expect(getModalities(Modality, ModalitiesInStudy)).toEqual(ModalitiesInStudy); + }); + + test('should return only the modalitues that exists in ModalitiesInStudy', () => { + const Modality = { + Value: ['DESIRED_VALUE'], + vr: 'DESIRED_VR', + }; + + const ModalitiesInStudy = { + Value: ['DESIRED_VALUE', 'NOT_DESIRED_VALUE'], + vr: 'DESIRED_VR', + }; + + expect(getModalities(Modality, ModalitiesInStudy)).toEqual(Modality); + }); + + test('should return the seek Modality when the desired Modality does not exist in ModalitiesInStudy', () => { + const Modality = { + Value: ['DESIRED_VALUE'], + vr: 'DESIRED_VR', + }; + + const ModalitiesInStudy = { + Value: ['NOT_DESIRED_VALUE'], + vr: 'DESIRED_VR', + }; + + expect(getModalities(Modality, ModalitiesInStudy)).toEqual(Modality); + }); + + test('should return the seek Modality when the desired Modality does not exist in ModalitiesInStudy VR', () => { + const Modality = { + Value: ['DESIRED_VALUE'], + vr: 'DESIRED_VR', + }; + + const ModalitiesInStudy = { + Value: ['NOT_DESIRED_VALUE'], + vr: 'ANOTHER_VR', + }; + + expect(getModalities(Modality, ModalitiesInStudy)).toEqual(ModalitiesInStudy); + }); +}); diff --git a/platform/core/src/DICOMWeb/getName.js b/platform/core/src/DICOMWeb/getName.js new file mode 100644 index 0000000..096a1b4 --- /dev/null +++ b/platform/core/src/DICOMWeb/getName.js @@ -0,0 +1,26 @@ +/** + * Returns the Alphabetic version of a PN + * + * @param element - The group/element of the element (e.g. '00200013') + * @param [defaultValue] - The default value to return if the element is not found + * @returns {*} + */ +export default function getName(element, defaultValue) { + if (!element) { + return defaultValue; + } + // Value is not present if the attribute has a zero length value + if (!element.Value) { + return defaultValue; + } + // Sanity check to make sure we have at least one entry in the array. + if (!element.Value.length) { + return defaultValue; + } + // Return the Alphabetic component group + if (element.Value[0].Alphabetic) { + return element.Value[0].Alphabetic; + } + // Orthanc does not return PN properly so this is a temporary workaround + return element.Value[0]; +} diff --git a/platform/core/src/DICOMWeb/getName.test.js b/platform/core/src/DICOMWeb/getName.test.js new file mode 100644 index 0000000..2003cb0 --- /dev/null +++ b/platform/core/src/DICOMWeb/getName.test.js @@ -0,0 +1,57 @@ +import getName from './getName'; + +describe('getName', () => { + it('should return a default value if element is null or undefined', () => { + const defaultValue = 'DEFAULT_NAME'; + const nullElement = null; + const undefinedElement = undefined; + + expect(getName(nullElement, defaultValue)).toEqual(defaultValue); + expect(getName(undefinedElement, defaultValue)).toEqual(defaultValue); + }); + + it('should return a default value if element.Value is null, undefined or not present', () => { + const defaultValue = 'DEFAULT_NAME'; + const nullElement = { + id: 0, + Value: null, + }; + const undefinedElement = { + id: 0, + Value: undefined, + }; + const noValuePresentElement = { + id: 0, + }; + + expect(getName(nullElement, defaultValue)).toEqual(defaultValue); + expect(getName(undefinedElement, defaultValue)).toEqual(defaultValue); + expect(getName(noValuePresentElement, defaultValue)).toEqual(defaultValue); + }); + + it('should return A for element when Alphabetic is [A, B, C, D]', () => { + const returnValue = 'A'; + const element = { + Value: [{ Alphabetic: 'A' }, { Alphabetic: 'B' }, { Alphabetic: 'C' }, { Alphabetic: 'D' }], + }; + expect(getName(element, null)).toEqual(returnValue); + }); + + it('should return FIRST for element when Alphabetic is [FIRST, SECOND]', () => { + const returnValue = 'FIRST'; + const element = { + Value: [{ Alphabetic: 'FIRST' }, { Alphabetic: 'SECOND' }], + }; + expect(getName(element, null)).toEqual(returnValue); + }); + + it('should return element.value[0] for element with not Alphabetic and when there is at least on element.Value', () => { + const returnValue = { + anyOtherProperty: 'FIRST', + }; + const element = { + Value: [{ anyOtherProperty: 'FIRST' }, { Alphabetic: 'SECOND' }], + }; + expect(getName(element, null)).toEqual(returnValue); + }); +}); diff --git a/platform/core/src/DICOMWeb/getNumber.js b/platform/core/src/DICOMWeb/getNumber.js new file mode 100644 index 0000000..f7a2126 --- /dev/null +++ b/platform/core/src/DICOMWeb/getNumber.js @@ -0,0 +1,21 @@ +/** + * Returns the first string value as a Javascript Number + * @param element - The group/element of the element (e.g. '00200013') + * @param [defaultValue] - The default value to return if the element does not exist + * @returns {*} + */ +export default function getNumber(element, defaultValue) { + if (!element) { + return defaultValue; + } + // Value is not present if the attribute has a zero length value + if (!element.Value) { + return defaultValue; + } + // Sanity check to make sure we have at least one entry in the array. + if (!element.Value.length) { + return defaultValue; + } + + return parseFloat(element.Value[0]); +} diff --git a/platform/core/src/DICOMWeb/getNumber.test.js b/platform/core/src/DICOMWeb/getNumber.test.js new file mode 100644 index 0000000..ab0cb08 --- /dev/null +++ b/platform/core/src/DICOMWeb/getNumber.test.js @@ -0,0 +1,55 @@ +import getNumber from './getNumber'; + +describe('getNumber', () => { + it('should return a default value if element is null or undefined', () => { + const defaultValue = 1.0; + const nullElement = null; + const undefinedElement = undefined; + + expect(getNumber(nullElement, defaultValue)).toEqual(defaultValue); + expect(getNumber(undefinedElement, defaultValue)).toEqual(defaultValue); + }); + + it('should return a default value if element.Value is null, undefined or not present', () => { + const defaultValue = 1.0; + const nullElement = { + id: 0, + Value: null, + }; + const undefinedElement = { + id: 0, + Value: undefined, + }; + const noValuePresentElement = { + id: 0, + }; + + expect(getNumber(nullElement, defaultValue)).toEqual(defaultValue); + expect(getNumber(undefinedElement, defaultValue)).toEqual(defaultValue); + expect(getNumber(noValuePresentElement, defaultValue)).toEqual(defaultValue); + }); + + it('should return 2.0 for element when element.Value[0] = 2', () => { + const returnValue = 2.0; + const element = { + Value: ['2'], + }; + expect(getNumber(element, null)).toEqual(returnValue); + }); + + it('should return -1.0 for element when element.Value[0] is -1', () => { + const returnValue = -1.0; + const element = { + Value: ['-1'], + }; + expect(getNumber(element, null)).toEqual(returnValue); + }); + + it('should return -1.0 for element when element.Value is [-1, 2, 5, -10] ', () => { + const returnValue = -1.0; + const element = { + Value: ['-1', '2', '5', '-10'], + }; + expect(getNumber(element, null)).toEqual(returnValue); + }); +}); diff --git a/platform/core/src/DICOMWeb/getString.js b/platform/core/src/DICOMWeb/getString.js new file mode 100644 index 0000000..b6ff678 --- /dev/null +++ b/platform/core/src/DICOMWeb/getString.js @@ -0,0 +1,23 @@ +/** + * Returns the specified element as a string. Multi-valued elements will be separated by a backslash + * + * @param element - The group/element of the element (e.g. '00200013') + * @param [defaultValue] - The value to return if the element is not present + * @returns {*} + */ +export default function getString(element, defaultValue) { + if (!element) { + return defaultValue; + } + // Value is not present if the attribute has a zero length value + if (!element.Value) { + return defaultValue; + } + // Sanity check to make sure we have at least one entry in the array. + if (!element.Value.length) { + return defaultValue; + } + // Join the array together separated by backslash + // NOTE: Orthanc does not correctly split values into an array so the join is a no-op + return element.Value.join('\\'); +} diff --git a/platform/core/src/DICOMWeb/getString.test.js b/platform/core/src/DICOMWeb/getString.test.js new file mode 100644 index 0000000..727df7f --- /dev/null +++ b/platform/core/src/DICOMWeb/getString.test.js @@ -0,0 +1,55 @@ +import getString from './getString'; + +describe('getString', () => { + it('should return a default value if element is null or undefined', () => { + const defaultValue = ['A', 'B', 'C'].join('\\'); + const nullElement = null; + const undefinedElement = undefined; + + expect(getString(nullElement, defaultValue)).toEqual(defaultValue); + expect(getString(undefinedElement, defaultValue)).toEqual(defaultValue); + }); + + it('should return a default value if element.Value is null, undefined or not present', () => { + const defaultValue = ['A', 'B', 'C'].join('\\'); + const nullElement = { + id: 0, + Value: null, + }; + const undefinedElement = { + id: 0, + Value: undefined, + }; + const noValuePresentElement = { + id: 0, + }; + + expect(getString(nullElement, defaultValue)).toEqual(defaultValue); + expect(getString(undefinedElement, defaultValue)).toEqual(defaultValue); + expect(getString(noValuePresentElement, defaultValue)).toEqual(defaultValue); + }); + + it('should return A,B,C,D for element when element.Value[0] = [A, B, C, D]', () => { + const returnValue = ['A', 'B', 'C'].join('\\'); + const element = { + Value: ['A', 'B', 'C'], + }; + expect(getString(element, null)).toEqual(returnValue); + }); + + it('should return 1,4,5,6 for element when element.Value[0] is [1, 4, 5, 6]', () => { + const returnValue = [1, 4, 5, 6].join('\\'); + const element = { + Value: [1, 4, 5, 6], + }; + expect(getString(element, null)).toEqual(returnValue); + }); + + it('should return A,1,3,R,7,-1 for element when element.Value is [-1, 2, 5, -10] ', () => { + const returnValue = ['A', '1', '3', 'R', '7', '-1'].join('\\'); + const element = { + Value: ['A', '1', '3', 'R', '7', '-1'], + }; + expect(getString(element, null)).toEqual(returnValue); + }); +}); diff --git a/platform/core/src/DICOMWeb/index.js b/platform/core/src/DICOMWeb/index.js new file mode 100644 index 0000000..0775d92 --- /dev/null +++ b/platform/core/src/DICOMWeb/index.js @@ -0,0 +1,19 @@ +import getAttribute from './getAttribute.js'; +import getAuthorizationHeader from './getAuthorizationHeader.js'; +import getModalities from './getModalities.js'; +import getName from './getName.js'; +import getNumber from './getNumber.js'; +import getString from './getString.js'; + +const DICOMWeb = { + getAttribute, + getAuthorizationHeader, + getModalities, + getName, + getNumber, + getString, +}; + +export { getAttribute, getAuthorizationHeader, getModalities, getName, getNumber, getString }; + +export default DICOMWeb; diff --git a/platform/core/src/DICOMWeb/index.test.js b/platform/core/src/DICOMWeb/index.test.js new file mode 100644 index 0000000..3db8a05 --- /dev/null +++ b/platform/core/src/DICOMWeb/index.test.js @@ -0,0 +1,18 @@ +import * as DICOMWeb from './index.js'; + +describe('Top level exports', () => { + test('should export the modules getAttribute, getAuthorizationHeader, getModalities, getName, getNumber, getString', () => { + const expectedExports = [ + 'getAttribute', + 'getAuthorizationHeader', + 'getModalities', + 'getName', + 'getNumber', + 'getString', + ].sort(); + + const exports = Object.keys(DICOMWeb.default).sort(); + + expect(exports).toEqual(expectedExports); + }); +}); diff --git a/platform/core/src/DataSources/IWebApiDataSource.js b/platform/core/src/DataSources/IWebApiDataSource.js new file mode 100644 index 0000000..9d9a558 --- /dev/null +++ b/platform/core/src/DataSources/IWebApiDataSource.js @@ -0,0 +1,85 @@ +import { DicomMetadataStore } from '../services/DicomMetadataStore'; +// TODO: Use above to inject so dependent datasources don't need to import or +// depend on @ohif/core? + +/** + * Factory function that creates a new "Web API" data source. + * A "Web API" data source is any source that fetches data over + * HTTP. This function serves as an "adapter" to wrap those calls + * so that all "Web API" data sources have the same interface and can + * be used interchangeably. + * + * It's worth noting that a single implementation of this interface + * can define different underlying sources for "read" and "write" operations. + */ +function create({ + query, + retrieve, + store, + reject, + initialize, + deleteStudyMetadataPromise, + getImageIdsForDisplaySet, + getImageIdsForInstance, + getConfig, + getStudyInstanceUIDs, +}) { + const defaultQuery = { + studies: { + /** + * @param {string} params.patientName + * @param {string} params.mrn + * @param {object} params.studyDate + * @param {string} params.description + * @param {string} params.modality + * @param {string} params.accession + * @param {string} params.sortBy + * @param {string} params.sortDirection - + * @param {number} params.page + * @param {number} params.resultsPerPage + */ + mapParams: params => params, + requestResults: () => {}, + processResults: results => results, + }, + series: {}, + instances: {}, + }; + + const defaultRetrieve = { + series: {}, + }; + + const defaultStore = { + dicom: async naturalizedDataset => { + throw new Error( + 'store.dicom(naturalizedDicom, StudyInstanceUID) not implemented for dataSource.' + ); + }, + }; + + const defaultReject = {}; + + const defaultGetConfig = () => { + return { dicomUploadEnabled: false }; + }; + + return { + query: query || defaultQuery, + retrieve: retrieve || defaultRetrieve, + reject: reject || defaultReject, + store: store || defaultStore, + initialize, + deleteStudyMetadataPromise, + getImageIdsForDisplaySet, + getImageIdsForInstance, + getConfig: getConfig || defaultGetConfig, + getStudyInstanceUIDs: getStudyInstanceUIDs, + }; +} + +const IWebApiDataSource = { + create, +}; + +export default IWebApiDataSource; diff --git a/platform/core/src/__mocks__/cornerstone-core.js b/platform/core/src/__mocks__/cornerstone-core.js new file mode 100644 index 0000000..ff8b4c5 --- /dev/null +++ b/platform/core/src/__mocks__/cornerstone-core.js @@ -0,0 +1 @@ +export default {}; diff --git a/platform/core/src/__mocks__/cornerstone-tools.js b/platform/core/src/__mocks__/cornerstone-tools.js new file mode 100644 index 0000000..ff8b4c5 --- /dev/null +++ b/platform/core/src/__mocks__/cornerstone-tools.js @@ -0,0 +1 @@ +export default {}; diff --git a/platform/core/src/__mocks__/cornerstone-wado-image-loader.js b/platform/core/src/__mocks__/cornerstone-wado-image-loader.js new file mode 100644 index 0000000..ff8b4c5 --- /dev/null +++ b/platform/core/src/__mocks__/cornerstone-wado-image-loader.js @@ -0,0 +1 @@ +export default {}; diff --git a/platform/core/src/__mocks__/dicom-parser.js b/platform/core/src/__mocks__/dicom-parser.js new file mode 100644 index 0000000..ff8b4c5 --- /dev/null +++ b/platform/core/src/__mocks__/dicom-parser.js @@ -0,0 +1 @@ +export default {}; diff --git a/platform/core/src/__mocks__/dicomweb-client.js b/platform/core/src/__mocks__/dicomweb-client.js new file mode 100644 index 0000000..b75c269 --- /dev/null +++ b/platform/core/src/__mocks__/dicomweb-client.js @@ -0,0 +1,17 @@ +// import { api } from 'dicomweb-client' + +const api = { + DICOMwebClient: jest.fn().mockImplementation(function () { + this.retrieveStudyMetadata = jest.fn().mockResolvedValue([]); + this.retrieveSeriesMetadata = jest.fn(function (options) { + const { studyInstanceUID, seriesInstanceUID } = options; + return Promise.resolve([{ studyInstanceUID, seriesInstanceUID }]); + }); + }), +}; + +export default { + api, +}; + +export { api }; diff --git a/platform/core/src/__mocks__/log.js b/platform/core/src/__mocks__/log.js new file mode 100644 index 0000000..474a588 --- /dev/null +++ b/platform/core/src/__mocks__/log.js @@ -0,0 +1,5 @@ +export default { + warn: jest.fn(), + error: jest.fn(), + info: jest.fn(), +}; diff --git a/platform/core/src/classes/CommandsManager.test.js b/platform/core/src/classes/CommandsManager.test.js new file mode 100644 index 0000000..390f9d4 --- /dev/null +++ b/platform/core/src/classes/CommandsManager.test.js @@ -0,0 +1,191 @@ +import CommandsManager from './CommandsManager'; +import log from './../log.js'; + +jest.mock('./../log.js'); + +describe('CommandsManager', () => { + let commandsManager, + contextName = 'VTK', + command = { + commandFn: jest.fn().mockReturnValue(true), + options: { passMeToCommandFn: ':wave:' }, + }, + commandsManagerConfig = { + getAppState: () => { + return { + viewers: 'Test', + }; + }, + }; + + beforeEach(() => { + commandsManager = new CommandsManager(commandsManagerConfig); + commandsManager.createContext('VIEWER'); + commandsManager.createContext('ACTIVE_VIEWER::CORNERSTONE'); + jest.clearAllMocks(); + }); + + it('has a contexts property', () => { + const localCommandsManager = new CommandsManager(commandsManagerConfig); + + expect(localCommandsManager).toHaveProperty('contexts'); + expect(localCommandsManager.contexts).toEqual({}); + }); + + describe('createContext()', () => { + it('creates a context', () => { + commandsManager.createContext(contextName); + + expect(commandsManager.contexts).toHaveProperty(contextName); + }); + + it('clears the context if it already exists', () => { + commandsManager.createContext(contextName); + commandsManager.registerCommand(contextName, 'TestCommand', command); + commandsManager.registerCommand(contextName, 'TestCommand2', command); + commandsManager.createContext(contextName); + + const registeredCommands = commandsManager.getContext(contextName); + + expect(registeredCommands).toEqual({}); + }); + }); + + describe('getContext()', () => { + it('returns all registered commands for a context', () => { + commandsManager.createContext(contextName); + commandsManager.registerCommand(contextName, 'TestCommand', command); + const registeredCommands = commandsManager.getContext(contextName); + + expect(registeredCommands).toHaveProperty('TestCommand'); + expect(registeredCommands['TestCommand']).toEqual(command); + }); + it('returns undefined if the context does not exist', () => { + const registeredCommands = commandsManager.getContext(contextName); + + expect(registeredCommands).toBe(undefined); + }); + }); + + describe('clearContext()', () => { + it('clears all registered commands for a context', () => { + commandsManager.createContext(contextName); + commandsManager.registerCommand(contextName, 'TestCommand', command); + commandsManager.registerCommand(contextName, 'TestCommand2', command); + commandsManager.clearContext(contextName); + + const registeredCommands = commandsManager.getContext(contextName); + + expect(registeredCommands).toEqual({}); + }); + }); + + describe('registerCommand()', () => { + it('registers commands to a context', () => { + commandsManager.createContext(contextName); + commandsManager.registerCommand(contextName, 'TestCommand', command); + const registeredCommands = commandsManager.getContext(contextName); + + expect(registeredCommands).toHaveProperty('TestCommand'); + expect(registeredCommands['TestCommand']).toEqual(command); + }); + }); + + describe('getCommand()', () => { + it('returns undefined if context does not exist', () => { + const result = commandsManager.getCommand('TestCommand', 'NonExistentContext'); + + expect(result).toBe(undefined); + }); + it('returns undefined if command does not exist in context', () => { + commandsManager.createContext(contextName); + const result = commandsManager.getCommand('TestCommand', contextName); + + expect(result).toBe(undefined); + }); + it('uses contextName param to get command', () => { + commandsManager.createContext('GLOBAL'); + commandsManager.registerCommand('GLOBAL', 'TestCommand', command); + const foundCommand = commandsManager.getCommand('TestCommand', 'GLOBAL'); + + expect(foundCommand).toBe(command); + }); + it('uses activeContexts, if contextName is not provided, to get command', () => { + commandsManager.registerCommand('VIEWER', 'TestCommand', command); + const foundCommand = commandsManager.getCommand('TestCommand'); + + expect(foundCommand).toBe(command); + }); + it('returns the expected command', () => { + commandsManager.createContext(contextName); + commandsManager.registerCommand(contextName, 'TestCommand', command); + const result = commandsManager.getCommand('TestCommand', contextName); + + expect(result).toEqual(command); + }); + }); + + describe('runCommand()', () => { + it('Logs a warning if commandName not found in context', () => { + const result = commandsManager.runCommand('CommandThatDoesNotExistInAnyContext'); + + expect(result).toBe(undefined); + expect(log.warn.mock.calls[0][0]).toEqual( + 'Command "CommandThatDoesNotExistInAnyContext" not found in current context' + ); + }); + + it('Logs a warning if command definition does not have a commandFn', () => { + const commandWithNoCommmandFn = { + commandFn: undefined, + options: {}, + }; + + commandsManager.createContext(contextName); + commandsManager.registerCommand(contextName, 'TestCommand', commandWithNoCommmandFn); + const result = commandsManager.runCommand('TestCommand', null, contextName); + + expect(result).toBe(undefined); + expect(log.warn.mock.calls[0][0]).toEqual( + 'No commandFn was defined for command "TestCommand"' + ); + }); + + it('Calls commandFn', () => { + commandsManager.registerCommand('VIEWER', 'TestCommand', command); + commandsManager.runCommand('TestCommand', {}, 'VIEWER'); + + expect(command.commandFn.mock.calls.length).toBe(1); + }); + + it('Calls commandFn w/ command definition options', () => { + commandsManager.registerCommand('VIEWER', 'TestCommand', command); + commandsManager.runCommand('TestCommand', {}, 'VIEWER'); + + expect(command.commandFn.mock.calls.length).toBe(1); + expect(command.commandFn.mock.calls[0][0].passMeToCommandFn).toEqual( + command.options.passMeToCommandFn + ); + }); + + it('Calls commandFn w/ runCommand "options" parameter', () => { + const runCommandOptions = { + test: ':+1:', + }; + + commandsManager.registerCommand('VIEWER', 'TestCommand', command); + commandsManager.runCommand('TestCommand', runCommandOptions, 'VIEWER'); + + expect(command.commandFn.mock.calls.length).toBe(1); + expect(command.commandFn.mock.calls[0][0].test).toEqual(runCommandOptions.test); + }); + + it('Returns the result of commandFn', () => { + commandsManager.registerCommand('VIEWER', 'TestCommand', command); + const result = commandsManager.runCommand('TestCommand', {}, 'VIEWER'); + + expect(command.commandFn.mock.calls.length).toBe(1); + expect(result).toBe(true); + }); + }); +}); diff --git a/platform/core/src/classes/CommandsManager.ts b/platform/core/src/classes/CommandsManager.ts new file mode 100644 index 0000000..ef58869 --- /dev/null +++ b/platform/core/src/classes/CommandsManager.ts @@ -0,0 +1,270 @@ +import log from '../log.js'; +import { Command, Commands, ComplexCommand } from '../types/Command'; + +export type RunInput = Command | Commands | Command[] | string | undefined; + +/** + * The definition of a command + * + * @typedef {Object} CommandDefinition + * @property {Function} commandFn - Command to call + * @property {Object} options - Object of params to pass action + */ + +/** + * The Commands Manager tracks named commands (or functions) that are scoped to + * a context. When we attempt to run a command with a given name, we look for it + * in our active contexts. If found, we run the command, passing in any application + * or call specific data specified in the command's definition. + * + * NOTE: A more robust version of the CommandsManager lives in v1. If you're looking + * to extend this class, please check it's source before adding new methods. + */ +export class CommandsManager { + private contexts = {}; + // Has the reverse order in which contexts are created, used for the default ordering + private contextOrder = new Array(); + + constructor(_options = {}) { + // No-op + } + + /** + * Allows us to create commands "per context". An example would be the "Cornerstone" + * context having a `SaveImage` command, and the "VTK" context having a `SaveImage` + * command. The distinction of a context allows us to call the command in either + * context, and have faith that the correct command will be run. + * + * @method + * @param {string} contextName - Namespace for commands + * @returns {undefined} + */ + createContext(contextName, priority?: number) { + if (!contextName) { + return; + } + + if (this.contexts[contextName]) { + return this.clearContext(contextName); + } + + this.contexts[contextName] = {}; + // Add the context name to the start of the list. + this.contextOrder.splice(0, 0, contextName); + } + + /** + * Returns all command definitions for a given context + * + * @method + * @param {string} contextName - Namespace for commands + * @returns {Object} - the matched context + */ + getContext(contextName) { + const context = this.contexts[contextName]; + + if (!context) { + return; + } + + return context; + } + + /** + * Clears all registered commands for a given context. + * + * @param {string} contextName - Namespace for commands + * @returns {undefined} + */ + clearContext(contextName) { + if (!contextName) { + return; + } + + this.contexts[contextName] = {}; + } + + /** + * Register a new command with the command manager. Scoped to a context, and + * with a definition to assist command callers w/ providing the necessary params + * + * @method + * @param {string} contextName - Namespace for command; often scoped to the extension that added it + * @param {string} commandName - Unique name identifying the command + * @param {CommandDefinition} definition - {@link CommandDefinition} + */ + registerCommand(contextName, commandName, definition) { + if (typeof definition !== 'object' && typeof definition !== 'function') { + return; + } + + // Validate and restrict keys to prevent prototype pollution + const isSafeKey = key => { + return key !== '__proto__' && key !== 'constructor' && key !== 'prototype'; + }; + + if (!isSafeKey(contextName) || !isSafeKey(commandName)) { + throw new Error('Invalid key name to prevent prototype pollution'); + } + + const context = this.getContext(contextName); + if (!context) { + return; + } + + if (typeof definition === 'function') { + context[commandName] = { commandFn: definition, options: {} }; + } else { + context[commandName] = definition; + } + } + + /** + * Finds a command with the provided name if it exists in the specified context, + * or a currently active context. + * + * @method + * @param {String} commandName - Command to find + * @param {String} [contextName] - Specific command to look in. Defaults to current activeContexts. + * Also allows an array of contexts to look in. + */ + getCommand = (commandName: string, contextName: string | string[] = this.contextOrder) => { + const contexts = []; + + if (Array.isArray(contextName)) { + contexts.push(...contextName.map(name => this.getContext(name)).filter(it => !!it)); + } else if (contextName) { + const context = this.getContext(contextName); + if (context) { + contexts.push(context); + } + } + + return contexts.find(context => !!context[commandName])?.[commandName]; + }; + + /** + * + * @method + * @param {String} commandName + * @param {Object} [options={}] - Extra options to pass the command. Like a mousedown event + * @param {String} [contextName] + */ + public runCommand(commandName: string, options = {}, contextName?: string | string[]) { + if (typeof commandName === 'function') { + // If commandName is a function, run it directly + return commandName(options); + } + + const definition = this.getCommand(commandName, contextName); + if (!definition) { + log.warn(`Command "${commandName}" not found in current context`); + return; + } + + const { commandFn } = definition; + const commandParams = Object.assign( + {}, + definition.options || {}, // "Command configuration" + options // "Time of call" info + ); + + if (typeof commandFn !== 'function') { + log.warn(`No commandFn was defined for command "${commandName}"`); + return; + } else { + return commandFn(commandParams); + } + } + + public static convertCommands(toRun: Command | Commands | Command[] | string | Function) { + if (typeof toRun === 'string') { + return [{ commandName: toRun }]; + } + if ('commandName' in toRun) { + return [toRun as ComplexCommand]; + } + if (typeof toRun === 'function') { + return [{ commandName: toRun }]; + } + if ('commands' in toRun) { + const commandsInput = (toRun as Commands).commands; + return this.convertCommands(commandsInput); + } + if (Array.isArray(toRun)) { + return toRun.map(command => CommandsManager.convertCommands(command)[0]); + } + + return []; + } + + private validate(input: RunInput, options: Record = {}): ComplexCommand[] { + if (!input) { + console.debug('No command to run'); + return []; + } + + // convert commands + const converted: ComplexCommand[] = CommandsManager.convertCommands(input); + if (!converted.length) { + console.debug('Command is not runnable', input); + return []; + } + + return converted.map(command => ({ + commandName: command.commandName, + commandOptions: { ...options, ...command.commandOptions }, + context: command.context, + })); + } + + /** + * Run one or more commands with specified extra options. + * Returns the result of the last command run. + * + * Example commands to run are: + * * 'updateMeasurement' + * * `{ commandName: 'displayWhatever'}` + * * `['updateMeasurement', {commandName: 'displayWhatever'}]` + * * `{ commands: 'updateMeasurement' }` + * * `{ commands: ['updateMeasurement', {commandName: 'displayWhatever'}]}` + * + * Note how the various styles can be mixed, simplifying the declaration of + * sets of commands. + * + * @param toRun - A specification of one or more commands, + * typically an object of { commandName, commandOptions, context } + * or an array of such objects. It can also be a single commandName as string + * if no options are needed. + * @param options - to include in the commands run beyond + * the commandOptions specified in the base. + */ + public run(input: RunInput, options: Record = {}): unknown { + const commands = this.validate(input, options); + + const results: unknown[] = []; + for (let i = 0; i < commands.length; i++) { + const command = commands[i]; + const { commandName, commandOptions, context } = command; + results.push(this.runCommand(commandName, commandOptions, context)); + } + + return results.length === 1 ? results[0] : results; + } + + /** Like run, but await each command before continuing */ + public async runAsync(input: RunInput, options: Record = {}): Promise { + const commands = this.validate(input, options); + + const results: unknown[] = []; + for (let i = 0; i < commands.length; i++) { + const command = commands[i]; + const { commandName, commandOptions, context } = command; + results.push(await this.runCommand(commandName, commandOptions, context)); + } + + return results.length === 1 ? results[0] : results; + } +} + +export default CommandsManager; diff --git a/platform/core/src/classes/Hotkey.ts b/platform/core/src/classes/Hotkey.ts new file mode 100644 index 0000000..73ca2d3 --- /dev/null +++ b/platform/core/src/classes/Hotkey.ts @@ -0,0 +1,8 @@ +export default interface Hotkey { + commandName: string; + commandOptions?: Record; + context?: string; + keys: string[]; + label: string; + isEditable?: boolean; +} diff --git a/platform/core/src/classes/HotkeysManager.test.js b/platform/core/src/classes/HotkeysManager.test.js new file mode 100644 index 0000000..971c171 --- /dev/null +++ b/platform/core/src/classes/HotkeysManager.test.js @@ -0,0 +1,190 @@ +import CommandsManager from './CommandsManager'; +import HotkeysManager from './HotkeysManager'; +import hotkeys from './../utils/hotkeys'; +import log from './../log'; +import objectHash from 'object-hash'; + +jest.mock('./CommandsManager'); +jest.mock('./../utils/hotkeys'); +jest.mock('./../log'); + +describe('HotkeysManager', () => { + let hotkeysManager, commandsManager; + + beforeEach(() => { + commandsManager = new CommandsManager(); + hotkeysManager = new HotkeysManager(commandsManager); + CommandsManager.mockClear(); + hotkeys.mockClear(); + log.warn.mockClear(); + jest.clearAllMocks(); + }); + it('has expected properties', () => { + const allProperties = Object.keys(hotkeysManager); + const expectedProperties = ['hotkeyDefinitions', 'hotkeyDefaults', 'isEnabled']; + + const containsAllExpectedProperties = expectedProperties.every(expected => + allProperties.includes(expected) + ); + + expect(containsAllExpectedProperties).toBe(true); + }); + + describe('disable()', () => { + beforeEach(() => hotkeys.pause.mockClear()); + + it('sets isEnabled property to false', () => { + hotkeysManager.disable(); + + expect(hotkeysManager.isEnabled).toBe(false); + }); + + it('calls hotkeys.pause()', () => { + hotkeysManager.disable(); + + expect(hotkeys.pause.mock.calls.length).toBe(1); + }); + }); + + describe('enable()', () => { + beforeEach(() => { + hotkeys.unpause = jest.fn(); + hotkeys.unpause.mockClear(); + }); + + it('sets isEnabled property to true', () => { + hotkeysManager.disable(); + hotkeysManager.enable(); + + expect(hotkeysManager.isEnabled).toBe(true); + }); + + it('calls hotkeys.unpause()', () => { + hotkeysManager.enable(); + + expect(hotkeys.unpause.mock.calls.length).toBe(1); + }); + }); + + describe('setHotkeys()', () => { + it('calls registerHotkeys for each hotkeyDefinition', () => { + const hotkeyDefinitions = [ + { commandName: 'dance', label: 'dance dance', keys: '+' }, + { commandName: 'celebrate', label: 'celebrate everything', keys: 'q' }, + ]; + + hotkeysManager.registerHotkeys = jest.fn(); + hotkeysManager.setHotkeys(hotkeyDefinitions); + + const numberOfCalls = hotkeysManager.registerHotkeys.mock.calls.length; + const firstCallArgs = hotkeysManager.registerHotkeys.mock.calls[0][0]; + const secondCallArgs = hotkeysManager.registerHotkeys.mock.calls[1][0]; + + expect(numberOfCalls).toBe(2); + expect(firstCallArgs).toEqual(hotkeyDefinitions[0]); + expect(secondCallArgs).toEqual(hotkeyDefinitions[1]); + }); + + it('does not set this.hotkeyDefaults when calling setHotKeys', () => { + const hotkeyDefinitions = [{ commandName: 'dance', keys: '+' }]; + + hotkeysManager.setHotkeys(hotkeyDefinitions); + + expect(hotkeysManager.hotkeyDefaults).toEqual([]); + }); + }); + + describe('setDefaultHotKeys()', () => { + it('it sets default hotkeys', () => { + const hotkeyDefinitions = [{ commandName: 'dance', keys: '+' }]; + + hotkeysManager.setDefaultHotKeys(hotkeyDefinitions); + + expect(hotkeysManager.hotkeyDefaults).toEqual(hotkeyDefinitions); + }); + }); + + describe('registerHotkeys()', () => { + it('throws an Error if a commandName is not provided', () => { + const definition = { commandName: undefined, keys: '+' }; + + expect(() => { + hotkeysManager.registerHotkeys(definition); + }).toThrow(); + }); + it('updates hotkeyDefinitions property with registered keys', () => { + const definition = { + commandName: 'dance', + commandOptions: {}, + label: 'hello', + keys: '+', + }; + + hotkeysManager.registerHotkeys(definition); + + const numOfHotkeyDefinitions = Object.keys(hotkeysManager.hotkeyDefinitions).length; + + const commandHash = objectHash({ + commandName: definition.commandName, + commandOptions: definition.commandOptions, + }); + const hotkeyDefinitionForRegisteredCommand = hotkeysManager.hotkeyDefinitions[commandHash]; + + expect(numOfHotkeyDefinitions).toBe(1); + expect(Object.keys(hotkeysManager.hotkeyDefinitions)[0]).toEqual(commandHash); + expect(hotkeyDefinitionForRegisteredCommand).toEqual(definition); + }); + it('calls hotkeys.bind for the group of keys', () => { + const definition = { commandName: 'dance', keys: ['shift', 'e'] }; + + hotkeysManager.registerHotkeys(definition); + + expect(hotkeys.bind.mock.calls.length).toBe(1); + expect(hotkeys.bind.mock.calls[0][0]).toBe('shift+e'); + }); + it('calls hotkeys.unbind if commandName was previously registered, for each previously registered set of keys', () => { + const firstDefinition = { + commandName: 'dance', + keys: ['alt', 'e'], + }; + const secondDefinition = { commandName: 'dance', keys: 'a' }; + + // First call + hotkeysManager.registerHotkeys(firstDefinition); + // Second call + hotkeysManager.registerHotkeys(secondDefinition); + + expect(hotkeys.unbind.mock.calls.length).toBe(1); + expect(hotkeys.unbind.mock.calls[0][0]).toBe('alt+e'); + }); + }); + + describe('restoreDefaults()', () => { + it('calls setHotkeys with hotkey defaults', () => { + hotkeysManager.setHotkeys = jest.fn(); + + hotkeysManager.restoreDefaultBindings(); + + expect(hotkeysManager.setHotkeys.mock.calls[0][0]).toEqual(hotkeysManager.hotkeyDefaults); + }); + }); + + describe('destroy()', () => { + it('clears default and definition properties', () => { + hotkeysManager.hotkeyDefaults = ['hotdog', 'jeremy', 'qasar']; + hotkeysManager.hotkeyDefinitions = { + hello: 'world', + }; + + hotkeysManager.destroy(); + + expect(hotkeysManager.hotkeyDefaults).toEqual([]); + expect(hotkeysManager.hotkeyDefinitions).toEqual({}); + }); + it('resets all hotkey bindings', () => { + hotkeysManager.destroy(); + + expect(hotkeys.reset.mock.calls.length).toEqual(1); + }); + }); +}); diff --git a/platform/core/src/classes/HotkeysManager.ts b/platform/core/src/classes/HotkeysManager.ts new file mode 100644 index 0000000..30305c7 --- /dev/null +++ b/platform/core/src/classes/HotkeysManager.ts @@ -0,0 +1,295 @@ +import objectHash from 'object-hash'; +import { hotkeys as mouseTrapAPI } from '../utils'; +import Hotkey from './Hotkey'; +import migrateOldHotkeyDefinitions from '../utils/hotkeys/migrateHotkeys'; + +/** + * + * + * @typedef {Object} HotkeyDefinition + * @property {String} commandName - Command to call + * @property {Object} commandOptions - Command options + * @property {String} label - Display name for hotkey + * @property {String[]} keys - Keys to bind; Follows Mousetrap.js binding syntax + */ +export class HotkeysManager { + private _servicesManager: AppTypes.ServicesManager; + private _commandsManager: AppTypes.CommandsManager; + private isEnabled: boolean = true; + public hotkeyDefinitions: Record = {}; + public hotkeyDefaults: any[] = []; + + constructor( + commandsManager: AppTypes.CommandsManager, + servicesManager: AppTypes.ServicesManager + ) { + this._servicesManager = servicesManager; + this._commandsManager = commandsManager; + + // Check for old hotkey definitions format and migrate if needed + migrateOldHotkeyDefinitions({ + generateHash: this.generateHash, + }); + } + + /** + * Exposes Mousetrap.js's `.record` method, added by the record plugin. + * + * @param {*} event + */ + record(event) { + return mouseTrapAPI.record(event); + } + + cancel() { + mouseTrapAPI.stopRecord(); + mouseTrapAPI.unpause(); + } + + /** + * Disables all hotkeys. Hotkeys added while disabled will not listen for + * input. + */ + disable() { + this.isEnabled = false; + mouseTrapAPI.pause(); + } + + /** + * Enables all hotkeys. + */ + enable() { + this.isEnabled = true; + mouseTrapAPI.unpause(); + } + + /** + * Uses most recent + * + * @returns {undefined} + */ + restoreDefaultBindings() { + this.setHotkeys(this.hotkeyDefaults); + } + + /** + * + */ + destroy() { + this.hotkeyDefaults = []; + this.hotkeyDefinitions = {}; + mouseTrapAPI.reset(); + } + + /** + * Registers a list of hotkey definitions. + * + * @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions + */ + setHotkeys(hotkeyDefinitions = []) { + try { + const definitions = this.getValidDefinitions(hotkeyDefinitions); + definitions.forEach(definition => this.registerHotkeys(definition)); + } catch (error) { + const { uiNotificationService } = this._servicesManager.services; + uiNotificationService.show({ + title: 'Hotkeys Manager', + message: 'Error while setting hotkeys', + type: 'error', + }); + } + } + + generateHash(definition) { + return objectHash({ + commandName: definition.commandName, + commandOptions: definition.commandOptions || {}, + }); + } + + /** + * Set default hotkey bindings. These + * values are used in `this.restoreDefaultBindings`. + * + * @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions + */ + setDefaultHotKeys(hotkeyDefinitions = []) { + const definitions = this.getValidDefinitions(hotkeyDefinitions); + this.hotkeyDefaults = definitions; + + // Get user preferred keys from localStorage + const userPreferredKeys = JSON.parse(localStorage.getItem('user-preferred-keys') || '{}'); + + // Update definitions with user preferred keys before setting + const updatedDefinitions = definitions.map(definition => { + const commandHash = this.generateHash(definition); + // If user has a preferred key binding, use it + if (userPreferredKeys[commandHash]) { + return { + ...definition, + keys: userPreferredKeys[commandHash], + }; + } + + return definition; + }); + + this.setHotkeys(updatedDefinitions); + } + + /** + * Take hotkey definitions that can be an array or object and make sure that it + * returns an array of hotkeys + * + * @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions + */ + getValidDefinitions(hotkeyDefinitions) { + const definitions = Array.isArray(hotkeyDefinitions) + ? [...hotkeyDefinitions] + : this._parseToArrayLike(hotkeyDefinitions); + + // make sure isEditable is true for all definitions if not provided + definitions.forEach(definition => { + if (definition.isEditable === undefined) { + definition.isEditable = true; + } + }); + + return definitions; + } + + /** + * Take hotkey definitions that can be an array and make sure that it + * returns an object of hotkeys definitions + * + * @param {HotkeyDefinition[]} [hotkeyDefinitions=[]] Contains hotkeys definitions + * @returns {Object} + */ + getValidHotkeyDefinitions(hotkeyDefinitions) { + const definitions = this.getValidDefinitions(hotkeyDefinitions); + const objectDefinitions = {}; + definitions.forEach(definition => { + const { commandName, commandOptions } = definition; + const commandHash = objectHash({ commandName, commandOptions }); + objectDefinitions[commandHash] = definition; + }); + return objectDefinitions; + } + + /** + * It parses given object containing hotkeyDefinition to array like. + * Each property of given object will be mapped to an object of an array. And its property name will be the value of a property named as commandName + * + * @param {HotkeyDefinition[] | Object} [hotkeyDefinitions={}] Contains hotkeys definitions + * @returns {HotkeyDefinition[]} + */ + _parseToArrayLike(hotkeyDefinitionsObj = {}) { + const copy = { ...hotkeyDefinitionsObj }; + return Object.entries(copy).map(entryValue => + this._parseToHotKeyObj(entryValue[0], entryValue[1]) + ); + } + + /** + * Return HotkeyDefinition object like based on given property name and property value + * @param {string} propertyName property name of hotkey definition object + * @param {object} propertyValue property value of hotkey definition object + */ + _parseToHotKeyObj(propertyName, propertyValue) { + return { + commandName: propertyName, + ...propertyValue, + }; + } + + /** + * (Unbinds and) binds the specified command to one or more key combinations. + * When the hotkey combination is triggered, the command name and active contexts + * are used to locate and execute the appropriate command. + * + * @param hotkey - The hotkey definition object. + * @throws {Error} Throws an error if no commandName is provided. + */ + registerHotkeys({ + commandName, + commandOptions = {}, + context, + keys, + label, + isEditable, + }: Hotkey): void { + if (!commandName) { + throw new Error(`No command was defined for hotkey "${keys}"`); + } + + const commandHash = this.generateHash({ commandName, commandOptions }); + const existingHotkey = this.hotkeyDefinitions[commandHash]; + + // If the hotkey has already been registered with the same keys, skip re-registration. + if (existingHotkey && existingHotkey.keys === keys) { + console.debug('HotkeysManager: Identical hotkey registration skipped.'); + return; + } + + const userPreferredKeys = JSON.parse(localStorage.getItem('user-preferred-keys') || '{}'); + + if (existingHotkey) { + userPreferredKeys[commandHash] = keys; + localStorage.setItem('user-preferred-keys', JSON.stringify(userPreferredKeys)); + this._unbindHotkeys(commandName, existingHotkey.keys); + } + + this.hotkeyDefinitions[commandHash] = { commandName, commandOptions, keys, label, isEditable }; + this._bindHotkeys(commandName, commandOptions, context, keys); + } + + /** + * Binds one or more set of hotkey combinations for a given command + * + * @private + * @param {string} commandName - The name of the command to trigger when hotkeys are used + * @param {string[]} keys - One or more key combinations that should trigger command + * @returns {undefined} + */ + _bindHotkeys(commandName, commandOptions = {}, context, keys) { + const isKeyDefined = keys === '' || keys === undefined; + if (isKeyDefined) { + return; + } + + const isKeyArray = keys instanceof Array; + const combinedKeys = isKeyArray ? keys.join('+') : keys; + + mouseTrapAPI.bind(combinedKeys, evt => { + evt.preventDefault(); + evt.stopPropagation(); + this._commandsManager.runCommand(commandName, { evt, ...commandOptions }, context); + }); + } + + /** + * unbinds one or more set of hotkey combinations for a given command + * + * @private + * @param {string} commandName - The name of the previously bound command + * @param {string[]} keys - One or more sets of previously bound keys + * @returns {undefined} + */ + _unbindHotkeys(commandName, keys) { + const isKeyDefined = keys !== '' && keys !== undefined; + if (!isKeyDefined) { + return; + } + + const isKeyArray = keys instanceof Array; + if (isKeyArray) { + const combinedKeys = keys.join('+'); + this._unbindHotkeys(commandName, combinedKeys); + return; + } + + mouseTrapAPI.unbind(keys); + } +} + +export default HotkeysManager; diff --git a/platform/core/src/classes/ImageSet.ts b/platform/core/src/classes/ImageSet.ts new file mode 100644 index 0000000..d541a79 --- /dev/null +++ b/platform/core/src/classes/ImageSet.ts @@ -0,0 +1,141 @@ +import guid from '../utils/guid.js'; +import { Vector3 } from 'cornerstone-math'; + +type Attributes = Record; +type Image = { + StudyInstanceUID?: string; + getData(): { + metadata: { + ImagePositionPatient: number[]; + ImageOrientationPatient: number[]; + }; + }; +}; + +/** + * This class defines an ImageSet object which will be used across the viewer. This object represents + * a list of images that are associated by any arbitrary criteria being thus content agnostic. Besides the + * main attributes (images and uid) it allows additional attributes to be appended to it (currently + * indiscriminately, but this should be changed). + */ +class ImageSet { + images: Image[]; + uid: string; + instances: Image[]; + instance?: Image; + StudyInstanceUID?: string; + + constructor(images: Image[]) { + if (!Array.isArray(images)) { + throw new Error('ImageSet expects an array of images'); + } + + // @property "images" + Object.defineProperty(this, 'images', { + enumerable: false, + configurable: false, + writable: false, + value: images, + }); + + // @property "uid" + Object.defineProperty(this, 'uid', { + enumerable: false, + configurable: false, + writable: false, + value: guid(), // Unique ID of the instance + }); + + this.instances = images; + this.instance = images[0]; + this.StudyInstanceUID = this.instance?.StudyInstanceUID; + } + + load: () => Promise; + + getUID(): string { + return this.uid; + } + + setAttribute(attribute: string, value: unknown): void { + this[attribute] = value; + } + + getAttribute(attribute: string): unknown { + return this[attribute]; + } + + setAttributes(attributes: Attributes): void { + if (typeof attributes === 'object' && attributes !== null) { + for (const [attribute, value] of Object.entries(attributes)) { + this[attribute] = value; + } + } + } + + getNumImages = (): number => this.images.length; + + getImage(index: number): Image { + return this.images[index]; + } + + sortBy(sortingCallback: (a: Image, b: Image) => number): Image[] { + return this.images.sort(sortingCallback); + } + + sortByImagePositionPatient(): void { + const images = this.images; + const referenceImagePositionPatient = _getImagePositionPatient(images[0]); + + const refIppVec = new Vector3( + referenceImagePositionPatient[0], + referenceImagePositionPatient[1], + referenceImagePositionPatient[2] + ); + + const ImageOrientationPatient = _getImageOrientationPatient(images[0]); + + const scanAxisNormal = new Vector3( + ImageOrientationPatient[0], + ImageOrientationPatient[1], + ImageOrientationPatient[2] + ).cross( + new Vector3( + ImageOrientationPatient[3], + ImageOrientationPatient[4], + ImageOrientationPatient[5] + ) + ); + + const distanceImagePairs = images.map(function (image: Image) { + const ippVec = new Vector3(..._getImagePositionPatient(image)); + const positionVector = refIppVec.clone().sub(ippVec); + const distance = positionVector.dot(scanAxisNormal); + + return { + distance, + image, + }; + }); + + distanceImagePairs.sort(function (a, b) { + return b.distance - a.distance; + }); + + const sortedImages = distanceImagePairs.map(a => a.image); + + images.sort(function (a, b) { + return sortedImages.indexOf(a) - sortedImages.indexOf(b); + }); + } +} + +function _getImagePositionPatient(image) { + return image.getData().metadata.ImagePositionPatient; +} + +function _getImageOrientationPatient(image) { + return image.getData().metadata.ImageOrientationPatient; +} + +export default ImageSet; diff --git a/platform/core/src/classes/MetadataProvider.ts b/platform/core/src/classes/MetadataProvider.ts new file mode 100644 index 0000000..96ca5e1 --- /dev/null +++ b/platform/core/src/classes/MetadataProvider.ts @@ -0,0 +1,580 @@ +import queryString from 'query-string'; +import dicomParser from 'dicom-parser'; +import { imageIdToURI } from '../utils'; +import getPixelSpacingInformation from '../utils/metadataProvider/getPixelSpacingInformation'; +import DicomMetadataStore from '../services/DicomMetadataStore'; +import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData'; +import toNumber from '../utils/toNumber'; +import combineFrameInstance from '../utils/combineFrameInstance'; +import formatPN from '../utils/formatPN'; + +class MetadataProvider { + private readonly imageURIToUIDs: Map = new Map(); + // Can be used to store custom metadata for a specific type. + // For instance, the scaling metadata for PET can be stored here + // as type "scalingModule" + private readonly customMetadata: Map = new Map(); + + addImageIdToUIDs(imageId, uids) { + if (!imageId) { + throw new Error('MetadataProvider::Empty imageId'); + } + + // This method is a fallback for when you don't have WADO-URI or WADO-RS. + // You can add instances fetched by any method by calling addInstance, and hook an imageId to point at it here. + // An example would be dicom hosted at some random site. + const imageURI = imageIdToURI(imageId); + this.imageURIToUIDs.set(imageURI, uids); + } + + addCustomMetadata(imageId, type, metadata) { + const imageURI = imageIdToURI(imageId); + if (!this.customMetadata.has(type)) { + this.customMetadata.set(type, {}); + } + + this.customMetadata.get(type)[imageURI] = metadata; + } + + _getInstance(imageId) { + if (!imageId) { + throw new Error('MetadataProvider::Empty imageId'); + } + + const uids = this.getUIDsFromImageID(imageId); + + if (!uids) { + return; + } + + const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID, frameNumber } = uids; + + const instance = DicomMetadataStore.getInstance( + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID + ); + + if (!instance) { + return; + } + + return (frameNumber && combineFrameInstance(frameNumber, instance)) || instance; + } + + get(query, imageId, options = { fallback: false }) { + if (Array.isArray(imageId)) { + return; + } + const instance = this._getInstance(imageId); + + if (query === INSTANCE) { + return instance; + } + + // check inside custom metadata + if (this.customMetadata.has(query)) { + const customMetadata = this.customMetadata.get(query); + const imageURI = imageIdToURI(imageId); + if (customMetadata[imageURI]) { + return customMetadata[imageURI]; + } + } + + return this.getTagFromInstance(query, instance, options); + } + + getTag(query, imageId, options) { + return this.get(query, imageId, options); + } + + getInstance(imageId) { + return this.get(INSTANCE, imageId); + } + + getTagFromInstance(naturalizedTagOrWADOImageLoaderTag, instance, options = { fallback: false }) { + if (!instance) { + return; + } + + // If its a naturalized dcmjs tag present on the instance, return. + if (instance[naturalizedTagOrWADOImageLoaderTag]) { + return instance[naturalizedTagOrWADOImageLoaderTag]; + } + + // Maybe its a legacy dicomImageLoader tag then: + return this._getCornerstoneDICOMImageLoaderTag(naturalizedTagOrWADOImageLoaderTag, instance); + } + + /** + * Adds a new handler for the given tag. The handler will be provided an + * instance object that it can read values from. + */ + public addHandler(wadoImageLoaderTag: string, handler) { + WADO_IMAGE_LOADER[wadoImageLoaderTag] = handler; + } + + _getCornerstoneDICOMImageLoaderTag(wadoImageLoaderTag, instance) { + let metadata = WADO_IMAGE_LOADER[wadoImageLoaderTag]?.(instance); + if (metadata) { + return metadata; + } + + switch (wadoImageLoaderTag) { + case WADO_IMAGE_LOADER_TAGS.GENERAL_SERIES_MODULE: + const { SeriesDate, SeriesTime } = instance; + + let seriesDate; + let seriesTime; + + if (SeriesDate) { + seriesDate = dicomParser.parseDA(SeriesDate); + } + + if (SeriesTime) { + seriesTime = dicomParser.parseTM(SeriesTime); + } + + metadata = { + modality: instance.Modality, + seriesInstanceUID: instance.SeriesInstanceUID, + seriesNumber: toNumber(instance.SeriesNumber), + studyInstanceUID: instance.StudyInstanceUID, + seriesDate, + seriesTime, + }; + break; + case WADO_IMAGE_LOADER_TAGS.PATIENT_STUDY_MODULE: + metadata = { + patientAge: toNumber(instance.PatientAge), + patientSize: toNumber(instance.PatientSize), + patientWeight: toNumber(instance.PatientWeight), + }; + break; + case WADO_IMAGE_LOADER_TAGS.PATIENT_DEMOGRAPHIC_MODULE: + metadata = { + patientSex: instance.PatientSex, + }; + break; + case WADO_IMAGE_LOADER_TAGS.IMAGE_PIXEL_MODULE: + metadata = { + samplesPerPixel: toNumber(instance.SamplesPerPixel), + photometricInterpretation: instance.PhotometricInterpretation, + rows: toNumber(instance.Rows), + columns: toNumber(instance.Columns), + bitsAllocated: toNumber(instance.BitsAllocated), + bitsStored: toNumber(instance.BitsStored), + highBit: toNumber(instance.HighBit), + pixelRepresentation: toNumber(instance.PixelRepresentation), + planarConfiguration: toNumber(instance.PlanarConfiguration), + pixelAspectRatio: toNumber(instance.PixelAspectRatio), + smallestPixelValue: toNumber(instance.SmallestPixelValue), + largestPixelValue: toNumber(instance.LargestPixelValue), + redPaletteColorLookupTableDescriptor: toNumber( + instance.RedPaletteColorLookupTableDescriptor + ), + greenPaletteColorLookupTableDescriptor: toNumber( + instance.GreenPaletteColorLookupTableDescriptor + ), + bluePaletteColorLookupTableDescriptor: toNumber( + instance.BluePaletteColorLookupTableDescriptor + ), + redPaletteColorLookupTableData: fetchPaletteColorLookupTableData( + instance, + 'RedPaletteColorLookupTableData', + 'RedPaletteColorLookupTableDescriptor' + ), + greenPaletteColorLookupTableData: fetchPaletteColorLookupTableData( + instance, + 'GreenPaletteColorLookupTableData', + 'GreenPaletteColorLookupTableDescriptor' + ), + bluePaletteColorLookupTableData: fetchPaletteColorLookupTableData( + instance, + 'BluePaletteColorLookupTableData', + 'BluePaletteColorLookupTableDescriptor' + ), + }; + + break; + case WADO_IMAGE_LOADER_TAGS.VOI_LUT_MODULE: + const { WindowCenter, WindowWidth, VOILUTFunction } = instance; + if (WindowCenter == null || WindowWidth == null) { + return; + } + const windowCenter = Array.isArray(WindowCenter) ? WindowCenter : [WindowCenter]; + const windowWidth = Array.isArray(WindowWidth) ? WindowWidth : [WindowWidth]; + + metadata = { + windowCenter: toNumber(windowCenter), + windowWidth: toNumber(windowWidth), + voiLUTFunction: VOILUTFunction, + }; + + break; + case WADO_IMAGE_LOADER_TAGS.MODALITY_LUT_MODULE: + const { RescaleIntercept, RescaleSlope } = instance; + if (RescaleIntercept === undefined || RescaleSlope === undefined) { + return; + } + + metadata = { + rescaleIntercept: toNumber(instance.RescaleIntercept), + rescaleSlope: toNumber(instance.RescaleSlope), + rescaleType: instance.RescaleType, + }; + break; + case WADO_IMAGE_LOADER_TAGS.SOP_COMMON_MODULE: + metadata = { + sopClassUID: instance.SOPClassUID, + sopInstanceUID: instance.SOPInstanceUID, + }; + break; + case WADO_IMAGE_LOADER_TAGS.PET_IMAGE_MODULE: + metadata = { + frameReferenceTime: instance.FrameReferenceTime, + actualFrameDuration: instance.ActualFrameDuration, + }; + break; + case WADO_IMAGE_LOADER_TAGS.PET_ISOTOPE_MODULE: + const { RadiopharmaceuticalInformationSequence } = instance; + + if (RadiopharmaceuticalInformationSequence) { + const RadiopharmaceuticalInformation = Array.isArray( + RadiopharmaceuticalInformationSequence + ) + ? RadiopharmaceuticalInformationSequence[0] + : RadiopharmaceuticalInformationSequence; + + const { RadiopharmaceuticalStartTime, RadionuclideTotalDose, RadionuclideHalfLife } = + RadiopharmaceuticalInformation; + + const radiopharmaceuticalInfo = { + radiopharmaceuticalStartTime: dicomParser.parseTM(RadiopharmaceuticalStartTime), + radionuclideTotalDose: RadionuclideTotalDose, + radionuclideHalfLife: RadionuclideHalfLife, + }; + metadata = { + radiopharmaceuticalInfo, + }; + } + + break; + case WADO_IMAGE_LOADER_TAGS.OVERLAY_PLANE_MODULE: + const overlays = []; + + for (let overlayGroup = 0x00; overlayGroup <= 0x1e; overlayGroup += 0x02) { + let groupStr = `60${overlayGroup.toString(16)}`; + + if (groupStr.length === 3) { + groupStr = `600${overlayGroup.toString(16)}`; + } + + const OverlayDataTag = `${groupStr}3000`; + const OverlayData = instance[OverlayDataTag]; + + if (!OverlayData) { + continue; + } + + const OverlayRowsTag = `${groupStr}0010`; + const OverlayColumnsTag = `${groupStr}0011`; + const OverlayType = `${groupStr}0040`; + const OverlayOriginTag = `${groupStr}0050`; + const OverlayDescriptionTag = `${groupStr}0022`; + const OverlayLabelTag = `${groupStr}1500`; + const ROIAreaTag = `${groupStr}1301`; + const ROIMeanTag = `${groupStr}1302`; + const ROIStandardDeviationTag = `${groupStr}1303`; + const OverlayOrigin = instance[OverlayOriginTag]; + + let rows = 0; + if (instance[OverlayRowsTag] instanceof Array) { + // The DICOM VR for overlay rows is US (unsigned short). + const rowsInt16Array = new Uint16Array(instance[OverlayRowsTag][0]); + rows = rowsInt16Array[0]; + } else { + rows = instance[OverlayRowsTag]; + } + + let columns = 0; + if (instance[OverlayColumnsTag] instanceof Array) { + // The DICOM VR for overlay columns is US (unsigned short). + const columnsInt16Array = new Uint16Array(instance[OverlayColumnsTag][0]); + columns = columnsInt16Array[0]; + } else { + columns = instance[OverlayColumnsTag]; + } + + let x = 0; + let y = 0; + if (OverlayOrigin.length === 1) { + // The DICOM VR for overlay origin is SS (signed short) with a multiplicity of 2. + const originInt16Array = new Int16Array(OverlayOrigin[0]); + x = originInt16Array[0]; + y = originInt16Array[1]; + } else { + x = OverlayOrigin[0]; + y = OverlayOrigin[1]; + } + + const overlay = { + rows: rows, + columns: columns, + type: instance[OverlayType], + x, + y, + pixelData: OverlayData, + description: instance[OverlayDescriptionTag], + label: instance[OverlayLabelTag], + roiArea: instance[ROIAreaTag], + roiMean: instance[ROIMeanTag], + roiStandardDeviation: instance[ROIStandardDeviationTag], + }; + + overlays.push(overlay); + } + + metadata = { + overlays, + }; + + break; + + case WADO_IMAGE_LOADER_TAGS.PATIENT_MODULE: + const { PatientName } = instance; + + let patientName; + if (PatientName) { + patientName = formatPN(PatientName); + } + + metadata = { + patientName, + patientId: instance.PatientID, + }; + + break; + + case WADO_IMAGE_LOADER_TAGS.GENERAL_IMAGE_MODULE: + metadata = { + sopInstanceUID: instance.SOPInstanceUID, + instanceNumber: toNumber(instance.InstanceNumber), + lossyImageCompression: instance.LossyImageCompression, + lossyImageCompressionRatio: instance.LossyImageCompressionRatio, + lossyImageCompressionMethod: instance.LossyImageCompressionMethod, + }; + + break; + case WADO_IMAGE_LOADER_TAGS.GENERAL_STUDY_MODULE: + metadata = { + studyDescription: instance.StudyDescription, + studyDate: instance.StudyDate, + studyTime: instance.StudyTime, + accessionNumber: instance.AccessionNumber, + }; + + break; + case WADO_IMAGE_LOADER_TAGS.CINE_MODULE: + metadata = { + frameTime: instance.FrameTime, + numberOfFrames: instance.NumberOfFrames ? Number(instance.NumberOfFrames) : 1, + }; + + break; + case WADO_IMAGE_LOADER_TAGS.PET_SERIES_MODULE: + metadata = { + correctedImage: instance.CorrectedImage, + units: instance.Units, + decayCorrection: instance.DecayCorrection, + }; + break; + case WADO_IMAGE_LOADER_TAGS.CALIBRATION_MODULE: + // map the DICOM tags to the cornerstone tags since cornerstone tags + // are camelCase and instance tags are all caps + metadata = { + sequenceOfUltrasoundRegions: instance.SequenceOfUltrasoundRegions?.map(region => { + return { + regionSpatialFormat: region.RegionSpatialFormat, + regionDataType: region.RegionDataType, + regionFlags: region.RegionFlags, + regionLocationMinX0: region.RegionLocationMinX0, + regionLocationMinY0: region.RegionLocationMinY0, + regionLocationMaxX1: region.RegionLocationMaxX1, + regionLocationMaxY1: region.RegionLocationMaxY1, + referencePixelX0: region.ReferencePixelX0, + referencePixelY0: region.ReferencePixelY0, + referencePixelPhysicalValueX: region.ReferencePixelPhysicalValueX, + referencePixelPhysicalValueY: region.ReferencePixelPhysicalValueY, + physicalUnitsXDirection: region.PhysicalUnitsXDirection, + physicalUnitsYDirection: region.PhysicalUnitsYDirection, + physicalDeltaX: region.PhysicalDeltaX, + physicalDeltaY: region.PhysicalDeltaY, + }; + }), + }; + break; + + /** + * Below are the tags and not the modules since they are not really + * consistent with the modules above + */ + case 'temporalPositionIdentifier': + metadata = { + temporalPositionIdentifier: instance.TemporalPositionIdentifier, + }; + break; + + default: + return; + } + + return metadata; + } + + /** + * Retrieves the frameNumber information, depending on the url style + * wadors /frames/1 + * wadouri &frame=1 + * @param {*} imageId + * @returns + */ + getFrameInformationFromURL(imageId) { + function getInformationFromURL(informationString, separator) { + let result = ''; + const splittedStr = imageId.split(informationString)[1]; + if (splittedStr.includes(separator)) { + result = splittedStr.split(separator)[0]; + } else { + result = splittedStr; + } + return result; + } + + if (imageId.includes('/frames')) { + return getInformationFromURL('/frames', '/'); + } + if (imageId.includes('&frame=')) { + return getInformationFromURL('&frame=', '&'); + } + return; + } + + getUIDsFromImageID(imageId) { + if (imageId.startsWith('wadors:')) { + const strippedImageId = imageId.split('/studies/')[1]; + const splitImageId = strippedImageId.split('/'); + + return { + StudyInstanceUID: splitImageId[0], // Note: splitImageId[1] === 'series' + SeriesInstanceUID: splitImageId[2], // Note: splitImageId[3] === 'instances' + SOPInstanceUID: splitImageId[4], + frameNumber: splitImageId[6], + }; + } else if (imageId.includes('?requestType=WADO')) { + const qs = queryString.parse(imageId); + + return { + StudyInstanceUID: qs.studyUID, + SeriesInstanceUID: qs.seriesUID, + SOPInstanceUID: qs.objectUID, + frameNumber: qs.frameNumber, + }; + } + + // Maybe its a non-standard imageId + // check if the imageId starts with http:// or https:// using regex + // Todo: handle non http imageIds + let imageURI; + const urlRegex = /^(http|https|dicomfile):\/\//; + if (urlRegex.test(imageId)) { + imageURI = imageId; + } else { + imageURI = imageIdToURI(imageId); + } + + // remove &frame=number from imageId + imageURI = imageURI.split('&frame=')[0]; + + const uids = this.imageURIToUIDs.get(imageURI); + const frameNumber = this.getFrameInformationFromURL(imageId) || '1'; + + if (uids && frameNumber !== undefined) { + return { ...uids, frameNumber }; + } + return uids; + } +} + +const metadataProvider = new MetadataProvider(); + +export default metadataProvider; + +const WADO_IMAGE_LOADER = { + imagePlaneModule: instance => { + const { ImageOrientationPatient } = instance; + + // Fallback for DX images. + // TODO: We should use the rest of the results of this function + // to update the UI somehow + const { PixelSpacing } = getPixelSpacingInformation(instance); + + let rowPixelSpacing; + let columnPixelSpacing; + + let rowCosines; + let columnCosines; + + if (PixelSpacing) { + rowPixelSpacing = PixelSpacing[0]; + columnPixelSpacing = PixelSpacing[1]; + } + + if (ImageOrientationPatient) { + rowCosines = ImageOrientationPatient.slice(0, 3); + columnCosines = ImageOrientationPatient.slice(3, 6); + } + + return { + frameOfReferenceUID: instance.FrameOfReferenceUID, + rows: toNumber(instance.Rows), + columns: toNumber(instance.Columns), + spacingBetweenSlices: toNumber(instance.SpacingBetweenSlices), + imageOrientationPatient: toNumber(ImageOrientationPatient) || [0, 1, 0, 0, 0, -1], + rowCosines: toNumber(rowCosines || [0, 1, 0]), + isDefaultValueSetForRowCosine: toNumber(rowCosines) ? false : true, + columnCosines: toNumber(columnCosines || [0, 0, -1]), + isDefaultValueSetForColumnCosine: toNumber(columnCosines) ? false : true, + imagePositionPatient: toNumber(instance.ImagePositionPatient || [0, 0, 0]), + sliceThickness: toNumber(instance.SliceThickness), + sliceLocation: toNumber(instance.SliceLocation), + pixelSpacing: toNumber(PixelSpacing || 1), + rowPixelSpacing: rowPixelSpacing ? toNumber(rowPixelSpacing) : null, + columnPixelSpacing: columnPixelSpacing ? toNumber(columnPixelSpacing) : null, + }; + }, +}; + +const WADO_IMAGE_LOADER_TAGS = { + // dicomImageLoader specific + GENERAL_SERIES_MODULE: 'generalSeriesModule', + PATIENT_STUDY_MODULE: 'patientStudyModule', + IMAGE_PIXEL_MODULE: 'imagePixelModule', + VOI_LUT_MODULE: 'voiLutModule', + MODALITY_LUT_MODULE: 'modalityLutModule', + SOP_COMMON_MODULE: 'sopCommonModule', + PET_IMAGE_MODULE: 'petImageModule', + PET_ISOTOPE_MODULE: 'petIsotopeModule', + PET_SERIES_MODULE: 'petSeriesModule', + OVERLAY_PLANE_MODULE: 'overlayPlaneModule', + PATIENT_DEMOGRAPHIC_MODULE: 'patientDemographicModule', + + // react-cornerstone-viewport specific + PATIENT_MODULE: 'patientModule', + GENERAL_IMAGE_MODULE: 'generalImageModule', + GENERAL_STUDY_MODULE: 'generalStudyModule', + CINE_MODULE: 'cineModule', + CALIBRATION_MODULE: 'calibrationModule', +}; + +const INSTANCE = 'instance'; diff --git a/platform/core/src/classes/index.js b/platform/core/src/classes/index.js new file mode 100644 index 0000000..ce7e107 --- /dev/null +++ b/platform/core/src/classes/index.js @@ -0,0 +1,15 @@ +import CommandsManager from './CommandsManager'; +import HotkeysManager from './HotkeysManager'; +import ImageSet from './ImageSet'; +import MetadataProvider from './MetadataProvider'; + +export { MetadataProvider, CommandsManager, HotkeysManager, ImageSet }; + +const classes = { + MetadataProvider, + CommandsManager, + HotkeysManager, + ImageSet, +}; + +export default classes; diff --git a/platform/core/src/contextProviders/SystemProvider.tsx b/platform/core/src/contextProviders/SystemProvider.tsx new file mode 100644 index 0000000..3a522a5 --- /dev/null +++ b/platform/core/src/contextProviders/SystemProvider.tsx @@ -0,0 +1,33 @@ +import React, { createContext, useContext } from 'react'; +import { CommandsManager, HotkeysManager } from '../classes'; +import { ExtensionManager } from '../extensions'; +import { ServicesManager } from '../services'; + +interface SystemContextProviderProps { + children: React.ReactNode | React.ReactNode[] | ((...args: any[]) => React.ReactNode); + servicesManager: ServicesManager; + commandsManager: CommandsManager; + extensionManager: ExtensionManager; + hotkeysManager: HotkeysManager; +} + +const systemContext = createContext(null); +const { Provider } = systemContext; + +export const useSystem = () => useContext(systemContext); + +export function SystemContextProvider({ + children, + servicesManager, + commandsManager, + extensionManager, + hotkeysManager, +}: SystemContextProviderProps) { + return ( + + {children} + + ); +} + +export default SystemContextProvider; diff --git a/platform/core/src/defaults/hotkeyBindings.ts b/platform/core/src/defaults/hotkeyBindings.ts new file mode 100644 index 0000000..eeea06e --- /dev/null +++ b/platform/core/src/defaults/hotkeyBindings.ts @@ -0,0 +1,185 @@ +const bindings = [ + { + commandName: 'setToolActive', + commandOptions: { toolName: 'Zoom' }, + label: 'Zoom', + keys: ['z'], + isEditable: true, + }, + { + commandName: 'scaleUpViewport', + label: 'Zoom In', + keys: ['+'], + isEditable: true, + }, + { + commandName: 'scaleDownViewport', + label: 'Zoom Out', + keys: ['-'], + isEditable: true, + }, + { + commandName: 'fitViewportToWindow', + label: 'Zoom to Fit', + keys: ['='], + isEditable: true, + }, + { + commandName: 'rotateViewportCW', + label: 'Rotate Right', + keys: ['r'], + isEditable: true, + }, + { + commandName: 'rotateViewportCCW', + label: 'Rotate Left', + keys: ['l'], + isEditable: true, + }, + { + commandName: 'flipViewportHorizontal', + label: 'Flip Horizontally', + keys: ['h'], + isEditable: true, + }, + { + commandName: 'flipViewportVertical', + label: 'Flip Vertically', + keys: ['v'], + isEditable: true, + }, + { + commandName: 'toggleCine', + label: 'Cine', + keys: ['c'], + }, + { + commandName: 'invertViewport', + label: 'Invert', + keys: ['i'], + isEditable: true, + }, + { + commandName: 'incrementActiveViewport', + label: 'Next Image Viewport', + keys: ['right'], + isEditable: true, + }, + { + commandName: 'decrementActiveViewport', + label: 'Previous Image Viewport', + keys: ['left'], + isEditable: true, + }, + { + commandName: 'updateViewportDisplaySet', + commandOptions: { + direction: -1, + }, + label: 'Previous Series', + keys: ['pageup'], + isEditable: true, + }, + { + commandName: 'updateViewportDisplaySet', + commandOptions: { + direction: 1, + }, + label: 'Next Series', + keys: ['pagedown'], + isEditable: true, + }, + { + commandName: 'nextStage', + context: 'DEFAULT', + label: 'Next Stage', + keys: ['.'], + isEditable: true, + }, + { + commandName: 'previousStage', + context: 'DEFAULT', + label: 'Previous Stage', + keys: [','], + isEditable: true, + }, + { + commandName: 'nextImage', + label: 'Next Image', + keys: ['down'], + isEditable: true, + }, + { + commandName: 'previousImage', + label: 'Previous Image', + keys: ['up'], + isEditable: true, + }, + { + commandName: 'firstImage', + label: 'First Image', + keys: ['home'], + isEditable: true, + }, + { + commandName: 'lastImage', + label: 'Last Image', + keys: ['end'], + isEditable: true, + }, + { + commandName: 'resetViewport', + label: 'Reset', + keys: ['space'], + isEditable: true, + }, + { + commandName: 'cancelMeasurement', + label: 'Cancel Cornerstone Measurement', + keys: ['esc'], + }, + { + commandName: 'setWindowLevelPreset', + commandOptions: { presetName: 'ct-soft-tissue', presetIndex: 0 }, + label: 'W/L Preset 1', + keys: ['1'], + }, + { + commandName: 'setWindowLevelPreset', + commandOptions: { presetName: 'ct-lung', presetIndex: 1 }, + label: 'W/L Preset 2', + keys: ['2'], + }, + { + commandName: 'setWindowLevelPreset', + commandOptions: { presetName: 'ct-bone', presetIndex: 2 }, + label: 'W/L Preset 3', + keys: ['3'], + }, + { + commandName: 'setWindowLevelPreset', + commandOptions: { presetName: 'ct-brain', presetIndex: 3 }, + label: 'W/L Preset 4', + keys: ['4'], + }, + { + commandName: 'deleteActiveAnnotation', + label: 'Delete Annotation', + keys: ['backspace'], + }, + // after we have the ui for undo/redo, we can add these back in + // { + // commandName: 'undo', + // label: 'Undo', + // keys: ['ctrl+z'], + // isEditable: true, + // }, + // { + // commandName: 'redo', + // label: 'Redo', + // keys: ['ctrl+y'], + // isEditable: true, + // }, +]; + +export default bindings; diff --git a/platform/core/src/defaults/index.js b/platform/core/src/defaults/index.js new file mode 100644 index 0000000..7054805 --- /dev/null +++ b/platform/core/src/defaults/index.js @@ -0,0 +1,7 @@ +import hotkeyBindings from './hotkeyBindings'; +import windowLevelPresets from './windowLevelPresets'; + +export default { + hotkeyBindings, + windowLevelPresets, +}; diff --git a/platform/core/src/defaults/windowLevelPresets.js b/platform/core/src/defaults/windowLevelPresets.js new file mode 100644 index 0000000..ca6a150 --- /dev/null +++ b/platform/core/src/defaults/windowLevelPresets.js @@ -0,0 +1,12 @@ +export default { + 1: { description: 'Soft tissue', window: '400', level: '40' }, + 2: { description: 'Lung', window: '1500', level: '-600' }, + 3: { description: 'Liver', window: '150', level: '90' }, + 4: { description: 'Bone', window: '2500', level: '480' }, + 5: { description: 'Brain', window: '80', level: '40' }, + 6: { description: 'Trest', window: '1', level: '1' }, + 7: { description: 'Empty1', window: 'Empty1', level: 'Empty1' }, + 8: { description: 'Empty2', window: 'Empty2', level: 'Empty2' }, + 9: { description: 'Empty3', window: 'Empty3', level: 'Empty3' }, + 10: { description: 'Empty4', window: 'Empty4', level: 'Empty4' }, +}; diff --git a/platform/core/src/enums/TimingEnum.ts b/platform/core/src/enums/TimingEnum.ts new file mode 100644 index 0000000..eddbc55 --- /dev/null +++ b/platform/core/src/enums/TimingEnum.ts @@ -0,0 +1,24 @@ +export enum TimingEnum { + // The time from when the users selects a study until the study metadata + // is loaded (and the display sets are ready) + STUDY_TO_DISPLAY_SETS = 'studyToDisplaySetsLoaded', + + // The time from when the user selects a study until any viewport renders + STUDY_TO_FIRST_IMAGE = 'studyToFirstImage', + + // The time from when display sets are loaded until any viewport renders + // an image. + DISPLAY_SETS_TO_FIRST_IMAGE = 'displaySetsToFirstImage', + + // The time from when display sets are loaded until all viewports have images + DISPLAY_SETS_TO_ALL_IMAGES = 'displaySetsToAllImages', + + // The time from when the user hits search until the worklist is displayed + SEARCH_TO_LIST = 'searchToList', + + // The time from when the html script first starts being evaluated (before + // any other scripts or CSS is loaded), until the time that the first image + // is viewed for viewer endpoints, or the time that the first search for studies + // completes. + SCRIPT_TO_VIEW = 'scriptToView', +} diff --git a/platform/core/src/enums/index.ts b/platform/core/src/enums/index.ts new file mode 100644 index 0000000..8daa802 --- /dev/null +++ b/platform/core/src/enums/index.ts @@ -0,0 +1,3 @@ +import { TimingEnum } from './TimingEnum'; + +export { TimingEnum }; diff --git a/platform/core/src/errorHandler.js b/platform/core/src/errorHandler.js new file mode 100644 index 0000000..4b0fc72 --- /dev/null +++ b/platform/core/src/errorHandler.js @@ -0,0 +1,6 @@ +// These should be overridden by the implementation +const errorHandler = { + getHTTPErrorHandler: () => null, +}; + +export default errorHandler; diff --git a/platform/core/src/extensions/ExtensionManager.test.js b/platform/core/src/extensions/ExtensionManager.test.js new file mode 100644 index 0000000..042a498 --- /dev/null +++ b/platform/core/src/extensions/ExtensionManager.test.js @@ -0,0 +1,280 @@ +import ExtensionManager from './ExtensionManager'; +import MODULE_TYPES from './MODULE_TYPES'; +import log from './../log.js'; + +jest.mock('./../log.js'); + +describe('ExtensionManager.ts', () => { + let extensionManager, commandsManager, servicesManager, appConfig; + + beforeEach(() => { + commandsManager = { + createContext: jest.fn(), + getContext: jest.fn(), + registerCommand: jest.fn(), + }; + servicesManager = { + registerService: jest.fn(), + services: { + // Required for DataSource Module initiation + UserAuthenticationService: jest.fn(), + HangingProtocolService: { + addProtocol: jest.fn(), + }, + }, + }; + appConfig = { + testing: true, + }; + extensionManager = new ExtensionManager({ + servicesManager, + commandsManager, + appConfig, + }); + log.warn.mockClear(); + jest.clearAllMocks(); + }); + + it('creates a module namespace for each module type', () => { + const moduleKeys = Object.keys(extensionManager.modules); + const moduleTypeValues = Object.values(MODULE_TYPES); + + expect(moduleKeys.sort()).toEqual(moduleTypeValues.sort()); + }); + + describe('registerExtensions()', () => { + it('calls registerExtension() for each extension', async () => { + extensionManager.registerExtension = jest.fn(); + + // SUT + const fakeExtensions = [{ one: '1' }, { two: '2' }, { three: '3 ' }]; + await extensionManager.registerExtensions(fakeExtensions); + + // Assert + expect(extensionManager.registerExtension.mock.calls.length).toBe(3); + }); + + it('calls registerExtension() for each extension passing its configuration if tuple', async () => { + const fakeConfiguration = { testing: true }; + extensionManager.registerExtension = jest.fn(); + + // SUT + const fakeExtensions = [{ one: '1' }, [{ two: '2' }, fakeConfiguration], { three: '3 ' }]; + await extensionManager.registerExtensions(fakeExtensions); + + // Assert + expect(extensionManager.registerExtension.mock.calls[1][1]).toEqual(fakeConfiguration); + }); + }); + + describe('registerExtension()', () => { + it('calls preRegistration() for extension', () => { + // SUT + const fakeExtension = { id: '1', preRegistration: jest.fn() }; + extensionManager.registerExtension(fakeExtension); + + // Assert + expect(fakeExtension.preRegistration.mock.calls.length).toBe(1); + }); + + it('calls preRegistration() passing dependencies and extension configuration to extension', () => { + const extensionConfiguration = { config: 'Some configuration' }; + + // SUT + const extension = { id: '1', preRegistration: jest.fn() }; + extensionManager.registerExtension(extension, extensionConfiguration); + + // Assert + expect(extension.preRegistration.mock.calls[0][0]).toEqual({ + servicesManager, + commandsManager, + extensionManager, + appConfig, + configuration: extensionConfiguration, + }); + }); + + it('logs a warning if the extension is null or undefined', async () => { + const undefinedExtension = undefined; + const nullExtension = null; + + await expect(extensionManager.registerExtension(undefinedExtension)).rejects.toThrow( + new Error('Attempting to register a null/undefined extension.') + ); + + await expect(extensionManager.registerExtension(nullExtension)).rejects.toThrow( + new Error('Attempting to register a null/undefined extension.') + ); + }); + + it('logs a warning if the extension does not have an id', async () => { + const extensionWithoutId = {}; + + await expect(extensionManager.registerExtension(extensionWithoutId)).rejects.toThrow( + new Error('Extension ID not set') + ); + }); + + it('tracks which extensions have been registered', () => { + const extension = { + id: 'hello-world', + }; + + extensionManager.registerExtension(extension); + + expect(extensionManager.registeredExtensionIds).toContain(extension.id); + }); + + it('logs a warning if the extension has an id that has already been registered', () => { + const extension = { id: 'hello-world' }; + extensionManager.registerExtension(extension); + + // SUT + extensionManager.registerExtension(extension); + + expect(log.warn.mock.calls.length).toBe(1); + }); + + it('logs a warning if a defined module returns null or undefined', () => { + const extensionWithBadModule = { + id: 'hello-world', + getViewportModule: () => { + return null; + }, + }; + + extensionManager.registerExtension(extensionWithBadModule); + + expect(log.warn.mock.calls.length).toBe(1); + expect(log.warn.mock.calls[0][0]).toContain('Null or undefined returned when registering'); + }); + + it('logs an error if an exception is thrown while retrieving a module', async () => { + const extensionWithBadModule = { + id: 'hello-world', + getViewportModule: () => { + throw new Error('Hello World'); + }, + }; + + await expect(extensionManager.registerExtension(extensionWithBadModule)).rejects.toThrow(); + }); + + it('successfully passes dependencies to each module along with extension configuration', () => { + const extensionConfiguration = { testing: true }; + + const extension = { + id: 'hello-world', + getViewportModule: jest.fn(), + getSopClassHandlerModule: jest.fn(), + getPanelModule: jest.fn(), + getToolbarModule: jest.fn(), + getCommandsModule: jest.fn(), + }; + + extensionManager.registerExtension(extension, extensionConfiguration); + + Object.keys(extension).forEach(module => { + if (typeof extension[module] === 'function') { + expect(extension[module].mock.calls[0][0]).toEqual({ + servicesManager, + commandsManager, + hotkeysManager: undefined, + appConfig, + configuration: extensionConfiguration, + extensionManager, + }); + } + }); + }); + + it('successfully registers a module for each module type', async () => { + const extension = { + id: 'hello-world', + getViewportModule: () => { + return [{ name: 'test' }]; + }, + getSopClassHandlerModule: () => { + return [{ name: 'test' }]; + }, + getPanelModule: () => { + return [{ name: 'test' }]; + }, + getToolbarModule: () => { + return [{ name: 'test' }]; + }, + getCommandsModule: () => { + return [{ name: 'test' }]; + }, + getLayoutTemplateModule: () => { + return [{ name: 'test' }]; + }, + getDataSourcesModule: () => { + return [{ name: 'test' }]; + }, + getHangingProtocolModule: () => { + return [{ name: 'test' }]; + }, + getContextModule: () => { + return [{ name: 'test' }]; + }, + getUtilityModule: () => { + return [{ name: 'test' }]; + }, + getCustomizationModule: () => { + return [{ name: 'test' }]; + }, + getStateSyncModule: () => { + return [{ name: 'test' }]; + }, + }; + + await extensionManager.registerExtension(extension); + + // Registers 1 module per module type + Object.keys(extensionManager.modules).forEach(moduleType => { + const modulesForType = extensionManager.modules[moduleType]; + console.log('moduleType', moduleType); + expect(modulesForType.length).toBe(1); + }); + }); + + it('calls commandsManager.registerCommand for each commandsModule command definition', () => { + const extension = { + id: 'hello-world', + getCommandsModule: () => { + return { + definitions: { + exampleDefinition: { + commandFn: () => {}, + options: {}, + }, + }, + }; + }, + }; + + // SUT + extensionManager.registerExtension(extension); + + expect(commandsManager.registerCommand.mock.calls.length).toBe(1); + }); + + it('logs a warning if the commandsModule contains no command definitions', () => { + const extension = { + id: 'hello-world', + getCommandsModule: () => { + return {}; + }, + }; + + // SUT + extensionManager.registerExtension(extension); + + expect(log.warn.mock.calls.length).toBe(1); + expect(log.warn.mock.calls[0][0]).toContain( + 'Commands Module contains no command definitions' + ); + }); + }); +}); diff --git a/platform/core/src/extensions/ExtensionManager.ts b/platform/core/src/extensions/ExtensionManager.ts new file mode 100644 index 0000000..af620b5 --- /dev/null +++ b/platform/core/src/extensions/ExtensionManager.ts @@ -0,0 +1,630 @@ +import MODULE_TYPES from './MODULE_TYPES'; +import log from '../log'; +import { PubSubService, ServiceProvidersManager } from '../services'; +import { HotkeysManager, CommandsManager } from '../classes'; +import type { DataSourceDefinition } from '../types'; +import type AppTypes from '../types/AppTypes'; + +/** + * This is the arguments given to create the extension. + */ +export interface ExtensionConstructor { + servicesManager: AppTypes.ServicesManager; + serviceProvidersManager: ServiceProvidersManager; + commandsManager: CommandsManager; + hotkeysManager: HotkeysManager; + appConfig: AppTypes.Config; +} + +/** + * The configuration of an extension. + * This uses type as the extension manager only knows that the configuration + * is an object of some sort, and doesn't know anything else about it. + */ +export type ExtensionConfiguration = Record; + +/** + * The parameters passed to the extension. + */ +export interface ExtensionParams extends ExtensionConstructor { + extensionManager: ExtensionManager; + servicesManager: AppTypes.ServicesManager; + serviceProvidersManager: ServiceProvidersManager; + configuration?: ExtensionConfiguration; + peerImport: (moduleId: string) => Promise; +} + +/** + * The type of an actual extension instance. + * This is an interface as it declares possible calls, but extensions can + * have more values than this. + */ +export interface Extension { + id: string; + preRegistration?: (p: ExtensionParams) => Promise | void; + getHangingProtocolModule?: (p: ExtensionParams) => unknown; + getCommandsModule?: (p: ExtensionParams) => CommandsModule; + getViewportModule?: (p: ExtensionParams) => unknown; + getUtilityModule?: (p: ExtensionParams) => unknown; + getCustomizationModule?: (p: ExtensionParams) => unknown; + getSopClassHandlerModule?: (p: ExtensionParams) => unknown; + getToolbarModule?: (p: ExtensionParams) => unknown; + getPanelModule?: (p: ExtensionParams) => unknown; + onModeEnter?: (p: AppTypes) => void; + onModeExit?: (p: AppTypes) => void; +} + +export type ExtensionRegister = { + id: string; + create: (p: ExtensionParams) => Extension; +}; + +export type CommandsModule = { + actions: Record; + definitions: Record; + defaultContext?: string; +}; + +export default class ExtensionManager extends PubSubService { + public static readonly EVENTS = { + ACTIVE_DATA_SOURCE_CHANGED: 'event::activedatasourcechanged', + }; + + public static readonly MODULE_TYPES = MODULE_TYPES; + + private _commandsManager: CommandsManager; + private _servicesManager: AppTypes.ServicesManager; + private _hotkeysManager: HotkeysManager; + private _serviceProvidersManager: ServiceProvidersManager; + private modulesMap: Record; + private modules: Record; + private registeredExtensionIds: string[]; + private moduleTypeNames: string[]; + private _appConfig: any; + private _extensionLifeCycleHooks: { + onModeEnter: Record; + onModeExit: Record; + }; + private dataSourceMap: Record; + private dataSourceDefs: Record; + private defaultDataSourceName: string; + private activeDataSource: string; + private peerImport: (moduleId) => Promise; + + constructor({ + commandsManager, + servicesManager, + serviceProvidersManager, + hotkeysManager, + appConfig = {}, + }: ExtensionConstructor) { + super(ExtensionManager.EVENTS); + this.modules = {}; + this.registeredExtensionIds = []; + this.moduleTypeNames = Object.values(MODULE_TYPES); + // + this._commandsManager = commandsManager; + this._servicesManager = servicesManager; + this._serviceProvidersManager = serviceProvidersManager; + this._hotkeysManager = hotkeysManager; + this._appConfig = appConfig; + + this.modulesMap = {}; + this.moduleTypeNames.forEach(moduleType => { + this.modules[moduleType] = []; + }); + this._extensionLifeCycleHooks = { onModeEnter: {}, onModeExit: {} }; + this.dataSourceMap = {}; + this.dataSourceDefs = {}; + this.defaultDataSourceName = appConfig.defaultDataSourceName; + this.activeDataSource = appConfig.defaultDataSourceName; + this.peerImport = appConfig.peerImport; + } + + public setActiveDataSource(dataSource: string): void { + if (this.activeDataSource === dataSource) { + return; + } + + this.activeDataSource = dataSource; + + this._broadcastEvent( + ExtensionManager.EVENTS.ACTIVE_DATA_SOURCE_CHANGED, + this.dataSourceDefs[this.activeDataSource] + ); + } + + public getRegisteredExtensionIds() { + return [...this.registeredExtensionIds]; + } + + private getUniqueServicesList(servicesManager: AppTypes.ServicesManager) { + // Make sure only one service instance is returned because almost all services are + // registered with different keys (eg: StudyPrefetcherService and studyPrefetcherService) + return Array.from(new Set(Object.values(servicesManager.services))); + } + + /** + * Calls all the services and extension on mode enters. + * The service onModeEnter is called first + * Then registered extensions onModeEnter is called + * This is supposed to setup the extension for a standard entry. + */ + public onModeEnter(): void { + const { + registeredExtensionIds, + _servicesManager, + _commandsManager, + _hotkeysManager, + _extensionLifeCycleHooks, + } = this; + const services = this.getUniqueServicesList(_servicesManager); + + // The onModeEnter of the service must occur BEFORE the extension + // onModeEnter in order to reset the state to a standard state + // before the extension restores and cached data. + for (const service of services) { + service?.onModeEnter?.(); + } + + registeredExtensionIds.forEach(extensionId => { + const onModeEnter = _extensionLifeCycleHooks.onModeEnter[extensionId]; + + if (typeof onModeEnter === 'function') { + onModeEnter({ + servicesManager: _servicesManager, + commandsManager: _commandsManager, + hotkeysManager: _hotkeysManager, + extensionManager: this, + }); + } + }); + } + + public onModeExit(): void { + const { registeredExtensionIds, _servicesManager, _commandsManager, _extensionLifeCycleHooks } = + this; + const services = this.getUniqueServicesList(_servicesManager); + + registeredExtensionIds.forEach(extensionId => { + const onModeExit = _extensionLifeCycleHooks.onModeExit[extensionId]; + + if (typeof onModeExit === 'function') { + onModeExit({ + servicesManager: _servicesManager, + commandsManager: _commandsManager, + }); + } + }); + + // The service onModeExit calls must occur after the extension ones + // so that extension ones can store/restore data. + for (const service of services) { + try { + service?.onModeExit?.(); + } catch (e) { + console.warn('onModeExit caught', e); + } + } + } + + /** + * An array of extensions, or an array of arrays that contains extension + * configuration pairs. + * + * @param {Object[]} extensions - Array of extensions + */ + public registerExtensions = async ( + extensions: (ExtensionRegister | [ExtensionRegister, ExtensionConfiguration])[], + dataSources: unknown[] = [] + ): Promise => { + // Todo: we ideally should be able to run registrations in parallel + // but currently since some extensions need to be registered before + // others, we need to run them sequentially. We need a postInit hook + // to avoid this sequential async registration + for (let i = 0; i < extensions.length; i++) { + const extension = extensions[i]; + const hasConfiguration = Array.isArray(extension); + try { + if (hasConfiguration) { + // Important: for some reason in the line below the type + // of extension is not recognized as [ExtensionRegister, + // ExtensionConfiguration] by babel DON"T CHANGE IT + // Same for the for loop above don't use + // for (const extension of extensions) + const ohifExtension = extension[0]; + const configuration = extension[1]; + await this.registerExtension(ohifExtension, configuration, dataSources); + } else { + await this.registerExtension(extension, {}, dataSources); + } + } catch (error) { + console.error(error); + } + } + }; + + /** + * + * TODO: Id Management: SopClassHandlers currently refer to viewport module by id; setting the extension id as viewport module id is a workaround for now + * @param {Object} extension + * @param {Object} configuration + */ + public registerExtension = async ( + extension: ExtensionRegister, + configuration = {}, + dataSources = [] + ): Promise => { + if (!extension) { + throw new Error('Attempting to register a null/undefined extension.'); + } + + const extensionId = extension.id; + + if (!extensionId) { + // Note: Mode framework cannot function without IDs. + log.warn(extension); + throw new Error(`Extension ID not set`); + } + + if (this.registeredExtensionIds.includes(extensionId)) { + log.warn( + `Extension ID ${extensionId} has already been registered. Exiting before duplicating modules.` + ); + return; + } + + // preRegistrationHook + if (extension.preRegistration) { + await extension.preRegistration({ + servicesManager: this._servicesManager, + serviceProvidersManager: this._serviceProvidersManager, + commandsManager: this._commandsManager, + hotkeysManager: this._hotkeysManager, + extensionManager: this, + appConfig: this._appConfig, + configuration, + }); + } + + if (extension.onModeEnter) { + this._extensionLifeCycleHooks.onModeEnter[extensionId] = extension.onModeEnter; + } + + if (extension.onModeExit) { + this._extensionLifeCycleHooks.onModeExit[extensionId] = extension.onModeExit; + } + + // Register Modules + this.moduleTypeNames.forEach(moduleType => { + const extensionModule = this._getExtensionModule( + moduleType, + extension, + extensionId, + configuration + ); + + if (!extensionModule) { + return; + } + + switch (moduleType) { + case MODULE_TYPES.COMMANDS: + this._initCommandsModule(extensionModule); + break; + + case MODULE_TYPES.DATA_SOURCE: + this._initDataSourcesModule(extensionModule, extensionId, dataSources); + break; + + case MODULE_TYPES.HANGING_PROTOCOL: + this._initHangingProtocolsModule(extensionModule, extensionId); + break; + + case MODULE_TYPES.PANEL: + this._initPanelModule(extensionModule, extensionId); + break; + + case MODULE_TYPES.TOOLBAR: + this._initToolbarModule(extensionModule, extensionId); + break; + + case MODULE_TYPES.VIEWPORT: + case MODULE_TYPES.SOP_CLASS_HANDLER: + case MODULE_TYPES.CONTEXT: + case MODULE_TYPES.LAYOUT_TEMPLATE: + case MODULE_TYPES.CUSTOMIZATION: + case MODULE_TYPES.STATE_SYNC: + case MODULE_TYPES.UTILITY: + this.processExtensionModule(extensionModule, extensionId, moduleType); + break; + default: + throw new Error(`Module type invalid: ${moduleType}`); + } + + this.modules[moduleType].push({ + extensionId, + module: extensionModule, + }); + }); + + // Track extension registration + this.registeredExtensionIds.push(extensionId); + }; + + /** + * Retrieves the module entry associated with the given string entry + * @param stringEntry - The string entry to retrieve the module entry for which is + * in the format of `${extensionId}.${moduleType}.${moduleName}` + * @returns The module entry associated with the given string entry. + */ + getModuleEntry = stringEntry => { + return this.modulesMap[stringEntry]; + }; + + /** + * Retrieves all modules of a given type for all registered extensions. + * + * @param moduleType - The type of modules to retrieve. + * @returns An array of modules of the specified type. + */ + getModulesByType = (moduleType: string) => { + return this.modules[moduleType]; + }; + + getDataSources = dataSourceName => { + if (dataSourceName === undefined) { + // Default to the activeDataSource + dataSourceName = this.activeDataSource; + } + + // Note: this currently uses the data source name, which feels weird... + return this.dataSourceMap[dataSourceName]; + }; + + getActiveDataSource = () => { + return this.dataSourceMap[this.activeDataSource]; + }; + + /** + * Gets the data source definition for the given data source name. + * If no data source name is provided, the active data source definition is + * returned. + * @param dataSourceName the data source name + * @returns the data source definition + */ + getDataSourceDefinition = dataSourceName => { + if (dataSourceName === undefined) { + // Default to the activeDataSource + dataSourceName = this.activeDataSource; + } + + return this.dataSourceDefs[dataSourceName]; + }; + + /** + * Gets the data source definition for the active data source. + */ + getActiveDataSourceDefinition = () => { + return this.getDataSourceDefinition(this.activeDataSource); + }; + + /** + * @private + * @param {string} moduleType + * @param {Object} extension + * @param {string} extensionId - Used for logging warnings + */ + _getExtensionModule = (moduleType, extension, extensionId, configuration) => { + const getModuleFnName = 'get' + _capitalizeFirstCharacter(moduleType); + const getModuleFn = extension[getModuleFnName]; + + if (!getModuleFn) { + return; + } + + try { + const extensionModule = extension[getModuleFnName]({ + appConfig: this._appConfig, + commandsManager: this._commandsManager, + servicesManager: this._servicesManager, + hotkeysManager: this._hotkeysManager, + extensionManager: this, + configuration, + }); + + if (!extensionModule) { + log.warn( + `Null or undefined returned when registering the ${getModuleFnName} module for the ${extensionId} extension` + ); + } + + return extensionModule; + } catch (ex) { + console.error(ex); + throw new Error( + `Exception thrown while trying to call ${getModuleFnName} for the ${extensionId} extension` + ); + } + }; + + _initHangingProtocolsModule = (extensionModule, extensionId) => { + const { hangingProtocolService } = this._servicesManager.services; + extensionModule.forEach(({ name, protocol }) => { + if (protocol) { + // Only auto-register if protocol specified, otherwise let mode register + hangingProtocolService.addProtocol(name, protocol); + } + }); + }; + + _initPanelModule = (extensionModule, extensionId) => { + this.processExtensionModule(extensionModule, extensionId, MODULE_TYPES.PANEL); + }; + + _initToolbarModule = (extensionModule, extensionId) => { + // check if the toolbar module has a handler function for evaluation of + // the toolbar button state + const { toolbarService } = this._servicesManager.services; + extensionModule.forEach(toolbarButton => { + if (toolbarButton.evaluate) { + toolbarService.registerEvaluateFunction(toolbarButton.name, toolbarButton.evaluate); + } + }); + }; + + /** + * Processes an extension module. + * @param extensionModule - The extension module to process. + * @param extensionId - The ID of the extension. + * @param moduleType - The type of the module. + */ + private processExtensionModule(extensionModule, extensionId: string, moduleType: string) { + extensionModule.forEach(element => { + if (!element.name) { + throw new Error(`Extension ID ${extensionId} module ${moduleType} element has no name`); + } + const id = `${extensionId}.${moduleType}.${element.name}`; + element.id = id; + this.modulesMap[id] = element; + }); + } + + /** + * Adds the given data source and optionally sets it as the active data source. + * The method does this by first creating the data source. + * @param dataSourceDef the data source definition to be added + * @param activate flag to indicate if the added data source should be set to the active data source + */ + addDataSource(dataSourceDef: DataSourceDefinition, options = { activate: false }) { + const existingDataSource = this.getDataSources(dataSourceDef.sourceName); + if (existingDataSource?.[0]) { + // The data source already exists and cannot be added. + return; + } + + this._createDataSourceInstance(dataSourceDef); + + if (options.activate) { + this.setActiveDataSource(dataSourceDef.sourceName); + } + } + + /** + * Updates the configuration of the given data source name. It first creates a new data source with + * the existing definition and the new configuration passed in. + * @param dataSourceName the name of the data source to update + * @param dataSourceConfiguration the new configuration to update the data source with + */ + updateDataSourceConfiguration(dataSourceName: string, dataSourceConfiguration: any) { + const existingDataSource = this.getDataSources(dataSourceName); + if (!existingDataSource?.[0]) { + // Cannot update a non existent data source. + return; + } + + const dataSourceDef = this.dataSourceDefs[dataSourceName]; + // Update the configuration. + dataSourceDef.configuration = dataSourceConfiguration; + this._createDataSourceInstance(dataSourceDef); + + if (this.activeDataSource === dataSourceName) { + // When the active data source is changed/set, fire an event to indicate that its configuration has changed. + this._broadcastEvent(ExtensionManager.EVENTS.ACTIVE_DATA_SOURCE_CHANGED, dataSourceDef); + } + } + + /** + * Creates a data source instance from the given definition. The definition is + * added to dataSourceDefs and the created instance is added to dataSourceMap. + * @param dataSourceDef + * @returns + */ + _createDataSourceInstance(dataSourceDef: DataSourceDefinition) { + const module = this.getModuleEntry(dataSourceDef.namespace); + + if (!module) { + return; + } + + this.dataSourceDefs[dataSourceDef.sourceName] = dataSourceDef; + + const dataSourceInstance = module.createDataSource( + dataSourceDef.configuration, + this._servicesManager, + this + ); + + this.dataSourceMap[dataSourceDef.sourceName] = [dataSourceInstance]; + } + + _initDataSourcesModule( + extensionModule, + extensionId, + dataSources: Array = [] + ): void { + extensionModule.forEach(element => { + this.modulesMap[`${extensionId}.${MODULE_TYPES.DATA_SOURCE}.${element.name}`] = element; + }); + + extensionModule.forEach(element => { + const namespace = `${extensionId}.${MODULE_TYPES.DATA_SOURCE}.${element.name}`; + + dataSources.forEach(dataSource => { + if (dataSource.namespace === namespace) { + this.addDataSource(dataSource); + } + }); + }); + } + + /** + * + * @private + * @param {Object[]} commandDefinitions + */ + _initCommandsModule = extensionModule => { + let { definitions, defaultContext } = extensionModule; + if (!definitions || Object.keys(definitions).length === 0) { + log.warn('Commands Module contains no command definitions'); + return; + } + + defaultContext = defaultContext || 'VIEWER'; + + if (!this._commandsManager.getContext(defaultContext)) { + this._commandsManager.createContext(defaultContext); + } + + Object.keys(definitions).forEach(commandName => { + let commandDefinition = definitions[commandName]; + if (typeof commandDefinition === 'function') { + commandDefinition = { commandFn: commandDefinition }; + } + const commandHasContextThatDoesNotExist = + commandDefinition.context && !this._commandsManager.getContext(commandDefinition.context); + + if (commandHasContextThatDoesNotExist) { + this._commandsManager.createContext(commandDefinition.context); + } + + this._commandsManager.registerCommand( + commandDefinition.context || defaultContext, + commandName, + commandDefinition + ); + }); + }; + + public get appConfig() { + return this._appConfig; + } +} + +/** + * @private + * @param {string} lower + */ +function _capitalizeFirstCharacter(lower) { + return lower.charAt(0).toUpperCase() + lower.substring(1); +} diff --git a/platform/core/src/extensions/MODULE_TYPES.js b/platform/core/src/extensions/MODULE_TYPES.js new file mode 100644 index 0000000..8260f77 --- /dev/null +++ b/platform/core/src/extensions/MODULE_TYPES.js @@ -0,0 +1,14 @@ +export default { + COMMANDS: 'commandsModule', + CUSTOMIZATION: 'customizationModule', + STATE_SYNC: 'stateSyncModule', + DATA_SOURCE: 'dataSourcesModule', + PANEL: 'panelModule', + SOP_CLASS_HANDLER: 'sopClassHandlerModule', + TOOLBAR: 'toolbarModule', + VIEWPORT: 'viewportModule', + CONTEXT: 'contextModule', + LAYOUT_TEMPLATE: 'layoutTemplateModule', + HANGING_PROTOCOL: 'hangingProtocolModule', + UTILITY: 'utilityModule', +}; diff --git a/platform/core/src/extensions/index.js b/platform/core/src/extensions/index.js new file mode 100644 index 0000000..e591987 --- /dev/null +++ b/platform/core/src/extensions/index.js @@ -0,0 +1,11 @@ +import ExtensionManager from './ExtensionManager'; +import MODULE_TYPES from './MODULE_TYPES'; + +const DEFAULT_EXPORTS = { + ExtensionManager, + MODULE_TYPES, +}; + +export default DEFAULT_EXPORTS; + +export { ExtensionManager, MODULE_TYPES }; diff --git a/platform/core/src/hooks/useActiveViewportDisplaySets.ts b/platform/core/src/hooks/useActiveViewportDisplaySets.ts new file mode 100644 index 0000000..f5f4252 --- /dev/null +++ b/platform/core/src/hooks/useActiveViewportDisplaySets.ts @@ -0,0 +1,60 @@ +import { useEffect, useState, useCallback } from 'react'; +import { DisplaySet } from '../types'; + +/** + * Hook that listens for changes in the active viewport and its display sets. + * It returns the display sets associated with the active viewport. + * + * @param servicesManager - Services manager instance + * @returns Array of display sets for the active viewport + */ +const useActiveViewportDisplaySets = ({ servicesManager }): DisplaySet[] => { + const [displaySets, setDisplaySets] = useState([]); + const { displaySetService, viewportGridService } = servicesManager.services; + + // Move this function outside useEffect and memoize it + const getDisplaySetsForViewport = useCallback( + (viewportId: string) => { + const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId) || []; + return displaySetUIDs.map(uid => displaySetService.getDisplaySetByUID(uid)).filter(Boolean); + }, + [displaySetService, viewportGridService] + ); + + useEffect(() => { + // Get initial state + const viewportId = viewportGridService.getActiveViewportId(); + setDisplaySets(getDisplaySetsForViewport(viewportId)); + + const handleViewportChange = ({ viewportId }) => { + setDisplaySets(getDisplaySetsForViewport(viewportId)); + }; + + const handleGridStateChange = ({ state }) => { + const activeViewportId = state.activeViewportId; + if (activeViewportId) { + setDisplaySets(getDisplaySetsForViewport(activeViewportId)); + } + }; + + // Subscribe to viewport changes + const subscriptions = [ + viewportGridService.subscribe( + viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED, + handleViewportChange + ), + viewportGridService.subscribe( + viewportGridService.EVENTS.GRID_STATE_CHANGED, + handleGridStateChange + ), + ]; + + return () => { + subscriptions.forEach(subscription => subscription.unsubscribe()); + }; + }, [viewportGridService, getDisplaySetsForViewport]); // Only depend on stable references + + return displaySets; +}; + +export default useActiveViewportDisplaySets; diff --git a/platform/core/src/hooks/useToolbar.tsx b/platform/core/src/hooks/useToolbar.tsx new file mode 100644 index 0000000..33ef806 --- /dev/null +++ b/platform/core/src/hooks/useToolbar.tsx @@ -0,0 +1,59 @@ +import { useCallback, useEffect, useState } from 'react'; + +export function useToolbar({ servicesManager, buttonSection = 'primary' }: withAppTypes) { + const { toolbarService, viewportGridService } = servicesManager.services; + const { EVENTS } = toolbarService; + + const [toolbarButtons, setToolbarButtons] = useState( + toolbarService.getButtonSection(buttonSection as string) + ); + + // Callback function for handling toolbar interactions + const onInteraction = useCallback( + args => { + const viewportId = viewportGridService.getActiveViewportId(); + const refreshProps = { + viewportId, + }; + toolbarService.recordInteraction(args, { + refreshProps, + }); + }, + [toolbarService, viewportGridService] + ); + + // Effect to handle toolbar modification events + useEffect(() => { + const handleToolbarModified = () => { + setToolbarButtons(toolbarService.getButtonSection(buttonSection as string)?.filter(Boolean)); + }; + + const subs = [EVENTS.TOOL_BAR_MODIFIED, EVENTS.TOOL_BAR_STATE_MODIFIED].map(event => { + return toolbarService.subscribe(event, handleToolbarModified); + }); + + return () => { + subs.forEach(sub => sub.unsubscribe()); + }; + }, [toolbarService]); + + // Effect to handle active viewportId change event + useEffect(() => { + const events = [ + viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED, + viewportGridService.EVENTS.VIEWPORTS_READY, + viewportGridService.EVENTS.LAYOUT_CHANGED, + ]; + + const subscriptions = events.map(event => { + return viewportGridService.subscribe(event, ({ viewportId }) => { + viewportId = viewportId || viewportGridService.getActiveViewportId(); + toolbarService.refreshToolbarState({ viewportId }); + }); + }); + + return () => subscriptions.forEach(sub => sub.unsubscribe()); + }, [viewportGridService, toolbarService]); + + return { toolbarButtons, onInteraction }; +} diff --git a/platform/core/src/ie.js b/platform/core/src/ie.js new file mode 100644 index 0000000..ed233f5 --- /dev/null +++ b/platform/core/src/ie.js @@ -0,0 +1,9 @@ +import writeScript from './lib/writeScript'; + +// Check if browser is IE and add the polyfill scripts +if (navigator && /MSIE \d|Trident.*rv:/.test(navigator.userAgent)) { + window.onload = () => { + // Fix SVG+USE issues by calling the SVG polyfill + writeScript('svgxuse.min.js'); + }; +} diff --git a/platform/core/src/index.ts b/platform/core/src/index.ts new file mode 100644 index 0000000..b9d8719 --- /dev/null +++ b/platform/core/src/index.ts @@ -0,0 +1,148 @@ +import { ExtensionManager, MODULE_TYPES } from './extensions'; +import { ServiceProvidersManager, ServicesManager } from './services'; +import classes, { CommandsManager, HotkeysManager } from './classes'; +import { SystemContextProvider, useSystem } from './contextProviders/SystemProvider'; + +import DICOMWeb from './DICOMWeb'; +import errorHandler from './errorHandler.js'; +import log from './log.js'; +import object from './object.js'; +import string from './string.js'; +import user from './user.js'; +import utils from './utils'; +import defaults from './defaults'; +import * as Types from './types'; +import * as Enums from './enums'; +import { useToolbar } from './hooks/useToolbar'; +import { + CineService, + UIDialogService, + UIModalService, + UINotificationService, + UIViewportDialogService, + // + DicomMetadataStore, + DisplaySetService, + ToolbarService, + MeasurementService, + ViewportGridService, + HangingProtocolService, + pubSubServiceInterface, + PubSubService, + UserAuthenticationService, + CustomizationService, + PanelService, + WorkflowStepsService, + StudyPrefetcherService, + MultiMonitorService, +} from './services'; + +import { DisplaySetMessage, DisplaySetMessageList } from './services/DisplaySetService'; + +import IWebApiDataSource from './DataSources/IWebApiDataSource'; +import useActiveViewportDisplaySets from './hooks/useActiveViewportDisplaySets'; + +const hotkeys = { + ...utils.hotkeys, + defaults: { hotkeyBindings: defaults.hotkeyBindings }, +}; + +const OHIF = { + MODULE_TYPES, + // + CommandsManager, + ExtensionManager, + HotkeysManager, + ServicesManager, + ServiceProvidersManager, + // + defaults, + utils, + hotkeys, + classes, + string, + user, + errorHandler, + object, + log, + DICOMWeb, + viewer: {}, + // + CineService, + CustomizationService, + UIDialogService, + UIModalService, + UINotificationService, + UIViewportDialogService, + DisplaySetService, + MeasurementService, + ToolbarService, + ViewportGridService, + HangingProtocolService, + UserAuthenticationService, + MultiMonitorService, + IWebApiDataSource, + DicomMetadataStore, + pubSubServiceInterface, + PubSubService, + PanelService, + useToolbar, + useActiveViewportDisplaySets, + WorkflowStepsService, + StudyPrefetcherService, +}; + +export { + MODULE_TYPES, + // + CommandsManager, + ExtensionManager, + HotkeysManager, + ServicesManager, + ServiceProvidersManager, + SystemContextProvider, + // + defaults, + utils, + hotkeys, + classes, + string, + user, + errorHandler, + object, + log, + DICOMWeb, + // + CineService, + CustomizationService, + UIDialogService, + UIModalService, + UINotificationService, + UIViewportDialogService, + DisplaySetService, + DisplaySetMessage, + DisplaySetMessageList, + MeasurementService, + MultiMonitorService, + ToolbarService, + ViewportGridService, + HangingProtocolService, + UserAuthenticationService, + IWebApiDataSource, + DicomMetadataStore, + pubSubServiceInterface, + PubSubService, + Enums, + PanelService, + WorkflowStepsService, + StudyPrefetcherService, + useSystem, + useToolbar, + useActiveViewportDisplaySets, +}; + +export { OHIF }; + +export type { Types }; + +export default OHIF; diff --git a/platform/core/src/log.js b/platform/core/src/log.js new file mode 100644 index 0000000..c44d164 --- /dev/null +++ b/platform/core/src/log.js @@ -0,0 +1,28 @@ +const log = { + error: console.error, + warn: console.warn, + info: console.log, + trace: console.trace, + debug: console.debug, + time: key => { + log.timingKeys[key] = true; + console.time(key); + }, + timeEnd: key => { + if (!log.timingKeys[key]) { + return; + } + log.timingKeys[key] = false; + console.timeEnd(key); + }, + // Store the timing keys to allow knowing whether or not to log events + timingKeys: { + // script time values are added during the index.html initial load, + // before log (this file) is loaded, and the log + // can't depend on the enums, so for this case recreate the string. + // See TimingEnum for details + scriptToView: true, + }, +}; + +export default log; diff --git a/platform/core/src/measurements/tools/polygonRoi.js b/platform/core/src/measurements/tools/polygonRoi.js new file mode 100644 index 0000000..05876e5 --- /dev/null +++ b/platform/core/src/measurements/tools/polygonRoi.js @@ -0,0 +1,24 @@ +const displayFunction = data => { + let meanValue = ''; + const { cachedStats } = data; + if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) { + meanValue = cachedStats.mean.toFixed(2) + ' HU'; + } + return meanValue; +}; + +export const polygonRoi = { + id: 'PolygonRoi', + name: 'Polygon', + toolGroup: 'allTools', + cornerstoneToolType: 'PlanarFreehandROITool', + options: { + measurementTable: { + displayFunction, + }, + caseProgress: { + include: true, + evaluate: true, + }, + }, +}; diff --git a/platform/core/src/object.js b/platform/core/src/object.js new file mode 100644 index 0000000..6201b21 --- /dev/null +++ b/platform/core/src/object.js @@ -0,0 +1,59 @@ +// Transforms a shallow object with keys separated by "." into a nested object +function getNestedObject(shallowObject) { + const nestedObject = {}; + for (let key in shallowObject) { + if (!shallowObject.hasOwnProperty(key)) { + continue; + } + const value = shallowObject[key]; + const propertyArray = key.split('.'); + let currentObject = nestedObject; + while (propertyArray.length) { + const currentProperty = propertyArray.shift(); + if (!propertyArray.length) { + currentObject[currentProperty] = value; + } else { + if (!currentObject[currentProperty]) { + currentObject[currentProperty] = {}; + } + + currentObject = currentObject[currentProperty]; + } + } + } + + return nestedObject; +} + +// Transforms a nested object into a shallowObject merging its keys with "." character +function getShallowObject(nestedObject) { + const shallowObject = {}; + const putValues = (baseKey, nestedObject, resultObject) => { + for (let key in nestedObject) { + if (!nestedObject.hasOwnProperty(key)) { + continue; + } + let currentKey = baseKey ? `${baseKey}.${key}` : key; + const currentValue = nestedObject[key]; + if (typeof currentValue === 'object') { + if (currentValue instanceof Array) { + currentKey += '[]'; + } + + putValues(currentKey, currentValue, resultObject); + } else { + resultObject[currentKey] = currentValue; + } + } + }; + + putValues('', nestedObject, shallowObject); + return shallowObject; +} + +const object = { + getNestedObject, + getShallowObject, +}; + +export default object; diff --git a/platform/core/src/services/CineService/CineService.ts b/platform/core/src/services/CineService/CineService.ts new file mode 100644 index 0000000..e2e6529 --- /dev/null +++ b/platform/core/src/services/CineService/CineService.ts @@ -0,0 +1,123 @@ +import { PubSubService } from '../_shared/pubSubServiceInterface'; + +class CineService extends PubSubService { + public static readonly EVENTS = { + CINE_STATE_CHANGED: 'event::cineStateChanged', + }; + + public static REGISTRATION = { + name: 'cineService', + altName: 'CineService', + create: ({ configuration = {} }) => { + return new CineService(); + }, + }; + + serviceImplementation = {}; + startedClips = new Map(); + closedViewports = new Set(); + + constructor() { + super(CineService.EVENTS); + this.serviceImplementation = {}; + } + + public getState() { + return this.serviceImplementation._getState(); + } + + public setCine({ id, frameRate, isPlaying }) { + return this.serviceImplementation._setCine({ id, frameRate, isPlaying }); + } + + public setIsCineEnabled(isCineEnabled) { + this.serviceImplementation._setIsCineEnabled(isCineEnabled); + // Todo: for some reason i need to do this setTimeout since the + // reducer state does not get updated right away and if we publish the + // event and we use the cineService.getState() it will return the old state + if (isCineEnabled) { + this.closedViewports.forEach(viewportId => { + this.clearViewportCineClosed(viewportId); + }); + } + + queueMicrotask(() => { + this._broadcastEvent(this.EVENTS.CINE_STATE_CHANGED, { isCineEnabled }); + }); + } + + public playClip(element, playClipOptions) { + const res = this.serviceImplementation._playClip(element, playClipOptions); + + this.startedClips.set(element, playClipOptions); + + this._broadcastEvent(this.EVENTS.CINE_STATE_CHANGED, { isPlaying: true }); + + return res; + } + + public stopClip(element, stopClipOptions) { + const res = this.serviceImplementation._stopClip(element, stopClipOptions); + + this._broadcastEvent(this.EVENTS.CINE_STATE_CHANGED, { isPlaying: false }); + + return res; + } + + public onModeExit() { + this.setIsCineEnabled(false); + this.startedClips.forEach((value, key) => { + this.stopClip(key, value); + }); + } + + public getSyncedViewports(viewportId) { + return this.serviceImplementation._getSyncedViewports(viewportId); + } + + public setViewportCineClosed(viewportId) { + this.closedViewports.add(viewportId); + } + + public isViewportCineClosed(viewportId) { + // Todo: we should move towards per viewport cine closed in next release + return this.closedViewports.size > 0; + } + + public clearViewportCineClosed(viewportId) { + this.closedViewports.delete(viewportId); + } + + public setServiceImplementation({ + getState: getStateImplementation, + setCine: setCineImplementation, + setIsCineEnabled: setIsCineEnabledImplementation, + playClip: playClipImplementation, + stopClip: stopClipImplementation, + getSyncedViewports: getSyncedViewportsImplementation, + }) { + if (getSyncedViewportsImplementation) { + this.serviceImplementation._getSyncedViewports = getSyncedViewportsImplementation; + } + + if (getStateImplementation) { + this.serviceImplementation._getState = getStateImplementation; + } + if (setCineImplementation) { + this.serviceImplementation._setCine = setCineImplementation; + } + if (setIsCineEnabledImplementation) { + this.serviceImplementation._setIsCineEnabled = setIsCineEnabledImplementation; + } + + if (playClipImplementation) { + this.serviceImplementation._playClip = playClipImplementation; + } + + if (stopClipImplementation) { + this.serviceImplementation._stopClip = stopClipImplementation; + } + } +} + +export default CineService; diff --git a/platform/core/src/services/CineService/index.ts b/platform/core/src/services/CineService/index.ts new file mode 100644 index 0000000..e9803dd --- /dev/null +++ b/platform/core/src/services/CineService/index.ts @@ -0,0 +1,2 @@ +import CineService from './CineService'; +export default CineService; diff --git a/platform/core/src/services/CustomizationService/CustomizationService.test.js b/platform/core/src/services/CustomizationService/CustomizationService.test.js new file mode 100644 index 0000000..84e049e --- /dev/null +++ b/platform/core/src/services/CustomizationService/CustomizationService.test.js @@ -0,0 +1,497 @@ +// File: CustomizationService.registrationAndOperations.test.js +import CustomizationService, { CustomizationScope } from './CustomizationService'; + +const commandsManager = {}; +const extensionManager = { + registeredExtensionIds: [], + moduleEntries: {}, + getRegisteredExtensionIds: () => extensionManager.registeredExtensionIds, + getModuleEntry: function (id) { + return this.moduleEntries[id]; + }, +}; + +const noop = () => {}; + +// A helper default customization module that mimics the structure returned by the module. +function getDefaultCustomizationModule() { + return { + // Simple types + showAddSegment: true, + somethingFalse: false, + onAddSegment: () => 'default add', + // Array of primitives + NumbersList: [1, 2, 3, 4], + // Object + SeriesInfo: { + label: 'Series Date', + sortFunction: noop, + views: ['sagittal', 'coronal', 'axial'], + advanced: { + subKey: 'original', + anotherKey: 42, + }, + }, + // Array of objects + studyBrowser: [ + { + id: 'seriesDate', + label: 'Series Date', + sortFunction: noop, + }, + ], + advanced: { + firstLabel: 'hello', + functions: [ + { + id: 'seriesDate', + label: 'Series Date', + sortFunction: () => {}, + viewFunctions: [ + { id: 'sagittal', label: 'Sagittal', sortFunction: () => {} }, + { id: 'coronal', label: 'Coronal', sortFunction: () => {} }, + { id: 'axial', label: 'Axial', sortFunction: () => {} }, + ], + }, + ], + }, + }; +} + +describe('CustomizationService - Registration + API Operations', () => { + let customizationService; + + beforeEach(() => { + customizationService = new CustomizationService({ commandsManager, configuration: {} }); + + // Simulate default registrations. + customizationService.addReferences(getDefaultCustomizationModule(), CustomizationScope.Default); + }); + + afterEach(() => { + customizationService.onModeExit(); + }); + + // Check that defaults are registered + it('has registered default customizations', () => { + const defaultShowAddSegment = customizationService.getCustomization('showAddSegment'); + expect(defaultShowAddSegment).toBe(true); + + const defaultNumbersList = customizationService.getCustomization('NumbersList'); + expect(defaultNumbersList).toEqual([1, 2, 3, 4]); + + const defaultSeriesInfo = customizationService.getCustomization('SeriesInfo'); + expect(defaultSeriesInfo.label).toBe('Series Date'); + expect(defaultSeriesInfo.advanced.subKey).toBe('original'); + + const defaultStudyBrowser = customizationService.getCustomization('studyBrowser'); + expect(Array.isArray(defaultStudyBrowser)).toBe(true); + expect(defaultStudyBrowser.length).toBe(1); + + // + const advanced = customizationService.getCustomization('advanced'); + expect(advanced.firstLabel).toBe('hello'); + expect(advanced.functions.length).toBe(1); + expect(advanced.functions[0].id).toBe('seriesDate'); + expect(advanced.functions[0].viewFunctions.length).toBe(3); + expect(advanced.functions[0].viewFunctions[0].id).toBe('sagittal'); + expect(advanced.functions[0].viewFunctions[1].id).toBe('coronal'); + expect(advanced.functions[0].viewFunctions[2].id).toBe('axial'); + }); + + // 1. Simple Data Types + describe('Simple Data Types', () => { + it('replaces boolean value using $set over the default', () => { + // Update the default value with a new one using $set. + customizationService.setCustomizations({ + showAddSegment: { $set: false }, + }); + const result = customizationService.getCustomization('showAddSegment'); + + // Mode/global should override the default. + expect(result).toBe(false); + }); + + it('replaces boolean value using $set over the default false', () => { + // Update the default value with a new one using $set. + customizationService.setCustomizations({ + somethingFalse: { $set: true }, + }); + const result = customizationService.getCustomization('somethingFalse'); + + // Mode/global should override the default. + expect(result).toBe(true); + }); + + it('replaces function value using $set over the default', () => { + // Original default returns "default add" + const original = customizationService.getCustomization('onAddSegment'); + expect(original()).toBe('default add'); + + // Now update the function + customizationService.setCustomizations({ + onAddSegment: { $set: () => 999 }, + }); + const updated = customizationService.getCustomization('onAddSegment'); + expect(updated()).toBe(999); + }); + + it('replaces two properties at once', () => { + // Original default returns "default add" + const original = customizationService.getCustomization('onAddSegment'); + expect(original()).toBe('default add'); + + // Now update the function + customizationService.setCustomizations({ + onAddSegment: { $set: () => 998 }, + showAddSegment: { $set: false }, + }); + expect(customizationService.getCustomization('onAddSegment')).toBeDefined(); + expect(customizationService.getCustomization('showAddSegment')).toBe(false); + expect(customizationService.getCustomization('onAddSegment')()).toBe(998); + }); + }); + + // 2. Arrays of Primitives + describe('Arrays of Primitives', () => { + it('replaces entire array with $set over default', () => { + customizationService.setCustomizations({ + NumbersList: { $set: [5, 6, 7, 8, 9] }, + }); + const result = customizationService.getCustomization('NumbersList'); + expect(result).toEqual([5, 6, 7, 8, 9]); + }); + + it('applies $push, $unshift, and $splice to default array', () => { + // Update array using merge commands + customizationService.setCustomizations({ + NumbersList: { + $push: [5, 6], + }, + }); + const result = customizationService.getCustomization('NumbersList'); + expect(result).toEqual([1, 2, 3, 4, 5, 6]); + }); + + it('applies $push, $unshift, and $splice to default array', () => { + // Update array using merge commands + customizationService.setCustomizations({ + NumbersList: { + $unshift: [0], + }, + }); + const result = customizationService.getCustomization('NumbersList'); + expect(result).toEqual([0, 1, 2, 3, 4]); + }); + + it('applies $push, $unshift, and $splice to default array', () => { + // Update array using merge commands + customizationService.setCustomizations({ + NumbersList: { + $splice: [ + [2, 1, 99], // At index 2, remove + ], + }, + }); + const result = customizationService.getCustomization('NumbersList'); + expect(result).toEqual([1, 2, 99, 4]); + }); + + it('applies $push, $unshift, and $splice to default array', () => { + // Update array using merge commands + customizationService.setCustomizations({ + NumbersList: { + $push: [5, 6], + $unshift: [0], + }, + }); + const result = customizationService.getCustomization('NumbersList'); + + expect(result).toEqual([0, 1, 2, 3, 4, 5, 6]); + }); + }); + + // 3. Objects + describe('Objects', () => { + it('replaces entire object with $set', () => { + customizationService.setCustomizations({ + SeriesInfo: { + $set: { + label: 'Series Number', + sortFunction: (a, b) => a?.SeriesNumber - b?.SeriesNumber, + views: ['3D'], + }, + }, + }); + const result = customizationService.getCustomization('SeriesInfo'); + + expect(result.label).toBe('Series Number'); + expect(result.sortFunction).not.toEqual(noop); + expect(result.views).toEqual(['3D']); + }); + + it('merges object fields with $merge over default', () => { + // Merge basic fields (in mode should override defaults) + customizationService.setCustomizations({ + SeriesInfo: { + $merge: { + label: 'New Label', + extraField: true, + }, + }, + }); + let result = customizationService.getCustomization('SeriesInfo'); + + expect(result.label).toBe('New Label'); + expect(result.extraField).toBe(true); + + // Merge deeper nested fields on the "advanced" property. + customizationService.setCustomizations({ + SeriesInfo: { + advanced: { + $merge: { + subKey: 'updatedSubValue', + newSubKey: 123, + }, + }, + }, + }); + result = customizationService.getCustomization('SeriesInfo'); + expect(result.advanced.subKey).toBe('updatedSubValue'); + expect(result.advanced.newSubKey).toBe(123); + expect(result.advanced.anotherKey).toBe(42); + }); + + it('applies a function to modify a property with $apply', () => { + customizationService.setCustomizations({ + SeriesInfo: { + $apply: oldValue => ({ + ...oldValue, + label: 'Series Number (via $apply)', + }), + }, + }); + const result = customizationService.getCustomization('SeriesInfo'); + + expect(result.label).toBe('Series Number (via $apply)'); + }); + }); + + // 4. Arrays of Objects + describe('Arrays of Objects', () => { + it('replaces entire array of objects using $set', () => { + customizationService.setCustomizations({ + studyBrowser: { + $set: [ + { + id: 'seriesNumber', + label: 'Series Number', + sortFunction: (a, b) => a?.SeriesNumber - b?.SeriesNumber, + }, + { + id: 'seriesDate', + label: 'Series Date', + sortFunction: (a, b) => new Date(b?.SeriesDate) - new Date(a?.SeriesDate), + }, + ], + }, + }); + const result = customizationService.getCustomization('studyBrowser'); + expect(result.length).toBe(2); + expect(result[0].label).toBe('Series Number'); + expect(result[1].label).toBe('Series Date'); + }); + + it('updates array of objects with $push and $splice', () => { + // Append a new item using $push + customizationService.setCustomizations({ + studyBrowser: { + $push: [ + { + id: 'seriesNumber', + label: 'Series Number', + sortFunction: (a, b) => a?.SeriesNumber - b?.SeriesNumber, + }, + ], + }, + }); + + let result = customizationService.getCustomization('studyBrowser'); + expect(result.length).toBe(2); + expect(result[0].label).toBe('Series Date'); + expect(result[1].label).toBe('Series Number'); + + // Insert at index 1 with $splice + customizationService.setCustomizations({ + studyBrowser: { + $splice: [ + [ + 1, + 0, + { + id: 'anotherItem', + label: 'Another Item', + sortFunction: noop, + }, + ], + ], + }, + }); + result = customizationService.getCustomization('studyBrowser'); + expect(result.length).toBe(3); + expect(result[0].label).toBe('Series Date'); + expect(result[1].label).toBe('Another Item'); + }); + }); + + // 5. Advanced Nested Structures + describe('Advanced Nested Structures', () => { + it('updates first level properties in advanced object', () => { + customizationService.setCustomizations({ + advanced: { + firstLabel: { + $set: 'newLabel', + }, + }, + }); + + const result = customizationService.getCustomization('advanced'); + expect(result.firstLabel).toBe('newLabel'); + expect(result.functions).toBeDefined(); + }); + + it('updates nested objects within functions array using $filter and $merge', () => { + customizationService.setCustomizations({ + advanced: { + // filter an object that is inside the advanced object + // and then merge the object + $filter: { + match: { id: 'seriesDate' }, + $merge: { + label: 'Series Data (via $filter)', + }, + }, + }, + }); + + const result = customizationService.getCustomization('advanced'); + + expect(result.functions.length).toBe(1); + expect(result.functions[0].label).toBe('Series Data (via $filter)'); + }); + + it('updates deeply nested view functions using $filter', () => { + customizationService.setCustomizations({ + advanced: { + $filter: { + match: { id: 'axial' }, + $merge: { + label: 'Axial (via $filter)', + }, + }, + }, + }); + + const result = customizationService.getCustomization('advanced'); + + expect(result.functions.length).toBe(1); + expect(result.functions[0].viewFunctions[2].label).toBe('Axial (via $filter)'); + }); + }); + + // 6. Multiple Default Registrations + describe('Multiple Default Registrations', () => { + it('allows subsequent default registrations to enhance previous ones', () => { + customizationService = new CustomizationService({ commandsManager, configuration: {} }); + + // First extension registers its defaults + const firstExtensionDefaults = { + simpleList: [1, 2, 3], + }; + customizationService.addReferences(firstExtensionDefaults, CustomizationScope.Default); + + // Second extension enhances the first one's defaults + const secondExtensionDefaults = { + simpleList: { $push: [4, 5] }, + }; + customizationService.addReferences(secondExtensionDefaults, CustomizationScope.Default); + + // Verify the final state combines both extensions' contributions + const result = customizationService.getCustomization('simpleList'); + expect(result).toEqual([1, 2, 3, 4, 5]); + }); + }); + + describe('CustomizationService - Inheritance (`inheritsFrom`)', () => { + it('inherits properties from the parent customization', () => { + // Register a parent customization + customizationService.setCustomizations( + { + 'test.overlayItem': { + label: 'Default Label', + color: 'blue', + }, + }, + CustomizationScope.Default + ); + + // Register a child customization with `inheritsFrom` + customizationService.setCustomizations({ + 'viewportOverlay.topLeft.StudyDate': { + $set: { + inheritsFrom: 'test.overlayItem', + label: 'Study Date', + title: ' date', + }, + }, + }); + + const customization = customizationService.getCustomization( + 'viewportOverlay.topLeft.StudyDate' + ); + + // Check that the inherited and overridden properties exist + expect(customization.label).toBe('Study Date'); // Overridden + expect(customization.color).toBe('blue'); // Inherited + }); + + it('executes transform methods from the parent customization', () => { + // Register a parent customization + customizationService.setCustomizations( + { + 'test.overlayItem': { + $set: { + $transform: function () { + return { + label: this.label, + additionalKey: 'transformedValue', + }; + }, + }, + }, + }, + CustomizationScope.Default + ); + + // Register a child customization with `inheritsFrom` + customizationService.setCustomizations({ + 'viewportOverlay.bottomRight.InstanceNumber': { + $set: { + inheritsFrom: 'test.overlayItem', + label: 'Instance Number', + title: 'Instance Title', + }, + }, + }); + + const customization = customizationService.getCustomization( + 'viewportOverlay.bottomRight.InstanceNumber' + ); + + // Verify that the transform function from the parent is executed + expect(customization.additionalKey).toBe('transformedValue'); + expect(customization.label).toBe('Instance Number'); + expect(customization.title).toBe(undefined); + }); + }); +}); diff --git a/platform/core/src/services/CustomizationService/CustomizationService.ts b/platform/core/src/services/CustomizationService/CustomizationService.ts new file mode 100644 index 0000000..e8ffa60 --- /dev/null +++ b/platform/core/src/services/CustomizationService/CustomizationService.ts @@ -0,0 +1,539 @@ +import update, { extend } from 'immutability-helper'; +import { PubSubService } from '../_shared/pubSubServiceInterface'; +import type { Customization } from './types'; +import type { CommandsManager } from '../../classes'; +import type { ExtensionManager } from '../../extensions'; + +const EVENTS = { + MODE_CUSTOMIZATION_MODIFIED: 'event::CustomizationService:modeModified', + GLOBAL_CUSTOMIZATION_MODIFIED: 'event::CustomizationService:globalModified', + DEFAULT_CUSTOMIZATION_MODIFIED: 'event::CustomizationService:defaultModified', +}; + +/** + * Enum representing the different scopes of customizations available in the system. + */ +export enum CustomizationScope { + /** + * Global customizations that override both mode and default customizations. + * These are applied universally across the application. + */ + Global = 'global', + + /** + * Mode-specific customizations that are only active during a particular mode. + * These are cleared and reset when switching between modes. + */ + Mode = 'mode', + + /** + * Default customizations that serve as fallbacks when no global or mode-specific + * customizations are defined. These can only be defined once. + */ + Default = 'default', +} + +/** + * The CustomizationService allows for retrieving of custom components + * and configuration for mode and global values. + * The intent of the items is to provide a react component. This can be + * done by straight out providing an entire react component or else can be + * done by configuring a react component, or configuring a part of a react + * component. These are intended to be fairly indistinguishable in use of + * it, although the internals of how that is implemented may need to know + * about the customization service. + * + * A customization value can be: + * 1. React function, taking (React, props) and returning a rendered component + * For example, createLogoComponentFn renders a component logo for display + * 2. Custom UI component configuration, as defined by the component which uses it. + * For example, context menus define a complex structure allowing site-determined + * context menus to be set. + * 3. A string name, being the extension id for retrieving one of the above. + * + * The default values for the extension come from the app_config value 'whiteLabeling', + * The whiteLabelling can have lists of extensions to load for the default global and + * mode extensions. These are: + * 'globalExtensions' which is a list of extension id's to load for global values + * 'modeExtensions' which is a list of extension id's to load for mode values + * They default to the list ['*'] if not otherwise provided, which means to check + * every module for the given id and to load it/add it to the extensions. + */ +export default class CustomizationService extends PubSubService { + public static EVENTS = EVENTS; + public Scope = CustomizationScope; + + public static REGISTRATION = { + name: 'customizationService', + create: ({ configuration, commandsManager }) => { + return new CustomizationService({ configuration, commandsManager }); + }, + }; + + commandsManager: CommandsManager; + extensionManager: ExtensionManager; + + /** + * A collection of global customizations that act as a priority layer. + * These customizations are applied universally, overriding both mode-specific + * and default customizations. Ideal for system-wide changes. + */ + private globalCustomizations = new Map(); + + /** + * A collection of mode-specific customizations. These allow modes to define + * their own behavior without impacting other modes. These customizations + * are cleared and redefined whenever a mode changes, ensuring isolation + * between modes. Read more about modes in the modes documentation. + */ + private modeCustomizations = new Map(); + + /** + * A collection of default customizations used as fallbacks. These serve as + * the base configuration and are registered at setup. Default customizations + * provide baseline values that can be overridden by mode or global customizations. + * Use these for cases where default values are necessary for predictable behavior. + */ + private defaultCustomizations = new Map(); + + /** + * Has the transformed/final customization value. This avoids needing to + * transform every time a customization is requested. + */ + private transformedCustomizations = new Map(); + private configuration: AppTypes.Config; + + constructor({ configuration, commandsManager }) { + super(EVENTS); + this.configuration = configuration; + this.commandsManager = commandsManager; + } + + public init(extensionManager: ExtensionManager): void { + this.extensionManager = extensionManager; + // Clear defaults as those are defined by the customization modules + this.defaultCustomizations.clear(); + // Clear modes because those are defined in onModeEnter functions. + this.modeCustomizations.clear(); + + this.extensionManager.getRegisteredExtensionIds().forEach(extensionId => { + const keyDefault = `${extensionId}.customizationModule.default`; + const defaultCustomizations = this._findExtensionValue(keyDefault); + if (defaultCustomizations) { + const { value } = defaultCustomizations; + this._addReference(value, CustomizationScope.Default); + } + const keyGlobal = `${extensionId}.customizationModule.global`; + const globalCustomizations = this._findExtensionValue(keyGlobal); + if (globalCustomizations) { + const { value } = globalCustomizations; + this._addReference(value, CustomizationScope.Global); + } + }); + + this.addReferences(this.configuration); + } + + public onModeEnter(): void { + this.clearTransformedCustomizations(); + + this.init(this.extensionManager); + } + + public onModeExit(): void { + this.clearTransformedCustomizations(); + } + + private clearTransformedCustomizations(): void { + super.reset(); + + const modeCustomizationKeys = Array.from(this.modeCustomizations.keys()); + for (const key of modeCustomizationKeys) { + this.transformedCustomizations.delete(key); + } + + this.modeCustomizations.clear(); + } + + /** + * Unified getter for customizations. + * + * @param customizationId - The ID of the customization to retrieve. + * @param scope - (Optional) The scope to retrieve from: 'global', 'mode', or 'default'. + * If not specified, it retrieves based on priority: global > mode > default. + * @returns The requested customization. + */ + public getCustomization(customizationId: string): Customization { + const transformed = this.transformedCustomizations.get(customizationId); + if (transformed) { + return transformed; + } + const customization = + this.globalCustomizations.get(customizationId) ?? + this.modeCustomizations.get(customizationId) ?? + this.defaultCustomizations.get(customizationId); + const newTransformed = this.transform(customization); + if (newTransformed !== undefined) { + this.transformedCustomizations.set(customizationId, newTransformed); + } + return newTransformed; + } + + /** + * Takes an object with multiple properties, each property containing + * immutability-helper commands, and applies them one by one. + * + * Example: + * customizationService.setCustomizations({ + * showAddSegment: { $set: false }, + * NumbersList: { $push: [99] }, + * }, CustomizationScope.Mode) + * + * Or you can simply apply a list of strings that are customization module items in the + * extension. + * + * Example: + * customizationService.setCustomizations(['@ohif/extension-cornerstone-dicom-seg.customizationModule.dicom-seg-sorts'], CustomizationScope.Mode) + */ + public setCustomizations( + customizations: string[] | Record, + scope: CustomizationScope = CustomizationScope.Mode + ): void { + if (Array.isArray(customizations)) { + customizations.forEach(customization => { + this._addReference(customization, scope); + }); + } else { + Object.entries(customizations).forEach(([key, value]) => { + this._setCustomization(key, value, scope); + }); + } + } + + /** + * @deprecated Use setCustomizations instead + */ + public setCustomization( + customizationId: string, + customization: Customization | string, + scope: CustomizationScope = CustomizationScope.Mode + ): void { + console.warn( + 'setCustomization is deprecated. Please use setCustomizations with an object instead.' + ); + this._setCustomization(customizationId, customization, scope); + } + + /** + * Internal method to set a single customization + */ + private _setCustomization( + customizationId: string, + customization: Customization, + scope: CustomizationScope = CustomizationScope.Mode + ): void { + // if (typeof customization === 'string') { + // const extensionValue = this._findExtensionValue(customization); + // customization = extensionValue.value; + // } + + switch (scope) { + case CustomizationScope.Global: + this.setGlobalCustomization(customizationId, customization); + break; + case CustomizationScope.Mode: + this.setModeCustomization(customizationId, customization); + break; + case CustomizationScope.Default: + this.setDefaultCustomization(customizationId, customization); + break; + default: + throw new Error(`Invalid customization scope: ${scope}`); + } + } + + /** + * Gets all customizations for a given scope. + * + * @param scope - The scope to retrieve customizations from: 'global', 'mode', or 'default' + * @returns A Map containing all customizations for the specified scope + */ + public getCustomizations(scope: CustomizationScope): Map { + if (scope === CustomizationScope.Global) { + return this.globalCustomizations; + } + if (scope === CustomizationScope.Mode) { + return this.modeCustomizations; + } + return this.defaultCustomizations; + } + + /** + * Returns true if there is a mode customization. Doesn't include defaults, but + * does return global overrides. + */ + public hasCustomization(customizationId: string) { + return ( + this.globalCustomizations.has(customizationId) || this.modeCustomizations.has(customizationId) + ); + } + + /** + * Applies any inheritance due to UI Type customization. + * This will look for inheritsFrom in the customization object + * and if that is found, will assign all iterable values from that + * type into the new type, allowing default behavior to be configured. + */ + public transform(customization: Customization): Customization { + if (!customization) { + return customization; + } + const { inheritsFrom } = customization; + if (!inheritsFrom) { + return customization; + } + const parent = this.getCustomization(inheritsFrom); + const result = parent ? Object.assign({}, parent, customization) : customization; + // Execute an nested type information + return result.$transform?.(this) || result; + } + + /** + * + * Sets a mode-specific customization. + * + * This method allows you to define or update a customization that applies only to the current mode. + * Mode customizations are temporary and isolated, reset whenever a mode changes. + * + * @param customizationId - The unique identifier for the customization. + * @param customization - The customization object containing the desired settings. + */ + private setModeCustomization(customizationId: string, customization: Customization): void { + const defaultCustomization = this.defaultCustomizations.get(customizationId); + const modeCustomization = this.modeCustomizations.get(customizationId); + const globCustomization = this.globalCustomizations.get(customizationId); + + const sourceCustomization = + modeCustomization || this._cloneIfNeeded(globCustomization) || defaultCustomization; + + const result = this._update(sourceCustomization, customization); + this.modeCustomizations.set(customizationId, result); + + this.transformedCustomizations.clear(); + this._broadcastEvent(this.EVENTS.CUSTOMIZATION_MODIFIED, { + buttons: this.modeCustomizations, + button: this.modeCustomizations.get(customizationId), + }); + } + + private setGlobalCustomization(id: string, value: Customization): void { + const defaultCustomization = this.defaultCustomizations.get(id); + const globCustomization = this.globalCustomizations.get(id); + + const sourceCustomization = this._cloneIfNeeded(globCustomization) || defaultCustomization; + this.globalCustomizations.set(id, this._update(sourceCustomization, value)); + + this.transformedCustomizations.clear(); + this._broadcastEvent(this.EVENTS.DEFAULT_CUSTOMIZATION_MODIFIED, { + buttons: this.defaultCustomizations, + button: this.defaultCustomizations.get(id), + }); + } + + private setDefaultCustomization(id: string, value: Customization): void { + if (this.defaultCustomizations.has(id)) { + console.warn(`Trying to update existing default for customization ${id}`); + } + this.transformedCustomizations.clear(); + + const sourceCustomization = this.defaultCustomizations.get(id); + this.defaultCustomizations.set(id, this._update(sourceCustomization, value)); + + this._broadcastEvent(this.EVENTS.DEFAULT_CUSTOMIZATION_MODIFIED, { + buttons: this.defaultCustomizations, + button: this.defaultCustomizations.get(id), + }); + } + + private _findExtensionValue(value: string) { + const entry = this.extensionManager.getModuleEntry(value); + return entry as { value: Customization }; + } + + /** + * Registers a custom command to be used in customization updates. + * @param commandName - The name of the command (without the $ prefix) + * it will be prefixed with $ + * @param handler - Function that handles the command it receives the value and the original value + */ + public registerCustomUpdateCommand( + commandName: string, + handler: (value: Customization, original: Customization) => Customization + ): void { + if (!commandName.startsWith('$')) { + commandName = '$' + commandName; + } + extend(commandName, handler); + } + + /** + * Uses immutability-helper to apply the user's commands (e.g. $set, $push, $apply, etc.) + * Takes into account the 'mergeType' if it's explicitly 'Replace'; otherwise does a normal update. + */ + private _update(oldValue: Customization | undefined, newValue: Customization): Customization { + if (!oldValue) { + oldValue = undefined; + } + + // Use immutability-helper to apply the commands + // if $ is not part of the value in the json string, then we just return the newValue + if (!hasDollarKey(newValue)) { + return newValue; + } + + const result = update(oldValue, newValue); + return result; + } + + private _cloneIfNeeded(value: any) { + // If it's null/undefined or not an object, return as is + if (!value || typeof value !== 'object') { + return value; + } + + // If it's an array, create a shallow copy + if (Array.isArray(value)) { + return [...value]; + } + + // Otherwise create a shallow copy of the object + return { ...value }; + } + + _addReference(value?: any, type = CustomizationScope.Global): void { + if (!value) { + return; + } + + if (typeof value === 'string') { + const extensionValue = this._findExtensionValue(value); + value = extensionValue.value; + } + + Object.entries(value).forEach(([id, customization]) => { + const setName = + (type === CustomizationScope.Global && 'setGlobalCustomization') || + (type === CustomizationScope.Default && 'setDefaultCustomization') || + 'setModeCustomization'; + this[setName](id as string, customization as Customization); + }); + } + + /** + * Customizations can be specified as an array of strings or customizations, + * or as an object whose key is the reference id, and the value is the string + * or customization. + */ + addReferences(references?: any, type = CustomizationScope.Global): void { + if (!references) { + return; + } + if (Array.isArray(references)) { + references.forEach(item => { + this._addReference(item, type); + }); + } else { + this._addReference(references, type); + } + } +} + +/** Add custom $filter command */ +extend('$filter', (query, original) => { + // This helper checks if an object matches all key/value pairs in `match` + function objectMatches(item, matchObj) { + return ( + item && typeof item === 'object' && Object.entries(matchObj).every(([k, v]) => item[k] === v) + ); + } + + // Recursively walk objects/arrays. Whenever we hit an array, we either filter + // or update items that match, depending on what was passed in via `query`. + function deepFilter(value, filterQuery) { + // If it's an array, apply the filtering/updating logic to each item + if (Array.isArray(value)) { + let result = value; + + // 1) If it's a function, filter array items + if (typeof filterQuery === 'function') { + return value.filter(filterQuery); + } + + // 2) If it's a string, remove items whose .id matches that string + if (typeof filterQuery === 'string') { + return value.filter(item => item.id !== filterQuery); + } + + // 3) If it's an object with .match and .merge, apply the merge to matched items + if (typeof filterQuery === 'object' && filterQuery.match && filterQuery.$merge) { + // First recurse into sub-objects/arrays so we handle deeply nested arrays + result = value.map(item => deepFilter(item, filterQuery)); + // Then update items that match + return result.map(item => { + if (objectMatches(item, filterQuery.match)) { + return { ...item, ...filterQuery.$merge }; + } + return item; + }); + } + + // 4) If it's an object with .id and .$merge, for backwards-compat + if (typeof filterQuery === 'object' && filterQuery.id && filterQuery.$merge) { + result = value.map(item => deepFilter(item, filterQuery)); + return result.map(item => { + if (item.id === filterQuery.id) { + return { ...item, ...filterQuery.$merge }; + } + return item; + }); + } + + // Otherwise, just recurse into sub-objects without filtering + return value.map(item => deepFilter(item, filterQuery)); + } + + // If it's a plain object, recurse into its properties + if (value && typeof value === 'object') { + const newObj = { ...value }; + for (const [key, val] of Object.entries(newObj)) { + newObj[key] = deepFilter(val, filterQuery); + } + return newObj; + } + + // If it's neither array nor object, just return it + return value; + } + + return deepFilter(original, query); +}); + +function hasDollarKey(value) { + if (Array.isArray(value)) { + for (const item of value) { + if (hasDollarKey(item)) { + return true; + } + } + } else if (value && typeof value === 'object') { + for (const key of Object.keys(value)) { + if (key.startsWith('$') && key !== '$transform') { + return true; + } + if (hasDollarKey(value[key])) { + return true; + } + } + } + return false; +} diff --git a/platform/core/src/services/CustomizationService/index.ts b/platform/core/src/services/CustomizationService/index.ts new file mode 100644 index 0000000..b80db00 --- /dev/null +++ b/platform/core/src/services/CustomizationService/index.ts @@ -0,0 +1,3 @@ +import CustomizationService from './CustomizationService'; + +export default CustomizationService; diff --git a/platform/core/src/services/CustomizationService/types.ts b/platform/core/src/services/CustomizationService/types.ts new file mode 100644 index 0000000..94be1ce --- /dev/null +++ b/platform/core/src/services/CustomizationService/types.ts @@ -0,0 +1,45 @@ +import { Command } from '../../types/Command'; +import { ComponentType } from 'react'; + +export type Obj = Record; + +export interface BaseCustomization extends Obj { + id?: string; + inheritsFrom?: string; + description?: string; + label?: string; + commands?: Command[]; + content?: (...props: any) => React.JSX.Element; +} + +export interface LabelCustomization extends BaseCustomization { + label: string; +} + +export interface CodeCustomization extends BaseCustomization { + code: string; +} + +export interface CommandCustomization extends BaseCustomization { + commands: Command[]; +} + +export interface ComponentCustomization extends BaseCustomization { + content: (...props: any) => React.JSX.Element; +} + +export type Customization = + | BaseCustomization + | LabelCustomization + | CommandCustomization + | CodeCustomization + | ComponentCustomization; + +export default Customization; + +export type ComponentReturn = { + component: ComponentType; + props?: Obj; +}; + +export type NestedStrings = string[] | NestedStrings[]; diff --git a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.ts b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.ts new file mode 100644 index 0000000..10442cc --- /dev/null +++ b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.ts @@ -0,0 +1,275 @@ +import dcmjs from 'dcmjs'; + +import pubSubServiceInterface from '../_shared/pubSubServiceInterface'; +import createStudyMetadata from './createStudyMetadata'; + +const EVENTS = { + STUDY_ADDED: 'event::dicomMetadataStore:studyAdded', + INSTANCES_ADDED: 'event::dicomMetadataStore:instancesAdded', + SERIES_ADDED: 'event::dicomMetadataStore:seriesAdded', + SERIES_UPDATED: 'event::dicomMetadataStore:seriesUpdated', +}; + +/** + * @example + * studies: [ + * { + * StudyInstanceUID: string, + * isLoaded: boolean, + * series: [ + * { + * Modality: string, + * SeriesInstanceUID: string, + * SeriesNumber: number, + * SeriesDescription: string, + * instances: [ + * { + * // naturalized instance metadata + * SOPInstanceUID: string, + * SOPClassUID: string, + * Rows: number, + * Columns: number, + * PatientSex: string, + * Modality: string, + * InstanceNumber: string, + * }, + * { + * // instance 2 + * }, + * ], + * }, + * { + * // series 2 + * }, + * ], + * }, + * ], + */ +const _model = { + studies: [], +}; + +function _getStudyInstanceUIDs() { + return _model.studies.map(aStudy => aStudy.StudyInstanceUID); +} + +function _getStudy(StudyInstanceUID) { + return _model.studies.find(aStudy => aStudy.StudyInstanceUID === StudyInstanceUID); +} + +function _getSeries(StudyInstanceUID, SeriesInstanceUID) { + if(!StudyInstanceUID) { + const series = _model.studies.map(study => study.series).flat(); + return series.find(aSeries => aSeries.SeriesInstanceUID === SeriesInstanceUID); + } + + const study = _getStudy(StudyInstanceUID); + + if (!study) { + return; + } + + return study.series.find(aSeries => aSeries.SeriesInstanceUID === SeriesInstanceUID); +} + +function _getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) { + const series = _getSeries(StudyInstanceUID, SeriesInstanceUID); + + if (!series) { + return; + } + + return series.getInstance(SOPInstanceUID); +} + +function _getInstanceByImageId(imageId) { + for (const study of _model.studies) { + for (const series of study.series) { + for (const instance of series.instances) { + if (instance.imageId === imageId) { + return instance; + } + } + } + } +} + +/** + * Update the metadata of a specific series + * @param {*} StudyInstanceUID + * @param {*} SeriesInstanceUID + * @param {*} metadata metadata inform of key value pairs + * @returns + */ +function _updateMetadataForSeries(StudyInstanceUID, SeriesInstanceUID, metadata) { + const study = _getStudy(StudyInstanceUID); + + if (!study) { + return; + } + + const series = study.series.find(aSeries => aSeries.SeriesInstanceUID === SeriesInstanceUID); + + const { instances } = series; + // update all instances metadata for this series with the new metadata + instances.forEach(instance => { + Object.keys(metadata).forEach(key => { + // if metadata[key] is an object, we need to merge it with the existing + // metadata of the instance + if (typeof metadata[key] === 'object') { + instance[key] = { ...instance[key], ...metadata[key] }; + } + // otherwise, we just replace the existing metadata with the new one + else { + instance[key] = metadata[key]; + } + }); + }); + + // broadcast the series updated event + this._broadcastEvent(EVENTS.SERIES_UPDATED, { + SeriesInstanceUID, + StudyInstanceUID, + madeInClient: true, + }); +} + +const BaseImplementation = { + EVENTS, + listeners: {}, + addInstance(dicomJSONDatasetOrP10ArrayBuffer) { + let dicomJSONDataset; + + // If Arraybuffer, parse to DICOMJSON before naturalizing. + if (dicomJSONDatasetOrP10ArrayBuffer instanceof ArrayBuffer) { + const dicomData = dcmjs.data.DicomMessage.readFile(dicomJSONDatasetOrP10ArrayBuffer); + + dicomJSONDataset = dicomData.dict; + } else { + dicomJSONDataset = dicomJSONDatasetOrP10ArrayBuffer; + } + + let naturalizedDataset; + + if (dicomJSONDataset['SeriesInstanceUID'] === undefined) { + naturalizedDataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomJSONDataset); + } else { + naturalizedDataset = dicomJSONDataset; + } + + const { StudyInstanceUID } = naturalizedDataset; + + let study = _model.studies.find(study => study.StudyInstanceUID === StudyInstanceUID); + + if (!study) { + _model.studies.push(createStudyMetadata(StudyInstanceUID)); + study = _model.studies[_model.studies.length - 1]; + } + + study.addInstanceToSeries(naturalizedDataset); + }, + addInstances(instances, madeInClient = false) { + const { StudyInstanceUID, SeriesInstanceUID } = instances[0]; + + let study = _model.studies.find(study => study.StudyInstanceUID === StudyInstanceUID); + + if (!study) { + _model.studies.push(createStudyMetadata(StudyInstanceUID)); + + study = _model.studies[_model.studies.length - 1]; + } + + study.addInstancesToSeries(instances); + + // Broadcast an event even if we used cached data. + // This is because the mode needs to listen to instances that are added to build up its active displaySets. + // It will see there are cached displaySets and end early if this Series has already been fired in this + // Mode session for some reason. + this._broadcastEvent(EVENTS.INSTANCES_ADDED, { + StudyInstanceUID, + SeriesInstanceUID, + madeInClient, + }); + }, + updateSeriesMetadata(seriesMetadata) { + const { StudyInstanceUID, SeriesInstanceUID } = seriesMetadata; + const series = _getSeries(StudyInstanceUID, SeriesInstanceUID); + if (!series) { + return; + } + + const study = _getStudy(StudyInstanceUID); + if (study) { + study.setSeriesMetadata(SeriesInstanceUID, seriesMetadata); + } + }, + addSeriesMetadata(seriesSummaryMetadata, madeInClient = false) { + if (!seriesSummaryMetadata || !seriesSummaryMetadata.length || !seriesSummaryMetadata[0]) { + return; + } + + const { StudyInstanceUID } = seriesSummaryMetadata[0]; + let study = _getStudy(StudyInstanceUID); + if (!study) { + study = createStudyMetadata(StudyInstanceUID); + // Will typically be undefined with a compliant DICOMweb server, reset later + study.StudyDescription = seriesSummaryMetadata[0].StudyDescription; + seriesSummaryMetadata.forEach(item => { + if (study.ModalitiesInStudy.indexOf(item.Modality) === -1) { + study.ModalitiesInStudy.push(item.Modality); + } + }); + study.NumberOfStudyRelatedSeries = seriesSummaryMetadata.length; + _model.studies.push(study); + } + + seriesSummaryMetadata.forEach(series => { + const { SeriesInstanceUID } = series; + + study.setSeriesMetadata(SeriesInstanceUID, series); + }); + + this._broadcastEvent(EVENTS.SERIES_ADDED, { + StudyInstanceUID, + seriesSummaryMetadata, + madeInClient, + }); + }, + addStudy(study) { + const { StudyInstanceUID } = study; + + const existingStudy = _model.studies.find(study => study.StudyInstanceUID === StudyInstanceUID); + + if (!existingStudy) { + const newStudy = createStudyMetadata(StudyInstanceUID); + + newStudy.PatientID = study.PatientID; + newStudy.PatientName = study.PatientName; + newStudy.StudyDate = study.StudyDate; + newStudy.ModalitiesInStudy = study.ModalitiesInStudy; + newStudy.StudyDescription = study.StudyDescription; + newStudy.AccessionNumber = study.AccessionNumber; + newStudy.NumInstances = study.NumInstances; // todo: Correct naming? + + _model.studies.push(newStudy); + } + }, + getStudyInstanceUIDs: _getStudyInstanceUIDs, + getStudy: _getStudy, + getSeries: _getSeries, + getInstance: _getInstance, + getInstanceByImageId: _getInstanceByImageId, + updateMetadataForSeries: _updateMetadataForSeries, +}; +const DicomMetadataStore = Object.assign( + // get study + + // iterate over all series + + {}, + BaseImplementation, + pubSubServiceInterface +); + +export { DicomMetadataStore }; +export default DicomMetadataStore; diff --git a/platform/core/src/services/DicomMetadataStore/createSeriesMetadata.js b/platform/core/src/services/DicomMetadataStore/createSeriesMetadata.js new file mode 100644 index 0000000..1dc6330 --- /dev/null +++ b/platform/core/src/services/DicomMetadataStore/createSeriesMetadata.js @@ -0,0 +1,27 @@ +function createSeriesMetadata(SeriesInstanceUID) { + const instances = []; + const instancesMap = new Map(); + + return { + SeriesInstanceUID, + instances, + addInstance: function (newInstance) { + this.addInstances([newInstance]); + }, + addInstances: function (newInstances) { + for (let i = 0, len = newInstances.length; i < len; i++) { + const instance = newInstances[i]; + + if (!instancesMap.has(instance.SOPInstanceUID)) { + instancesMap.set(instance.SOPInstanceUID, instance); + instances.push(instance); + } + } + }, + getInstance: function (SOPInstanceUID) { + return instancesMap.get(SOPInstanceUID); + }, + }; +} + +export default createSeriesMetadata; diff --git a/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js b/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js new file mode 100644 index 0000000..eaaab19 --- /dev/null +++ b/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js @@ -0,0 +1,49 @@ +import createSeriesMetadata from './createSeriesMetadata'; + +function createStudyMetadata(StudyInstanceUID) { + return { + StudyInstanceUID, + StudyDescription: '', + ModalitiesInStudy: [], + isLoaded: false, + series: [], + /** + * @param {object} instance + */ + addInstanceToSeries: function (instance) { + this.addInstancesToSeries([instance]); + }, + /** + * @param {object[]} instances + * @param {string} instances[].SeriesInstanceUID + * @param {string} instances[].StudyDescription + */ + addInstancesToSeries: function (instances) { + const { SeriesInstanceUID } = instances[0]; + if (!this.StudyDescription) { + this.StudyDescription = instances[0].StudyDescription; + } + let series = this.series.find(s => s.SeriesInstanceUID === SeriesInstanceUID); + + if (!series) { + series = createSeriesMetadata(SeriesInstanceUID); + this.series.push(series); + } + + series.addInstances(instances); + }, + + setSeriesMetadata: function (SeriesInstanceUID, seriesMetadata) { + let existingSeries = this.series.find(s => s.SeriesInstanceUID === SeriesInstanceUID); + + if (existingSeries) { + existingSeries = Object.assign(existingSeries, seriesMetadata); + } else { + const series = createSeriesMetadata(SeriesInstanceUID); + this.series.push(Object.assign(series, seriesMetadata)); + } + }, + }; +} + +export default createStudyMetadata; diff --git a/platform/core/src/services/DicomMetadataStore/index.ts b/platform/core/src/services/DicomMetadataStore/index.ts new file mode 100644 index 0000000..e41b2e5 --- /dev/null +++ b/platform/core/src/services/DicomMetadataStore/index.ts @@ -0,0 +1,4 @@ +import DicomMetadataStore from './DicomMetadataStore'; + +export { DicomMetadataStore }; +export default DicomMetadataStore; diff --git a/platform/core/src/services/DisplaySetService/DisplaySetMessage.ts b/platform/core/src/services/DisplaySetService/DisplaySetMessage.ts new file mode 100644 index 0000000..6a139da --- /dev/null +++ b/platform/core/src/services/DisplaySetService/DisplaySetMessage.ts @@ -0,0 +1,50 @@ +/** + * Defines a displaySet message, that could be any pf the potential problems of a displaySet + */ +class DisplaySetMessage { + id: number; + static CODES = { + NO_VALID_INSTANCES: 1, + NO_POSITION_INFORMATION: 2, + NOT_RECONSTRUCTABLE: 3, + MULTIFRAME_NO_PIXEL_MEASUREMENTS: 4, + MULTIFRAME_NO_ORIENTATION: 5, + MULTIFRAME_NO_POSITION_INFORMATION: 6, + MISSING_FRAMES: 7, + IRREGULAR_SPACING: 8, + INCONSISTENT_DIMENSIONS: 9, + INCONSISTENT_COMPONENTS: 10, + INCONSISTENT_ORIENTATIONS: 11, + INCONSISTENT_POSITION_INFORMATION: 12, + UNSUPPORTED_DISPLAYSET: 13, + }; + + constructor(id: number) { + this.id = id; + } +} +/** + * Defines a list of displaySet messages + */ +class DisplaySetMessageList { + messages = []; + + public addMessage(messageId: number): void { + const message = new DisplaySetMessage(messageId); + this.messages.push(message); + } + + public size(): number { + return this.messages.length; + } + + public includesMessage(messageId: number): boolean { + return this.messages.some(message => message.id === messageId); + } + + public includesAllMessages(messageIdList: number[]): boolean { + return messageIdList.every(messageId => this.include(messageId)); + } +} + +export { DisplaySetMessage, DisplaySetMessageList }; diff --git a/platform/core/src/services/DisplaySetService/DisplaySetService.ts b/platform/core/src/services/DisplaySetService/DisplaySetService.ts new file mode 100644 index 0000000..3bcb79f --- /dev/null +++ b/platform/core/src/services/DisplaySetService/DisplaySetService.ts @@ -0,0 +1,447 @@ +import { ExtensionManager } from '../../extensions'; +import { DisplaySet, InstanceMetadata } from '../../types'; +import { PubSubService } from '../_shared/pubSubServiceInterface'; +import EVENTS from './EVENTS'; + +const displaySetCache = new Map(); + +/** + * Filters the instances set by instances not in + * display sets. Done in O(n) time. + */ +const filterInstances = ( + instances: InstanceMetadata[], + displaySets: DisplaySet[] +): InstanceMetadata[] => { + const dsInstancesSOP = new Set(); + displaySets.forEach(ds => { + const dsInstances = ds.instances; + if (!dsInstances) { + console.warn('No instances in', ds); + } else { + dsInstances.forEach(instance => dsInstancesSOP.add(instance.SOPInstanceUID)); + } + }); + + return instances.filter(instance => !dsInstancesSOP.has(instance.SOPInstanceUID)); +}; + +export default class DisplaySetService extends PubSubService { + public static REGISTRATION = { + altName: 'DisplaySetService', + name: 'displaySetService', + create: ({ configuration = {} }) => { + return new DisplaySetService(); + }, + }; + + public activeDisplaySets = []; + public unsupportedSOPClassHandler; + extensionManager: ExtensionManager; + + protected activeDisplaySetsMap = new Map(); + + // Record if the active display sets changed - used to group change events so + // that fewer events need to be fired when creating multiple display sets + protected activeDisplaySetsChanged = false; + + constructor() { + super(EVENTS); + this.unsupportedSOPClassHandler = + '@ohif/extension-default.sopClassHandlerModule.not-supported-display-sets-handler'; + } + + public init(extensionManager, SOPClassHandlerIds): void { + this.extensionManager = extensionManager; + this.SOPClassHandlerIds = SOPClassHandlerIds; + this.activeDisplaySets = []; + this.activeDisplaySetsMap.clear(); + } + + _addDisplaySetsToCache(displaySets: DisplaySet[]) { + displaySets.forEach(displaySet => { + displaySetCache.set(displaySet.displaySetInstanceUID, displaySet); + }); + } + + _addActiveDisplaySets(displaySets: DisplaySet[]) { + const { activeDisplaySets, activeDisplaySetsMap } = this; + + displaySets.forEach(displaySet => { + if (!activeDisplaySetsMap.has(displaySet.displaySetInstanceUID)) { + this.activeDisplaySetsChanged = true; + activeDisplaySets.push(displaySet); + activeDisplaySetsMap.set(displaySet.displaySetInstanceUID, displaySet); + } + }); + } + + /** + * Sets the handler for unsupported sop classes + * @param sopClassHandlerUID + */ + public setUnsuportedSOPClassHandler(sopClassHandler) { + this.unsupportedSOPClassHandler = sopClassHandler; + } + + /** + * Adds new display sets directly, as specified. + * Use this function when the display sets are created externally directly + * rather than using the default sop class handlers to create display sets. + */ + public addDisplaySets(...displaySets: DisplaySet[]): string[] { + this._addDisplaySetsToCache(displaySets); + this._addActiveDisplaySets(displaySets); + + // The activeDisplaySetsChanged flag is only seen if we add display sets + // so, don't broadcast the change if all the display sets were pre-existing. + this.activeDisplaySetsChanged = false; + this._broadcastEvent(EVENTS.DISPLAY_SETS_ADDED, { + displaySetsAdded: displaySets, + options: { madeInClient: displaySets[0].madeInClient }, + }); + return displaySets; + } + + public getDisplaySetCache(): Map { + return displaySetCache; + } + + public getMostRecentDisplaySet(): DisplaySet { + return this.activeDisplaySets[this.activeDisplaySets.length - 1]; + } + + public getActiveDisplaySets(): DisplaySet[] { + return this.activeDisplaySets; + } + + public getDisplaySetsForSeries = (seriesInstanceUID: string): DisplaySet[] => { + return [...displaySetCache.values()].filter( + displaySet => displaySet.SeriesInstanceUID === seriesInstanceUID + ); + }; + + public getDisplaySetForSOPInstanceUID( + sopInstanceUID: string, + seriesInstanceUID: string, + frameNumber?: number + ): DisplaySet { + const displaySets = seriesInstanceUID + ? this.getDisplaySetsForSeries(seriesInstanceUID) + : [...this.getDisplaySetCache().values()]; + + const displaySet = displaySets.find(ds => { + return ds.instances?.some(i => i.SOPInstanceUID === sopInstanceUID); + }); + + return displaySet; + } + + public setDisplaySetMetadataInvalidated( + displaySetInstanceUID: string, + invalidateData = true + ): void { + const displaySet = this.getDisplaySetByUID(displaySetInstanceUID); + + if (!displaySet) { + return; + } + + // broadcast event to update listeners with the new displaySets + this._broadcastEvent(EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, { + displaySetInstanceUID, + invalidateData, + }); + } + + public deleteDisplaySet(displaySetInstanceUID) { + if (!displaySetInstanceUID) { + return; + } + const { activeDisplaySets, activeDisplaySetsMap } = this; + + const activeDisplaySetsIndex = activeDisplaySets.findIndex( + ds => ds.displaySetInstanceUID === displaySetInstanceUID + ); + + displaySetCache.delete(displaySetInstanceUID); + activeDisplaySets.splice(activeDisplaySetsIndex, 1); + activeDisplaySetsMap.delete(displaySetInstanceUID); + + this._broadcastEvent(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets); + this._broadcastEvent(EVENTS.DISPLAY_SETS_REMOVED, { + displaySetInstanceUIDs: [displaySetInstanceUID], + }); + } + + /** + * @param {string} displaySetInstanceUID + * @returns {object} displaySet + */ + public getDisplaySetByUID = (displaySetInstanceUid: string): DisplaySet => { + if (typeof displaySetInstanceUid !== 'string') { + throw new Error( + `getDisplaySetByUID: displaySetInstanceUid must be a string, you passed ${displaySetInstanceUid}` + ); + } + + return displaySetCache.get(displaySetInstanceUid); + }; + + /** + * + * @param {*} input + * @param {*} param1: settings: initialViewportSettings by HP or callbacks after rendering + * @returns {string[]} - added displaySetInstanceUIDs + */ + makeDisplaySets = (input, { batch = false, madeInClient = false, settings = {} } = {}) => { + if (!input || !input.length) { + throw new Error('No instances were provided.'); + } + + if (batch && !input[0].length) { + throw new Error('Batch displaySet creation does not contain array of array of instances.'); + } + + // If array of instances => One instance. + const displaySetsAdded = new Array(); + + if (batch) { + for (let i = 0; i < input.length; i++) { + const instances = input[i]; + const displaySets = this.makeDisplaySetForInstances(instances, settings); + + displaySetsAdded.push(...displaySets); + } + } else { + const displaySets = this.makeDisplaySetForInstances(input, settings); + + displaySetsAdded.push(...displaySets); + } + + const options = {}; + + if (madeInClient) { + options.madeInClient = true; + } + + if (this.activeDisplaySetsChanged) { + this.activeDisplaySetsChanged = false; + this._broadcastEvent(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets); + } + if (displaySetsAdded?.length) { + // The response from displaySetsAdded will only contain newly added + // display sets. + this._broadcastEvent(EVENTS.DISPLAY_SETS_ADDED, { + displaySetsAdded, + options, + }); + + return displaySetsAdded; + } + }; + + /** + * The onModeExit returns the display set service to the initial state, + * that is without any display sets. To avoid recreating display sets, + * the mode specific onModeExit is called before this method and should + * store the active display sets and the cached data. + */ + public onModeExit(): void { + this.getDisplaySetCache().clear(); + this.activeDisplaySets.length = 0; + this.activeDisplaySetsMap.clear(); + } + + /** + * This function hides the old makeDisplaySetForInstances function to first + * separate the instances by sopClassUID so each call have only instances + * with the same sopClassUID, to avoid a series composed by different + * sopClassUIDs be filtered inside one of the SOPClassHandler functions and + * didn't appear in the series list. + * @param instancesSrc + * @param settings + * @returns + */ + public makeDisplaySetForInstances(instancesSrc: InstanceMetadata[], settings): DisplaySet[] { + // creating a sopClassUID list and for each sopClass associate its respective + // instance list + const instancesForSetSOPClasses = instancesSrc.reduce((sopClassList, instance) => { + if (!(instance.SOPClassUID in sopClassList)) { + sopClassList[instance.SOPClassUID] = []; + } + sopClassList[instance.SOPClassUID].push(instance); + return sopClassList; + }, {}); + // for each sopClassUID, call the old makeDisplaySetForInstances with a + // instance list composed only by instances with the same sopClassUID and + // accumulate the displaySets in the variable allDisplaySets + const sopClasses = Object.keys(instancesForSetSOPClasses); + let allDisplaySets = []; + sopClasses.forEach(sopClass => { + const displaySets = this._makeDisplaySetForInstances( + instancesForSetSOPClasses[sopClass], + settings + ); + allDisplaySets = [...allDisplaySets, ...displaySets]; + }); + return allDisplaySets; + } + + /** + * Creates new display sets for the instances contained in instancesSrc + * according to the sop class handlers registered. + * This is idempotent in that calling it a second time with the + * same set of instances will not result in new display sets added. + * However, the response for the subsequent call will be empty as the data + * is already present. + * Calling it with some new instances and some existing instances will + * result in the new instances being added to existing display sets if + * they support the addInstances call, OR to new instances otherwise. + * Only the new instances are returned - the others are updated. + * + * @param instancesSrc are instances to add + * @param settings are settings to add + * @returns Array of the display sets added. + */ + private _makeDisplaySetForInstances(instancesSrc: InstanceMetadata[], settings): DisplaySet[] { + // Some of the sop class handlers take a direct reference to instances + // so make sure it gets copied here so that they have their own ref + let instances = [...instancesSrc]; + const instance = instances[0]; + + const existingDisplaySets = this.getDisplaySetsForSeries(instance.SeriesInstanceUID) || []; + + const SOPClassHandlerIds = this.SOPClassHandlerIds; + const allDisplaySets = []; + + // Iterate over the sop class handlers while there are still instances to add + for (let i = 0; i < SOPClassHandlerIds.length && instances.length; i++) { + const SOPClassHandlerId = SOPClassHandlerIds[i]; + const handler = this.extensionManager.getModuleEntry(SOPClassHandlerId); + + if (handler.sopClassUids.includes(instance.SOPClassUID)) { + // Check if displaySets are already created using this SeriesInstanceUID/SOPClassHandler pair. + let displaySets = existingDisplaySets.filter( + displaySet => displaySet.SOPClassHandlerId === SOPClassHandlerId + ); + + if (displaySets.length) { + // This case occurs when there are already display sets, so remove + // any instances in existing display sets. + instances = filterInstances(instances, displaySets); + // See if an existing display set can add this instance to it, + // for example, if it is a new image to be added to the existing set + for (const ds of displaySets) { + const addedDs = ds.addInstances?.(instances, this); + if (addedDs) { + this.activeDisplaySetsChanged = true; + instances = filterInstances(instances, [addedDs]); + this._addActiveDisplaySets([addedDs]); + this.setDisplaySetMetadataInvalidated(addedDs.displaySetInstanceUID); + } + // This means that all instances already existed or got added to + // existing display sets, and had an invalidated event fired + if (!instances.length) { + return allDisplaySets; + } + } + + if (!instances.length) { + // Everything is already added - this is just an update caused + // by something else + this._addActiveDisplaySets(displaySets); + return allDisplaySets; + } + } + + // The instances array still contains some instances, so try + // creating additional display sets using the sop class handler + displaySets = handler.getDisplaySetsFromSeries(instances); + + if (!displaySets || !displaySets.length) { + continue; + } + + // applying hp-defined viewport settings to the displaysets + displaySets.forEach(ds => { + Object.keys(settings).forEach(key => { + ds[key] = settings[key]; + }); + }); + + this._addDisplaySetsToCache(displaySets); + this._addActiveDisplaySets(displaySets); + + // It is possible that this SOP class handler handled some instances + // but there may need to be other instances handled by other handlers, + // so remove the handled instances + instances = filterInstances(instances, displaySets); + + allDisplaySets.push(...displaySets); + } + } + // applying the default sopClassUID handler + if (allDisplaySets.length === 0) { + // applying hp-defined viewport settings to the displaysets + const handler = this.extensionManager.getModuleEntry(this.unsupportedSOPClassHandler); + const displaySets = handler.getDisplaySetsFromSeries(instances); + if (displaySets?.length) { + displaySets.forEach(ds => { + Object.keys(settings).forEach(key => { + ds[key] = settings[key]; + }); + }); + + this._addDisplaySetsToCache(displaySets); + this._addActiveDisplaySets(displaySets); + + allDisplaySets.push(...displaySets); + } + } + return allDisplaySets; + } + + /** + * Iterates over displaysets and invokes comparator for each element. + * It returns a list of items that has being succeed by comparator method. + * + * @param comparator - method to be used on the validation + * @returns list of displaysets + */ + public getDisplaySetsBy(comparator: (DisplaySet) => boolean): DisplaySet[] { + const result = []; + + if (typeof comparator !== 'function') { + throw new Error(`The comparator ${comparator} was not a function`); + } + + this.getActiveDisplaySets().forEach(displaySet => { + if (comparator(displaySet)) { + result.push(displaySet); + } + }); + + return result; + } + + /** + * + * @param sortFn function to sort the display sets + * @param direction direction to sort the display sets + * @returns void + */ + public sortDisplaySets( + sortFn: (a: DisplaySet, b: DisplaySet) => number, + direction: string, + suppressEvent = false + ): void { + this.activeDisplaySets.sort(sortFn); + if (direction === 'descending') { + this.activeDisplaySets.reverse(); + } + if (!suppressEvent) { + this._broadcastEvent(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets); + } + } +} diff --git a/platform/core/src/services/DisplaySetService/EVENTS.js b/platform/core/src/services/DisplaySetService/EVENTS.js new file mode 100644 index 0000000..445736a --- /dev/null +++ b/platform/core/src/services/DisplaySetService/EVENTS.js @@ -0,0 +1,9 @@ +const EVENTS = { + DISPLAY_SETS_ADDED: 'event::displaySetService:displaySetsAdded', + DISPLAY_SETS_CHANGED: 'event::displaySetService:displaySetsChanged', + DISPLAY_SETS_REMOVED: 'event::displaySetService:displaySetsRemoved', + DISPLAY_SET_SERIES_METADATA_INVALIDATED: + 'event::displaySetService:displaySetSeriesMetadataInvalidated', +}; + +export default EVENTS; diff --git a/platform/core/src/services/DisplaySetService/index.ts b/platform/core/src/services/DisplaySetService/index.ts new file mode 100644 index 0000000..5893d7b --- /dev/null +++ b/platform/core/src/services/DisplaySetService/index.ts @@ -0,0 +1,5 @@ +import DisplaySetService from './DisplaySetService'; +import { DisplaySetMessage, DisplaySetMessageList } from './DisplaySetMessage'; + +export default DisplaySetService; +export { DisplaySetMessage, DisplaySetMessageList }; diff --git a/platform/core/src/services/HangingProtocolService/HPMatcher.js b/platform/core/src/services/HangingProtocolService/HPMatcher.js new file mode 100644 index 0000000..6a874ee --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/HPMatcher.js @@ -0,0 +1,173 @@ +import validate from './lib/validator'; + +/** + * Match a Metadata instance against rules using Validate.js for validation. + * @param {InstanceMetadata} metadataInstance Metadata instance object + * @param {Array} rules Array of MatchingRules instances (StudyMatchingRule|SeriesMatchingRule|ImageMatchingRule) for the match + * @param {object} options is an object containing additional information + * @param {object[]} options.studies is a list of all the studies + * @param {object[]} options.displaySets is a list of the display sets + * @return {Object} Matching Object with score and details (which rule passed or failed) + */ +const match = (metadataInstance, rules = [], customAttributeRetrievalCallbacks, options) => { + const validateOptions = { + format: 'grouped', + }; + + const details = { + passed: [], + failed: [], + }; + + const readValues = {}; + + let requiredFailed = false; + let score = 0; + + // Allow for matching against current or prior specifically + const prior = options?.studies?.[1]; + const current = options?.studies?.[0]; + const instance = metadataInstance.instances?.[0]; + const fromSrc = { + prior, + current, + instance, + ...options, + options, + metadataInstance, + }; + + rules.forEach(rule => { + const { attribute, from = 'metadataInstance' } = rule; + // Do not use the custom attribute from the metadataInstance since it is subject to change + if (customAttributeRetrievalCallbacks.hasOwnProperty(attribute)) { + readValues[attribute] = customAttributeRetrievalCallbacks[attribute].callback.call( + rule, + metadataInstance, + options + ); + } else { + readValues[attribute] = fromSrc[from]?.[attribute] ?? instance?.[attribute]; + } + + // handle cases where the constraint is also a custom attribute + const resolvedConstraint = resolveConstraintAttributes( + readValues, + rule.constraint, + customAttributeRetrievalCallbacks, + fromSrc + ); + + // Format the constraint as required by Validate.js + const testConstraint = { + [attribute]: resolvedConstraint, + }; + + // Create a single attribute object to be validated, since metadataInstance is an + // instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata) + const attributeMap = { + [attribute]: readValues[attribute], + }; + + // Use Validate.js to evaluate the constraints on the specified metadataInstance + let errorMessages; + try { + errorMessages = validate(attributeMap, testConstraint, [validateOptions]); + } catch (e) { + errorMessages = ['Something went wrong during validation.', e]; + } + + // TODO: move to a logger + // console.log( + // 'Test', + // `${from}.${attribute}`, + // readValues[attribute], + // JSON.stringify(rule.constraint), + // !errorMessages + // ); + + if (!errorMessages) { + // If no errorMessages were returned, then validation passed. + + // Add the rule's weight to the total score + score += parseInt(rule.weight || 1, 10); + // Log that this rule passed in the matching details object + details.passed.push({ + rule, + }); + } else { + // If errorMessages were present, then validation failed + + // If the rule that failed validation was Required, then + // mark that a required Rule has failed + if (rule.required) { + requiredFailed = true; + } + + // Log that this rule failed in the matching details object + // and include any error messages + details.failed.push({ + rule, + errorMessages, + }); + } + }); + + // If a required Rule has failed Validation, set the matching score to zero + if (requiredFailed) { + score = 0; + } + + return { + score, + details, + requiredFailed, + }; +}; + +// New helper function to resolve constraint attributes +const resolveConstraintAttributes = ( + readValues, + constraint, + customAttributeRetrievalCallbacks, + fromSrc +) => { + if (typeof constraint !== 'object' || constraint === null) { + return constraint; + } + + const resolvedConstraint = {}; + Object.entries(constraint).forEach(([key, value]) => { + if (typeof value === 'object' && Object.keys(value).length > 0 && 'attribute' in value) { + const attributeName = value.attribute; + const attributeFrom = value.from ?? 'metadataInstance'; + + if (customAttributeRetrievalCallbacks.hasOwnProperty(attributeName)) { + const value = customAttributeRetrievalCallbacks[attributeName].callback.call( + null, + fromSrc[attributeFrom], + fromSrc.options + ); + + resolvedConstraint[key] = { + value, + }; + } else { + resolvedConstraint[key] = { + value: + fromSrc[attributeFrom]?.[attributeName] ?? fromSrc.metadataInstance?.[attributeName], + }; + } + } else { + resolvedConstraint[key] = value; + } + }, {}); + + return resolvedConstraint; +}; + +const HPMatcher = { + match, +}; + +export { HPMatcher }; diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js b/platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js new file mode 100644 index 0000000..cbc726c --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js @@ -0,0 +1,192 @@ +import HangingProtocolService from './HangingProtocolService'; + +const testProtocol = { + id: 'test', + name: 'Default', + protocolMatchingRules: [ + { + attribute: 'StudyDescription', + constraint: { + contains: 'PETCT', + }, + }, + ], + displaySetSelectors: { + displaySetSelector: { + seriesMatchingRules: [ + { + weight: 1, + attribute: 'Modality', + constraint: { + equals: 'CT', + }, + required: true, + }, + { + weight: 1, + attribute: 'numImageFrames', + constraint: { + greaterThan: 10, + }, + }, + ], + studyMatchingRules: [], + }, + }, + stages: [ + { + name: 'default', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 1, + }, + }, + viewports: [ + { + viewportOptions: { + viewportId: 'ctAXIAL', + viewportType: 'volume', + orientation: 'axial', + toolGroupId: 'ctToolGroup', + customViewportOptions: { + initialScale: 2.5, + }, + initialImageOptions: { + // index: 5, + preset: 'first', // 'first', 'last', 'middle' + }, + syncGroups: [ + { + type: 'cameraPosition', + id: 'axialSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'displaySetSelector', + }, + ], + }, + ], + }, + ], + numberOfPriorsReferenced: -1, +}; + +function testProtocolGenerator({ servicesManager }) { + servicesManager.services.TestService.toCall(); + + return { + protocol: testProtocol, + }; +} + +const studyMatch = { + StudyInstanceUID: 'studyMatch', + StudyDescription: 'A PETCT study type', +}; + +const displaySet1 = { + ...studyMatch, + SeriesInstanceUID: 'ds1', + displaySetInstanceUID: 'displaySet1', + numImageFrames: 11, + Modality: 'CT', +}; + +const displaySet2 = { + ...displaySet1, + SeriesInstanceUID: 'ds2', + displaySetInstanceUID: 'displaySet2', + Modality: 'PT', +}; + +const displaySet3 = { + ...displaySet1, + numImageFrames: 3, + displaySetInstanceUID: 'displaySet3', +}; + +const studyMatchDisplaySets = [displaySet3, displaySet2, displaySet1]; + +function checkHpsBestMatch(hps) { + hps.run({ studies: [studyMatch], displaySets: studyMatchDisplaySets }); + const { viewportMatchDetails } = hps.getMatchDetails(); + expect(viewportMatchDetails.size).toBe(1); + expect(viewportMatchDetails.get('ctAXIAL')).toMatchObject({ + viewportOptions: { + viewportId: 'ctAXIAL', + viewportType: 'volume', + orientation: 'axial', + toolGroupId: 'ctToolGroup', + }, + // Matches ds1 because it matches 2 rules, a required and an optional + // ds2 fails to match required and ds3 fails to match an optional. + displaySetsInfo: [ + { + displaySetInstanceUID: 'displaySet1', + displaySetOptions: { + id: 'displaySetSelector', + options: {}, + }, + }, + ], + }); +} + +describe('HangingProtocolService', () => { + const mockedFunction = jest.fn(); + const commandsManager = { + run: mockedFunction, + }; + const servicesManager = { + services: { + TestService: { + toCall: mockedFunction, + }, + }, + }; + const hangingProtocolService = new HangingProtocolService(commandsManager, servicesManager); + let initialScaling; + + afterEach(() => { + mockedFunction.mockClear(); + }); + + describe('with a static protocol', () => { + beforeAll(() => { + hangingProtocolService.addProtocol(testProtocol.id, testProtocol); + }); + + it('has one protocol', () => { + expect(hangingProtocolService.getProtocols().length).toBe(1); + }); + + describe('run', () => { + it('matches best image match', () => { + checkHpsBestMatch(hangingProtocolService); + }); + }); + }); + + describe('with protocol generator', () => { + beforeAll(() => { + hangingProtocolService.addProtocol(testProtocol.id, testProtocolGenerator); + }); + + it('has one protocol', () => { + expect(hangingProtocolService.getProtocols().length).toBe(1); + }); + + describe('run', () => { + it('matches best image match', () => { + checkHpsBestMatch(hangingProtocolService); + }); + }); + }); +}); diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts new file mode 100644 index 0000000..fdb6a81 --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts @@ -0,0 +1,1701 @@ +import cloneDeep from 'lodash.clonedeep'; + +import { PubSubService } from '../_shared/pubSubServiceInterface'; +import sortBy from '../../utils/sortBy'; +import ProtocolEngine from './ProtocolEngine'; +import { StudyMetadata } from '../../types/StudyMetadata'; +import DisplaySet from '../DisplaySetService/DisplaySet'; +import { CommandsManager } from '../../classes'; +import * as HangingProtocol from '../../types/HangingProtocol'; +import { isDisplaySetFromUrl, sopInstanceLocation } from './custom-attribute/isDisplaySetFromUrl'; +import numberOfDisplaySetsWithImages from './custom-attribute/numberOfDisplaySetsWithImages'; +import seriesDescriptionsFromDisplaySets from './custom-attribute/seriesDescriptionsFromDisplaySets'; +import uuidv4 from '../../utils/uuidv4'; + +type Protocol = HangingProtocol.Protocol | HangingProtocol.ProtocolGenerator; + +const DEFAULT_VIEWPORT_OPTIONS: HangingProtocol.ViewportOptions = { + toolGroupId: 'default', + viewportType: 'stack', +}; + +export default class HangingProtocolService extends PubSubService { + static EVENTS = { + // The PROTOCOL_CHANGED event is fired when the protocol changes + // and should be immediately applied + PROTOCOL_CHANGED: 'event::hanging_protocol_changed', + // The PROTOCOL_RESTORED event is fired instead of a changed event to indicate + // that an earlier state has been restored as part of a state update, but + // is not being directly re-applied, but just restored. + PROTOCOL_RESTORED: 'event::hanging_protocol_restore', + // The layout has been decided for the hanging protocol - deprecated + NEW_LAYOUT: 'event::hanging_protocol_new_layout', + // Fired when the stages within the current protocol are known to have + // the status set - that is, they are activated (or deactivated). + STAGE_ACTIVATION: 'event::hanging_protocol_stage_activation', + CUSTOM_IMAGE_LOAD_PERFORMED: 'event::hanging_protocol_custom_image_load_performed', + }; + + public static REGISTRATION = { + name: 'hangingProtocolService', + altName: 'HangingProtocolService', + create: ({ configuration = {}, commandsManager, servicesManager }) => { + return new HangingProtocolService(commandsManager, servicesManager); + }, + }; + + studies: StudyMetadata[]; + // stores all the protocols (object or function that returns an object) in a map + protocols: Map; + // Contains the list of currently active keys + activeProtocolIds: string[]; + // the current protocol that is being applied to the viewports in object format + protocol: HangingProtocol.Protocol; + // The version of the protocol that must not be modified with customizations + // if it was defined in the protocol definition. This is a copy of the protocol + // that is used to recompute the computedOptions when necessary as we override + // the computedOptions in the protocol object itself. + _originalProtocol: HangingProtocol.Protocol; + + stageIndex = 0; + _commandsManager: CommandsManager; + _servicesManager: AppTypes.ServicesManager; + protocolEngine: ProtocolEngine; + customViewportSettings = []; + displaySets: DisplaySet[] = []; + activeStudy: StudyMetadata; + debugLogging: false; + + customAttributeRetrievalCallbacks = { + NumberOfStudyRelatedSeries: { + name: 'The number of series in the study', + callback: metadata => metadata.NumberOfStudyRelatedSeries ?? metadata.series?.length, + }, + NumberOfSeriesRelatedInstances: { + name: 'The number of instances in the display set', + callback: metadata => metadata.numImageFrames, + }, + ModalitiesInStudy: { + name: 'Gets the array of the modalities for the series', + callback: metadata => + metadata.ModalitiesInStudy ?? + (metadata.series || []).reduce((prev, curr) => { + const { Modality } = curr; + if (Modality && prev.indexOf(Modality) == -1) { + prev.push(Modality); + } + return prev; + }, []), + }, + isReconstructable: { + name: 'Checks if the display set is reconstructable', + // we can add more advanced checking here + callback: displaySet => displaySet.isReconstructable ?? false, + }, + isDisplaySetFromUrl: { + name: 'Checks if the display set is as specified in the URL', + callback: isDisplaySetFromUrl, + }, + sopInstanceLocation: { + name: 'Gets the position of the specified sop instance', + callback: sopInstanceLocation, + }, + seriesDescriptions: { + name: 'seriesDescriptions', + description: 'List of Series Descriptions', + callback: seriesDescriptionsFromDisplaySets, + }, + numberOfDisplaySetsWithImages: { + name: 'numberOfDisplaySetsWithImages', + description: 'Number of displays sets with images', + callback: numberOfDisplaySetsWithImages, + }, + }; + listeners = {}; + registeredImageLoadStrategies = {}; + activeImageLoadStrategyName = null; + customImageLoadPerformed = false; + + /** + * displaySetMatchDetails = + * DisplaySetId is the id defined in the hangingProtocol object itself + * and match is an object that contains information about + */ + displaySetMatchDetails: Map< + string, // protocol displaySetId in the displayset selector + HangingProtocol.DisplaySetMatchDetails + > = new Map(); + + /** + * An array that contains for each viewport (viewportId) specified in the + * hanging protocol, an object of the form + */ + viewportMatchDetails: Map< + string, // viewportId + HangingProtocol.ViewportMatchDetails + > = new Map(); + + constructor(commandsManager: CommandsManager, servicesManager: AppTypes.ServicesManager) { + super(HangingProtocolService.EVENTS); + this._commandsManager = commandsManager; + this._servicesManager = servicesManager; + this.protocols = new Map(); + this.protocolEngine = undefined; + this.protocol = undefined; + this.stageIndex = undefined; + + this.studies = []; + } + + public destroy(): void { + this.reset(); + this.protocols = new Map(); + } + + public reset(): void { + this.studies = []; + this.viewportMatchDetails = new Map(); + this.displaySetMatchDetails = new Map(); + this.protocol = undefined; + this.stageIndex = undefined; + this.protocolEngine = undefined; + } + + /** Leave the hanging protocol in the initialized state */ + public onModeEnter(): void { + this.reset(); + } + + /** + * Gets the active protocol information directly, including the direct + * protocol, stage and active study objects. + * Should NOT be stored longer term as the protocol + * object can change internally or be regenerated. + * Can be used to store the state to recover from exceptions. + * + * @returns protocol, stage, activeStudy + */ + public getActiveProtocol(): { + protocol: HangingProtocol.Protocol; + _originalProtocol: HangingProtocol.Protocol; + stage: HangingProtocol.ProtocolStage; + stageIndex: number; + activeStudy?: StudyMetadata; + viewportMatchDetails: Map; + displaySetMatchDetails: Map; + activeImageLoadStrategyName: string; + } { + return { + protocol: this.protocol, + _originalProtocol: this._originalProtocol, + stage: this.protocol?.stages?.[this.stageIndex], + stageIndex: this.stageIndex, + activeStudy: this.activeStudy, + viewportMatchDetails: this.viewportMatchDetails, + displaySetMatchDetails: this.displaySetMatchDetails, + activeImageLoadStrategyName: this.activeImageLoadStrategyName, + }; + } + + /** Gets the hanging protocol state information, which is a storable + * state information for the hanging protocol consisting of the: + * protocolId, stageIndex, stageId and activeStudyUID + */ + public getState(): HangingProtocol.HPInfo { + if (!this.protocol) { + return; + } + return { + protocolId: this.protocol.id, + stageIndex: this.stageIndex, + stageId: this.protocol.stages[this.stageIndex].id, + activeStudyUID: this.activeStudy?.StudyInstanceUID, + }; + } + + /** + * Filters the series required for running a hanging protocol. + * + * This can be extended in the future with more complex selection rules e.g. + * N series of a given type, and M of a different type, such as all CT series, + * and all SR, and then everything else. + * + * @param protocolId - The ID of the hanging protocol. + * @param seriesPromises - An array of promises representing the series. + * @returns An object containing the required series and the remaining series. + */ + public filterSeriesRequiredForRun(protocolId, seriesPromises) { + if (Array.isArray(protocolId)) { + protocolId = protocolId[0]; + } + const minSeriesLoadedToRunHP = + this.getProtocolById(protocolId)?.hpInitiationCriteria?.minSeriesLoaded || + seriesPromises.length; + const requiredSeries = seriesPromises.slice(0, minSeriesLoadedToRunHP); + const remaining = seriesPromises.slice(minSeriesLoadedToRunHP); + return { + requiredSeries, + remaining, + }; + } + + /** Gets the protocol with id 'default' */ + public getDefaultProtocol(): HangingProtocol.Protocol { + return this.getProtocolById('default'); + } + + /** Gets the viewport match details. + * @deprecated because this method is expected to go away as the HP service + * becomes more stateless. + */ + public getMatchDetails(): HangingProtocol.HangingProtocolMatchDetails { + return { + viewportMatchDetails: this.viewportMatchDetails, + displaySetMatchDetails: this.displaySetMatchDetails, + }; + } + + /** + * It loops over the protocols map object, and checks whether the protocol + * is a function, if so, it executes it and returns the result as a protocol object + * otherwise it returns the protocol object itself + * + * @returns all the hanging protocol registered in the HangingProtocolService + */ + public getProtocols(): HangingProtocol.Protocol[] { + // this.protocols is a map of protocols with the protocol id as the key + // and the protocol or a function that returns a protocol as the value + const protocols = []; + const keys = this.activeProtocolIds || this.protocols.keys(); + for (const protocolId of keys) { + const protocol = this.getProtocolById(protocolId); + if (protocol) { + protocols.push(protocol); + } + } + + return protocols; + } + + /** + * Returns the protocol with the given id, it will get the protocol from the + * protocols map object and if it is a function, it will execute it and return + * the result as a protocol object + * + * @param protocolId - the id of the protocol + * @returns protocol - the protocol with the given id + */ + public getProtocolById(protocolId: string, caseInsensitive = true): HangingProtocol.Protocol { + if (!protocolId) { + return; + } + if (protocolId === this.protocol?.id) { + return this.protocol; + } + + let protocol = this.protocols.get(protocolId); + if (!protocol && caseInsensitive) { + const lowerCaseId = protocolId.toLowerCase(); + for (const [key] of this.protocols) { + if (key.toLowerCase() === lowerCaseId) { + protocol = this.getProtocolById(key); + break; + } + } + } + + if (!protocol) { + throw new Error(`No protocol ${protocolId} found`); + } + + if (protocol instanceof Function) { + try { + const { protocol: generatedProtocol } = this._getProtocolFromGenerator(protocol); + + return generatedProtocol; + } catch (error) { + console.warn( + `Error while executing protocol generator for protocol ${protocolId}: ${error}` + ); + } + } else { + return this._validateProtocol(protocol); + } + } + + /** + * It adds a protocol to the protocols map object. If a protocol with the given + * id already exists, warn the user and overwrite it. This can be used to + * set a new "default" protocol. + * + * @param {string} protocolId - The id of the protocol. + * @param {Protocol} protocol - Protocol - This is the protocol that you want to + * add to the protocol manager. + */ + public addProtocol(protocolId: string, protocol: Protocol): void { + if (this.protocols.has(protocolId)) { + console.warn(`A protocol with id ${protocolId} already exists. It will be overwritten.`); + } + + if (!(protocol instanceof Function)) { + protocol = this._validateProtocol(protocol as HangingProtocol.Protocol); + } + + this.protocols.set(protocolId, protocol); + } + + /** + * Add a given protocol object as active. + * If active protocols ids is null right now, then the specified + * protocol will become the only active protocol. + */ + public addActiveProtocolId(id: string): void { + if (!id) { + return; + } + if (!this.activeProtocolIds) { + this.activeProtocolIds = []; + } + this.activeProtocolIds.push(id); + } + + /** + * Sets the active hanging protocols to use, by name. If the value is empty, + * then resets the active protocols to all the named items. + */ + public setActiveProtocolIds(protocolId?: string[] | string): void { + if (!protocolId || !protocolId.length) { + this.activeProtocolIds = null; + console.log('No active protocols, setting all to active'); + return; + } + if (typeof protocolId === 'string') { + this.setActiveProtocolIds([protocolId]); + return; + } + this.activeProtocolIds = [...protocolId]; + } + + /** + * Sets the active study. + * This is the study that the hanging protocol will consider active and + * may or may not be the study that is being shown by the protocol currently, + * for example, a prior view hanging protocol will NOT show the active study + * specifically, but will show another study instead. + */ + public setActiveStudyUID(activeStudyUID: string) { + if (!activeStudyUID || activeStudyUID === this.activeStudy?.StudyInstanceUID) { + return; + } + this.activeStudy = this.studies.find(it => it.StudyInstanceUID === activeStudyUID); + return this.activeStudy; + } + + public hasStudyUID(studyUID: string): boolean { + return this.studies.some(it => it.StudyInstanceUID === studyUID); + } + + public addStudy(study) { + if (!this.hasStudyUID(study.StudyInstanceUID)) { + this.studies.push(study); + } + } + + /** + * Run the hanging protocol decisions tree on the active study, + * studies list and display sets, firing a PROTOCOL_CHANGED event when + * complete to indicate the hanging protocol is ready, and which stage + * got applied/activated. + * + * Also fires a STAGES_ACTIVE event to indicate which stages are able to be + * activated. + * + * @param params is the dataset to run the hanging protocol on. + * @param params.activeStudy is the "primary" study to hang This may or may + * not be displayed by the actual viewports. + * @param params.studies is the list of studies to hang. If absent, will reuse the previous set. + * @param params.displaySets is the list of display sets associated with + * the studies to display in viewports. + * @param protocol is a specific protocol to apply. + */ + public run({ studies, displaySets, activeStudy }, protocolId, options = {}) { + this.studies = [...(studies || this.studies)]; + this.displaySets = displaySets; + this.setActiveStudyUID( + activeStudy?.StudyInstanceUID || (activeStudy || this.studies[0])?.StudyInstanceUID + ); + + this.protocolEngine = new ProtocolEngine( + this.getProtocols(), + this.customAttributeRetrievalCallbacks + ); + + // Resets the full protocol status here. + this.protocol = null; + + if (protocolId && typeof protocolId === 'string') { + const protocol = this.getProtocolById(protocolId); + this._setProtocol(protocol, options); + } else { + const matchedProtocol = this.protocolEngine.run({ + studies: this.studies, + activeStudy, + displaySets, + }); + this._setProtocol(matchedProtocol); + } + + if (this.protocol?.callbacks?.onProtocolEnter) { + this._commandsManager.run(this.protocol?.callbacks?.onProtocolEnter); + } + } + + /** + * Returns true, if the hangingProtocol has a custom loading strategy for the images + * and its callback has been added to the HangingProtocolService + * @returns A boolean indicating whether a custom image load strategy has been added or not. + */ + public hasCustomImageLoadStrategy(): boolean { + return ( + this.activeImageLoadStrategyName !== null && + this.registeredImageLoadStrategies[this.activeImageLoadStrategyName] instanceof Function + ); + } + + /** + * Returns a boolean indicating whether a custom image load has been performed or not. + * A custom image load is performed when a custom image load strategy is used to load images. + * This method is used internally by the HangingProtocolService to determine whether to perform + * a custom image load or not. + * + * @returns A boolean indicating whether a custom image load has been performed or not. + */ + private getCustomImageLoadPerformed(): boolean { + return this.customImageLoadPerformed; + } + + /** + * Returns a boolean indicating whether a custom image load should be performed or not. + * A custom image load should be performed if a custom image load strategy has been added to the HangingProtocolService + * and it has not been performed yet. + * + * @returns A boolean indicating whether a custom image load should be performed or not. + */ + public getShouldPerformCustomImageLoad(): boolean { + return this.hasCustomImageLoadStrategy() && !this.getCustomImageLoadPerformed(); + } + + /** + * Set the strategy callback for loading images to the HangingProtocolService + * @param {string} name strategy name + * @param {Function} callback image loader callback + */ + public registerImageLoadStrategy(name, callback): void { + if (callback instanceof Function && name) { + this.registeredImageLoadStrategies[name] = callback; + } + } + + /** + * Adds a custom attribute to be used in the HangingProtocol UI and matching rules, including a + * callback that will be used to calculate the attribute value. + * + * @param attributeId The ID used to refer to the attribute (e.g. 'timepointType') + * @param attributeName The name of the attribute to be displayed (e.g. 'Timepoint Type') + * @param callback The function used to calculate the attribute value from the other attributes at its level (e.g. study/series/image) + * @param options to add to the "this" object for the custom attribute retriever + */ + public addCustomAttribute( + attributeId: string, + attributeName: string, + callback: (metadata: Record, extraData?: Record) => unknown, + options: Record = {} + ): void { + this.customAttributeRetrievalCallbacks[attributeId] = { + ...options, + id: attributeId, + name: attributeName, + callback, + }; + } + + /** + * Executes the callback function for the custom loading strategy for the images + * if no strategy is set, the default strategy is used + */ + runImageLoadStrategy(data): boolean { + const loader = this.registeredImageLoadStrategies[this.activeImageLoadStrategyName]; + const loadedData = loader({ + data, + displaySetsMatchDetails: this.displaySetMatchDetails, + viewportMatchDetails: this.viewportMatchDetails, + }); + + // if loader successfully re-arranged the data with the custom strategy + // and returned the new props, then broadcast them + if (!loadedData) { + console.warn('Not able to load data with custom strategy'); + return false; + } + + this.customImageLoadPerformed = true; + this._broadcastEvent(this.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED, loadedData); + return true; + } + + _validateProtocol(protocol: HangingProtocol.Protocol): HangingProtocol.Protocol { + protocol.name = protocol.name ?? protocol.id; + const { stages } = protocol; + + if (!stages) { + console.warn('Protocol has not stages:', protocol.id, protocol); + return; + } + + for (const id of Object.keys(protocol.displaySetSelectors)) { + const selector = protocol.displaySetSelectors[id]; + selector.id = id; + const { seriesMatchingRules } = selector; + if (!seriesMatchingRules) { + console.warn('Selector has no series matching rules', protocol.id, id); + return; + } + } + + // Generate viewports automatically as required. + stages.forEach(stage => { + if (!stage.viewports) { + stage.name = stage.name || stage.id; + stage.viewports = []; + const { rows, columns } = stage.viewportStructure.properties; + + for (let i = 0; i < rows * columns; i++) { + const defaultViewport = stage.defaultViewport || protocol.defaultViewport || {}; + stage.viewports.push({ + viewportOptions: { + ...defaultViewport.viewportOptions, + viewportId: i === 0 ? 'default' : uuidv4(), + }, + displaySets: [], + }); + } + } else { + // Clone each viewport to ensure independent objects + stage.viewports = stage.viewports.map((viewport, index) => { + const defaultViewport = stage.defaultViewport || protocol.defaultViewport || {}; + const existingViewportId = viewport.viewportOptions?.viewportId; + + return { + ...viewport, + viewportOptions: { + ...defaultViewport.viewportOptions, + ...viewport.viewportOptions, + viewportId: existingViewportId + ? existingViewportId + : index === 0 + ? 'default' + : uuidv4(), + }, + displaySets: viewport.displaySets || [], + }; + }); + stage.viewports.forEach(viewport => { + viewport.displaySets.forEach(displaySet => { + displaySet.options = displaySet.options || {}; + }); + }); + } + }); + + return protocol; + } + + private _getProtocolFromGenerator(protocolGenerator: HangingProtocol.ProtocolGenerator): { + protocol: HangingProtocol.Protocol; + } { + const { protocol } = protocolGenerator({ + servicesManager: this._servicesManager, + commandsManager: this._commandsManager, + }); + + const validatedProtocol = this._validateProtocol(protocol); + + return { + protocol: validatedProtocol, + }; + } + + /** + * This will return the viewports that need to be updated based on the + * hanging protocol layout and the displaySetInstanceUID that needs to be updated. + * + * This is useful, when for instance we drag and drop a displaySet into a viewport + * which is in MPR, and we need to update the other viewports that are showing the same + * layout. + * + * However, sometimes since we get out of sync with the hanging protocol layout, when + * the user use the custom grid layout, we should not update the other viewports, and that is + * when the isHangingProtocolLayout is set to false. + * + * @param viewportId - the id of the viewport that needs to be updated + * @param displaySetInstanceUID - the displaySetInstanceUID that needs to be updated + * @param isHangingProtocolLayout - whether the layout is a hanging protocol layout + * @returns + */ + getViewportsRequireUpdate(viewportId, displaySetInstanceUID, isHangingProtocolLayout = true) { + const newDisplaySetInstanceUID = displaySetInstanceUID; + const defaultReturn = [ + { + viewportId, + displaySetInstanceUIDs: [newDisplaySetInstanceUID], + }, + ]; + + if (!isHangingProtocolLayout) { + return defaultReturn; + } + + const { displaySetService } = this._servicesManager.services; + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + if (displaySet?.unsupported) { + throw new Error('Unsupported displaySet'); + } + const protocol = this.protocol; + const protocolStage = protocol.stages[this.stageIndex]; + const protocolViewports = protocolStage.viewports; + + if (!protocolViewports) { + return defaultReturn; + } + + const protocolViewport = protocolViewports.find( + pv => pv.viewportOptions.viewportId === viewportId + ); + + // if no viewport, then we can assume there is no predefined set of + // rules that should be applied to this viewport while matching + if (!protocolViewport) { + return defaultReturn; + } + + // no support for drag and drop into fusion viewports yet + // Todo: smart drag and drop would look at the displaySets and + // replace the same modality type, but later + if (protocolViewport.displaySets.length > 1) { + throw new Error('Cannot update viewport with multiple displaySets yet'); + } + + // If there is no displaySet, then we can assume that the viewport + // is empty and we can just add the new displaySet to it + if (protocolViewport.displaySets.length === 0) { + return defaultReturn; + } + + // If the viewport options says to allow any instance, then we can assume + // it just updates this viewport. This is deprecated and will be removed + if (protocolViewport.viewportOptions.allowUnmatchedView) { + return defaultReturn; + } + + // if the viewport is not empty, then we check the displaySets it is showing + // currently, which means we need to check if the requested updated displaySet + // follow the same rules as the current displaySets + const { id: displaySetSelectorId, matchedDisplaySetsIndex = 0 } = + protocolViewport.displaySets[0]; + const displaySetSelector = protocol.displaySetSelectors[displaySetSelectorId]; + + // The display set can allow any view + if (!displaySetSelector || displaySetSelector.allowUnmatchedView) { + return defaultReturn; + } + + // so let's check if the new displaySetInstanceUIDs follow the same rules + this._validateViewportSpecificMatch( + { + displaySetInstanceUIDs: [newDisplaySetInstanceUID], + viewportOptions: {}, + displaySetOptions: [], + }, + protocolViewport, + protocol.displaySetSelectors + ); + // if we reach here, it means there are some rules that should be applied + + // if we don't have any match details for the displaySetSelector the viewport + // is currently showing, then we can assume that the new displaySetInstanceUID + // does not + if (!this.displaySetMatchDetails.get(displaySetSelectorId)) { + return defaultReturn; + } + + const originalProtocol = this._originalProtocol; + let originalProtocolStage; + if (!(originalProtocol instanceof Function)) { + originalProtocolStage = originalProtocol.stages[this.stageIndex]; + } + + // if we reach here, it means that the displaySetInstanceUIDs to be dropped + // for the desired viewportId are valid, and we can proceed with the update. However + // we need to check if the displaySets that the viewport were showing + // was also referenced by other viewports, and if so, we need to update those + // viewports as well + + // check if displaySetSelectors are used by other viewports, and + // store the viewportId and displaySetInstanceUIDs that need to be updated + const viewportsToUpdate = []; + protocolViewports.forEach((viewport, index) => { + const viewportNeedsUpdate = viewport.displaySets.some( + displaySet => + displaySet.id === displaySetSelectorId && + (displaySet.matchedDisplaySetsIndex || 0) === matchedDisplaySetsIndex + ); + + if (viewportNeedsUpdate) { + // Try to recompute the viewport options based on the current + // viewportId that needs update but from its old/original un-computed + // viewport & displaySet options + if (originalProtocolStage) { + const originalViewport = originalProtocolStage.viewports[index]; + const originalViewportOptions = originalViewport.viewportOptions; + const originalDisplaySetOptions = originalViewport.displaySets; + + viewport.viewportOptions = this.getComputedOptions(originalViewportOptions, [ + newDisplaySetInstanceUID, + ]); + + viewport.displaySets = this.getComputedOptions(originalDisplaySetOptions, [ + newDisplaySetInstanceUID, + ]); + } + + const displaySetInstanceUIDs = []; + const displaySetOptions = []; + + this._updateDisplaySetInstanceUIDs( + viewport, + displaySetSelectorId, + newDisplaySetInstanceUID, + this.displaySetMatchDetails, + displaySetInstanceUIDs, + displaySetOptions + ); + + viewportsToUpdate.push({ + viewportId: viewport.viewportOptions.viewportId, + displaySetInstanceUIDs, + viewportOptions: viewport.viewportOptions, + displaySetOptions, + }); + } + }); + + return viewportsToUpdate; + } + + public runMatchingRules(metadataArray, matchingRules, options) { + return this.protocolEngine.findMatch(metadataArray, matchingRules, options); + } + + private _updateDisplaySetInstanceUIDs( + viewport: HangingProtocol.Viewport, + displaySetSelectorId: string, + newDisplaySetInstanceUID: string, + displaySetMatchDetails: Map, + displaySetInstanceUIDs: string[], + displaySetOptions: HangingProtocol.DisplaySetOptions[] + ) { + viewport.displaySets.forEach(displaySet => { + const { id } = displaySet; + const displaySetMatchDetail = displaySetMatchDetails.get(id); + + const { displaySetInstanceUID: oldDisplaySetInstanceUID } = displaySetMatchDetail; + + const displaySetInstanceUID = + displaySet.id === displaySetSelectorId + ? newDisplaySetInstanceUID + : oldDisplaySetInstanceUID; + + displaySetMatchDetail.displaySetInstanceUID = displaySetInstanceUID; + + displaySetInstanceUIDs.push(displaySetInstanceUID); + displaySetOptions.push(displaySet); + }); + } + + /** + * Gets a computed options value, or a copy of the options + * This allows computing values such as the initial image index to use + * based on custom attribute functions, the same as the validators. + * Computing individual values is something that can be declared statically + * as long as the named functions are provided ahead of time, which is much + * simpler than recomputing the entire protocol. + */ + public getComputedOptions( + options: Record | Array>, + displaySetUIDs: string[] + ): any { + // Base case: if options is an array, map over the array and recursively call getComputedOptions + if (Array.isArray(options)) { + return options.map(option => this.getComputedOptions(option, displaySetUIDs)); + } + + if (options === null) { + return options; + } + if (typeof options !== 'object') { + return options; + } + + // If options is an object with a custom attribute, compute a new options object + if (options.custom) { + const displaySets = this.displaySets.filter(displaySet => + displaySetUIDs.includes(displaySet.displaySetInstanceUID) + ); + + const customKey = options.custom as string; + if (!(customKey in this.customAttributeRetrievalCallbacks)) { + throw new Error( + `Custom key "${customKey}" not found in customAttributeRetrievalCallbacks.` + ); + } + + const callback = this.customAttributeRetrievalCallbacks[customKey].callback; + let newOptions = callback.call(options, displaySets); + + if (newOptions === undefined) { + newOptions = options.defaultValue; + } + + return this.getComputedOptions(newOptions, displaySetUIDs); + } + + // If options is an object without a custom attribute, recursively call getComputedOptions on its properties + const newOptions = {} as Record; + for (const key in options) { + // if not undefined + if (options[key] !== undefined) { + newOptions[key] = this.getComputedOptions(options[key], displaySetUIDs); + } + } + + return newOptions; + } + + /** + * It applied the protocol to the current studies and display sets based on the + * protocolId that is provided. + * @param protocolId - name of the registered protocol to be set + * @param options - options to be passed to the protocol, this is either an array + * of the displaySetInstanceUIDs to be set on ALL VIEWPORTS OF THE PROTOCOL or an object + * that contains viewportId as the key and displaySetInstanceUIDs as the value + * for each viewport that needs to be set. + * @param errorCallback - callback to be called if there is an error + * during the protocol application + * + * @returns boolean - true if the protocol was applied and no errors were found + */ + public setProtocol( + protocolId: string, + options = {} as HangingProtocol.SetProtocolOptions, + errorCallback = null + ): void { + const foundProtocol = this.protocols.get(protocolId); + + if (!foundProtocol) { + console.warn( + `ProtocolEngine::setProtocol - Protocol with id ${protocolId} not found - you should register it first via addProtocol` + ); + return; + } + + try { + const protocol = this._validateProtocol(foundProtocol); + + if (options) { + this._validateOptions(options); + } + + this._setProtocol(protocol, options); + } catch (error) { + console.log(error); + + if (errorCallback) { + errorCallback(error); + } + + throw new Error(error); + } + + this._commandsManager.run(this.protocol?.callbacks?.onProtocolEnter); + } + + protected matchActivation( + matchedViewports: number, + activation: HangingProtocol.StageActivation = {}, + minViewportsMatched: number + ): boolean { + const { displaySetSelectors } = this.protocol; + + const { displaySetSelectorsMatched = [] } = activation; + for (const dsName of displaySetSelectorsMatched) { + const displaySetSelector = displaySetSelectors[dsName]; + if (!displaySetSelector) { + console.warn('No display set selector for', dsName); + return false; + } + const { bestMatch } = this._matchImages(displaySetSelector); + if (!bestMatch) { + return false; + } + } + const min = activation.minViewportsMatched ?? minViewportsMatched; + + return matchedViewports >= min; + } + /** + * Updates the stage activation, setting the stageActivation values to + * 'disabled', 'active', 'passive' where: + * * disabled means there are insufficient viewports filled to show this + * * passive means there aren't enough preferred viewports filled to show + * this stage by default, but it can be manually selected + * * enabled means there are enough viewports to select this viewport by default + * + * The logic is currently simple, just count how many viewports would be + * filled, and compare to the required/preferred count, but the intent is + * to allow more complex rules in the future as required. + * + * @returns the stage number to apply initially, given the options. + */ + private _updateStageStatus(options = null as HangingProtocol.SetProtocolOptions) { + const stages = this.protocol.stages; + for (let i = 0; i < stages.length; i++) { + const stage = stages[i]; + + const { matchedViewports } = this._matchAllViewports(stage, options, new Map()); + const activation = stage.stageActivation || {}; + if (this.matchActivation(matchedViewports, activation.passive, 0)) { + if (this.matchActivation(matchedViewports, activation.enabled, 1)) { + stage.status = 'enabled'; + } else { + stage.status = 'passive'; + } + } else { + stage.status = 'disabled'; + } + } + + this._broadcastEvent(this.EVENTS.STAGE_ACTIVATION, { + protocol: this.protocol, + stages: this.protocol.stages, + }); + } + + private _findStageIndex(options = null as HangingProtocol.SetProtocolOptions): number | void { + const stageId = options?.stageId; + const protocol = this.protocol; + const stages = protocol.stages; + + if (stageId) { + for (let i = 0; i < stages.length; i++) { + const stage = stages[i]; + if (stage.id === stageId && stage.status !== 'disabled') { + return i; + } + } + return; + } + + const stageIndex = options?.stageIndex; + if (stageIndex !== undefined) { + return stages[stageIndex]?.status !== 'disabled' ? stageIndex : undefined; + } + + let firstNotDisabled: number; + + for (let i = 0; i < stages.length; i++) { + if (stages[i].status === 'enabled') { + return i; + } + if (firstNotDisabled === undefined && stages[i].status !== 'disabled') { + firstNotDisabled = i; + } + } + + return firstNotDisabled; + } + + private _setProtocol( + protocol: HangingProtocol.Protocol, + options = null as HangingProtocol.SetProtocolOptions + ): void { + const old = this.getActiveProtocol(); + + try { + if (!this.protocol || this.protocol.id !== protocol.id) { + this.stageIndex = options?.stageIndex || 0; + //Reset load performed to false to re-fire loading strategy at new study opening + this.customImageLoadPerformed = false; + this._originalProtocol = this._copyProtocol(protocol); + + // before reassigning the protocol, we need to check if there is a callback + // on the old protocol that needs to be called + // Send the notification about updating the state + if (this.protocol?.callbacks?.onProtocolExit) { + this._commandsManager.run(this.protocol.callbacks.onProtocolExit); + } + + this.protocol = protocol; + + const { imageLoadStrategy } = protocol; + if (imageLoadStrategy) { + // check if the imageLoadStrategy is a valid strategy + if (this.registeredImageLoadStrategies[imageLoadStrategy] instanceof Function) { + this.activeImageLoadStrategyName = imageLoadStrategy; + } + } else { + this.activeImageLoadStrategyName = null; + } + + this._updateStageStatus(options); + } + + const stage = this._findStageIndex(options); + if (stage === undefined) { + throw new Error(`Can't find applicable stage ${protocol.id} ${options?.stageIndex}`); + } + this.stageIndex = stage as number; + this._updateViewports(options); + } catch (error) { + console.log(error); + Object.assign(this, old); + throw new Error(error); + } + + if (options?.restoreProtocol !== true) { + this._broadcastEvent(HangingProtocolService.EVENTS.PROTOCOL_CHANGED, { + viewportMatchDetails: this.viewportMatchDetails, + displaySetMatchDetails: this.displaySetMatchDetails, + protocol: this.protocol, + stageIdx: this.stageIndex, + stage: this.protocol.stages[this.stageIndex], + activeStudyUID: this.activeStudy?.StudyInstanceUID, + }); + } else { + this._broadcastEvent(HangingProtocolService.EVENTS.PROTOCOL_RESTORED, { + protocol: this.protocol, + stageIdx: this.stageIndex, + stage: this.protocol.stages[this.stageIndex], + activeStudyUID: this.activeStudy?.StudyInstanceUID, + }); + } + } + + public getStageIndex(protocolId: string, options): number { + const protocol = this.getProtocolById(protocolId); + const { stageId, stageIndex } = options; + if (stageId !== undefined) { + return protocol.stages.findIndex(it => it.id === stageId); + } + if (stageIndex !== undefined) { + return stageIndex; + } + return 0; + } + + /** + * Retrieves the number of Stages in the current Protocol or + * undefined if no protocol or stages are set + */ + _getNumProtocolStages() { + if (!this.protocol || !this.protocol.stages || !this.protocol.stages.length) { + return; + } + + return this.protocol.stages.length; + } + + /** + * Retrieves the current Stage from the current Protocol and stage index + * + * @returns {*} The Stage model for the currently displayed Stage + */ + _getCurrentStageModel() { + return this.protocol.stages[this.stageIndex]; + } + + /** + * Gets a new viewport object for missing viewports. Used to fill + * new viewports. + * Looks first for the stage, to see if there is a missingViewport defined, + * and secondly looks to the overall protocol. + * + * Returns a matchInfo object, which can be used to create the actual + * viewport object (which this class knows nothing about). + */ + public getMissingViewport( + protocolId: string, + stageIdx: number, + options + ): HangingProtocol.ViewportMatchDetails { + if (this.protocol.id !== protocolId) { + console.warn('setting protocol'); + this.protocol = this.getProtocolById(protocolId); + this.stageIndex = 0; + } + const protocol = this.protocol; + const stage = protocol.stages[stageIdx] ?? protocol.stages[this.stageIndex]; + const defaultViewport = stage.defaultViewport || protocol.defaultViewport; + if (!defaultViewport) { + return; + } + + const useViewport = { ...defaultViewport }; + return this._matchViewport(useViewport, options); + } + + /** + * Gets a sort function that is consistent with the display set sorting performed + * to match display sets to viewports. + * @returns a display set sort function + */ + public getDisplaySetSortFunction(): (displaySetA: DisplaySet, displaySetB: DisplaySet) => number { + return (displaySetA, displaySetB) => { + const seriesA = this._getSeriesSortInfoForDisplaySetSort(displaySetA); + const seriesB = this._getSeriesSortInfoForDisplaySetSort(displaySetB); + + return sortBy(this._getSeriesFieldForDisplaySetSort())(seriesA, seriesB); + }; + } + + /** + * Updates the viewports with the selected protocol stage. + */ + _updateViewports(options = null as HangingProtocol.SetProtocolOptions): void { + // Make sure we have an active protocol with a non-empty array of display sets + if (!this._getNumProtocolStages()) { + throw new Error('No protocol or stages found'); + } + + // each time we are updating the viewports, we need to reset the + // matching applied + this.viewportMatchDetails = new Map(); + this.displaySetMatchDetails = new Map(); + + // Retrieve the current stage + const stageModel = this._getCurrentStageModel(); + + // If the current stage does not fulfill the requirements to be displayed, + // stop here. + if ( + !stageModel || + !stageModel.viewportStructure || + !stageModel.viewports || + !stageModel.viewports.length + ) { + console.log('Stage cannot be applied', stageModel); + return; + } + + const { layoutType } = stageModel.viewportStructure; + // Retrieve the properties associated with the current display set's viewport structure template + // If no such layout properties exist, stop here. + const layoutProps = stageModel.viewportStructure.properties; + if (!layoutProps) { + console.log('No viewportStructure.properties in', stageModel); + return; + } + + const { columns: numCols, rows: numRows, layoutOptions = [] } = layoutProps; + + if (this.protocol?.callbacks?.onViewportDataInitialized) { + this._commandsManager.runCommand('attachProtocolViewportDataListener', { + protocol: this.protocol, + stageIndex: this.stageIndex, + }); + } + + this._broadcastEvent(this.EVENTS.NEW_LAYOUT, { + layoutType, + numRows, + numCols, + layoutOptions, + }); + + // Loop through each viewport + this._matchAllViewports(this.protocol.stages[this.stageIndex], options); + } + + private _matchAllViewports( + stageModel: HangingProtocol.ProtocolStage, + options?: HangingProtocol.SetProtocolOptions, + viewportMatchDetails = this.viewportMatchDetails, + displaySetMatchDetails = this.displaySetMatchDetails + ): { + matchedViewports: number; + viewportMatchDetails: Map; + displaySetMatchDetails: Map; + } { + this.activeStudy ||= this.studies[0]; + let matchedViewports = 0; + stageModel.viewports.forEach(viewport => { + const viewportId = viewport.viewportOptions.viewportId; + const matchDetails = this._matchViewport( + viewport, + options, + viewportMatchDetails, + displaySetMatchDetails + ); + if (matchDetails) { + if ( + matchDetails.displaySetsInfo?.length && + matchDetails.displaySetsInfo[0].displaySetInstanceUID + ) { + matchedViewports++; + } else { + console.log('Adding an empty set of display sets for mapping purposes'); + matchDetails.displaySetsInfo = viewport.displaySets.map(it => ({ + displaySetOptions: it, + })); + } + viewportMatchDetails.set(viewportId, matchDetails); + } + }); + return { + matchedViewports, + viewportMatchDetails, + displaySetMatchDetails, + }; + } + + protected findDeduplicatedMatchDetails( + matchDetails: HangingProtocol.DisplaySetMatchDetails, + offset: number, + options: HangingProtocol.SetProtocolOptions = {} + ): HangingProtocol.DisplaySetMatchDetails { + if (!matchDetails) { + return; + } + if (offset === 0) { + return matchDetails; + } + const { matchingScores = [] } = matchDetails; + if (offset === -1) { + const { inDisplay } = options; + if (!inDisplay) { + return matchDetails; + } + for (let i = 0; i < matchDetails.matchingScores.length; i++) { + if (inDisplay.indexOf(matchDetails.matchingScores[i].displaySetInstanceUID) === -1) { + const match = matchDetails.matchingScores[i]; + return match.matchingScore > 0 + ? { + matchingScores, + ...matchDetails.matchingScores[i], + } + : null; + } + } + return; + } + const matchFound = matchingScores[offset]; + return matchFound ? { ...matchFound, matchingScores } : undefined; + } + + protected validateDisplaySetSelectMatch( + match: HangingProtocol.DisplaySetMatchDetails, + id: string, + displaySetUID: string + ): void { + if (match.displaySetInstanceUID === displaySetUID) { + return; + } + if (!match.matchingScores) { + throw new Error('No matchingScores found in ' + match); + } + for (const subMatch of match.matchingScores) { + if (subMatch.displaySetInstanceUID === displaySetUID) { + return; + } + } + throw new Error(`Reused viewport details ${id} with ds ${displaySetUID} not valid`); + } + + protected _matchViewport( + viewport: HangingProtocol.Viewport, + options: HangingProtocol.SetProtocolOptions, + viewportMatchDetails = this.viewportMatchDetails, + displaySetMatchDetails = this.displaySetMatchDetails + ): HangingProtocol.ViewportMatchDetails { + const displaySetSelectorMap = options?.displaySetSelectorMap || {}; + const { displaySetSelectors = {} } = this.protocol; + + // Matching the displaySets + for (const displaySet of viewport.displaySets) { + const { id: displaySetId } = displaySet; + + const displaySetSelector = displaySetSelectors[displaySetId]; + + if (!displaySetSelector) { + console.warn('No display set selector for', displaySetId); + continue; + } + const { bestMatch, matchingScores } = this._matchImages(displaySetSelector); + displaySetMatchDetails.set(displaySetId, bestMatch); + + if (bestMatch) { + bestMatch.matchingScores = matchingScores; + } + } + + // Loop through each viewport + const { viewportOptions = DEFAULT_VIEWPORT_OPTIONS } = viewport; + // DisplaySets for the viewport, Note: this is not the actual displaySet, + // but it is a info to locate the displaySet from the displaySetService + const displaySetsInfo = []; + const { StudyInstanceUID: activeStudyUID } = this.activeStudy; + viewport.displaySets.forEach(displaySetOptions => { + const { id, matchedDisplaySetsIndex = 0 } = displaySetOptions; + const reuseDisplaySetUID = + id && displaySetSelectorMap[`${activeStudyUID}:${id}:${matchedDisplaySetsIndex || 0}`]; + const viewportDisplaySetMain = this.displaySetMatchDetails.get(id); + + const viewportDisplaySet = this.findDeduplicatedMatchDetails( + viewportDisplaySetMain, + matchedDisplaySetsIndex, + options + ); + + // Use the display set provided instead + if (reuseDisplaySetUID) { + // This display set should have already been validated + const displaySetInfo: HangingProtocol.DisplaySetInfo = { + displaySetInstanceUID: reuseDisplaySetUID, + displaySetOptions, + }; + + displaySetsInfo.push(displaySetInfo); + return; + } + + // Use the display set index to allow getting the "next" match, eg + // matching all display sets, and get the matchedDisplaySetsIndex'th item + if (viewportDisplaySet) { + const { displaySetInstanceUID } = viewportDisplaySet; + + const displaySetInfo: HangingProtocol.DisplaySetInfo = { + displaySetInstanceUID, + displaySetOptions, + }; + + displaySetsInfo.push(displaySetInfo); + } else { + console.warn( + ` + The hanging protocol viewport is requesting to display ${id} displaySet that is not + matched based on the provided criteria (e.g. matching rules). + ` + ); + } + }); + return { + viewportOptions, + displaySetsInfo, + }; + } + + private _validateViewportSpecificMatch( + displaySetAndViewportOptions: HangingProtocol.DisplaySetAndViewportOptions, + protocolViewport: HangingProtocol.Viewport, + displaySetSelectors: Record + ): void { + const { displaySetService } = this._servicesManager.services; + const protocolViewportDisplaySets = protocolViewport.displaySets; + const numDisplaySetsToSet = displaySetAndViewportOptions.displaySetInstanceUIDs.length; + + if ( + protocolViewportDisplaySets.length > 0 && + numDisplaySetsToSet !== protocolViewportDisplaySets.length + ) { + throw new Error( + `The number of displaySets to set ${numDisplaySetsToSet} does not match the number of displaySets in the protocol ${protocolViewportDisplaySets} - not currently implemented` + ); + } + + displaySetAndViewportOptions.displaySetInstanceUIDs.forEach(displaySetInstanceUID => { + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + + const { displaySets: displaySetsInfo } = protocolViewport; + + for (const displaySetInfo of displaySetsInfo) { + const displaySetSelector = displaySetSelectors[displaySetInfo.id]; + + if (!displaySetSelector) { + continue; + } + this._validateRequiredSelectors(displaySetSelector, displaySet); + } + }); + } + + public areRequiredSelectorsValid( + displaySetSelectors: HangingProtocol.DisplaySetSelector[], + displaySet: DisplaySet + ): boolean { + let pass = true; + for (const displaySetSelector of displaySetSelectors) { + try { + this._validateRequiredSelectors(displaySetSelector, displaySet); + } catch (error) { + pass = false; + break; + } + } + return pass; + } + + private _validateRequiredSelectors( + displaySetSelector: HangingProtocol.DisplaySetSelector, + displaySet: DisplaySet + ) { + const { seriesMatchingRules } = displaySetSelector; + + // only match the required rules + const requiredRules = seriesMatchingRules.filter(rule => rule.required); + if (requiredRules.length) { + const matched = this.protocolEngine.findMatch(displaySet, requiredRules); + + if (!matched || matched.score === 0) { + throw new Error( + `The displaySetInstanceUID ${displaySet.displaySetInstanceUID} does not satisfy the required seriesMatching criteria for the protocol` + ); + } + } + } + + _validateOptions(options: HangingProtocol.SetProtocolOptions): void { + const { displaySetService } = this._servicesManager.services; + const { displaySetSelectorMap } = options; + if (displaySetSelectorMap) { + Object.entries(displaySetSelectorMap).forEach(([key, displaySetInstanceUID]) => { + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + + if (!displaySet) { + throw new Error( + `The displaySetInstanceUID ${displaySetInstanceUID} is not found in the displaySetService` + ); + } + }); + } + } + + // Match images given a list of Studies and a Viewport's image matching reqs + protected _matchImages(displaySetRules) { + // TODO: matching is applied on study and series level, instance + // level matching needs to be added in future + + // Todo: handle fusion viewports by not taking the first displaySet rule for the viewport + const { id, studyMatchingRules = [], seriesMatchingRules } = displaySetRules; + + const matchingScores = []; + let highestSeriesMatchingScore = 0; + + console.log('ProtocolEngine::matchImages', studyMatchingRules, seriesMatchingRules); + const matchActiveOnly = this.protocol.numberOfPriorsReferenced === -1; + this.studies.forEach((study, studyInstanceUIDsIndex) => { + // Skip non-active if active only + if (matchActiveOnly && this.activeStudy !== study) { + return; + } + + const studyDisplaySets = this.displaySets.filter( + it => it.StudyInstanceUID === study.StudyInstanceUID && !it?.unsupported + ); + + const studyMatchDetails = this.protocolEngine.findMatch(study, studyMatchingRules, { + studies: this.studies, + displaySets: studyDisplaySets, + allDisplaySets: this.displaySets, + displaySetMatchDetails: this.displaySetMatchDetails, + studyInstanceUIDsIndex, + }); + + // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed + if (studyMatchDetails.requiredFailed === true) { + return; + } + + this.debug('study', study.StudyInstanceUID, 'display sets #', studyDisplaySets.length); + studyDisplaySets.forEach(displaySet => { + const { StudyInstanceUID, SeriesInstanceUID, displaySetInstanceUID } = displaySet; + const seriesMatchDetails = this.protocolEngine.findMatch( + displaySet, + seriesMatchingRules, + // Todo: why we have images here since the matching type does not have it + { + studies: this.studies, + instance: displaySet.images?.[0], + displaySetMatchDetails: this.displaySetMatchDetails, + displaySets: studyDisplaySets, + } + ); + + // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed + if (seriesMatchDetails.requiredFailed === true) { + this.debug('Display set required failed', displaySet, seriesMatchingRules); + return; + } + + this.debug('Found displaySet for rules', displaySet); + highestSeriesMatchingScore = Math.max(seriesMatchDetails.score, highestSeriesMatchingScore); + + const matchDetails = { + passed: [], + failed: [], + }; + + matchDetails.passed = matchDetails.passed.concat(seriesMatchDetails.details.passed); + matchDetails.passed = matchDetails.passed.concat(studyMatchDetails.details.passed); + + matchDetails.failed = matchDetails.failed.concat(seriesMatchDetails.details.failed); + matchDetails.failed = matchDetails.failed.concat(studyMatchDetails.details.failed); + + const totalMatchScore = seriesMatchDetails.score + studyMatchDetails.score; + + const imageDetails = { + StudyInstanceUID, + SeriesInstanceUID, + displaySetInstanceUID, + matchingScore: totalMatchScore, + matchDetails: matchDetails, + sortingInfo: { + score: totalMatchScore, + study: study.StudyInstanceUID, + ...this._getSeriesSortInfoForDisplaySetSort(displaySet), + }, + }; + + this.debug('Adding display set', displaySet, imageDetails); + matchingScores.push(imageDetails); + }); + }); + + if (matchingScores.length === 0) { + console.log('No match found', id); + } + + // Sort the matchingScores + const sortingFunction = sortBy( + { + name: 'score', + reverse: true, + }, + { + name: 'study', + reverse: true, + }, + this._getSeriesFieldForDisplaySetSort() + ); + matchingScores.sort((a, b) => sortingFunction(a.sortingInfo, b.sortingInfo)); + + const bestMatch = matchingScores[0]; + + console.log('ProtocolEngine::matchImages bestMatch', bestMatch, matchingScores); + + return { + bestMatch, + matchingScores, + }; + } + + private _getSeriesSortInfoForDisplaySetSort(displaySet) { + return { + [this._getSeriesFieldForDisplaySetSort().name]: + displaySet.SeriesNumber != null + ? parseInt(displaySet.SeriesNumber) + : parseInt(displaySet.seriesNumber), + }; + } + + private _getSeriesFieldForDisplaySetSort() { + return { name: 'series' }; + } + + /** + * Check if the next stage is available + * @return {Boolean} True if next stage is available or false otherwise + */ + _isNextStageAvailable() { + const numberOfStages = this._getNumProtocolStages(); + + return this.stageIndex + 1 < numberOfStages; + } + + /** + * Check if the previous stage is available + * @return {Boolean} True if previous stage is available or false otherwise + */ + _isPreviousStageAvailable(): boolean { + return this.stageIndex - 1 >= 0; + } + + /** + * Changes the current stage to a new stage index in the display set sequence. + * It checks if the next stage exists. + * + * @param {Integer} stageAction An integer value specifying whether next (1) or previous (-1) stage + * @return {Boolean} True if new stage has set or false, otherwise + */ + _setCurrentProtocolStage( + stageAction: number, + options: HangingProtocol.SetProtocolOptions + ): boolean { + // Check if previous or next stage is available + let i; + for ( + i = this.stageIndex + stageAction; + i >= 0 && i < this.protocol.stages.length; + i += stageAction + ) { + if (this.protocol.stages[i].status !== 'disabled') { + break; + } + } + if (i < 0 || i >= this.protocol.stages.length) { + return false; + } + + // Sets the new stage + this.stageIndex = i; + + // Log the new stage + this.debug(`ProtocolEngine::setCurrentProtocolStage stage = ${this.stageIndex}`); + + // Since stage has changed, we need to update the viewports + // and redo matchings + this._updateViewports(options); + + // Everything went well, broadcast the update, exactly identical to + // HP applied + this._broadcastEvent(this.EVENTS.PROTOCOL_CHANGED, { + viewportMatchDetails: this.viewportMatchDetails, + displaySetMatchDetails: this.displaySetMatchDetails, + protocol: this.protocol, + stageIdx: this.stageIndex, + stage: this.protocol.stages[this.stageIndex], + }); + return true; + } + + /** Set this.debugLogging to true to show debug level logging - needed + * to be able to figure out why hanging protocols are or are not applying. + */ + debug(...args): void { + if (this.debugLogging) { + console.log(...args); + } + } + + _copyProtocol(protocol: Protocol) { + return cloneDeep(protocol); + } +} diff --git a/platform/core/src/services/HangingProtocolService/ProtocolEngine.js b/platform/core/src/services/HangingProtocolService/ProtocolEngine.js new file mode 100644 index 0000000..40a2947 --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/ProtocolEngine.js @@ -0,0 +1,174 @@ +import { HPMatcher } from './HPMatcher.js'; +import { sortByScore } from './lib/sortByScore'; + +export default class ProtocolEngine { + constructor(protocols, customAttributeRetrievalCallbacks) { + this.protocols = protocols; + this.customAttributeRetrievalCallbacks = customAttributeRetrievalCallbacks; + this.matchedProtocols = new Map(); + this.matchedProtocolScores = {}; + this.study = undefined; + } + + /** Evaluate the hanging protocol matches on the given: + * @param props.studies is a list of studies to compare against (for priors evaluation) + * @param props.activeStudy is the current metadata for the study to display. + * @param props.displaySets are the list of display sets which can be modified. + */ + run({ studies, displaySets, activeStudy }) { + this.studies = studies; + this.study = activeStudy || studies[0]; + this.displaySets = displaySets; + return this.getBestProtocolMatch(); + } + + // /** + // * Resets the ProtocolEngine to the best match + // */ + // reset() { + // const protocol = this.getBestProtocolMatch(); + + // this.setHangingProtocol(protocol); + // } + + /** + * Return the best matched Protocol to the current study or set of studies + * @returns {*} + */ + getBestProtocolMatch() { + // Run the matching to populate matchedProtocols Set and Map + this.updateProtocolMatches(); + + // Retrieve the highest scoring Protocol + const bestMatch = this._getHighestScoringProtocol(); + + console.log('ProtocolEngine::getBestProtocolMatch bestMatch', bestMatch); + + return bestMatch; + } + + /** + * Populates the MatchedProtocols Collection by running the matching procedure + */ + updateProtocolMatches() { + console.log('ProtocolEngine::updateProtocolMatches'); + + // Clear all data currently in matchedProtocols + this._clearMatchedProtocols(); + + // TODO: handle more than one study - this.studies has the list of studies + const matched = this.findMatchByStudy(this.study, { + studies: this.studies, + displaySets: this.displaySets, + }); + + // For each matched protocol, check if it is already in MatchedProtocols + matched.forEach(matchedDetail => { + const protocol = matchedDetail.protocol; + if (!protocol) { + return; + } + + // If it is not already in the MatchedProtocols Collection, insert it with its score + if (!this.matchedProtocols.has(protocol.id)) { + console.log( + 'ProtocolEngine::updateProtocolMatches inserting protocol match', + matchedDetail + ); + this.matchedProtocols.set(protocol.id, protocol); + this.matchedProtocolScores[protocol.id] = matchedDetail.score; + } + }); + } + + /** + * finds the match results against the given display set or + * study instance by testing the given rules against this, and using + * the provided options for testing. + * + * @param {*} metaData to match against as primary value + * @param {*} rules to apply + * @param {*} options are additional values that can be used for matching + * @returns + */ + findMatch(metaData, rules, options) { + return HPMatcher.match(metaData, rules, this.customAttributeRetrievalCallbacks, options); + } + + /** + * Finds the best protocols from Protocol Store, matching each protocol matching rules + * with the given study. The best protocol are ordered by score and returned in an array + * @param {Object} study StudyMetadata instance object + * @param {object} options containing additional matching data. + * @return {Array} Array of match objects or an empty array if no match was found + * Each match object has the score of the matching and the matched + * protocol + */ + findMatchByStudy(study, options) { + const matched = []; + + this.protocols.forEach(protocol => { + // Clone the protocol's protocolMatchingRules array + // We clone it so that we don't accidentally add the + // numberOfPriorsReferenced rule to the Protocol itself. + let rules = protocol.protocolMatchingRules.slice(); + if (!rules || !rules.length) { + console.warn( + 'ProtocolEngine::findMatchByStudy no matching rules - specify protocolMatchingRules for', + protocol.id + ); + return; + } + + // Run the matcher and get matching details + const matchedDetails = this.findMatch(study, rules, options); + const score = matchedDetails.score; + + // The protocol matched some rule, add it to the matched list + if (score > 0) { + matched.push({ + score, + protocol, + }); + } + }); + + // If no matches were found, select the default protocol if provided + // if not select the first protocol in the list + if (!matched.length) { + const protocol = + this.protocols.find(protocol => protocol.id === 'default') ?? this.protocols[0]; + console.log('No protocol matches, defaulting to', protocol); + return [ + { + score: 0, + protocol, + }, + ]; + } + + // Sort the matched list by score + sortByScore(matched); + + console.log('ProtocolEngine::findMatchByStudy matched', matched); + + return matched; + } + + _clearMatchedProtocols() { + this.matchedProtocols.clear(); + this.matchedProtocolScores = {}; + } + + _largestKeyByValue(obj) { + return Object.keys(obj).reduce((a, b) => (obj[a] > obj[b] ? a : b)); + } + + _getHighestScoringProtocol() { + if (!Object.keys(this.matchedProtocolScores).length) { + return; + } + const highestScoringProtocolId = this._largestKeyByValue(this.matchedProtocolScores); + return this.matchedProtocols.get(highestScoringProtocolId); + } +} diff --git a/platform/core/src/services/HangingProtocolService/custom-attribute/isDisplaySetFromUrl.ts b/platform/core/src/services/HangingProtocolService/custom-attribute/isDisplaySetFromUrl.ts new file mode 100644 index 0000000..0689e0c --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/custom-attribute/isDisplaySetFromUrl.ts @@ -0,0 +1,48 @@ +import { getSplitParam } from '../../../utils'; + +/** Indicates if the given display set is the one specified in the + * displaySet parameter in the URL + * The parameters are: + * initialSeriesInstanceUID + * initialSOPInstanceUID + */ +const isDisplaySetFromUrl = (displaySet): boolean => { + const params = new URLSearchParams(window.location.search); + const initialSeriesInstanceUID = getSplitParam('initialseriesinstanceuid', params); + const initialSOPInstanceUID = getSplitParam('initialsopinstanceuid', params); + if (!initialSeriesInstanceUID && !initialSOPInstanceUID) { + return false; + } + + const isSeriesMatch = initialSeriesInstanceUID?.some( + seriesUID => displaySet.SeriesInstanceUID === seriesUID + ); + + const isSopMatch = initialSOPInstanceUID?.some(sopUID => + displaySet.instances?.some(instance => sopUID === instance.SOPInstanceUID) + ); + + return isSeriesMatch || isSopMatch; +}; + +/** Returns the index location of the requested image, or the defaultValue in this. + * Returns undefined to fallback to the defaultValue + */ +function sopInstanceLocation(displaySets) { + const displaySet = displaySets?.[0]; + if (!displaySet) { + return; + } + const initialSOPInstanceUID = getSplitParam('initialsopinstanceuid'); + if (!initialSOPInstanceUID) { + return; + } + + const index = displaySet.instances.findIndex(instance => + initialSOPInstanceUID.includes(instance.SOPInstanceUID) + ); + // Need to return in the initial position specified format. + return index === -1 ? undefined : { index }; +} + +export { isDisplaySetFromUrl, sopInstanceLocation }; diff --git a/platform/core/src/services/HangingProtocolService/custom-attribute/numberOfDisplaySetsWithImages.ts b/platform/core/src/services/HangingProtocolService/custom-attribute/numberOfDisplaySetsWithImages.ts new file mode 100644 index 0000000..d61ff3c --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/custom-attribute/numberOfDisplaySetsWithImages.ts @@ -0,0 +1,5 @@ +export default (study, extraData) => { + const ret = extraData?.displaySets?.filter(ds => ds.numImageFrames > 0)?.length; + console.log('number of display sets with images', ret); + return ret; +}; diff --git a/platform/core/src/services/HangingProtocolService/custom-attribute/seriesDescriptionsFromDisplaySets.ts b/platform/core/src/services/HangingProtocolService/custom-attribute/seriesDescriptionsFromDisplaySets.ts new file mode 100644 index 0000000..cb32f1b --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/custom-attribute/seriesDescriptionsFromDisplaySets.ts @@ -0,0 +1 @@ +export default (study, extraData) => extraData?.displaySets?.map(ds => ds.SeriesDescription); diff --git a/platform/core/src/services/HangingProtocolService/getGridMapping.ts b/platform/core/src/services/HangingProtocolService/getGridMapping.ts new file mode 100644 index 0000000..9024d86 --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/getGridMapping.ts @@ -0,0 +1,356 @@ +// This can be calculated by some formula probably, but for now we just use a constant since +// this might be objective +const GRID_MAPPINGS = { + // 1x2 + '1x2:1x1': { + 0: 0, + }, + '1x2:1x3': { + 0: 0, + 1: 1, + }, + '1x2:2x1': { + 0: 0, + 1: 1, + }, + '1x2:2x2': { + 0: 0, + 1: 1, + }, + '1x2:2x3': { + 0: 0, + 1: 1, + }, + '1x2:3x1': { + 0: 0, + 1: 1, + }, + '1x2:3x2': { + 0: 0, + 1: 1, + }, + '1x2:3x3': { + 0: 0, + 1: 1, + }, + // 1x3 + '1x3:1x1': { + 0: 0, + }, + '1x3:1x2': { + 0: 0, + 1: 1, + }, + '1x3:2x1': { + 0: 0, + 1: 1, + }, + '1x3:2x2': { + 0: 0, + 1: 1, + 2: 2, + }, + '1x3:2x3': { + 0: 0, + 1: 1, + 2: 2, + }, + '1x3:3x1': { + 0: 0, + 1: 1, + 2: 2, + }, + '1x3:3x2': { + 0: 0, + 1: 1, + 2: 2, + }, + '1x3:3x3': { + 0: 0, + 1: 1, + 2: 2, + }, + // 2x1 + '2x1:1x2': { + 0: 0, + 1: 1, + }, + '2x1:1x3': { + 0: 0, + 1: 1, + }, + '2x1:2x2': { + 0: 0, + 2: 1, + }, + '2x1:2x3': { + 0: 0, + 3: 1, + }, + '2x1:3x1': { + 0: 0, + 1: 1, + }, + '2x1:3x2': { + 0: 0, + 2: 1, + }, + '2x1:3x3': { + 0: 0, + 3: 1, + }, + // 2x2 + '2x2:1x2': { + 0: 0, + 1: 1, + }, + '2x2:1x3': { + 0: 0, + 1: 1, + 2: 2, + }, + '2x2:2x1': { + 0: 0, + 1: 2, + }, + '2x2:2x3': { + 0: 0, + 1: 1, + 3: 2, + 4: 3, + }, + '2x2:3x1': { + 0: 0, + 1: 1, + 2: 2, + }, + '2x2:3x2': { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + }, + '2x2:3x3': { + 0: 0, + 1: 1, + 3: 2, + 4: 3, + }, + // 2x3 + '2x3:1x2': { + 0: 0, + 1: 1, + }, + '2x3:1x3': { + 0: 0, + 1: 1, + 2: 2, + }, + '2x3:2x1': { + 0: 0, + 1: 3, + }, + '2x3:2x2': { + 0: 0, + 1: 1, + 2: 3, + 3: 4, + }, + '2x3:3x1': { + 0: 0, + 1: 1, + 2: 2, + }, + '2x3:3x2': { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + }, + '2x3:3x3': { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + }, + // 3x1 + '3x1:1x2': { + 0: 0, + 1: 1, + }, + '3x1:1x3': { + 0: 0, + 1: 1, + 2: 2, + }, + '3x1:2x1': { + 0: 0, + 1: 1, + }, + // TODO: I'm not sure about the following + '3x1:2x2': { + 0: 0, + 1: 1, + 2: 2, + }, + '3x1:2x3': { + 0: 0, + 1: 1, + 2: 2, + }, + '3x1:3x2': { + 0: 0, + 2: 1, + 4: 2, + }, + '3x1:3x3': { + 0: 0, + 3: 1, + 6: 2, + }, + // 3x2 + '3x2:1x2': { + 0: 0, + 1: 1, + }, + '3x2:1x3': { + 0: 0, + 1: 1, + 2: 2, + }, + '3x2:2x1': { + 0: 0, + 1: 2, + }, + '3x2:2x2': { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + }, + '3x2:2x3': { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + }, + '3x2:3x1': { + 0: 0, + 1: 2, + 2: 4, + }, + '3x2:3x3': { + 0: 0, + 1: 1, + 3: 2, + 4: 3, + 6: 4, + 7: 5, + }, + // 3x3 + '3x3:1x2': { + 0: 0, + 1: 1, + }, + '3x3:1x3': { + 0: 0, + 1: 1, + 2: 2, + }, + '3x3:2x1': { + 0: 0, + 1: 3, + }, + '3x3:2x2': { + 0: 0, + 1: 1, + 2: 3, + 3: 4, + }, + '3x3:2x3': { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + }, + '3x3:3x1': { + 0: 0, + 1: 3, + 2: 6, + }, + '3x3:3x2': { + 0: 0, + 1: 1, + 2: 3, + 3: 4, + 4: 6, + 5: 7, + }, +}; + +/** + * The purpose of this function is to convert a grid with numRows and numCols + * and index for each cell into another grid with different dimensions, but it should + * intelligently use the data from the original grid to fill the new grid at + * correct locations. + * + * For instance: + * if the old grid is a 2x2 (numRows = 2, numCols = 2) and the new grid is a 3x3 + * it should intelligently insert the cells in the new grid so that the cells + * are added to the right most column. Then the mapping is as follows: + * 0 -> 0, 1 -> 1, 3 -> 2, 4 -> 3 (viewport 2 in the old grid can be used in + * the place of viewport 3 in the new grid) + * + * Or if the old grid is 2x2 and new grid is 2x4, the mapping is as follows: + * 0 -> 0, 1 -> 1, 4 -> 2 and 5 -> 3 + * + * Or if the old grid is 2x2 and the new grid is 1x2, the mapping is as follows: + * 0 -> 0, 1 -> 2 + * + * @param {Object} oldGrid + * @param {number} oldGrid.numRows + * @param {number} oldGrid.numCols + * + * @param {Object} newGrid + * @param {number} newGrid.numRows + * @param {number} newGrid.numCols + * + * @returns {Map} A map that maps the new indices to the old indices + * + */ +const getGridMapping = (oldGrid, newGrid) => { + const mapping = {}; + const { numRows: oldNumRows, numCols: oldNumCols } = oldGrid; + const { numRows: newNumRows, numCols: newNumCols } = newGrid; + + if (oldNumRows === 1 && oldNumCols === 1) { + // If the old grid is 1x1, then we can just return the first cell + mapping[0] = 0; + return mapping; + } + + if (newNumRows === 1 && newNumCols === 1) { + // If the new grid is 1x1, then we can just return the first cell + mapping[0] = 0; + return mapping; + } + + const key = `${oldNumRows}x${oldNumCols}:${newNumRows}x${newNumCols}`; + const map = GRID_MAPPINGS[key]; + + if (!map) { + throw new Error(`No mapping found for ${key}`); + } + + return map; +}; + +export default getGridMapping; diff --git a/platform/core/src/services/HangingProtocolService/index.ts b/platform/core/src/services/HangingProtocolService/index.ts new file mode 100644 index 0000000..2d5a417 --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/index.ts @@ -0,0 +1,3 @@ +import HangingProtocolService from './HangingProtocolService'; + +export default HangingProtocolService; diff --git a/platform/core/src/services/HangingProtocolService/lib/comparators.js b/platform/core/src/services/HangingProtocolService/lib/comparators.js new file mode 100644 index 0000000..ce76a4d --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/lib/comparators.js @@ -0,0 +1,98 @@ +const comparators = [ + { + id: 'equals', + name: '= (Equals)', + validator: 'equals', + validatorOption: 'value', + description: 'The attribute must equal this value.', + }, + { + id: 'doesNotEqual', + name: '!= (Does not equal)', + validator: 'doesNotEqual', + validatorOption: 'value', + description: 'The attribute must not equal this value.', + }, + { + id: 'contains', + name: 'Contains', + validator: 'contains', + validatorOption: 'value', + description: 'The attribute must contain this value.', + }, + { + id: 'doesNotContain', + name: 'Does not contain', + validator: 'doesNotContain', + validatorOption: 'value', + description: 'The attribute must not contain this value.', + }, + { + id: 'startsWith', + name: 'Starts with', + validator: 'startsWith', + validatorOption: 'value', + description: 'The attribute must start with this value.', + }, + { + id: 'endsWith', + name: 'Ends with', + validator: 'endsWith', + validatorOption: 'value', + description: 'The attribute must end with this value.', + }, + { + id: 'onlyInteger', + name: 'Only Integers', + validator: 'numericality', + validatorOption: 'onlyInteger', + description: "Real numbers won't be allowed.", + }, + { + id: 'greaterThan', + name: '> (Greater than)', + validator: 'numericality', + validatorOption: 'greaterThan', + description: 'The attribute has to be greater than this value.', + }, + { + id: 'greaterThanOrEqualTo', + name: '>= (Greater than or equal to)', + validator: 'numericality', + validatorOption: 'greaterThanOrEqualTo', + description: 'The attribute has to be at least this value.', + }, + { + id: 'lessThanOrEqualTo', + name: '<= (Less than or equal to)', + validator: 'numericality', + validatorOption: 'lessThanOrEqualTo', + description: 'The attribute can be this value at the most.', + }, + { + id: 'lessThan', + name: '< (Less than)', + validator: 'numericality', + validatorOption: 'lessThan', + description: 'The attribute has to be less than this value.', + }, + { + id: 'odd', + name: 'Odd', + validator: 'numericality', + validatorOption: 'odd', + description: 'The attribute has to be odd.', + }, + { + id: 'even', + name: 'Even', + validator: 'numericality', + validatorOption: 'even', + description: 'The attribute has to be even.', + }, +]; + +// Immutable object +Object.freeze(comparators); + +export { comparators }; diff --git a/platform/core/src/services/HangingProtocolService/lib/displayConstraint.js b/platform/core/src/services/HangingProtocolService/lib/displayConstraint.js new file mode 100644 index 0000000..97f85be --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/lib/displayConstraint.js @@ -0,0 +1,70 @@ +const attributeCache = Object.create(null); +const REGEXP = /^\([x0-9a-f]+\)/; + +const humanize = text => { + let humanized = text.replace(/([A-Z])/g, ' $1'); // insert a space before all caps + + humanized = humanized.replace(/^./, str => { + // uppercase the first character + return str.toUpperCase(); + }); + + return humanized; +}; + +/** + * Get the text of an attribute for a given attribute + * @param {String} attributeId The attribute ID + * @param {Array} attributes Array of attributes objects with id and text properties + * @return {String} If found return the attribute text or an empty string otherwise + */ +const getAttributeText = (attributeId, attributes) => { + // If the attribute is already in the cache, return it + if (attributeId in attributeCache) { + return attributeCache[attributeId]; + } + + // Find the attribute with given attributeId + const attribute = attributes.find(attribute => attribute.id === attributeId); + + let attributeText; + + // If attribute was found get its text and save it on the cache + if (attribute) { + attributeText = attribute.text.replace(REGEXP, ''); + attributeCache[attributeId] = attributeText; + } + + return attributeText || ''; +}; + +function displayConstraint(attributeId, constraint, attributes) { + if (!constraint || !attributeId) { + return; + } + + const validatorType = Object.keys(constraint)[0]; + if (!validatorType) { + return; + } + + const validator = Object.keys(constraint[validatorType])[0]; + if (!validator) { + return; + } + + const value = constraint[validatorType][validator]; + if (value === void 0) { + return; + } + + let comparator = validator; + if (validator === 'value') { + comparator = validatorType; + } + + const attributeText = getAttributeText(attributeId, attributes); + const constraintText = attributeText + ' ' + humanize(comparator).toLowerCase() + ' ' + value; + + return constraintText; +} diff --git a/platform/core/src/services/HangingProtocolService/lib/removeFromArray.js b/platform/core/src/services/HangingProtocolService/lib/removeFromArray.js new file mode 100644 index 0000000..602cc41 --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/lib/removeFromArray.js @@ -0,0 +1,32 @@ +/** + * Removes the first instance of an element from an array, if an equal value exists + * + * @param array + * @param input + * + * @returns {boolean} Whether or not the element was found and removed + */ +const removeFromArray = (array, input) => { + // If the array is empty, stop here + if (!array || !array.length) { + return false; + } + + array.forEach((value, index) => { + // TODO: Double check whether or not this deep equality check is necessary + //if (_.isEqual(value, input)) { + if (value === input) { + indexToRemove = index; + return false; + } + }); + + if (indexToRemove === void 0) { + return false; + } + + array.splice(indexToRemove, 1); + return true; +}; + +export { removeFromArray }; diff --git a/platform/core/src/services/HangingProtocolService/lib/sortByScore.js b/platform/core/src/services/HangingProtocolService/lib/sortByScore.js new file mode 100644 index 0000000..9020725 --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/lib/sortByScore.js @@ -0,0 +1,8 @@ +// Sorts an array by score +const sortByScore = arr => { + arr.sort((a, b) => { + return b.score - a.score; + }); +}; + +export { sortByScore }; diff --git a/platform/core/src/services/HangingProtocolService/lib/validator.js b/platform/core/src/services/HangingProtocolService/lib/validator.js new file mode 100644 index 0000000..72c0a95 --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/lib/validator.js @@ -0,0 +1,463 @@ +import validate from 'validate.js'; +/** + * check if the value is strictly equal to options + * + * @example + * value = ['abc', 'def', 'GHI'] + * testValue = 'abc' (Fail) + * = ['abc'] (Fail) + * = ['abc', 'def', 'GHI'] (Valid) + * = ['abc', 'GHI', 'def'] (Fail) + * = ['abc', 'def'] (Fail) + * + * value = 'Attenuation Corrected' + * testValue = 'Attenuation Corrected' (Valid) + * testValue = 'Attenuation' (Fail) + * + * value = ['Attenuation Corrected'] + * testValue = ['Attenuation Corrected'] (Valid) + * = 'Attenuation Corrected' (Valid) + * = 'Attenuation' (Fail) + * + * */ +validate.validators.equals = function (value, options, key) { + const testValue = getTestValue(options); + const dicomArrayValue = dicomTagToArray(value); + + // If options is an array, then we need to validate each element in the array + if (Array.isArray(testValue)) { + // If the array has only one element, then we need to compare the value to that element + if (testValue.length !== dicomArrayValue.length) { + return `${key} must be an array of length ${testValue.length}`; + } else { + for (let i = 0; i < testValue.length; i++) { + if (testValue[i] !== dicomArrayValue[i]) { + return `${key} ${testValue[i]} must equal ${dicomArrayValue[i]}`; + } + } + } + } else if (testValue !== dicomArrayValue[0]) { + return `${key} must equal ${testValue}`; + } +}; +/** + * check if the value is not equal to options + * + * @example + * value = ['abc', 'def', 'GHI'] + * testValue = 'abc' (Valid) + * = ['abc'] (Valid) + * = ['abc', 'def', 'GHI'] (Fail) + * = ['abc', 'GHI', 'def'] (Valid) + * = ['abc', 'def'] (Valid) + * + * value = 'Attenuation Corrected' + * = 'Attenuation Corrected' (Fail) + * = 'Attenuation' (Valid) + * + * value = ['Attenuation Corrected'] + * testValue = ['Attenuation Corrected'] (Fail) + * = 'Attenuation Corrected' (Fail) + * = 'Attenuation' (Fail) + * */ +validate.validators.doesNotEqual = function (value, options, key) { + const testValue = getTestValue(options); + const dicomArrayValue = dicomTagToArray(value); + + if (Array.isArray(testValue)) { + if (testValue.length === dicomArrayValue.length) { + let score = 0; + testValue.forEach((x, i) => { + if (x === dicomArrayValue[i]) { + score++; + } + }); + if (score === testValue.length) { + return `${key} must not equal to ${testValue}`; + } + } + } else if (testValue === dicomArrayValue[0]) { + console.log(dicomArrayValue, testValue); + return `${key} must not equal to ${testValue}`; + } +}; + +/** + * Check if a value includes one or more specified options. + * + * @example + * value = ['abc', 'def', 'GHI'] + * testValue = โ€˜abcโ€™ (Fail) + * = โ€˜dogโ€™ (Fail) + * = [โ€˜abcโ€™] (Valid) + * = [โ€˜attโ€™, โ€˜abcโ€™] (Valid) + * = ['abc', 'def', 'dog'] (Valid) + * = ['cat', 'dog'] (Fail) + * + * value = ['Attenuation Corrected'] + * testValue = 'Attenuation Corrected' (Fail) + * = ['Attenuation Corrected', 'Corrected'] (Valid) + * = ['Attenuation', 'Corrected'] (Fail) + * + * value = 'Attenuation Corrected' + * testValue = ['Attenuation Corrected', 'Corrected'] (Valid) + * = ['Attenuation', 'Corrected'] (Fail) + * */ +validate.validators.includes = function (value, options, key) { + const testValue = getTestValue(options); + const dicomArrayValue = dicomTagToArray(value); + + if (Array.isArray(testValue)) { + const includedValues = testValue.filter(el => dicomArrayValue.includes(el)); + if (includedValues.length === 0) { + return `${key} must include at least one of the following values: ${testValue.join(', ')}`; + } + } else { + return `${key} ${testValue} must be an array`; + } + // else if (!value.includes(testValue)) { + // return `${key} ${value} must include ${testValue}`; + // } +}; +/** + * Check if a value does not include one or more specified options. + * + * @example + * value = ['abc', 'def', 'GHI'] + * testValue = ['Corr'] (Valid) + * = 'abc' (Fail) + * = ['abc'] (Fail) + * = [โ€˜attโ€™, โ€˜corโ€™] (Valid) + * = ['abc', 'def', 'dog'] (Fail) + * + * value = ['Attenuation Corrected'] + * testValue = 'Attenuation Corrected' (Fail) + * = ['Attenuation Corrected', 'Corrected'] (Fail) + * = ['Attenuation', 'Corrected'] (Valid) + * + * value = 'Attenuation Corrected' + * testValue = ['Attenuation Corrected', 'Corrected'] (Fail) + * = ['Attenuation', 'Corrected'] (Valid) + * */ +validate.validators.doesNotInclude = function (value, options, key) { + const testValue = getTestValue(options); + const dicomArrayValue = dicomTagToArray(value); + + // if (!Array.isArray(value) || value.length === 1) { + // return `${key} is not allowed as a single value`; + // } + if (Array.isArray(testValue)) { + const includedValues = testValue.filter(el => dicomArrayValue.includes(el)); + if (includedValues.length > 0) { + return `${key} must not include the following value: ${includedValues}`; + } + } else { + return `${key} ${testValue} must be an array`; + } +}; +// Ignore case contains. +// options testValue MUST be in lower case already, otherwise it won't match +/** + * @example + * value = 'Attenuation Corrected' + * testValue = โ€˜Corrโ€™ (Valid) + * = โ€˜corrโ€™ (Valid) + * = [โ€˜attโ€™, โ€˜corโ€™] (Valid) + * = [โ€˜Attโ€™, โ€˜Wallโ€™] (Valid) + * = [โ€˜catโ€™, โ€˜dogโ€™] (Fail) + * + * value = ['abc', 'def', 'GHI'] + * testValue = 'def' (Valid) + * = 'dog' (Fail) + * = ['gh', 'de'] (Valid) + * = ['cat', 'dog'] (Fail) + * + * */ +validate.validators.containsI = function (value, options, key) { + const testValue = getTestValue(options); + if (Array.isArray(value)) { + if (value.some(item => !validate.validators.containsI(item.toLowerCase(), options, key))) { + return undefined; + } + return `No item of ${value.join(',')} contains ${JSON.stringify(testValue)}`; + } + if (Array.isArray(testValue)) { + if ( + testValue.some(subTest => !validate.validators.containsI(value, subTest.toLowerCase(), key)) + ) { + return; + } + return `${key} must contain at least one of ${testValue.join(',')}`; + } + if (testValue && value.indexOf && value.toLowerCase().indexOf(testValue.toLowerCase()) === -1) { + return key + 'must contain any case of' + testValue; + } +}; +/** + * @example + * value = 'Attenuation Corrected' + * testValue = โ€˜Corrโ€™ (Valid) + * = โ€˜corrโ€™ (Fail) + * = [โ€˜attโ€™, โ€˜corโ€™] (Fail) + * = [โ€˜Attโ€™, โ€˜Wallโ€™] (Valid) + * = [โ€˜catโ€™, โ€˜dogโ€™] (Fail) + * + * value = ['abc', 'def', 'GHI'] + * testValue = 'def' (Valid) + * = 'dog' (Fail) + * = ['cat', 'de'] (Valid) + * = ['cat', 'dog'] (Fail) + * + * */ +validate.validators.contains = function (value, options, key) { + const testValue = getTestValue(options); + if (Array.isArray(value)) { + if (value.some(item => !validate.validators.contains(item, options, key))) { + return undefined; + } + return `No item of ${value.join(',')} contains ${JSON.stringify(testValue)}`; + } + if (Array.isArray(testValue)) { + if (testValue.some(subTest => !validate.validators.contains(value, subTest, key))) { + return; + } + return `${key} must contain at least one of ${testValue.join(',')}`; + } + if (testValue && value.indexOf && value.indexOf(testValue) === -1) { + return key + 'must contain ' + testValue; + } +}; +/** + * @example + * value = 'Attenuation Corrected' + * testValue = โ€˜Corrโ€™ (Fail) + * = โ€˜corrโ€™ (Valid) + * = [โ€˜attโ€™, โ€˜corโ€™] (Valid) + * = [โ€˜Attโ€™, โ€˜Wallโ€™] (Fail) + * = [โ€˜catโ€™, โ€˜dogโ€™] (Valid) + * + * value = ['abc', 'def', 'GHI'] + * testValue = 'def' (Fail) + * = 'dog' (Valid) + * = ['cat', 'de'] (Fail) + * = ['cat', 'dog'] (Valid) + * + * */ +validate.validators.doesNotContain = function (value, options, key) { + const containsResult = validate.validators.contains(value, options, key); + if (!containsResult) { + return `No item of ${value} should contain ${getTestValue(options)}`; + } +}; + +/** + * @example + * value = 'Attenuation Corrected' + * testValue = โ€˜Corrโ€™ (Fail) + * = โ€˜corrโ€™ (Fail) + * = [โ€˜attโ€™, โ€˜corโ€™] (Fail) + * = [โ€˜Attโ€™, โ€˜Wallโ€™] (Fail) + * = [โ€˜catโ€™, โ€˜dogโ€™] (Valid) + * + * value = ['abc', 'def', 'GHI'] + * testValue = 'DEF' (Fail) + * = 'dog' (Valid) + * = ['cat', 'gh'] (Fail) + * = ['cat', 'dog'] (Valid) + * + * */ +validate.validators.doesNotContainI = function (value, options, key) { + const containsResult = validate.validators.containsI(value, options, key); + if (!containsResult) { + return `No item of ${value} should not contain ${getTestValue(options)}`; + } +}; +/** + * @example + * value = 'Attenuation Corrected' + * testValue = โ€˜Corrโ€™ (Fail) + * = โ€˜Attโ€™ (Fail) + * = ['cat', 'dog', 'Att'] (Valid) + * = [โ€˜catโ€™, โ€˜dogโ€™] (Fail) + * + * value = ['abc', 'def', 'GHI'] + * testValue = 'deg' (Valid) + * = ['cat', 'GH'] (Valid) + * = ['cat', 'gh'] (Fail) + * = ['cat', 'dog'] (Fail) + * + * */ +validate.validators.startsWith = function (value, options, key) { + let testValues = getTestValue(options); + + if (typeof testValues === 'string') { + testValues = [testValues]; + } + + if (typeof value === 'string') { + if (!testValues.some(testValue => value.startsWith(testValue))) { + return key + ' must start with any of these values: ' + testValues; + } + } else if (Array.isArray(value)) { + let valid = false; + for (let i = 0; i < value.length; i++) { + for (let j = 0; j < testValues.length; j++) { + if (value[i].startsWith(testValues[j])) { + valid = true; // set valid flag to true if a match is found + break; + } + } + if (valid) { + return undefined; // break out of loop if a match is found + } + } + + if (!valid) { + return key + ' must start with any of these values: ' + testValues; // return undefined if no match is found + } + } else { + return 'Value must be a string or an array'; + } +}; + +/** + * @example + * value = 'Attenuation Corrected' + * testValue = โ€˜TEDโ€™ (Fail) + * = โ€˜tedโ€™ (Valid) + * = ['cat', 'dog', 'ted'] (Valid) + * = [โ€˜catโ€™, โ€˜dogโ€™] (Fail) + * + * value = ['abc', 'def', 'GHI'] + * testValue = 'deg' (Valid) + * = ['cat', 'HI'] (Valid) + * = ['cat', 'hi'] (Fail) + * = ['cat', 'dog'] (Fail) + * + * */ +validate.validators.endsWith = function (value, options, key) { + let testValues = getTestValue(options); + + if (typeof testValues === 'string') { + testValues = [testValues]; + } + + if (typeof value === 'string') { + if (!testValues.some(testValue => value.endsWith(testValue))) { + return key + ' must end with any of these values: ' + testValues; + } + } else if (Array.isArray(value)) { + let valid = false; + for (let i = 0; i < value.length; i++) { + for (let j = 0; j < testValues.length; j++) { + if (value[i].endsWith(testValues[j])) { + valid = true; // set valid flag to true if a match is found + break; + } + } + if (valid) { + return undefined; // break out of loop if a match is found + } + } + + if (!valid) { + return key + ' must end with any of these values: ' + testValues; // return undefined if no match is found + } + } else { + return key + ' must be a string or an array'; + } +}; +/** + * @example + * value = 30 + * testValue = 20 (Valid) + * = 40 (Fail) + * + * */ +validate.validators.greaterThan = function (value, options, key) { + const testValue = getTestValue(options); + if (Array.isArray(value) || typeof value === 'string') { + return `${key} is not allowed as an array or string`; + } + if (Array.isArray(testValue)) { + if (testValue.length === 1) { + if (!(value >= testValue[0])) { + return `${key} must be greater than or equal to ${testValue[0]}, but was ${value}`; + } + } else if (testValue.length > 1) { + return key + ' must be an array of length 1'; + } + } else { + if (!(value >= testValue)) { + return key + ' must be greater than ' + testValue; + } + } +}; +/** + * @example + * value = 30 + * testValue = 40 (Valid) + * = 20 (Fail) + * + * */ +validate.validators.lessThan = function (value, options, key) { + const testValue = getTestValue(options); + if (Array.isArray(testValue)) { + if (testValue.length === 1) { + if (!(value <= testValue[0])) { + return `${key} must be less than or equal to ${testValue[0]}, but was ${value}`; + } + } else if (testValue.length > 1) { + return key + ' must be an array of length 1'; + } + } else { + if (!(value <= testValue)) { + return key + ' must be less than ' + testValue; + } + } +}; +/** + * @example + + * + * value = 50 + * testValue = [10,60] (Valid) + * = [60, 10] (Valid) + * = [0, 10] (Fail) + * = [70, 80] (Fail) + * = 45 (Fail) + * = [45] (Fail) + * + * */ +validate.validators.range = function (value, options, key) { + const testValue = getTestValue(options); + if (Array.isArray(testValue) && testValue.length === 2) { + const min = Math.min(testValue[0], testValue[1]); + const max = Math.max(testValue[0], testValue[1]); + if (value === undefined || value < min || value > max) { + return `${key} with value ${value} must be between ${min} and ${max}`; + } + } else { + return `${key} must be an array of length 2`; + } +}; + +validate.validators.notNull = value => + value === null || value === undefined ? 'Value is null' : undefined; +const getTestValue = options => { + if (Array.isArray(options)) { + return options.map(option => option?.value ?? option); + } else { + return options?.value ?? options; + } +}; +const dicomTagToArray = value => { + let dicomArrayValue; + if (!Array.isArray(value)) { + dicomArrayValue = [value]; + } else { + dicomArrayValue = [...value]; + } + return dicomArrayValue; +}; +export default validate; diff --git a/platform/core/src/services/HangingProtocolService/lib/validator.test.js b/platform/core/src/services/HangingProtocolService/lib/validator.test.js new file mode 100644 index 0000000..4fa2001 --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/lib/validator.test.js @@ -0,0 +1,358 @@ +import validate from './validator.js'; + +describe('validator', () => { + const attributeMap = { + str: 'Attenuation Corrected', + upper: 'UPPER', + num: 3, + nullValue: null, + list: ['abc', 'def', 'GHI'], + listStr: ['Attenuation Corrected'], + }; + + const options = { + format: 'grouped', + }; + + describe('equals', () => { + it('returned undefined on strictly equals', () => { + expect( + validate(attributeMap, { listStr: { equals: ['Attenuation'] } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { listStr: { equals: 'Attenuation' } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { listStr: { equals: 'Attenuation Corrected' } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { listStr: { equals: ['Attenuation Corrected'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { str: { equals: 'Attenuation Corrected' } }, [options]) + ).toBeUndefined(); + + expect( + validate(attributeMap, { str: { equals: { value: 'Attenuation Corrected' } } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { str: { equals: ['Attenuation Corrected'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { str: { equals: ['Attenuation'] } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { list: { equals: ['abc', 'def', 'GHI'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { list: { equals: ['abc', 'GHI', 'def'] } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { list: { equals: { value: ['abc', 'def', 'GHI'] } } }, [options]) + ).toBeUndefined(); + }); + }); + describe('doesNotEqual', () => { + it('returns undefined if value does not equal ', () => { + expect( + validate(attributeMap, { listStr: { doesNotEqual: 'Attenuation Corrected' } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { listStr: { doesNotEqual: ['Attenuation Corrected'] } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { listStr: { doesNotEqual: 'Attenuation' } }, [options]) + ).toBeUndefined(); + + expect( + validate(attributeMap, { str: { doesNotEqual: 'Attenuation' } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { str: { doesNotEqual: { value: 'Attenuation' } } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { str: { doesNotEqual: ['Attenuation'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { str: { doesNotEqual: ['abc', 'def'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { list: { doesNotEqual: ['abc', 'GHI', 'def'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { list: { doesNotEqual: ['abc', 'def', 'GHI'] } }, [options]) + ).not.toBeUndefined(); + }); + }); + describe('includes', () => { + it('returns match any list includes', () => { + expect( + validate(attributeMap, { listStr: { includes: 'Attenuation Corrected' } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { listStr: { includes: ['Attenuation Corrected'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { listStr: { includes: ['Attenuation Corrected', 'Corrected'] } }, [ + options, + ]) + ).toBeUndefined(); + expect( + validate(attributeMap, { listStr: { includes: ['Attenuation', 'Corrected'] } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { str: { includes: ['Attenuation Corrected', 'Corrected'] } }, [ + options, + ]) + ).toBeUndefined(); + expect( + validate(attributeMap, { str: { includes: ['Attenuation', 'Corrected'] } }, [options]) + ).not.toBeUndefined(); + expect(validate(attributeMap, { list: { includes: ['abc'] } }, [options])).toBeUndefined(); + expect( + validate(attributeMap, { list: { includes: ['GHI', 'HI'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { list: { includes: ['HI', 'bye'] } }, [options]) + ).not.toBeUndefined(); + }); + }); + describe('doesNotInclude', () => { + it('returns undefined if list does not includes', () => { + expect( + validate(attributeMap, { listStr: { doesNotInclude: 'Attenuation Corrected' } }, [options]) + ).not.toBeUndefined(); + expect( + validate( + attributeMap, + { + listStr: { doesNotInclude: ['Attenuation Corrected', 'Corrected'] }, + }, + [options] + ) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { listStr: { doesNotInclude: ['Attenuation', 'Corrected'] } }, [ + options, + ]) + ).toBeUndefined(); + expect( + validate( + attributeMap, + { str: { doesNotInclude: ['Attenuation Corrected', 'Corrected'] } }, + [options] + ) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { str: { doesNotInclude: ['Attenuation', 'Corrected'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { list: { doesNotInclude: ['Corr'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { list: { doesNotInclude: 'abc' } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { list: { doesNotInclude: { value: ['abc'] } } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { list: { doesNotInclude: { value: ['att', 'cor'] } } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { list: { doesNotInclude: { value: ['abc', 'def', 'dog'] } } }, [ + options, + ]) + ).not.toBeUndefined(); + }); + }); + describe('containsI', () => { + it('returns match any list contains case insensitive', () => { + expect( + validate(attributeMap, { upper: { containsI: ['hi', 'pre'] } }, [options]) + ).not.toBeUndefined(); + expect(validate(attributeMap, { list: { containsI: 'hi' } }, [options])).toBeUndefined(); + expect( + validate(attributeMap, { list: { containsI: ['ghi', 'bye'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { list: { containsI: ['bye', 'hi'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { list: { containsI: ['ig', 'hi'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { upper: { containsI: ['bye', 'per'] } }, [options]) + ).toBeUndefined(); + }); + }); + describe('contains', () => { + it('returns match any list contains', () => { + expect(validate(attributeMap, { str: { contains: 'Corr' } }, [options])).toBeUndefined(); + expect( + validate(attributeMap, { str: { contains: { value: 'Corr' } } }, [options]) + ).toBeUndefined(); + expect(validate(attributeMap, { str: { contains: ['Corr'] } }, [options])).toBeUndefined(); + expect( + validate(attributeMap, { str: { contains: ['corr'] } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { str: { contains: ['Att', 'Wall'] } }, [options]) + ).toBeUndefined(); + expect(validate(attributeMap, { list: { contains: 'GH' } }, [options])).toBeUndefined(); + expect(validate(attributeMap, { list: { contains: ['ab'] } }, [options])).toBeUndefined(); + + expect( + validate(attributeMap, { list: { contains: ['z', 'bc'] } }, [options]) + ).toBeUndefined(); + expect(validate(attributeMap, { list: { contains: ['z'] } }, [options])).not.toBeUndefined(); + }); + }); + + describe('doesNotContain', () => { + it('returns undefined if string does not contain specified value', () => { + expect( + validate(attributeMap, { str: { doesNotContain: ['att', 'wall'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { str: { doesNotContain: 'Corr' } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { str: { doesNotContain: 'corr' } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { str: { doesNotContain: { value: 'corr' } } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { str: { doesNotContain: ['att', 'cor'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { str: { doesNotContain: ['Att', 'cor'] } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { str: { doesNotContain: ['bye', 'hi'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { list: { doesNotContain: ['GHI', 'hi'] } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { list: { doesNotContain: ['hi'] } }, [options]) + ).toBeUndefined(); + }); + }); + describe('doesNotContainI', () => { + it('returns undefined if string does not contain specified value', () => { + expect( + validate(attributeMap, { str: { doesNotContainI: 'corr' } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { str: { doesNotContainI: 'Corr' } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { str: { doesNotContainI: ['att', 'cor'] } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { str: { doesNotContainI: ['Att', 'wall'] } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { str: { doesNotContainI: ['bye', 'hi'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { list: { doesNotContainI: ['bye', 'ABC'] } }, [options]) + ).not.toBeUndefined(); + expect( + validate(attributeMap, { list: { doesNotContainI: 'bye' } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { list: { doesNotContainI: ['bye', 'ABC'] } }, [options]) + ).not.toBeUndefined(); + }); + }); + describe('startsWith', () => { + it('returns undefined if string starts with specified value', () => { + expect( + validate(attributeMap, { str: { startsWith: { value: 'Atte' } } }, [options]) + ).toBeUndefined(); + expect(validate(attributeMap, { str: { startsWith: 'Att' } }, [options])).toBeUndefined(); + expect( + validate(attributeMap, { str: { startsWith: ['cat', 'dog', 'Att'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { str: { startsWith: ['cat', 'dog'] } }, [options]) + ).not.toBeUndefined(); + expect(validate(attributeMap, { list: { startsWith: ['GH'] } }, [options])).toBeUndefined(); + expect( + validate(attributeMap, { list: { startsWith: ['de', 'bye'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { list: { startsWith: ['hi', 'bye'] } }, [options]) + ).not.toBeUndefined(); + }); + }); + describe('endsWith', () => { + it('returns undefined if string ends with specified value', () => { + expect(validate(attributeMap, { str: { endsWith: 'ted' } }, [options])).toBeUndefined(); + expect( + validate(attributeMap, { str: { endsWith: { value: 'ted' } } }, [options]) + ).toBeUndefined(); + expect(validate(attributeMap, { str: { endsWith: ['ted'] } }, [options])).toBeUndefined(); + expect(validate(attributeMap, { str: { endsWith: ['Att'] } }, [options])).not.toBeUndefined(); + expect( + validate(attributeMap, { str: { endsWith: ['cat', 'dog', 'ted'] } }, [options]) + ).toBeUndefined(); + expect(validate(attributeMap, { list: { endsWith: ['HI'] } }, [options])).toBeUndefined(); + expect( + validate(attributeMap, { list: { endsWith: ['bc', 'dog', 'ted'] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { list: { endsWith: ['bye', 'dog'] } }, [options]) + ).not.toBeUndefined(); + }); + }); + + describe('greaterThan', () => { + it('returns undefined on greaterThan', () => { + expect( + validate(attributeMap, { num: { greaterThan: { value: attributeMap.num - 1 } } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { num: { greaterThan: attributeMap.num - 1 } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { num: { greaterThan: [attributeMap.num - 1] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { num: { greaterThan: [attributeMap.num + 1] } }, [options]) + ).not.toBeUndefined(); + }); + }); + describe('lessThan', () => { + it('returns undefined on lessThan', () => { + expect( + validate(attributeMap, { num: { lessThan: { value: attributeMap.num + 1 } } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { num: { lessThan: attributeMap.num + 1 } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { num: { lessThan: [attributeMap.num + 1] } }, [options]) + ).toBeUndefined(); + expect( + validate(attributeMap, { num: { lessThan: [attributeMap.num - 1] } }, [options]) + ).not.toBeUndefined(); + }); + }); + describe('range', () => { + it('returns undefined if the value is between', () => { + expect( + validate(attributeMap, { num: { range: [attributeMap.num + 1, attributeMap.num - 1] } }, [ + options, + ]) + ).toBeUndefined(); + expect(validate(attributeMap, { num: { range: [1, 4] } }, [options])).toBeUndefined(); + expect(validate(attributeMap, { num: { range: [1, 2] } }, [options])).not.toBeUndefined(); + expect(validate(attributeMap, { num: { range: [4, 5] } }, [options])).not.toBeUndefined(); + expect(validate(attributeMap, { num: { range: [5] } }, [options])).not.toBeUndefined(); + expect(validate(attributeMap, { num: { range: 5 } }, [options])).not.toBeUndefined(); + }); + }); +}); diff --git a/platform/core/src/services/MeasurementService/MeasurementService.test.js b/platform/core/src/services/MeasurementService/MeasurementService.test.js new file mode 100644 index 0000000..51ec9b6 --- /dev/null +++ b/platform/core/src/services/MeasurementService/MeasurementService.test.js @@ -0,0 +1,505 @@ +import MeasurementService from './MeasurementService'; +import log from '../../log'; + +jest.mock('../../log', () => ({ + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), +})); + +describe('MeasurementService.js', () => { + const unmappedMeasurementUID = 'unmappedMeasurementUId'; + let measurementService; + let measurement; + let unmappedMeasurement; + let source; + let annotationType; + let matchingCriteria; + let toSourceSchema; + let toMeasurement; + let toMeasurementThrowsError; + let annotation; + + beforeEach(() => { + measurementService = new MeasurementService(); + source = measurementService.createSource('Test', '1'); + annotationType = 'Length'; + annotation = { + toolName: annotationType, + measurementData: {}, + }; + measurement = { + SOPInstanceUID: '123', + FrameOfReferenceUID: '1234', + referenceSeriesUID: '12345', + label: 'Label', + description: 'Description', + unit: 'mm', + area: 123, + type: measurementService.VALUE_TYPES.POLYLINE, + points: [ + { x: 1, y: 2 }, + { x: 1, y: 2 }, + ], + source: source, + }; + // A measurement with various metadata missing (e.g. referenced SOPInstanceUID) that + // would not typically get mapped my the MeasurementService possibly because it was + // made in a non-acquisition plane of a volume. + unmappedMeasurement = { + uid: unmappedMeasurementUID, + SOPInstanceUID: undefined, + FrameOfReferenceUID: undefined, + referenceSeriesUID: undefined, + label: 'Label', + description: 'Description', + unit: 'mm', + area: 123, + type: measurementService.VALUE_TYPES.POLYLINE, + points: [ + { x: 1, y: 2 }, + { x: 1, y: 2 }, + ], + source: source, + }; + toSourceSchema = () => annotation; + toMeasurement = () => { + if (Object.keys(measurement).includes('invalidProperty')) { + throw new Error('Measurement does not match schema'); + } + + return measurement; + }; + toMeasurementThrowsError = () => { + throw new Error('Unmapped measurement.'); + }; + matchingCriteria = { + valueType: measurementService.VALUE_TYPES.POLYLINE, + points: 2, + }; + log.warn.mockClear(); + jest.clearAllMocks(); + }); + + describe('createSource()', () => { + it('creates new source with name and version', () => { + measurementService.createSource('Testing', '1'); + }); + + it('throws Error if no name provided', () => { + expect(() => { + measurementService.createSource(null, '1'); + }).toThrow(new Error('Source name not provided.')); + }); + + it('throws Error if no version provided', () => { + expect(() => { + measurementService.createSource('Testing', null); + }).toThrow(new Error('Source version not provided.')); + }); + }); + + describe('addMapping()', () => { + it('adds new mapping', () => { + measurementService.addMapping( + source, + annotationType, + matchingCriteria, + toSourceSchema, + toMeasurement + ); + }); + + it('throws Error if invalid source provided', () => { + expect(() => { + const invalidSource = {}; + + measurementService.addMapping( + invalidSource, + annotationType, + matchingCriteria, + toSourceSchema, + toMeasurement + ); + }).toThrow(new Error('Invalid source.')); + }); + + it('throws Error if no matching criteria provided', () => { + expect(() => { + measurementService.addMapping(source, annotationType, null, toSourceSchema, toMeasurement); + }).toThrow(new Error('Matching criteria not provided.')); + }); + + it('throws Error if no source provided', () => { + expect(() => { + measurementService.addMapping( + null /* source */, + annotationType, + matchingCriteria, + toSourceSchema, + toMeasurement + ); + }).toThrow(new Error('Invalid source.')); + }); + + it('logs warning and return early if no AnnotationType provided', () => { + expect(() => { + measurementService.addMapping( + source, + null /* AnnotationType */, + matchingCriteria, + toSourceSchema, + toMeasurement + ); + }).toThrow(new Error('annotationType not provided.')); + }); + + it('throws Error if no measurement mapping function provided', () => { + expect(() => { + measurementService.addMapping( + source, + annotationType, + matchingCriteria, + null /* toSourceSchema */, + toMeasurement + ); + }).toThrow(new Error('Mapping function to source schema not provided.')); + }); + + it('throws Error if no annotation mapping function provided', () => { + expect(() => { + measurementService.addMapping( + source, + annotationType, + matchingCriteria, + toSourceSchema, + null /* toMeasurement */ + ); + }).toThrow(new Error('Measurement mapping function not provided.')); + }); + }); + + describe('getAnnotation()', () => { + it('get annotation based on matched criteria', () => { + measurementService.addMapping( + source, + annotationType, + matchingCriteria, + toSourceSchema, + toMeasurement + ); + const measurementId = source.annotationToMeasurement(annotationType, annotation); + const mappedAnnotation = source.getAnnotation(annotationType, measurementId); + + expect(annotation).toBe(mappedAnnotation); + }); + + it('get annotation based on source and annotationType', () => { + measurementService.addMapping(source, annotationType, {}, toSourceSchema, toMeasurement); + const measurementId = source.annotationToMeasurement(annotationType, annotation); + const mappedAnnotation = source.getAnnotation(annotationType, measurementId); + + expect(annotation).toBe(mappedAnnotation); + }); + }); + + describe('getMeasurements()', () => { + it('return all measurement service measurements', () => { + const anotherMeasurement = { + ...measurement, + label: 'Label2', + unit: 'HU', + }; + + measurementService.addMapping( + source, + annotationType, + matchingCriteria, + toSourceSchema, + toMeasurement + ); + + source.annotationToMeasurement(annotationType, measurement); + source.annotationToMeasurement(annotationType, anotherMeasurement); + + const measurements = measurementService.getMeasurements(); + + expect(measurements.length).toEqual(2); + }); + }); + + describe('getMeasurement()', () => { + it('return measurement service measurement with given id', () => { + measurementService.addMapping( + source, + annotationType, + matchingCriteria, + toSourceSchema, + toMeasurement + ); + + const uid = source.annotationToMeasurement(annotationType, measurement); + const returnedMeasurement = measurementService.getMeasurement(uid); + + /* Clear dynamic data */ + delete returnedMeasurement.modifiedTimestamp; + + expect({ uid, ...measurement }).toEqual(returnedMeasurement); + }); + }); + + describe('annotationToMeasurement()', () => { + it('adds new measurements', () => { + measurementService.addMapping( + source, + annotationType, + matchingCriteria, + toSourceSchema, + toMeasurement + ); + + source.annotationToMeasurement(annotationType, measurement); + source.annotationToMeasurement(annotationType, measurement); + + const measurements = measurementService.getMeasurements(); + + expect(measurements.length).toBe(2); + }); + + it('fails to add new measurements when no mapping', () => { + expect(() => { + source.annotationToMeasurement(annotationType, measurement); + }).toThrow(); + }); + + it('fails to add new measurements when invalid mapping function', () => { + measurementService.addMapping( + source, + annotationType, + matchingCriteria, + toSourceSchema, + 1 /* Invalid */ + ); + + expect(() => { + source.annotationToMeasurement(annotationType, measurement); + }).toThrow(); + }); + + it('adds new measurement with custom uid', () => { + const newMeasurement = { uid: 1, ...measurement }; + + measurementService.addMapping( + source, + annotationType, + matchingCriteria, + toSourceSchema, + toMeasurement + ); + + /* Add new measurement */ + source.annotationToMeasurement(annotationType, newMeasurement); + const savedMeasurement = measurementService.getMeasurement(newMeasurement.uid); + + /* Clear dynamic data */ + delete newMeasurement.modifiedTimestamp; + delete savedMeasurement.modifiedTimestamp; + + expect(newMeasurement).toEqual(savedMeasurement); + }); + + it('throws Error if adding invalid measurement', () => { + measurement.invalidProperty = {}; + + measurementService.addMapping( + source, + annotationType, + matchingCriteria, + toSourceSchema, + toMeasurement + ); + + expect(() => { + source.annotationToMeasurement(annotationType, measurement); + }).toThrow(); + }); + + it('throws Error if adding measurement with unknown schema key', () => { + measurementService.addMapping( + source, + annotationType, + matchingCriteria, + toSourceSchema, + () => { + return { + ...measurement, + invalidSchemaKey: 0, + }; + } + ); + + expect(() => { + source.annotationToMeasurement(annotationType, measurement); + }).toThrow(); + }); + + it('updates existing measurement', () => { + measurementService.addMapping( + source, + annotationType, + matchingCriteria, + toSourceSchema, + toMeasurement + ); + + const uid = source.annotationToMeasurement(annotationType, measurement); + + measurement.unit = 'HU'; + + source.annotationToMeasurement(annotationType, { uid, ...measurement }); + const updatedMeasurement = measurementService.getMeasurement(uid); + + expect(updatedMeasurement.unit).toBe('HU'); + }); + }); + + describe('subscribe()', () => { + it('subscribers receive broadcasted add event', () => { + measurementService.addMapping( + source, + annotationType, + matchingCriteria, + toSourceSchema, + toMeasurement + ); + + const { MEASUREMENT_ADDED } = measurementService.EVENTS; + let addCallbackWasCalled = false; + + /* Subscribe to add event */ + measurementService.subscribe(MEASUREMENT_ADDED, () => (addCallbackWasCalled = true)); + + /* Add new measurement - two calls needed for the start and the other for the completed*/ + const uid = source.annotationToMeasurement(annotationType, measurement); + source.annotationToMeasurement(annotationType, { uid, ...measurement }); + + expect(addCallbackWasCalled).toBe(true); + }); + + it('subscribers receive broadcasted update event', () => { + measurementService.addMapping( + source, + annotationType, + matchingCriteria, + toSourceSchema, + toMeasurement + ); + + const { MEASUREMENT_UPDATED } = measurementService.EVENTS; + let updateCallbackWasCalled = false; + + /* Subscribe to update event */ + measurementService.subscribe(MEASUREMENT_UPDATED, () => (updateCallbackWasCalled = true)); + + /* Create measurement */ + const uid = source.annotationToMeasurement(annotationType, measurement); + + /* Update measurement */ + source.annotationToMeasurement(annotationType, { uid, ...measurement }, true); + + expect(updateCallbackWasCalled).toBe(true); + }); + + it('unsubscribes a listener', () => { + measurementService.addMapping( + source, + annotationType, + matchingCriteria, + toSourceSchema, + toMeasurement + ); + + let updateCallbackWasCalled = false; + const { MEASUREMENT_ADDED } = measurementService.EVENTS; + + /* Subscribe to Add event */ + const { unsubscribe } = measurementService.subscribe( + MEASUREMENT_ADDED, + () => (updateCallbackWasCalled = true) + ); + + /* Unsubscribe */ + unsubscribe(); + + /* Create measurement - two calls needed one to start and one to complete */ + const uid = source.annotationToMeasurement(annotationType, measurement); + source.annotationToMeasurement(annotationType, { uid, ...measurement }); + + expect(updateCallbackWasCalled).toBe(false); + }); + + it('subscribers do NOT receive add unmapped measurements event', () => { + measurementService.addMapping( + source, + annotationType, + matchingCriteria, + toSourceSchema, + toMeasurementThrowsError + ); + + const { MEASUREMENT_ADDED } = measurementService.EVENTS; + let addCallbackWasCalled = false; + + /* Subscribe to add event */ + measurementService.subscribe(MEASUREMENT_ADDED, () => (addCallbackWasCalled = true)); + + /* Add new measurement - two calls needed for the start and the other for the completed*/ + // expect exceptions for unmapped measurements + expect(() => { + source.annotationToMeasurement(annotationType, unmappedMeasurement); + }).toThrow(); + + expect(() => { + source.annotationToMeasurement(annotationType, { + unmappedMeasurementUID, + ...unmappedMeasurement, + }); + }).toThrow(); + + expect(addCallbackWasCalled).toBe(false); + }); + + it('subscribers do receive remove unmapped measurements event', () => { + measurementService.addMapping( + source, + annotationType, + matchingCriteria, + toSourceSchema, + toMeasurementThrowsError + ); + + const { MEASUREMENT_REMOVED } = measurementService.EVENTS; + let removeCallbackWasCalled = false; + + /* Subscribe to add event */ + measurementService.subscribe(MEASUREMENT_REMOVED, () => (removeCallbackWasCalled = true)); + + /* Add new measurement - two calls needed for the start and the other for the completed*/ + // expect exceptions for unmapped measurements + expect(() => { + source.annotationToMeasurement(annotationType, unmappedMeasurement); + }).toThrow(); + + expect(() => { + source.annotationToMeasurement(annotationType, { + unmappedMeasurementUID, + ...unmappedMeasurement, + }); + }).toThrow(); + + measurementService.remove(unmappedMeasurementUID); + + expect(removeCallbackWasCalled).toBe(true); + }); + }); +}); diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts new file mode 100644 index 0000000..0fa3bf7 --- /dev/null +++ b/platform/core/src/services/MeasurementService/MeasurementService.ts @@ -0,0 +1,808 @@ +import log from '../../log'; +import guid from '../../utils/guid'; +import { PubSubService } from '../_shared/pubSubServiceInterface'; + +/** + * Measurement source schema + * + * @typedef {Object} MeasurementSource + * @property {number} id - + * @property {string} name - + * @property {string} version - + */ + +/** + * Measurement schema + * + * @typedef {Object} Measurement + * @property {number} uid - + * @property {string} SOPInstanceUID - + * @property {string} FrameOfReferenceUID - + * @property {string} referenceSeriesUID - + * @property {string} label - + * @property {string} description - + * @property {string} type - + * @property {string} unit - + * @property {number} area - + * @property {Array} points - + * @property {MeasurementSource} source - + * @property {boolean} selected - + */ + +/* Measurement schema keys for object validation. */ +const MEASUREMENT_SCHEMA_KEYS = [ + 'uid', + 'color', + 'data', + 'getReport', + 'displayText', + 'SOPInstanceUID', + 'FrameOfReferenceUID', + 'referenceStudyUID', + 'referenceSeriesUID', + 'frameNumber', + 'displaySetInstanceUID', + 'label', + 'isLocked', + 'isVisible', + 'description', + 'type', + 'unit', + 'points', + 'source', + 'toolName', + 'metadata', + // Todo: we shouldn't need to have all these here. + 'area', // TODO: Add concept names instead (descriptor) + 'mean', + 'stdDev', + 'perimeter', + 'length', + 'shortestDiameter', + 'longestDiameter', + 'cachedStats', + 'isSelected', + 'textBox', + 'referencedImageId', +]; + +const EVENTS = { + MEASUREMENT_UPDATED: 'event::measurement_updated', + INTERNAL_MEASUREMENT_UPDATED: 'event:internal_measurement_updated', + MEASUREMENT_ADDED: 'event::measurement_added', + RAW_MEASUREMENT_ADDED: 'event::raw_measurement_added', + MEASUREMENT_REMOVED: 'event::measurement_removed', + MEASUREMENTS_CLEARED: 'event::measurements_cleared', + // Give the viewport a chance to jump to the measurement + JUMP_TO_MEASUREMENT_VIEWPORT: 'event:jump_to_measurement_viewport', + // Give the layout a chance to jump to the measurement + JUMP_TO_MEASUREMENT_LAYOUT: 'event:jump_to_measurement_layout', +}; + +const VALUE_TYPES = { + ANGLE: 'value_type::polyline', + POLYLINE: 'value_type::polyline', + POINT: 'value_type::point', + BIDIRECTIONAL: 'value_type::shortAxisLongAxis', // TODO -> Discuss with Danny. => just using SCOORD values isn't enough here. + ELLIPSE: 'value_type::ellipse', + RECTANGLE: 'value_type::rectangle', + MULTIPOINT: 'value_type::multipoint', + CIRCLE: 'value_type::circle', + ROI_THRESHOLD: 'value_type::roiThreshold', + ROI_THRESHOLD_MANUAL: 'value_type::roiThresholdManual', +}; + +export type MeasurementFilter = (measurement) => boolean; + +/** + * MeasurementService class that supports source management and measurement management. + * Sources can be any library that can provide "annotations" (e.g. cornerstone-tools, cornerstone, etc.) + * The flow, is that by creating a source and mappings (annotation <-> measurement), we + * can convert back and forth between the two. MeasurementPanel in OHIF uses the measurement service + * to manage the measurements, and any edit to the measurements will be reflected back at the + * library level state (e.g. cornerstone-tools, cornerstone, etc.) by converting the + * edited measurements back to the original annotations and then updating the annotations. + * + * Note and Todo: We should be able to support measurements that are composed of multiple + * annotations, but that is not the case at the moment. + */ +class MeasurementService extends PubSubService { + public static REGISTRATION = { + name: 'measurementService', + altName: 'MeasurementService', + create: _options => { + return new MeasurementService(); + }, + }; + + public static readonly EVENTS = EVENTS; + public static VALUE_TYPES = VALUE_TYPES; + public readonly VALUE_TYPES = VALUE_TYPES; + + private measurements = new Map(); + private unmappedMeasurements = new Map(); + + private sources = {}; + private mappings = {}; + + constructor() { + super(EVENTS); + } + + /** + * Adds the given schema to the measurement service schema list. + * This method should be used to add custom tool schema to the measurement service. + * @param {Array} schema schema for validation + */ + public addMeasurementSchemaKeys(schema): void { + if (!Array.isArray(schema)) { + schema = [schema]; + } + + MEASUREMENT_SCHEMA_KEYS.push(...schema); + } + + /** + * Adds the given valueType to the measurement service valueType object. + * This method should be used to add custom valueType to the measurement service. + * @param {*} valueType + * @returns + */ + addValueType(valueType) { + if (VALUE_TYPES[valueType]) { + return; + } + + // check if valuetype is valid , and if values are strings + if (!valueType || typeof valueType !== 'object') { + console.warn(`MeasurementService: addValueType: invalid valueType: ${valueType}`); + return; + } + + Object.keys(valueType).forEach(key => { + if (!VALUE_TYPES[key]) { + VALUE_TYPES[key] = valueType[key]; + } + }); + } + + /** + * Gets measurements, optionally filtered by the filter + * function. + * + * @return {Measurement[]} Array of measurements + */ + public getMeasurements(filter?: MeasurementFilter) { + const measurements = [...this.measurements.values()]; + return filter + ? measurements.filter(measurement => filter.call(this, measurement)) + : measurements; + } + + /** + * Get specific measurement by its uid. + * + * @param {string} uid measurement uid + * @return {Measurement} Measurement instance + */ + public getMeasurement(measurementUID: string) { + return this.measurements.get(measurementUID); + } + + public setMeasurementSelected(measurementUID: string, selected: boolean): void { + const measurement = this.getMeasurement(measurementUID); + if (!measurement) { + return; + } + + measurement.isSelected = selected; + + this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, { + source: measurement.source, + measurement, + notYetUpdatedAtSource: false, + }); + } + + /** + * Create a new source. + * + * @param {string} name Name of the source + * @param {string} version Source name + * @return {MeasurementSource} Measurement source instance + */ + createSource(name, version) { + if (!name) { + throw new Error('Source name not provided.'); + } + + if (!version) { + throw new Error('Source version not provided.'); + } + + // Go over all the keys inside the sources and check if the source + // name and version matches with the existing sources. + const sourceKeys = Object.keys(this.sources); + + for (let i = 0; i < sourceKeys.length; i++) { + const source = this.sources[sourceKeys[i]]; + if (source.name === name && source.version === version) { + return source; + } + } + + const uid = guid(); + const source = { + uid, + name, + version, + }; + + source.annotationToMeasurement = (annotationType, annotation, isUpdate = false) => { + return this.annotationToMeasurement(source, annotationType, annotation, isUpdate); + }; + + source.remove = (measurementUID, eventDetails) => { + return this.remove(measurementUID, source, eventDetails); + }; + + source.getAnnotation = (annotationType, measurementId) => { + return this.getAnnotation(source, annotationType, measurementId); + }; + + log.info(`New '${name}@${version}' source added.`); + this.sources[uid] = source; + + return source; + } + + getSource(name, version) { + const { sources } = this; + const uid = this._getSourceUID(name, version); + + return sources[uid]; + } + + getSourceMappings(name, version) { + const { mappings } = this; + const uid = this._getSourceUID(name, version); + + return mappings[uid]; + } + + /** + * Add a new measurement matching criteria along with mapping functions. + * + * @param {MeasurementSource} source Measurement source instance + * @param {string} annotationType annotation type to match which can be e.g., Length, Bidirectional, etc. + * @param {MatchingCriteria} matchingCriteria The matching criteria + * @param {Function} toAnnotationSchema Mapping function to annotation schema + * @param {Function} toMeasurementSchema Mapping function to measurement schema + * @return void + */ + addMapping(source, annotationType, matchingCriteria, toAnnotationSchema, toMeasurementSchema) { + if (!this._isValidSource(source)) { + throw new Error('Invalid source.'); + } + + if (!matchingCriteria) { + throw new Error('Matching criteria not provided.'); + } + + if (!annotationType) { + throw new Error('annotationType not provided.'); + } + + if (!toAnnotationSchema) { + throw new Error('Mapping function to source schema not provided.'); + } + + if (!toMeasurementSchema) { + throw new Error('Measurement mapping function not provided.'); + } + + const mapping = { + matchingCriteria, + annotationType, + toAnnotationSchema, + toMeasurementSchema, + }; + + if (Array.isArray(this.mappings[source.uid])) { + this.mappings[source.uid].push(mapping); + } else { + this.mappings[source.uid] = [mapping]; + } + + log.info(`New measurement mapping added to source '${this._getSourceToString(source)}'.`); + } + + /** + * Get annotation for specific source. + * + * @param {MeasurementSource} source Measurement source instance + * @param {string} annotationType The source annotationType + * @param {string} measurementUID The measurement service measurement uid + * @return {Object} Source measurement schema + */ + getAnnotation(source, annotationType, measurementUID) { + if (!this._isValidSource(source)) { + log.warn('Invalid source. Exiting early.'); + return; + } + + if (!annotationType) { + log.warn('No source annotationType provided. Exiting early.'); + return; + } + + const measurement = this.getMeasurement(measurementUID); + const mapping = this._getMappingByMeasurementSource(measurement, annotationType); + + if (mapping) { + return mapping.toAnnotationSchema(measurement, annotationType); + } + + const matchingMapping = this._getMatchingMapping(source, annotationType, measurement); + + if (matchingMapping) { + log.info('Matching mapping found:', matchingMapping); + const { toAnnotationSchema, annotationType } = matchingMapping; + return toAnnotationSchema(measurement, annotationType); + } + } + + update(measurementUID: string, measurement, notYetUpdatedAtSource = false) { + if (!this.measurements.has(measurementUID)) { + return; + } + + const updatedMeasurement = { + ...measurement, + modifiedTimestamp: Math.floor(Date.now() / 1000), + }; + + log.info(`Updating internal measurement representation...`, updatedMeasurement); + + this.measurements.set(measurementUID, updatedMeasurement); + + this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, { + source: measurement.source, + measurement: updatedMeasurement, + notYetUpdatedAtSource, + }); + + return updatedMeasurement.uid; + } + + /** + * Add a raw measurement into a source so that it may be + * Converted to/from annotation in the same way. E.g. import serialized data + * of the same form as the measurement source. + * @param {MeasurementSource} source The measurement source instance. + * @param {string} annotationType The source annotationType you want to add the measurement to. + * @param {object} data The data you wish to add to the source. + * @param {function} toMeasurementSchema A function to get the `data` into the same shape as the source annotationType. + */ + addRawMeasurement(source, annotationType, data, toMeasurementSchema, dataSource = {}) { + if (!this._isValidSource(source)) { + log.warn('Invalid source. Exiting early.'); + return; + } + + const sourceInfo = this._getSourceToString(source); + + if (!annotationType) { + log.warn('No source annotationType provided. Exiting early.'); + return; + } + + if (!this._sourceHasMappings(source)) { + log.warn(`No measurement mappings found for '${sourceInfo}' source. Exiting early.`); + return; + } + + let measurement = {}; + try { + measurement = toMeasurementSchema(data); + measurement.source = source; + } catch (error) { + log.warn( + `Failed to map '${sourceInfo}' measurement for annotationType ${annotationType}:`, + error.message + ); + return; + } + + if (!this._isValidMeasurement(measurement)) { + log.warn( + `Attempting to add or update a invalid measurement provided by '${sourceInfo}'. Exiting early.` + ); + return; + } + + let internalUID = data.id; + if (!internalUID) { + internalUID = guid(); + log.warn(`Measurement ID not found. Generating UID: ${internalUID}`); + } + + const annotationData = data.annotation.data; + + const newMeasurement = { + finding: annotationData.finding, + findingSites: annotationData.findingSites, + site: annotationData.findingSites?.[0], + ...measurement, + modifiedTimestamp: Math.floor(Date.now() / 1000), + uid: internalUID, + }; + + if (this.measurements.get(internalUID)) { + this.measurements.set(internalUID, newMeasurement); + this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, { + source, + measurement: newMeasurement, + }); + } else { + log.info('Measurement added', newMeasurement); + this.measurements.set(internalUID, newMeasurement); + this._broadcastEvent(this.EVENTS.RAW_MEASUREMENT_ADDED, { + source, + measurement: newMeasurement, + data, + dataSource, + }); + } + + return newMeasurement.uid; + } + + /** + * Adds or update persisted measurements. + * + * @param {MeasurementSource} source The measurement source instance + * @param {string} annotationType The source annotationType + * @param {EventDetail} sourceAnnotationDetail for the annotation event + * @param {boolean} isUpdate is this an update or an add/completed instead? + * @return {string} A measurement uid + */ + annotationToMeasurement(source, annotationType, sourceAnnotationDetail, isUpdate = false) { + if (!this._isValidSource(source)) { + throw new Error('Invalid source.'); + } + if (!annotationType) { + throw new Error('No source annotationType provided.'); + } + + const sourceInfo = this._getSourceToString(source); + if (!this._sourceHasMappings(source)) { + throw new Error(`No measurement mappings found for '${sourceInfo}' source. Exiting early.`); + } + + let measurement = {}; + try { + const sourceMappings = this.mappings[source.uid]; + const sourceMapping = sourceMappings.find( + mapping => mapping.annotationType === annotationType + ); + if (!sourceMapping) { + console.log('No source mapping', source); + return; + } + const { toMeasurementSchema } = sourceMapping; + + /* Convert measurement */ + measurement = toMeasurementSchema(sourceAnnotationDetail); + if (!measurement) { + return; + } + + measurement.source = source; + } catch (error) { + // Todo: handle other + this.unmappedMeasurements.set(sourceAnnotationDetail.uid, { + ...sourceAnnotationDetail, + source: { + name: source.name, + version: source.version, + uid: source.uid, + }, + }); + + console.log('Failed to map', error); + throw new Error( + `Failed to map '${sourceInfo}' measurement for annotationType ${annotationType}: ${error.message}` + ); + } + + if (!this._isValidMeasurement(measurement)) { + throw new Error( + `Attempting to add or update a invalid measurement provided by '${sourceInfo}'. Exiting early.` + ); + } + + // Todo: we are using uid on the eventDetail, it should be uid of annotation + let internalUID = sourceAnnotationDetail.uid; + if (!internalUID) { + internalUID = guid(); + log.info( + `Annotation does not have UID, Generating UID for the created Measurement: ${internalUID}` + ); + } + + const oldMeasurement = this.measurements.get(internalUID); + + const newMeasurement = { + ...oldMeasurement, + ...measurement, + modifiedTimestamp: Math.floor(Date.now() / 1000), + uid: internalUID, + }; + + if (oldMeasurement) { + // TODO: Ultimately, each annotation should have a selected flag right from the source. + // For now, it is just added in OHIF here and in setMeasurementSelected. + this.measurements.set(internalUID, newMeasurement); + if (isUpdate) { + this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, { + source, + measurement: newMeasurement, + notYetUpdatedAtSource: false, + }); + } else { + log.info('Measurement added.', newMeasurement); + this._broadcastEvent(this.EVENTS.MEASUREMENT_ADDED, { + source, + measurement: newMeasurement, + }); + } + } else { + log.info('Measurement started.', newMeasurement); + this.measurements.set(internalUID, newMeasurement); + } + + return newMeasurement.uid; + } + + /** + * Removes a measurement and broadcasts the removed event. + * + * @param {string} measurementUID The measurement uid + */ + remove(measurementUID: string): void { + const measurement = + this.measurements.get(measurementUID) || this.unmappedMeasurements.get(measurementUID); + + if (!measurementUID || !measurement) { + console.debug(`No uid provided, or unable to find measurement by uid.`); + return; + } + + const source = measurement.source; + + this.unmappedMeasurements.delete(measurementUID); + this.measurements.delete(measurementUID); + this._broadcastEvent(this.EVENTS.MEASUREMENT_REMOVED, { + source, + measurement: measurementUID, + }); + } + + /** + * Clears measurements that match the filter, defaulting to all of them. + * That allows, for example, clearing all of a single studies measurements + * without needing to clear other measurements. + */ + public clearMeasurements(filter?: MeasurementFilter) { + // Make a copy of the measurements + const toClear = this.getMeasurements(filter); + const unmappedClear = filter + ? [...this.unmappedMeasurements.values()].filter(filter) + : this.unmappedMeasurements; + const measurements = [...toClear, ...unmappedClear]; + unmappedClear.forEach(measurement => this.unmappedMeasurements.delete(measurement.uid)); + toClear.forEach(measurement => this.measurements.delete(measurement.uid)); + this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED, { measurements }); + } + + /** + * Called after the mode.onModeExit is called to reset the state. + * To store measurements for later use, store them in the mode.onModeExit + * and restore them in the mode onModeEnter. + */ + onModeExit() { + this.clearMeasurements(); + } + + /** + * This method calls the subscriptions for JUMP_TO_MEASUREMENT_VIEWPORT + * and JUMP_TO_MEASUREMENT_LAYOUT. There are two events which are + * fired because there are two different items which might want to handle + * the event. First, there might already be a viewport which can handle + * the event. If so, then the layout doesn't need to necessarily change. + * This is communicated by the isConsumed value on the event itself. + * Otherwise, the layout itself may need to be navigated to in order + * to provide a viewport which can show the given measurement. + * + * When a viewport decides to apply the event, it should call the consume() + * method on the event, so that other listeners know they do not need to + * navigate. This does NOT affect whether the layout event is fired, and + * merely causes it to fire the event with the isConsumed set to true. + */ + + public jumpToMeasurement(viewportId: string, measurementUID: string): void { + const measurement = this.measurements.get(measurementUID); + + if (!measurement) { + log.warn(`No measurement uid, or unable to find by uid.`); + return; + } + const consumableEvent = this.createConsumableEvent({ + viewportId, + measurement, + }); + + this._broadcastEvent(EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT, consumableEvent); + this._broadcastEvent(EVENTS.JUMP_TO_MEASUREMENT_LAYOUT, consumableEvent); + } + + _getSourceUID(name, version) { + const { sources } = this; + + const sourceUID = Object.keys(sources).find(sourceUID => { + const source = sources[sourceUID]; + + return source.name === name && source.version === version; + }); + + return sourceUID; + } + + _getMappingByMeasurementSource(measurement, annotationType) { + if (this._isValidSource(measurement.source)) { + return this.mappings[measurement.source.uid].find(m => m.annotationType === annotationType); + } + } + + /** + * Get measurement mapping function if matching criteria. + * + * @param {MeasurementSource} source Measurement source instance + * @param {string} annotationType The source annotationType + * @param {Measurement} measurement The measurement service measurement + * @return {Object} The mapping based on matched criteria + */ + _getMatchingMapping(source, annotationType, measurement) { + const sourceMappings = this.mappings[source.uid]; + + const sourceMappingsByDefinition = sourceMappings.filter( + mapping => mapping.annotationType === annotationType + ); + + /* Criteria Matching */ + return sourceMappingsByDefinition.find(({ matchingCriteria }) => { + return measurement.points && measurement.points.length === matchingCriteria.points; + }); + } + + /** + * Returns formatted string with source info. + * + * @param {MeasurementSource} source Measurement source + * @return {string} Source information + */ + _getSourceToString(source) { + return `${source.name}@${source.version}`; + } + + /** + * Checks if given source is valid. + * + * @param {MeasurementSource} source Measurement source + * @return {boolean} Measurement source validation + */ + _isValidSource(source) { + return source && this.sources[source.uid]; + } + + /** + * Checks if a given source has mappings. + * + * @param {MeasurementSource} source The measurement source + * @return {boolean} Validation if source has mappings + */ + _sourceHasMappings(source) { + return Array.isArray(this.mappings[source.uid]) && this.mappings[source.uid].length; + } + + /** + * Check if a given measurement data is valid. + * + * @param {Measurement} measurementData Measurement data + * @return {boolean} Measurement validation + */ + _isValidMeasurement(measurementData) { + return Object.keys(measurementData).every(key => { + if (!MEASUREMENT_SCHEMA_KEYS.includes(key)) { + log.warn(`Invalid measurement key: ${key}`); + return false; + } + + return true; + }); + } + + /** + * Check if a given measurement service event is valid. + * + * @param {string} eventName The name of the event + * @return {boolean} Event name validation + // */ + // _isValidEvent(eventName) { + // return Object.values(this.EVENTS).includes(eventName); + // } + + /** + * Converts object of objects to array. + * + * @return {Array} Array of objects + */ + _arrayOfObjects = obj => { + return Object.entries(obj).map(e => ({ [e[0]]: e[1] })); + }; + + public toggleLockMeasurement(measurementUID: string): void { + const measurement = this.measurements.get(measurementUID); + + if (!measurement) { + console.debug(`No measurement found for uid: ${measurementUID}`); + return; + } + + measurement.isLocked = !measurement.isLocked; + + this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, { + source: measurement.source, + measurement, + notYetUpdatedAtSource: true, + }); + } + + public toggleVisibilityMeasurement(measurementUID: string): void { + const measurement = this.measurements.get(measurementUID); + + if (!measurement) { + console.debug(`No measurement found for uid: ${measurementUID}`); + return; + } + + measurement.isVisible = !measurement.isVisible; + + this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, { + source: measurement.source, + measurement, + notYetUpdatedAtSource: true, + }); + } + + public updateColorMeasurement(measurementUID: string, color: number[]): void { + const measurement = this.measurements.get(measurementUID); + + if (!measurement) { + console.debug(`No measurement found for uid: ${measurementUID}`); + return; + } + + measurement.color = color; + + this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, { + source: measurement.source, + measurement, + notYetUpdatedAtSource: true, + }); + } +} + +export default MeasurementService; +export { EVENTS, VALUE_TYPES }; diff --git a/platform/core/src/services/MeasurementService/index.ts b/platform/core/src/services/MeasurementService/index.ts new file mode 100644 index 0000000..2560cc8 --- /dev/null +++ b/platform/core/src/services/MeasurementService/index.ts @@ -0,0 +1,3 @@ +import MeasurementService from './MeasurementService'; + +export default MeasurementService; diff --git a/platform/core/src/services/MultiMonitorService.ts b/platform/core/src/services/MultiMonitorService.ts new file mode 100644 index 0000000..810ec5e --- /dev/null +++ b/platform/core/src/services/MultiMonitorService.ts @@ -0,0 +1,204 @@ +/** + * This service manages multiple monitors or windows. + */ +export class MultiMonitorService { + public readonly numberOfScreens: number; + private windowsConfig; + private screenConfig; + private launchWindows = []; + private basePath: string; + + public readonly screenNumber: number; + public readonly isMultimonitor: boolean; + + public static readonly SOURCE_SCREEN = { + id: 'source', + // This is the primary screen, so don't launch is separately, but use primary + launch: 'source', + screen: null, + location: { + screen: null, + width: 1, + height: 1, + left: 0, + top: 0, + }, + }; + + public static REGISTRATION = { + name: 'multiMonitorService', + create: ({ configuration, commandsManager }): MultiMonitorService => { + const service = new MultiMonitorService(configuration, commandsManager); + return service; + }, + }; + + constructor(configuration, commandsManager) { + const params = new URLSearchParams(window.location.search); + const screenNumber = params.get('screenNumber'); + const multimonitor = params.get('multimonitor'); + const testParams = { params, screenNumber, multimonitor }; + this.screenNumber = screenNumber ? Number(screenNumber) : -1; + this.commandsManager = commandsManager; + const windowAny = window as any; + windowAny.multimonitor ||= { + setLaunchWindows: this.setLaunchWindows, + launchWindows: this.launchWindows, + commandsManager, + }; + windowAny.multimonitor.commandsManager = commandsManager; + this.launchWindows = (window as any).multimonitor?.launchWindows || this.launchWindows; + if (this.screenNumber !== -1) { + this.launchWindows[this.screenNumber] = window; + } + windowAny.commandsManager = (...args) => configuration.commandsManager; + for (const windowsConfig of Array.isArray(configuration) ? configuration : []) { + if (windowsConfig.test(testParams)) { + this.isMultimonitor = true; + this.numberOfScreens = windowsConfig.screens.length; + this.windowsConfig = windowsConfig; + if (this.screenNumber === -1 || this.screenNumber === null) { + this.screenConfig = MultiMonitorService.SOURCE_SCREEN; + } else { + this.screenConfig = windowsConfig.screens[this.screenNumber]; + if (!this.screenConfig) { + throw new Error(`Screen ${screenNumber} not configured in ${this.windowsConfig}`); + } + window.name = this.screenConfig.id; + } + return; + } + this.numberOfScreens = 1; + this.isMultimonitor = false; + } + } + + public async run(screenDelta = 1, commands, options) { + const screenNumber = (this.screenNumber + (screenDelta ?? 1)) % this.numberOfScreens; + const otherWindow = await this.getWindow(screenNumber); + if (!otherWindow) { + console.warn('No multimonitor found for screen', screenNumber, commands); + return; + } + if (!otherWindow.multimonitor?.commandsManager) { + console.warn("Didn't find a commands manager to run in the other window", otherWindow); + return; + } + otherWindow.multimonitor.commandsManager.runAsync(commands, options); + } + + /** Sets the launch windows for later use, shared amongst all windows. */ + public setLaunchWindows = launchWindows => { + this.launchWindows = launchWindows; + (window as any).multimonitor.launchWindows = launchWindows; + }; + + public async launchWindow(studyUid: string, screenDelta = 1, hashParams = '') { + const forScreen = (this.screenNumber + screenDelta) % this.numberOfScreens; + return this.getWindow(forScreen, studyUid ? `StudyInstanceUIDs=${studyUid}${hashParams}` : ''); + } + + public async getWindow(screenNumber, hashParam?: string) { + if (screenNumber === this.screenNumber) { + return window; + } + if (this.launchWindows[screenNumber] && !this.launchWindows[screenNumber].closed) { + return this.launchWindows[screenNumber]; + } + return await this.createWindow(screenNumber, hashParam); + } + + /** + * Creates a new window showing the given url by default, or gets an existing + * window. + */ + public async createWindow(screenNumber, urlToUse?: string) { + if (screenNumber === this.screenNumber) { + return window; + } + const screenInfo = this.windowsConfig.screens[screenNumber]; + const screenDetails = await window.getScreenDetails?.(); + const screen = + (screenInfo.screen >= 0 && screenDetails.screens[screenInfo.screen]) || + screenDetails.currentScreen || + window.screen; + const { width = 1024, height = 1024, availLeft = 0, availTop = 0 } = screen || {}; + const newScreen = this.windowsConfig.screens[screenNumber]; + const { + width: widthPercent = 1, + height: heightPercent = 1, + top: topPercent = 0, + left: leftPercent = 0, + } = newScreen.location || {}; + + const useLeft = Math.round(availLeft + leftPercent * width); + const useTop = Math.round(availTop + topPercent * height); + const useWidth = Math.round(width * widthPercent); + const useHeight = Math.round(height * heightPercent); + + const baseFinalUrl = `${this.basePath}&screenNumber=${screenNumber}`; + const finalUrl = urlToUse ? `${baseFinalUrl}#${urlToUse}` : baseFinalUrl; + + const newId = newScreen.id; + const options = newScreen.options || ''; + const position = `screenX=${useLeft},screenY=${useTop},width=${useWidth},height=${useHeight},${options}`; + + let newWindow = window.open('', newId, position); + if (!newWindow?.location.href.startsWith(baseFinalUrl)) { + newWindow = window.open(finalUrl, newId, position); + } + if (!newWindow) { + console.warn('Unable to launch window', finalUrl, 'called', newId, 'at', position); + return; + } + + // Wait for the window to fully load + await new Promise(resolve => { + if (newWindow.document.readyState === 'complete') { + resolve(); + } else { + newWindow.addEventListener('load', () => resolve()); + } + }); + + this.launchWindows[screenNumber] = newWindow; + return newWindow; + } + + /** Launches all the windows using the initial configuration */ + public launchAll() { + for (let i = 0; i < this.numberOfScreens; i++) { + this.createWindow(i); + } + } + + /** + * Sets the base path to use for launching other windows, based on the + * original base path without hash values in order to preserve consistent + * URLs so that windows are refreshed on relaunch. + */ + public setBasePath() { + const url = new URL(window.location.href); + url.searchParams.delete('screenNumber'); + url.searchParams.delete('protocolId'); + url.searchParams.delete('launchAll'); + url.searchParams.set('multimonitor', url.searchParams.get('multimonitor') || 'split'); + url.hash = ''; + this.basePath = url.toString(); + } + + /** + * Try moving the screen to the correct location - this will only work with + * screens opened with openWindow containing no more than 1 tab. + */ + public async onModeEnter() { + this.setBasePath(); + + if ( + (this.isMultimonitor && this.screenNumber === -1) || + window.location.href.toLowerCase().indexOf('launchall') !== -1 + ) { + this.launchAll(); + } + } +} diff --git a/platform/core/src/services/PanelService/PanelService.tsx b/platform/core/src/services/PanelService/PanelService.tsx new file mode 100644 index 0000000..91a01ac --- /dev/null +++ b/platform/core/src/services/PanelService/PanelService.tsx @@ -0,0 +1,223 @@ +import React from 'react'; +import { ActivatePanelTriggers } from '../../types'; +import { Subscription } from '../../types/IPubSub'; +import { PubSubService } from '../_shared/pubSubServiceInterface'; +import { ExtensionManager } from '../../extensions'; + +export const EVENTS = { + PANELS_CHANGED: 'event::panelService:panelsChanged', + ACTIVATE_PANEL: 'event::panelService:activatePanel', +}; + +type PanelData = { + id: string; + iconName: string; + iconLabel: string; + label: string; + name: string; + content: unknown; +}; + +export enum PanelPosition { + Left = 'left', + Right = 'right', + Bottom = 'bottom', +} + +export default class PanelService extends PubSubService { + private _extensionManager: ExtensionManager; + + public static REGISTRATION = { + name: 'panelService', + create: ({ extensionManager }): PanelService => { + return new PanelService(extensionManager); + }, + }; + + private _panelsGroups: Map = new Map(); + + constructor(extensionManager: ExtensionManager) { + super(EVENTS); + this._extensionManager = extensionManager; + } + + public get PanelPosition(): typeof PanelPosition { + return PanelPosition; + } + + private _getPanelComponent(panelId: string) { + const entry = this._extensionManager.getModuleEntry(panelId); + + if (!entry) { + // Check for similar panel names + const similarPanels = this._getSimilarPanels(panelId); + + if (similarPanels.length > 0) { + const suggestion = `Did you mean: ${similarPanels.join(', ')}?`; + throw new Error( + `${panelId} is not a valid entry for an extension module. ${suggestion} Please check your configuration or make sure the extension is registered.` + ); + } else { + throw new Error( + `${panelId} is not a valid entry for an extension module, please check your configuration or make sure the extension is registered.` + ); + } + } + + if (!entry?.component) { + throw new Error( + `No component found from extension ${panelId}. Check the reference string to the extension in your Mode configuration` + ); + } + + const content = entry.component; + + return { entry, content }; + } + + private _getSimilarPanels(panelId: string, threshold = 0.8): string[] { + const registeredPanels = Object.keys(this._extensionManager.modulesMap).filter(name => + name.includes('panelModule') + ); + + const similarPanels = registeredPanels.filter(registeredPanelId => { + const similarity = this._calculateSimilarity(panelId, registeredPanelId); + return similarity >= threshold; + }); + + return similarPanels; + } + + private _calculateSimilarity(str1: string, str2: string): number { + const set1 = new Set(str1.toLowerCase().split('')); + const set2 = new Set(str2.toLowerCase().split('')); + const intersection = new Set([...set1].filter(x => set2.has(x))); + const union = new Set([...set1, ...set2]); + return intersection.size / union.size; + } + + public getPanelData(panelId): PanelData { + let content, entry; + if (Array.isArray(panelId)) { + const panelsData = panelId.map(id => this._getPanelComponent(id)); + + // use the first panel's entry for the combined panel + entry = panelsData[0].entry; + + // stack the content of the panels in one react component + content = props => ( + <> + {panelsData.map(({ content: PanelContent }, index) => ( + + ))} + + ); + } else { + ({ content, entry } = this._getPanelComponent(panelId)); + } + + return { + id: entry.id, + iconName: entry.iconName, + iconLabel: entry.iconLabel, + label: entry.label, + name: entry.name, + content, + }; + } + + public addPanel(position: PanelPosition, panelId: string, options): void { + let panels = this._panelsGroups.get(position); + + if (!panels) { + panels = []; + this._panelsGroups.set(position, panels); + } + + const panelComponent = this.getPanelData(panelId); + + panels.push(panelComponent); + this._broadcastEvent(EVENTS.PANELS_CHANGED, { position, options }); + } + + public addPanels(position: PanelPosition, panelsIds: string[], options): void { + if (!Array.isArray(panelsIds)) { + throw new Error('Invalid "panelsIds" array'); + } + + panelsIds.forEach(panelId => this.addPanel(position, panelId, options)); + } + + public setPanels(panels: { [key in PanelPosition]: string[] }, options): void { + this.reset(); + + Object.keys(panels).forEach((position: PanelPosition) => { + this.addPanels(position, panels[position], options); + }); + } + + public getPanels(position: PanelPosition): PanelData[] { + const panels = this._panelsGroups.get(position) ?? []; + + // Return a new array to preserve the internal state + return [...panels]; + } + + public reset(): void { + const affectedPositions = Array.from(this._panelsGroups.keys()); + + this._panelsGroups.clear(); + + affectedPositions.forEach(position => + this._broadcastEvent(EVENTS.PANELS_CHANGED, { position }) + ); + } + + public onModeExit(): void { + this.reset(); + } + + /**5 + * Activates the panel with the given id. If the forceActive flag is false + * then it is up to the component containing the panel whether to activate + * it immediately or not. For instance, the panel might not be activated when + * the forceActive flag is false in the case where the user might have + * activated/displayed and then closed the panel already. + * Note that this method simply fires a broadcast event: ActivatePanelEvent. + * @param panelId the panel's id + * @param forceActive optional flag indicating if the panel should be forced to be activated or not + */ + public activatePanel(panelId: string, forceActive = false): void { + this._broadcastEvent(EVENTS.ACTIVATE_PANEL, { panelId, forceActive }); + } + + /** + * Adds a mapping of events (activatePanelTriggers.sourceEvents) broadcast by + * activatePanelTrigger.sourcePubSubService that + * when fired/broadcasted must in turn activate the panel with the given id. + * The subscriptions created are returned such that they can be managed and unsubscribed + * as appropriate. + * @param panelId the id of the panel to activate + * @param activatePanelTriggers an array of triggers + * @param forceActive optional flag indicating if the panel should be forced to be activated or not + * @returns an array of the subscriptions subscribed to + */ + public addActivatePanelTriggers( + panelId: string, + activatePanelTriggers: ActivatePanelTriggers[], + forceActive = false + ): Subscription[] { + return activatePanelTriggers + .map(trigger => + trigger.sourceEvents.map(eventName => + trigger.sourcePubSubService.subscribe(eventName, () => + this.activatePanel(panelId, forceActive) + ) + ) + ) + .flat(); + } +} diff --git a/platform/core/src/services/PanelService/index.ts b/platform/core/src/services/PanelService/index.ts new file mode 100644 index 0000000..0474f74 --- /dev/null +++ b/platform/core/src/services/PanelService/index.ts @@ -0,0 +1,3 @@ +import PanelService from './PanelService'; + +export default PanelService; diff --git a/platform/core/src/services/ServiceProvidersManager.ts b/platform/core/src/services/ServiceProvidersManager.ts new file mode 100644 index 0000000..ce2150b --- /dev/null +++ b/platform/core/src/services/ServiceProvidersManager.ts @@ -0,0 +1,31 @@ +import log from './../log.js'; + +/** + * The ServiceProvidersManager allows for a React context provider class to be registered + * for a particular service. This allows for extensions to register services + * with context providers and the providers will be instantiated and added to the + * DOM dynamically. + */ +export default class ServiceProvidersManager { + public providers = {}; + + public constructor() { + this.providers = {}; + } + + registerProvider(serviceName, provider) { + if (!serviceName) { + log.warn( + 'Attempting to register a provider to a null/undefined service name. Exiting early.' + ); + return; + } + + if (!provider) { + log.warn('Attempting to register a null/undefined provider. Exiting early.'); + return; + } + + this.providers[serviceName] = provider; + } +} diff --git a/platform/core/src/services/ServicesManager.test.js b/platform/core/src/services/ServicesManager.test.js new file mode 100644 index 0000000..1a43370 --- /dev/null +++ b/platform/core/src/services/ServicesManager.test.js @@ -0,0 +1,97 @@ +import ServicesManager from './ServicesManager'; +import log from '../log'; + +jest.mock('./../log'); + +describe('ServicesManager', () => { + let servicesManager, commandsManager; + + beforeEach(() => { + commandsManager = { + createContext: jest.fn(), + getContext: jest.fn(), + registerCommand: jest.fn(), + }; + servicesManager = new ServicesManager(commandsManager); + log.warn.mockClear(); + jest.clearAllMocks(); + }); + + describe('registerServices()', () => { + it('calls registerService() for each service', () => { + servicesManager.registerService = jest.fn(); + + servicesManager.registerServices([ + { name: 'UINotificationTestService', create: jest.fn() }, + { name: 'UIModalTestService', create: jest.fn() }, + ]); + + expect(servicesManager.registerService.mock.calls.length).toBe(2); + }); + + it('calls registerService() for each service passing its configuration if tuple', () => { + servicesManager.registerService = jest.fn(); + const fakeConfiguration = { testing: true }; + + servicesManager.registerServices([ + { name: 'UINotificationTestService', create: jest.fn() }, + [{ name: 'UIModalTestService', create: jest.fn() }, fakeConfiguration], + ]); + + expect(servicesManager.registerService.mock.calls[1][1]).toEqual(fakeConfiguration); + }); + }); + + describe('registerService()', () => { + const fakeService = { name: 'UINotificationService', create: jest.fn() }; + + it('logs a warning if the service is null or undefined', () => { + const undefinedService = undefined; + const nullService = null; + + servicesManager.registerService(undefinedService); + servicesManager.registerService(nullService); + + expect(log.warn.mock.calls.length).toBe(2); + }); + + it('logs a warning if the service does not have a name', () => { + const serviceWithEmptyName = { name: '', create: jest.fn() }; + const serviceWithoutName = { create: jest.fn() }; + + servicesManager.registerService(serviceWithEmptyName); + servicesManager.registerService(serviceWithoutName); + + expect(log.warn.mock.calls.length).toBe(2); + }); + + it('logs a warning if the service does not have a create factory function', () => { + const serviceWithoutCreate = { name: 'UINotificationService' }; + + servicesManager.registerService(serviceWithoutCreate); + + expect(log.warn.mock.calls.length).toBe(1); + }); + + it('tracks which services have been registered', () => { + servicesManager.registerService(fakeService); + + expect(servicesManager.registeredServiceNames).toContain(fakeService.name); + }); + + it('logs a warning if the service has an name that has already been registered', () => { + servicesManager.registerService(fakeService); + servicesManager.registerService(fakeService); + + expect(log.warn.mock.calls.length).toBe(1); + }); + + it('pass dependencies and configuration to service create factory function', () => { + const configuration = { config: 'Some configuration' }; + + servicesManager.registerService(fakeService, configuration); + + expect(fakeService.create.mock.calls[0][0].configuration.config).toBe(configuration.config); + }); + }); +}); diff --git a/platform/core/src/services/ServicesManager.ts b/platform/core/src/services/ServicesManager.ts new file mode 100644 index 0000000..8517455 --- /dev/null +++ b/platform/core/src/services/ServicesManager.ts @@ -0,0 +1,84 @@ +import log from './../log.js'; +import CommandsManager from '../classes/CommandsManager'; +import ExtensionManager from '../extensions/ExtensionManager'; + +export default class ServicesManager { + public services: AppTypes.Services = {}; + public registeredServiceNames: string[] = []; + private _commandsManager: CommandsManager; + private _extensionManager: ExtensionManager; + + constructor(commandsManager: CommandsManager) { + this._commandsManager = commandsManager; + this._extensionManager = null; + this.services = {}; + this.registeredServiceNames = []; + } + + public setExtensionManager(extensionManager) { + this._extensionManager = extensionManager; + } + + /** + * Registers a new service. + * + * @param {Object} service + * @param {Object} configuration + */ + public registerService(service, configuration = {}) { + if (!service) { + log.warn('Attempting to register a null/undefined service. Exiting early.'); + return; + } + + if (!service.name) { + log.warn(`Service name not set. Exiting early.`); + return; + } + + if (this.registeredServiceNames.includes(service.name)) { + log.warn( + `Service name ${service.name} has already been registered. Exiting before duplicating services.` + ); + return; + } + + if (service.create) { + this.services[service.name] = service.create({ + configuration, + extensionManager: this._extensionManager, + commandsManager: this._commandsManager, + servicesManager: this, + }); + if (service.altName) { + // TODO - remove this registration + this.services[service.altName] = this.services[service.name]; + } + } else { + log.warn(`Service create factory function not defined. Exiting early.`); + return; + } + + /* Track service registration */ + this.registeredServiceNames.push(service.name); + } + + /** + * An array of services, or an array of arrays that contains service + * configuration pairs. + * + * @param {Object[]} services - Array of services + */ + public registerServices(services) { + services.forEach(service => { + const hasConfiguration = Array.isArray(service); + + if (hasConfiguration) { + const [ohifService, configuration] = service; + this.registerService(ohifService, configuration); + } else { + this.registerService(service); + } + }); + } +} diff --git a/platform/core/src/services/StudyPrefetcherService/StudyPrefetcherService.ts b/platform/core/src/services/StudyPrefetcherService/StudyPrefetcherService.ts new file mode 100644 index 0000000..398974e --- /dev/null +++ b/platform/core/src/services/StudyPrefetcherService/StudyPrefetcherService.ts @@ -0,0 +1,698 @@ +import { PubSubService } from '../_shared/pubSubServiceInterface'; +import { ExtensionManager } from '../../extensions'; +import ServicesManager from '../ServicesManager'; +import ViewportGridService from '../ViewportGridService'; +import { DisplaySet } from '../../types'; + +enum RequestType { + /** Highest priority for loading*/ + Interaction = 'interaction', + /** Second highest priority for loading*/ + Thumbnail = 'thumbnail', + /** Third highest priority for loading, usually used for image loading in the background*/ + Prefetch = 'prefetch', + /** Lower priority, often used for background computations in the worker */ + Compute = 'compute', +} + +export const EVENTS = { + SERVICE_STARTED: 'event::studyPrefetcherService:started', + SERVICE_STOPPED: 'event::studyPrefetcherService:stopped', + DISPLAYSET_LOAD_PROGRESS: 'event::studyPrefetcherService:displaySetLoadProgress', + DISPLAYSET_LOAD_COMPLETE: 'event::studyPrefetcherService:displaySetLoadComplete', +}; + +/** + * Order used for prefetching display set + */ +enum StudyPrefetchOrder { + closest = 'closest', + downward = 'downward', + upward = 'upward', +} + +/** + * Study Prefetcher configuration + */ +type StudyPrefetcherConfig = { + /* Enable/disable study prefetching service */ + enabled: boolean; + /* Number of displaysets to be prefetched */ + displaySetsCount: number; + /** + * Max number of concurrent prefetch requests + * High numbers may impact on the time to load a new dropped series because + * the browser will be busy with all prefetching requests. As soon as the + * prefetch requests get fulfilled the new ones from the new dropped series + * are sent to the server. + * + * TODO: abort all prefetch requests when a new series is loaded on a viewport. + * (need to add support for `AbortController` on Cornerstone) + * */ + maxNumPrefetchRequests: number; + /* Display sets prefetching order (closest, downward and upward) */ + order: StudyPrefetchOrder; +}; + +type DisplaySetLoadingState = { + displaySetInstanceUID: string; + numInstances: number; + pendingImageIds: Set; + loadedImageIds: Set; + failedImageIds: Set; + loadingProgress: number; +}; + +type ImageRequest = { + displaySetInstanceUID: string; + imageId: string; + aborted: boolean; +}; + +type PubSubServiceSubscription = { unsubscribe: () => any }; + +interface ICache { + isImageCached(imageId: string): boolean; +} + +interface IImageLoadPoolManager { + addRequest( + requestFn: () => Promise, + type: string, + additionalDetails: Record, + priority?: number + ); + clearRequestStack(type: string): void; +} + +interface IImageLoader { + loadAndCacheImage(imageId: string, options: any): Promise; +} + +type EventSubscription = { + unsubscribe: () => void; +}; + +interface IImageLoadEventsManager { + addEventListeners( + onImageLoaded: (evt: any) => void, + onImageLoadFailed: (evt: any) => void + ): EventSubscription[]; +} + +class StudyPrefetcherService extends PubSubService { + private _extensionManager: ExtensionManager; + private _servicesManager: ServicesManager; + private _subscriptions: PubSubServiceSubscription[]; + private _activeDisplaySetsInstanceUIDs: string[] = []; + private _pendingRequests: ImageRequest[] = []; + private _inflightRequests = new Map(); + private _isRunning = false; + private _displaySetLoadingStates = new Map(); + private _imageIdsToDisplaySetsMap = new Map>(); + private config: StudyPrefetcherConfig = { + /* Enable/disable study prefetching service */ + enabled: false, + /* Number of displaysets to be prefetched */ + displaySetsCount: 1, + /** + * Max number of concurrent prefetch requests + * High numbers may impact on the time to load a new dropped series because + * the browser will be busy with all prefetching requests. As soon as the + * prefetch requests get fulfilled the new ones from the new dropped series + * are sent to the server. + * + * TODO: abort all prefetch requests when a new series is loaded on a viewport. + * (need to add support for `AbortController` on Cornerstone) + * */ + maxNumPrefetchRequests: 10, + /* Display sets prefetching order (closest, downward and upward) */ + order: StudyPrefetchOrder.downward, + }; + + // Properties set by Cornerstone extension (initStudyPrefetcherService) + public requestType: string = RequestType.Prefetch; + public cache: ICache; + public imageLoadPoolManager: IImageLoadPoolManager; + public imageLoader: IImageLoader; + public imageLoadEventsManager: IImageLoadEventsManager; + + public static REGISTRATION = { + name: 'studyPrefetcherService', + altName: 'StudyPrefetcherService', + create: ({ configuration, servicesManager, extensionManager }): StudyPrefetcherService => { + return new StudyPrefetcherService({ + servicesManager, + extensionManager, + configuration, + }); + }, + }; + + constructor({ + servicesManager, + extensionManager, + configuration, + }: { + servicesManager: ServicesManager; + extensionManager: ExtensionManager; + configuration: StudyPrefetcherConfig; + }) { + super(EVENTS); + + this._servicesManager = servicesManager; + this._extensionManager = extensionManager; + this._subscriptions = []; + + Object.assign(this.config, configuration); + } + + public onModeEnter(): void { + this._addEventListeners(); + } + + /** + * The onModeExit returns the service to the initial state. + */ + public onModeExit(): void { + this._removeEventListeners(); + this._stopPrefetching(); + } + + private _addImageLoadingEventsListeners() { + const fnOnImageLoadCompleted = (imageId: string) => { + // `sendNextRequests` must be called after image loaded/failed events + // to make sure prefetch requests shall be sent as soon as the active + // displaySets (active viewport) are loaded. + // + // PS: active display sets are not loaded by this service and that is why + // the requests shall not be in the inflight queue. + if (!this._inflightRequests.get(imageId)) { + this._sendNextRequests(); + } + }; + + const fnImageLoadedEventListener = evt => { + const { image } = evt.detail; + const { imageId } = image; + + this._moveImageIdToLoadedSet(imageId); + fnOnImageLoadCompleted(imageId); + }; + + const fnImageLoadFailedEventListener = evt => { + const { imageId } = evt.detail; + + this._moveImageIdToFailedSet(imageId); + fnOnImageLoadCompleted(imageId); + }; + + return this.imageLoadEventsManager.addEventListeners( + fnImageLoadedEventListener, + fnImageLoadFailedEventListener + ); + } + + private _addServicesListeners() { + const { displaySetService, viewportGridService } = this._servicesManager.services; + + // Restart the prefetcher after any change to the displaySets + // (eg: sorting the displaySets on StudyBrowser) + const displaySetsChangedSubscription = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SETS_CHANGED, + () => this._syncWithActiveViewport({ forceRestart: true }) + ); + + // Loads new datasets when making a new viewport active + const viewportGridActiveViewportIdSubscription = viewportGridService.subscribe( + ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED, + ({ viewportId }) => this._syncWithActiveViewport({ activeViewportId: viewportId }) + ); + + // Continue loading datasets after changing the layout (eg: from 1x1 to 2x1) + const viewportGridLayoutChangedSubscription = viewportGridService.subscribe( + ViewportGridService.EVENTS.LAYOUT_CHANGED, + () => this._syncWithActiveViewport() + ); + + // Loads new datasets after loading a new display set on a viewport + const viewportGridStateChangedSubscription = viewportGridService.subscribe( + ViewportGridService.EVENTS.GRID_STATE_CHANGED, + () => this._syncWithActiveViewport() + ); + + // Loads the first datasets right after opening the viewer + const viewportGridViewportreadySubscription = viewportGridService.subscribe( + ViewportGridService.EVENTS.VIEWPORTS_READY, + () => { + this._syncWithActiveViewport(); + this._startPrefetching(); + } + ); + + return [ + displaySetsChangedSubscription, + viewportGridActiveViewportIdSubscription, + viewportGridLayoutChangedSubscription, + viewportGridStateChangedSubscription, + viewportGridViewportreadySubscription, + ]; + } + + private _addEventListeners() { + const imageLoadingEventsSubscriptions = this._addImageLoadingEventsListeners(); + const servicesSubscriptions = this._addServicesListeners(); + + this._subscriptions.push(...imageLoadingEventsSubscriptions); + this._subscriptions.push(...servicesSubscriptions); + } + + private _removeEventListeners() { + this._subscriptions.forEach(subscription => subscription.unsubscribe()); + this._subscriptions = []; + } + + private _syncWithActiveViewport({ + activeViewportId, + forceRestart, + }: { + activeViewportId?: string; + forceRestart?: boolean; + } = {}) { + const { viewportGridService } = this._servicesManager.services; + const viewportGridServiceState = viewportGridService.getState(); + const { viewports } = viewportGridServiceState; + + activeViewportId = activeViewportId ?? viewportGridServiceState.activeViewportId; + + // If may be null when the viewer is loaded + if (!activeViewportId) { + return; + } + + const activeViewport = viewports.get(activeViewportId); + const displaySetUpdated = this._setActiveDisplaySetsUIDs(activeViewport.displaySetInstanceUIDs); + + if (forceRestart || displaySetUpdated) { + this._restartPrefetching(); + } + } + + private _setActiveDisplaySetsUIDs(newActiveDisplaySetInstanceUIDs: string[]): boolean { + const sameDisplaySets = + newActiveDisplaySetInstanceUIDs.length === this._activeDisplaySetsInstanceUIDs.length && + newActiveDisplaySetInstanceUIDs.every(uid => + this._activeDisplaySetsInstanceUIDs.includes(uid) + ); + + if (sameDisplaySets) { + return false; + } + + this._activeDisplaySetsInstanceUIDs = [...newActiveDisplaySetInstanceUIDs]; + this._restartPrefetching(); + + return true; + } + + private _areActiveDisplaySetsLoaded() { + const { _activeDisplaySetsInstanceUIDs: displaySetsInstanceUIDs } = this; + + return ( + displaySetsInstanceUIDs.length && + displaySetsInstanceUIDs.every( + displaySetsInstanceUID => + this._displaySetLoadingStates.get(displaySetsInstanceUID).loadingProgress >= 1 + ) + ); + } + + private _getClosestDisplaySets(displaySets: DisplaySet[], activeDisplaySetIndex: number) { + const sortedDisplaySets = []; + let previousIndex = activeDisplaySetIndex - 1; + let nextIndex = activeDisplaySetIndex + 1; + + while (previousIndex >= 0 || nextIndex < displaySets.length) { + if (previousIndex >= 0) { + sortedDisplaySets.push(displaySets[previousIndex]); + previousIndex--; + } + + if (nextIndex < displaySets.length) { + sortedDisplaySets.push(displaySets[nextIndex]); + nextIndex++; + } + } + + return sortedDisplaySets; + } + + private _getDownwardDisplaySets(displaySets: DisplaySet[], activeDisplaySetIndex: number) { + const sortedDisplaySets = []; + + for (let i = activeDisplaySetIndex + 1; i < displaySets.length; i++) { + sortedDisplaySets.push(displaySets[i]); + } + + return sortedDisplaySets; + } + + private _getUpwardDisplaySets(displaySets: DisplaySet[], activeDisplaySetIndex: number) { + const sortedDisplaySets = []; + + for (let i = activeDisplaySetIndex - 1; i >= 0 && i !== activeDisplaySetIndex; i--) { + sortedDisplaySets.push(displaySets[i]); + } + + return sortedDisplaySets; + } + + private _getSortedDisplaySetsToPrefetch(displaySets: DisplaySet[]): DisplaySet[] { + if (!this._activeDisplaySetsInstanceUIDs?.length) { + return []; + } + + const { displaySetsCount } = this.config; + const activeDisplaySetsInstanceUIDs = this._activeDisplaySetsInstanceUIDs; + const [activeDisplaySetUID] = activeDisplaySetsInstanceUIDs; + const activeDisplaySetIndex = displaySets.findIndex( + ds => ds.displaySetInstanceUID === activeDisplaySetUID + ); + const getDisplaySetsFunctionsMap = { + [StudyPrefetchOrder.closest]: this._getClosestDisplaySets, + [StudyPrefetchOrder.downward]: this._getDownwardDisplaySets, + [StudyPrefetchOrder.upward]: this._getUpwardDisplaySets, + }; + const { order } = this.config; + const fnGetDisplaySets = getDisplaySetsFunctionsMap[order]; + + if (!fnGetDisplaySets) { + throw new Error(`Invalid order (${order})`); + } + + // Creates a `Set` to look for UIDs in O(1) instead of O(n) + const uidsSet = new Set(activeDisplaySetsInstanceUIDs); + + // Remove any active displaySet that may still be in the activeDisplaySetsInstanceUIDs. + // That may happen when activeDisplaySetsInstanceUIDs has more than one element. + return fnGetDisplaySets + .call(this, displaySets, activeDisplaySetIndex) + .filter(ds => !uidsSet.has(ds.displaySetInstanceUID)) + .slice(0, displaySetsCount); + } + + private _getDisplaySets() { + const { displaySetService } = this._servicesManager.services; + const displaySets = [...displaySetService.getActiveDisplaySets()]; + const displaySetsToPrefetch = this._getSortedDisplaySetsToPrefetch(displaySets); + + return { displaySets, displaySetsToPrefetch }; + } + + private _updateImageIdsDisplaySetMap(displaySetInstanceUID: string, imageIds: string[]): void { + for (const imageId of imageIds) { + let displaySetsInstanceUIDsMap = this._imageIdsToDisplaySetsMap.get(imageId); + + if (!displaySetsInstanceUIDsMap) { + displaySetsInstanceUIDsMap = new Set(); + this._imageIdsToDisplaySetsMap.set(imageId, displaySetsInstanceUIDsMap); + } + + displaySetsInstanceUIDsMap.add(displaySetInstanceUID); + } + } + + private _getImageIdsForDisplaySet(displaySet: DisplaySet): string[] { + const dataSource = this._extensionManager.getActiveDataSource()[0]; + + return dataSource.getImageIdsForDisplaySet(displaySet); + } + + private _updateDisplaySetLoadingProgress(displaySetLoadingState: DisplaySetLoadingState) { + const { numInstances, loadedImageIds, failedImageIds } = displaySetLoadingState; + const loadingProgress = (loadedImageIds.size + failedImageIds.size) / numInstances; + + displaySetLoadingState.loadingProgress = loadingProgress; + } + + private _addDisplaySetLoadingState(displaySet: DisplaySet): void { + const { displaySetInstanceUID } = displaySet; + const imageIds = this._getImageIdsForDisplaySet(displaySet); + let displaySetLoadingState = this._displaySetLoadingStates.get(displaySetInstanceUID); + + if (displaySetLoadingState) { + return; + } + + const pendingImageIds = new Set(imageIds); + const loadedImageIds = new Set(); + + // Needs to check which image is already loaded to update the progress properly + // because some images may already be loaded (thumbnails and viewports). + for (const imageId of imageIds) { + if (this.cache.isImageCached(imageId)) { + loadedImageIds.add(imageId); + } else { + pendingImageIds.add(imageId); + } + } + + displaySetLoadingState = { + displaySetInstanceUID, + numInstances: imageIds.length, + pendingImageIds, + loadedImageIds, + failedImageIds: new Set(), + loadingProgress: 0, + }; + + this._updateDisplaySetLoadingProgress(displaySetLoadingState); + this._displaySetLoadingStates.set(displaySetInstanceUID, displaySetLoadingState); + this._updateImageIdsDisplaySetMap(displaySetInstanceUID, imageIds); + + // Notify the UI that something is already loaded (eg: update StudyBrowser) + if (loadedImageIds.size) { + this._triggerDisplaySetEvents(displaySetInstanceUID); + } + } + + private _loadDisplaySets() { + const { displaySets, displaySetsToPrefetch } = this._getDisplaySets(); + + displaySets.forEach(displaySet => this._addDisplaySetLoadingState(displaySet)); + displaySetsToPrefetch.forEach(displaySet => this._enqueueDisplaySetImagesRequests(displaySet)); + } + + private _moveImageIdToLoadedSet(imageId: string): boolean { + const displaySetsInstanceUIDs = this._imageIdsToDisplaySetsMap.get(imageId); + + if (!displaySetsInstanceUIDs) { + return; + } + + for (const displaySetInstanceUID of Array.from(displaySetsInstanceUIDs.values())) { + const displaySetLoadingState = this._displaySetLoadingStates.get(displaySetInstanceUID); + const { pendingImageIds, loadedImageIds } = displaySetLoadingState; + + pendingImageIds.delete(imageId); + loadedImageIds.add(imageId); + + this._updateDisplaySetLoadingProgress(displaySetLoadingState); + this._triggerDisplaySetEvents(displaySetInstanceUID); + } + + return true; + } + + private _moveImageIdToFailedSet(imageId: string): boolean { + const displaySetsInstanceUIDs = this._imageIdsToDisplaySetsMap.get(imageId); + + if (!displaySetsInstanceUIDs) { + return; + } + + for (const displaySetInstanceUID of Array.from(displaySetsInstanceUIDs.values())) { + const displaySetLoadingState = this._displaySetLoadingStates.get(displaySetInstanceUID); + const { pendingImageIds, failedImageIds } = displaySetLoadingState; + + pendingImageIds.delete(imageId); + failedImageIds.add(imageId); + + this._updateDisplaySetLoadingProgress(displaySetLoadingState); + this._triggerDisplaySetEvents(displaySetInstanceUID); + } + + return true; + } + + private _triggerDisplaySetEvents(displaySetInstanceUID: string) { + const displaySetLoadingState = this._displaySetLoadingStates.get(displaySetInstanceUID); + const { loadingProgress, numInstances } = displaySetLoadingState; + + this._broadcastEvent(this.EVENTS.DISPLAYSET_LOAD_PROGRESS, { + displaySetInstanceUID, + numInstances, + loadingProgress, + }); + + if (loadingProgress >= 1) { + this._broadcastEvent(this.EVENTS.DISPLAYSET_LOAD_COMPLETE, { + displaySetInstanceUID, + }); + } + } + + private _onImagePrefetchSuccess(imageRequest: ImageRequest) { + if (imageRequest.aborted) { + return; + } + + const { imageId } = imageRequest; + + this._inflightRequests.delete(imageId); + this._moveImageIdToLoadedSet(imageId); + + // `sendNextRequests` must be called after removing the request from the inflight + // queue otherwise it shall not be able to send the request (maxNumPrefetchRequests) + this._sendNextRequests(); + } + + private _onImagePrefetchFailed(imageRequest, error) { + if (imageRequest.aborted) { + return; + } + + console.warn(`An error ocurred when trying to load "${imageRequest.imageId}"`, error); + + const { imageId } = imageRequest; + + this._inflightRequests.delete(imageId); + this._moveImageIdToFailedSet(imageId); + + // `sendNextRequests` must be called after removing the request from the inflight + // queue otherwise it shall not be able to send the request (maxNumPrefetchRequests) + this._sendNextRequests(); + } + + private async _sendNextRequests() { + // If the service has stopped with async requests in progress this method may + // get called again when each of those requests are fulfilled. + if (!this._isRunning) { + return; + } + + // Does not send any prefetch request until the active display sets are loaded + if (!this._areActiveDisplaySetsLoaded()) { + return; + } + + const { _pendingRequests: pendingRequests, _inflightRequests: inflightRequests } = this; + const { maxNumPrefetchRequests } = this.config; + + if (!pendingRequests.length || inflightRequests.size >= maxNumPrefetchRequests) { + return; + } + + const numImageRequests = Math.min( + pendingRequests.length, + maxNumPrefetchRequests - inflightRequests.size + ); + const imageRequests = this._pendingRequests.splice(0, numImageRequests); + + imageRequests.forEach(imageRequest => { + const { imageId } = imageRequest; + const options = { + priority: -5, + requestType: this.requestType, + additionalDetails: { imageId }, + preScale: { + enabled: true, + }, + }; + + this.imageLoadPoolManager.addRequest( + async () => + this.imageLoader.loadAndCacheImage(imageId, options).then( + _image => this._onImagePrefetchSuccess(imageRequest), + error => this._onImagePrefetchFailed(imageRequest, error) + ), + this.requestType, + { imageId } + ); + + inflightRequests.set(imageId, imageRequest); + }); + } + + private _enqueueDisplaySetImagesRequests(displaySet: DisplaySet) { + const { displaySetInstanceUID } = displaySet; + const imageIds = this._getImageIdsForDisplaySet(displaySet); + + imageIds.forEach(imageId => { + if (this.cache.isImageCached(imageId)) { + this._moveImageIdToLoadedSet(imageId); + return; + } + + this._pendingRequests.push({ + displaySetInstanceUID, + imageId, + aborted: false, + }); + }); + } + + /** + * Start prefetching the display sets based on the active viewport and app configuration. + */ + private _startPrefetching(): void { + if (this._isRunning) { + return; + } + + if (!this.config.enabled) { + console.log('StudyPrefetcher is not enabled'); + return; + } + + this._isRunning = true; + + this._loadDisplaySets(); + this._sendNextRequests(); + this._broadcastEvent(this.EVENTS.SERVICE_STARTED, {}); + } + + /** + * Stop prefetching the display sets. + * All internal variables are cleared but activeDisplaySetsInstanceUIDs otherwise restart would not work. + */ + private _stopPrefetching(): void { + if (!this._isRunning) { + return; + } + this._isRunning = false; + + // Mark all inflight requests as aborted before clearing the map. + this._inflightRequests.forEach(inflightRequest => (inflightRequest.aborted = true)); + + this._pendingRequests = []; + this._displaySetLoadingStates.clear(); + this._imageIdsToDisplaySetsMap.clear(); + this._inflightRequests.clear(); + this.imageLoadPoolManager.clearRequestStack(this.requestType); + + this._broadcastEvent(this.EVENTS.SERVICE_STOPPED, {}); + } + + /** + * Restart prefetching in case it is already running. + */ + private _restartPrefetching(): void { + if (this._isRunning) { + this._stopPrefetching(); + this._startPrefetching(); + } + } +} + +export { StudyPrefetcherService as default, StudyPrefetcherService }; diff --git a/platform/core/src/services/StudyPrefetcherService/index.ts b/platform/core/src/services/StudyPrefetcherService/index.ts new file mode 100644 index 0000000..51c2229 --- /dev/null +++ b/platform/core/src/services/StudyPrefetcherService/index.ts @@ -0,0 +1,3 @@ +import { StudyPrefetcherService } from './StudyPrefetcherService'; + +export { StudyPrefetcherService as default, StudyPrefetcherService }; diff --git a/platform/core/src/services/ToolBarService/ToolbarService.ts b/platform/core/src/services/ToolBarService/ToolbarService.ts new file mode 100644 index 0000000..280568b --- /dev/null +++ b/platform/core/src/services/ToolBarService/ToolbarService.ts @@ -0,0 +1,575 @@ +import { CommandsManager } from '../../classes'; +import { ExtensionManager } from '../../extensions'; +import { PubSubService } from '../_shared/pubSubServiceInterface'; +import type { RunCommand } from '../../types/Command'; +import { Button, ButtonProps, EvaluateFunction, EvaluatePublic, NestedButtonProps } from './types'; + +const EVENTS = { + TOOL_BAR_MODIFIED: 'event::toolBarService:toolBarModified', + TOOL_BAR_STATE_MODIFIED: 'event::toolBarService:toolBarStateModified', +}; + +export default class ToolbarService extends PubSubService { + public static REGISTRATION = { + name: 'toolbarService', + altName: 'ToolBarService', + create: ({ commandsManager, extensionManager, servicesManager }) => { + return new ToolbarService(commandsManager, extensionManager, servicesManager); + }, + }; + + public static createButton(options: { + id: string; + label: string; + commands: RunCommand; + icon?: string; + tooltip?: string; + evaluate?: EvaluatePublic; + listeners?: Record; + }): ButtonProps { + const { id, icon, label, commands, tooltip, evaluate, listeners } = options; + return { + id, + icon, + label, + commands, + tooltip: tooltip || label, + evaluate, + listeners, + }; + } + + state: { + // all buttons in the toolbar with their props + buttons: Record; + // the buttons in the toolbar, grouped by section, with their ids + buttonSections: Record; + } = { + buttons: {}, + buttonSections: {}, + }; + + _commandsManager: CommandsManager; + _extensionManager: ExtensionManager; + _servicesManager: AppTypes.ServicesManager; + _evaluateFunction: Record = {}; + _serviceSubscriptions = []; + + constructor( + commandsManager: CommandsManager, + extensionManager: ExtensionManager, + servicesManager: AppTypes.ServicesManager + ) { + super(EVENTS); + this._commandsManager = commandsManager; + this._extensionManager = extensionManager; + this._servicesManager = servicesManager; + } + + public reset(): void { + // this.unsubscriptions.forEach(unsub => unsub()); + this.state = { + buttons: {}, + buttonSections: {}, + }; + this.unsubscriptions = []; + } + + public onModeEnter(): void { + this.reset(); + } + + /** + * Registers an evaluate function with the specified name. + * + * @param name - The name of the evaluate function. + * @param handler - The evaluate function handler. + */ + public registerEvaluateFunction(name: string, handler: EvaluateFunction) { + this._evaluateFunction[name] = handler; + } + + /** + * Registers a service and its event to listen for updates and refreshes the toolbar state when the event is triggered. + * @param service - The service to register. + * @param event - The event to listen for. + */ + public registerEventForToolbarUpdate(service, events) { + const { viewportGridService } = this._servicesManager.services; + const callback = () => { + const viewportId = viewportGridService.getActiveViewportId(); + this.refreshToolbarState({ viewportId }); + }; + + const unsubscriptions = events.map(event => { + if (service.subscribe) { + return service.subscribe(event, callback); + } else if (service.addEventListener) { + return service.addEventListener(event, callback); + } + }); + + unsubscriptions.forEach(unsub => this._serviceSubscriptions.push(unsub)); + } + + /** + * Removes buttons from the toolbar. + * @param buttonId - The button to be removed. + */ + public removeButton(buttonId: string) { + if (this.state.buttons[buttonId]) { + delete this.state.buttons[buttonId]; + } + this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, { + ...this.state, + }); + } + + /** + * Adds buttons to the toolbar. + * @param buttons - The buttons to be added. + * @param replace - Flag indicating if any existing button with the same id as one being added should be replaced + */ + public addButtons(buttons: Button[], replace: boolean = false): void { + buttons.forEach(button => { + if (replace || !this.state.buttons[button.id]) { + if (!button.props) { + button.props = {}; + } + + this.state.buttons[button.id] = button; + } + }); + + this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, { + ...this.state, + }); + } + + /** + * + * @param {*} interaction - can be undefined to run nothing + * @param {*} options is an optional set of extra commandOptions + * used for calling the specified interaction. That is, the command is + * called with {...commandOptions,...options} + */ + public recordInteraction( + interaction, + options?: { + refreshProps: Record; + [key: string]: unknown; + } + ) { + // if interaction is a string, we can assume it is the itemId + // and get the props to get the other properties + if (typeof interaction === 'string') { + interaction = this.getButtonProps(interaction); + } + + const itemId = interaction.itemId ?? interaction.id; + interaction.itemId = itemId; + + let commands = Array.isArray(interaction.commands) + ? interaction.commands + : [interaction.commands]; + + if (!commands?.length) { + this.refreshToolbarState({ + ...options?.refreshProps, + itemId, + interaction, + }); + } + + const commandOptions = { ...options, ...interaction }; + + commands = commands.map(command => { + if (typeof command === 'function') { + return () => { + command({ + ...commandOptions, + commandsManager: this._commandsManager, + servicesManager: this._servicesManager, + }); + }; + } + + return command; + }); + + // if still no commands, return + commands = commands.filter(Boolean); + + if (!commands.length) { + return; + } + + // Loop through commands and run them with the combined options + this._commandsManager.run(commands, commandOptions); + + this.refreshToolbarState({ + ...options?.refreshProps, + itemId, + interaction, + }); + } + + /** + * Consolidates the state of the toolbar after an interaction, it accepts + * props that get passed to the buttons + * + * @param refreshProps - The props that buttons need to get evaluated, they can be + * { viewportId, toolGroup} for cornerstoneTools. + * + * Todo: right now refreshToolbarState should be used in the context where + * we have access to the toolGroup and viewportId, but we should be able to + * pass the props to the toolbar service and it should be able to decide + * which buttons to evaluate based on the props + */ + public refreshToolbarState(refreshProps) { + const buttons = this.state.buttons; + + // Tracks evaluated buttons to avoid re-evaluating them (this will + // cause issue for toggles where if the button is in primary + // and secondary it will be evaluated twice) + const evaluationResults = new Map(); + + const evaluateButtonProps = (button, props, refreshProps) => { + if (evaluationResults.has(button.id)) { + const { disabled, disabledText, className, isActive } = evaluationResults.get(button.id); + return { ...props, disabled, disabledText, className, isActive }; + } else { + const evaluated = props.evaluate?.({ ...refreshProps, button }); + const updatedProps = { + ...props, + ...evaluated, + disabled: evaluated?.disabled || false, + className: evaluated?.className || '', + isActive: evaluated?.isActive, // isActive will be undefined for buttons without this prop + }; + evaluationResults.set(button.id, updatedProps); + return updatedProps; + } + }; + + const refreshedButtons = Object.values(buttons).reduce((acc, button: Button) => { + const isNested = (button.props as NestedButtonProps)?.groupId; + + if (!isNested) { + this.handleEvaluate(button.props); + const buttonProps = button.props as ButtonProps; + + const updatedProps = evaluateButtonProps(button, buttonProps, refreshProps); + acc[button.id] = { + ...button, + props: updatedProps, + }; + } else { + let buttonProps = button.props as NestedButtonProps; + // if it is nested we should perform evaluate on each item in the group + this.handleEvaluateNested(buttonProps); + + const { evaluate: groupEvaluate } = buttonProps; + + const groupEvaluated = groupEvaluate?.({ ...refreshProps, button }); + // handle group evaluate function which might switch the primary + // item in the group + buttonProps = { + ...buttonProps, + primary: groupEvaluated?.primary ?? buttonProps.primary, + disabled: groupEvaluated?.disabled ?? buttonProps.disabled, + disabledText: groupEvaluated?.disabledText ?? buttonProps.disabledText, + }; + + const { primary, items } = buttonProps; + + // primary and items evaluate functions + let updatedPrimary; + if (primary) { + updatedPrimary = evaluateButtonProps(primary, primary, refreshProps); + } + const updatedItems = items.map(item => evaluateButtonProps(item, item, refreshProps)); + buttonProps = { + ...buttonProps, + primary: updatedPrimary, + items: updatedItems, + }; + + acc[button.id] = { + ...button, + props: buttonProps, + }; + } + + return acc; + }, {}); + + this.setButtons(refreshedButtons); + return this.state; + } + + /** + * Sets the buttons for the toolbar, don't use this method to record an + * interaction, since it doesn't update the state of the buttons, use + * this if you know the buttons you want to set and you want to set them + * all at once. + * @param buttons - The buttons to set. + */ + public setButtons(buttons) { + this.state.buttons = buttons; + this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, { + buttons: this.state.buttons, + buttonSections: this.state.buttonSections, + }); + } + + /** + * Retrieves a button by its ID. + * @param id - The ID of the button to retrieve. + * @returns The button with the specified ID. + */ + public getButton(id: string): Button { + return this.state.buttons[id]; + } + + /** + * Retrieves the buttons from the toolbar service. + * @returns An array of buttons. + */ + public getButtons() { + return this.state.buttons; + } + + /** + * Retrieves the button properties for the specified button ID. + * It prioritizes nested buttons over regular buttons if the ID is found + * in both. + * + * @param id - The ID of the button. + * @returns The button properties. + */ + public getButtonProps(id: string): ButtonProps { + for (const buttonId of Object.keys(this.state.buttons)) { + const { primary, items } = (this.state.buttons[buttonId].props as NestedButtonProps) || {}; + if (primary?.id === id) { + return primary; + } + const found = items?.find(childButton => childButton.id === id); + if (found) { + return found; + } + } + + // This should be checked after we checked the nested buttons, since + // we are checking based on the ids, the nested objects are higher priority + // and more specific + if (this.state.buttons[id]) { + return this.state.buttons[id].props as ButtonProps; + } + } + + _getButtonUITypes() { + const registeredToolbarModules = this._extensionManager.modules['toolbarModule']; + + if (!Array.isArray(registeredToolbarModules)) { + return {}; + } + + return registeredToolbarModules.reduce((buttonTypes, toolbarModule) => { + toolbarModule.module.forEach(def => { + buttonTypes[def.name] = def; + }); + + return buttonTypes; + }, {}); + } + + /** + * Creates a button section with the specified key and buttons. + * Buttons already in the section (i.e. with the same ids) will NOT be added twice. + * @param {string} key - The key of the button section. + * @param {Array} buttons - The buttons to be added to the section. + */ + createButtonSection(key, buttons) { + if (this.state.buttonSections[key]) { + this.state.buttonSections[key].push( + ...buttons.filter( + button => !this.state.buttonSections[key].find(sectionButton => sectionButton === button) + ) + ); + } else { + this.state.buttonSections[key] = buttons; + } + this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, { ...this.state }); + } + + /** + * Retrieves the button section with the specified sectionId. + * + * @param sectionId - The ID of the button section to retrieve. + * @param props - Optional additional properties for mapping the button to display. + * @returns An array of buttons in the specified section, mapped to their display representation. + */ + getButtonSection(sectionId: string, props?: Record) { + const buttonSectionIds = this.state.buttonSections[sectionId]; + + return ( + buttonSectionIds?.map(btnId => { + const btn = this.state.buttons[btnId]; + return this._mapButtonToDisplay(btn, props); + }) || [] + ); + } + + /** + * Retrieves the tool name for a given button. + * @param button - The button object. + * @returns The tool name associated with the button. + */ + getToolNameForButton(button) { + const { props } = button; + + const commands = props?.commands || button.commands; + const commandsArray = Array.isArray(commands) ? commands : [commands]; + const firstCommand = commandsArray[0]; + + if (firstCommand?.commandOptions) { + return firstCommand.commandOptions.toolName ?? props?.id ?? button.id; + } + + // use id as a fallback for toolName + return props?.id ?? button.id; + } + + /** + * + * @param {*} btn + * @param {*} btnSection + * @param {*} metadata + * @param {*} props - Props set by the Viewer layer + */ + _mapButtonToDisplay(btn, props) { + if (!btn) { + return; + } + + const { id, uiType, component } = btn; + const { groupId } = btn.props; + + const buttonTypes = this._getButtonUITypes(); + + const buttonType = buttonTypes[uiType]; + + if (!buttonType && !component) { + return; + } + + !groupId ? this.handleEvaluate(btn.props) : this.handleEvaluateNested(btn.props); + + return { + id, + Component: component || buttonType.defaultComponent, + componentProps: Object.assign({}, btn.props, props), + }; + } + + handleEvaluateNested = props => { + const { primary, items } = props; + // handle group evaluate function + this.handleEvaluate(props); + + // primary and items evaluate functions + if (primary) { + this.handleEvaluate(primary); + } + items.forEach(item => this.handleEvaluate(item)); + }; + + handleEvaluate = props => { + const { evaluate, options } = props; + + if (typeof options === 'string') { + // get the custom option component from the extension manager and set it as the optionComponent + const buttonTypes = this._getButtonUITypes(); + const optionComponent = buttonTypes[options]?.defaultComponent; + props.options = { + optionComponent, + }; + } + + if (typeof evaluate === 'function') { + return; + } + + if (Array.isArray(evaluate)) { + const evaluators = evaluate.map(evaluator => { + const isObject = typeof evaluator === 'object'; + + const evaluatorName = isObject ? evaluator.name : evaluator; + + const evaluateFunction = this._evaluateFunction[evaluatorName]; + + if (!evaluateFunction) { + throw new Error( + `Evaluate function not found for name: ${evaluatorName}, you can register an evaluate function with the getToolbarModule in your extensions` + ); + } + + if (isObject) { + return args => evaluateFunction({ ...args, ...evaluator }); + } + + return evaluateFunction; + }); + + props.evaluate = args => { + const results = evaluators.map(evaluator => evaluator(args)); + const mergedResult = results.reduce((acc, result) => { + return { + ...acc, + ...result, + }; + }, {}); + + return mergedResult; + }; + + return; + } + + if (typeof evaluate === 'string') { + const evaluateFunction = this._evaluateFunction[evaluate]; + + if (evaluateFunction) { + props.evaluate = evaluateFunction; + return; + } + + throw new Error( + `Evaluate function not found for name: ${evaluate}, you can register an evaluate function with the getToolbarModule in your extensions` + ); + } + + if (typeof evaluate === 'object') { + const { name, ...options } = evaluate; + const evaluateFunction = this._evaluateFunction[name]; + if (evaluateFunction) { + props.evaluate = args => evaluateFunction({ ...args, ...options }); + return; + } + + throw new Error( + `Evaluate function not found for name: ${name}, you can register an evaluate function with the getToolbarModule in your extensions` + ); + } + }; + + getButtonComponentForUIType(uiType: string) { + return uiType ? (this._getButtonUITypes()[uiType]?.defaultComponent ?? null) : null; + } + + clearButtonSection(buttonSection: string) { + this.state.buttonSections[buttonSection] = []; + this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, { ...this.state }); + } +} diff --git a/platform/core/src/services/ToolBarService/index.ts b/platform/core/src/services/ToolBarService/index.ts new file mode 100644 index 0000000..0147896 --- /dev/null +++ b/platform/core/src/services/ToolBarService/index.ts @@ -0,0 +1,3 @@ +import ToolbarService from './ToolbarService'; + +export default ToolbarService; diff --git a/platform/core/src/services/ToolBarService/types.ts b/platform/core/src/services/ToolBarService/types.ts new file mode 100644 index 0000000..3abadb4 --- /dev/null +++ b/platform/core/src/services/ToolBarService/types.ts @@ -0,0 +1,53 @@ +import type { RunCommand } from '../../types/Command'; + +export type EvaluatePublic = + | string + | EvaluateFunction + | EvaluateObject + | (string | EvaluateFunction | EvaluateObject)[]; + +export type EvaluateFunction = (props: Record) => { + disabled: boolean; + className: string; +}; + +export type EvaluateObject = { + name: string; + // Allow any additional properties + [key: string]: unknown; +}; + +export type ButtonProps = { + id: string; + icon: string; + label: string; + tooltip?: string; + commands?: RunCommand; + disabled?: boolean; + className?: string; + evaluate?: EvaluatePublic; + listeners?: Record; +}; + +export type NestedButtonProps = { + groupId: string; + // group evaluate which is different + // from the evaluate function for the primary and items + evaluate?: EvaluatePublic; + items: ButtonProps[]; + primary: ButtonProps & { + // Todo: this is really ugly but really we don't have any other option + // the ui design requires this since the button should be rounded if + // active otherwise it should not be rounded + isActive?: boolean; + }; + secondary: ButtonProps; +}; + +export type Button = { + id: string; + props: ButtonProps | NestedButtonProps; + // button ui type (e.g. 'ohif.splitButton', 'ohif.radioGroup') + // extensions can provide custom components for these types + uiType: string; +}; diff --git a/platform/core/src/services/UIDialogService/UIDialogService.ts b/platform/core/src/services/UIDialogService/UIDialogService.ts new file mode 100644 index 0000000..dd7acda --- /dev/null +++ b/platform/core/src/services/UIDialogService/UIDialogService.ts @@ -0,0 +1,82 @@ +import { PubSubService } from '../_shared/pubSubServiceInterface'; + +class UIDialogService extends PubSubService { + public static readonly EVENTS = {}; + + public static REGISTRATION = { + name: 'uiDialogService', + altName: 'UIDialogService', + create: ({ configuration = {} }) => { + return new UIDialogService(); + }, + }; + + serviceImplementation = { + _dismiss: () => console.warn('dismiss() NOT IMPLEMENTED'), + _dismissAll: () => console.warn('dismissAll() NOT IMPLEMENTED'), + _create: () => console.warn('create() NOT IMPLEMENTED'), + }; + + constructor() { + super(UIDialogService.EVENTS); + this.serviceImplementation = { + ...this.serviceImplementation, + }; + } + + public create({ + id, + content, + contentProps, + onStart, + onDrag, + onStop, + centralize = false, + preservePosition = true, + isDraggable = true, + showOverlay = false, + defaultPosition, + onClickOutside, + }) { + return this.serviceImplementation._create({ + id, + content, + contentProps, + onStart, + onDrag, + onStop, + centralize, + preservePosition, + isDraggable, + showOverlay, + defaultPosition, + onClickOutside, + }); + } + + public dismiss({ id }) { + return this.serviceImplementation._dismiss({ id }); + } + + public dismissAll() { + return this.serviceImplementation._dismissAll(); + } + + public setServiceImplementation({ + dismiss: dismissImplementation, + dismissAll: dismissAllImplementation, + create: createImplementation, + }) { + if (dismissImplementation) { + this.serviceImplementation._dismiss = dismissImplementation; + } + if (dismissAllImplementation) { + this.serviceImplementation._dismissAll = dismissAllImplementation; + } + if (createImplementation) { + this.serviceImplementation._create = createImplementation; + } + } +} + +export default UIDialogService; diff --git a/platform/core/src/services/UIDialogService/index.ts b/platform/core/src/services/UIDialogService/index.ts new file mode 100644 index 0000000..3d787a5 --- /dev/null +++ b/platform/core/src/services/UIDialogService/index.ts @@ -0,0 +1,3 @@ +import UIDialogService from './UIDialogService'; + +export default UIDialogService; diff --git a/platform/core/src/services/UIModalService/index.ts b/platform/core/src/services/UIModalService/index.ts new file mode 100644 index 0000000..73aa565 --- /dev/null +++ b/platform/core/src/services/UIModalService/index.ts @@ -0,0 +1,111 @@ +/** + * UI Modal + * + * @typedef {Object} ModalProps + * @property {ReactElement|HTMLElement} [content=null] Modal content. + * @property {Object} [contentProps=null] Modal content props. + * @property {boolean} [shouldCloseOnEsc=false] Modal is dismissible via the esc key. + * @property {boolean} [isOpen=true] Make the Modal visible or hidden. + * @property {boolean} [closeButton=true] Should the modal body render the close button. + * @property {string} [title=null] Should the modal render the title independently of the body content. + * @property {string} [customClassName=null] The custom class to style the modal. + */ + +const name = 'uiModalService'; + +const serviceImplementation = { + _hide: () => console.warn('hide() NOT IMPLEMENTED'), + _show: () => console.warn('show() NOT IMPLEMENTED'), + _customComponent: null, +}; + +class UIModalService { + static REGISTRATION = { + name, + altName: 'UIModalService', + create: (): UIModalService => { + return new UIModalService(); + }, + }; + + readonly name = name; + + /** + * Show a new UI modal; + * + * @param {ModalProps} props { content, contentProps, shouldCloseOnEsc, isOpen, closeButton, title, customClassName } + */ + show({ + content = null, + contentProps = null, + shouldCloseOnEsc = true, + isOpen = true, + closeButton = true, + title = null, + customClassName = null, + movable = false, + containerDimensions = null, + contentDimensions = null, + shouldCloseOnOverlayClick = true, + shouldCloseImmediately = false, + }) { + return serviceImplementation._show({ + content, + contentProps, + shouldCloseOnEsc, + isOpen, + closeButton, + title, + customClassName, + movable, + containerDimensions, + contentDimensions, + shouldCloseOnOverlayClick, + shouldCloseImmediately, + }); + } + + /** + * Hides/dismisses the modal, if currently shown + * + * @returns void + */ + hide() { + return serviceImplementation._hide(); + } + + /** + * This provides flexibility in customizing the Modal's default component + * + * @returns {React.Component} + */ + getCustomComponent() { + return serviceImplementation._customComponent; + } + + /** + * + * + * @param {*} { + * hide: hideImplementation, + * show: showImplementation, + * } + */ + setServiceImplementation({ + hide: hideImplementation, + show: showImplementation, + customComponent: customComponentImplementation, + }) { + if (hideImplementation) { + serviceImplementation._hide = hideImplementation; + } + if (showImplementation) { + serviceImplementation._show = showImplementation; + } + if (customComponentImplementation) { + serviceImplementation._customComponent = customComponentImplementation; + } + } +} + +export default UIModalService; diff --git a/platform/core/src/services/UINotificationService/index.ts b/platform/core/src/services/UINotificationService/index.ts new file mode 100644 index 0000000..f1f565d --- /dev/null +++ b/platform/core/src/services/UINotificationService/index.ts @@ -0,0 +1,152 @@ +const serviceImplementation = { + _hide: () => console.debug('hide() NOT IMPLEMENTED'), + _show: showArguments => { + console.debug('show() NOT IMPLEMENTED'); + return null; + }, +}; + +type ToastType = 'success' | 'error' | 'info' | 'warning' | 'loading'; + +class UINotificationService { + static REGISTRATION = { + name: 'uiNotificationService', + altName: 'UINotificationService', + create: (): UINotificationService => { + return new UINotificationService(); + }, + }; + + /** + * + * + * @param {*} { + * hide: hideImplementation, + * show: showImplementation, + * } + */ + public setServiceImplementation({ hide: hideImplementation, show: showImplementation }): void { + if (hideImplementation) { + serviceImplementation._hide = hideImplementation; + } + if (showImplementation) { + serviceImplementation._show = showImplementation; + } + } + + /** + * Hides/dismisses the notification, if currently shown + * + * @param {number} id - id of the notification to hide/dismiss + * @returns undefined + */ + public hide(id: string) { + return serviceImplementation._hide(id); + } + + /** + * Create and show a new UI notification; returns the + * ID of the created notification. Can also handle promises for loading states. + * + * @param {object} notification - The notification object + * @param {string} notification.title - The title of the notification + * @param {string | function} notification.message - The message content of the notification or a function that returns a message + * @param {number} [notification.duration=5000] - The duration to show the notification (in milliseconds) + * @param {'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center'} [notification.position='bottom-right'] - The position of the notification + * @param {ToastType} [notification.type='info'] - The type of the notification + * @param {boolean} [notification.autoClose=true] - Whether the notification should auto-close + * @param {Promise} [notification.promise] - A promise to track for loading, success, and error states + * @param {object} [notification.promiseMessages] - Custom messages for promise states + * @param {string} [notification.promiseMessages.loading] - Message to show while promise is pending + * @param {string | function} [notification.promiseMessages.success] - Message to show when promise resolves + * @param {string | function} [notification.promiseMessages.error] - Message to show when promise rejects + * @returns {string} id - The ID of the created notification + */ + show({ + title, + message, + duration = 5000, + position = 'bottom-right', + type = 'info', + autoClose = true, + promise, + promiseMessages, + }: { + title: string; + message: string | ((data?: any) => string); + duration?: number; + position?: + | 'top-left' + | 'top-right' + | 'bottom-left' + | 'bottom-right' + | 'top-center' + | 'bottom-center'; + type?: ToastType; + autoClose?: boolean; + promise?: Promise; + promiseMessages?: { + loading?: string; + success?: string | ((data: any) => string); + error?: string | ((error: any) => string); + }; + }): string { + if (promise && promiseMessages) { + const loadingId = serviceImplementation._show({ + title, + message: promiseMessages.loading || 'Loading...', + type: 'loading', + autoClose: false, + position, + }); + + promise.then( + data => { + const successMessage = + typeof promiseMessages.success === 'function' + ? promiseMessages.success(data) + : promiseMessages.success || 'Success'; + + serviceImplementation._show({ + title, + message: successMessage, + type: 'success', + duration, + position, + autoClose, + }); + this.hide(loadingId); + }, + error => { + const errorMessage = + typeof promiseMessages.error === 'function' + ? promiseMessages.error(error) + : promiseMessages.error || 'Error'; + + serviceImplementation._show({ + title, + message: errorMessage, + type: 'error', + duration, + position, + autoClose, + }); + this.hide(loadingId); + } + ); + + return loadingId; + } + + return serviceImplementation._show({ + title, + message, + duration, + position, + type, + autoClose, + }); + } +} + +export default UINotificationService; diff --git a/platform/core/src/services/UIViewportDialogService/UIViewportDialogService.ts b/platform/core/src/services/UIViewportDialogService/UIViewportDialogService.ts new file mode 100644 index 0000000..37b6418 --- /dev/null +++ b/platform/core/src/services/UIViewportDialogService/UIViewportDialogService.ts @@ -0,0 +1,52 @@ +import { PubSubService } from '../_shared/pubSubServiceInterface'; + +class UIViewportDialogService extends PubSubService { + public static readonly EVENTS = {}; + public static REGISTRATION = { + name: 'uiViewportDialogService', + altName: 'UIViewportDialogService', + create: ({ configuration = {} }) => { + return new UIViewportDialogService(); + }, + }; + + serviceImplementation = { + _hide: () => console.warn('hide() NOT IMPLEMENTED'), + _show: () => console.warn('show() NOT IMPLEMENTED'), + }; + + constructor() { + super(UIViewportDialogService.EVENTS); + this.serviceImplementation = { + ...this.serviceImplementation, + }; + } + + public show({ viewportId, id, type, message, actions, onSubmit, onOutsideClick, onKeyPress }) { + return this.serviceImplementation._show({ + viewportId, + id, + type, + message, + actions, + onSubmit, + onOutsideClick, + onKeyPress, + }); + } + + public hide() { + return this.serviceImplementation._hide(); + } + + public setServiceImplementation({ hide: hideImplementation, show: showImplementation }) { + if (hideImplementation) { + this.serviceImplementation._hide = hideImplementation; + } + if (showImplementation) { + this.serviceImplementation._show = showImplementation; + } + } +} + +export default UIViewportDialogService; diff --git a/platform/core/src/services/UIViewportDialogService/index.ts b/platform/core/src/services/UIViewportDialogService/index.ts new file mode 100644 index 0000000..a968569 --- /dev/null +++ b/platform/core/src/services/UIViewportDialogService/index.ts @@ -0,0 +1,3 @@ +import UIViewportDialogService from './UIViewportDialogService'; + +export default UIViewportDialogService; diff --git a/platform/core/src/services/UserAuthenticationService/UserAuthenticationService.ts b/platform/core/src/services/UserAuthenticationService/UserAuthenticationService.ts new file mode 100644 index 0000000..6ae3c8e --- /dev/null +++ b/platform/core/src/services/UserAuthenticationService/UserAuthenticationService.ts @@ -0,0 +1,92 @@ +import { PubSubService } from '../_shared/pubSubServiceInterface'; + +class UserAuthenticationService extends PubSubService { + public static readonly EVENTS = {}; + + public static REGISTRATION = { + name: 'userAuthenticationService', + altName: 'UserAuthenticationService', + create: ({ configuration = {} }) => { + return new UserAuthenticationService(); + }, + }; + + serviceImplementation = { + _getState: () => console.warn('getState() NOT IMPLEMENTED'), + _setUser: () => console.warn('_setUser() NOT IMPLEMENTED'), + _getUser: () => console.warn('_getUser() NOT IMPLEMENTED'), + _getAuthorizationHeader: () => {}, // TODO: Implement this method + _handleUnauthenticated: () => console.warn('_handleUnauthenticated() NOT IMPLEMENTED'), + _reset: () => console.warn('reset() NOT IMPLEMENTED'), + _set: () => console.warn('set() NOT IMPLEMENTED'), + }; + + constructor() { + super(UserAuthenticationService.EVENTS); + this.serviceImplementation = { + ...this.serviceImplementation, + }; + } + + public getState() { + return this.serviceImplementation._getState(); + } + + public setUser(user) { + return this.serviceImplementation._setUser(user); + } + + public getUser() { + return this.serviceImplementation._getUser(); + } + + public getAuthorizationHeader() { + return this.serviceImplementation._getAuthorizationHeader(); + } + + public handleUnauthenticated() { + return this.serviceImplementation._handleUnauthenticated(); + } + + public reset() { + return this.serviceImplementation._reset(); + } + + public set(state) { + return this.serviceImplementation._set(state); + } + + public setServiceImplementation({ + getState: getStateImplementation, + setUser: setUserImplementation, + getUser: getUserImplementation, + getAuthorizationHeader: getAuthorizationHeaderImplementation, + handleUnauthenticated: handleUnauthenticatedImplementation, + reset: resetImplementation, + set: setImplementation, + }) { + if (getStateImplementation) { + this.serviceImplementation._getState = getStateImplementation; + } + if (setUserImplementation) { + this.serviceImplementation._setUser = setUserImplementation; + } + if (getUserImplementation) { + this.serviceImplementation._getUser = getUserImplementation; + } + if (getAuthorizationHeaderImplementation) { + this.serviceImplementation._getAuthorizationHeader = getAuthorizationHeaderImplementation; + } + if (handleUnauthenticatedImplementation) { + this.serviceImplementation._handleUnauthenticated = handleUnauthenticatedImplementation; + } + if (resetImplementation) { + this.serviceImplementation._reset = resetImplementation; + } + if (setImplementation) { + this.serviceImplementation._set = setImplementation; + } + } +} + +export default UserAuthenticationService; diff --git a/platform/core/src/services/UserAuthenticationService/index.ts b/platform/core/src/services/UserAuthenticationService/index.ts new file mode 100644 index 0000000..a80dd6b --- /dev/null +++ b/platform/core/src/services/UserAuthenticationService/index.ts @@ -0,0 +1,3 @@ +import UserAuthenticationService from './UserAuthenticationService'; + +export default UserAuthenticationService; diff --git a/platform/core/src/services/ViewportGridService/ViewportGridService.ts b/platform/core/src/services/ViewportGridService/ViewportGridService.ts new file mode 100644 index 0000000..856bb32 --- /dev/null +++ b/platform/core/src/services/ViewportGridService/ViewportGridService.ts @@ -0,0 +1,321 @@ +import { PubSubService } from '../_shared/pubSubServiceInterface'; + +class ViewportGridService extends PubSubService { + public static readonly EVENTS = { + ACTIVE_VIEWPORT_ID_CHANGED: 'event::activeviewportidchanged', + LAYOUT_CHANGED: 'event::layoutChanged', + GRID_STATE_CHANGED: 'event::gridStateChanged', + GRID_SIZE_CHANGED: 'event::gridSizeChanged', + VIEWPORTS_READY: 'event::viewportsReady', + }; + + public static REGISTRATION = { + name: 'viewportGridService', + altName: 'ViewportGridService', + create: ({ configuration = {}, servicesManager }) => { + return new ViewportGridService({ servicesManager }); + }, + }; + + serviceImplementation = {}; + servicesManager: AppTypes.ServicesManager; + presentationIdProviders: Map< + string, + (id: string, { viewport, viewports, isUpdatingSameViewport, servicesManager }) => unknown + >; + + constructor({ servicesManager }) { + super(ViewportGridService.EVENTS); + this.servicesManager = servicesManager; + this.serviceImplementation = {}; + this.presentationIdProviders = new Map(); + } + + public addPresentationIdProvider( + id: string, + provider: (id: string, { viewport, viewports, isUpdatingSameViewport }) => unknown + ): void { + this.presentationIdProviders.set(id, provider); + } + + public getPresentationId(id: string, viewportId: string): string | null { + const state = this.getState(); + const viewport = state.viewports.get(viewportId); + return this._getPresentationId(id, { + viewport, + viewports: state.viewports, + }); + } + + private _getPresentationId(id, { viewport, viewports }) { + const isUpdatingSameViewport = [...viewports.values()].some( + v => + v.displaySetInstanceUIDs?.toString() === viewport.displaySetInstanceUIDs?.toString() && + v.viewportId === viewport.viewportId + ); + + const provider = this.presentationIdProviders.get(id); + if (provider) { + const result = provider(id, { + viewport, + viewports, + isUpdatingSameViewport, + servicesManager: this.servicesManager, + }); + return result; + } + return null; + } + + public getPresentationIds({ viewport, viewports }) { + // Use the keys of the Map to get all registered provider IDs + const registeredPresentationProviders = Array.from(this.presentationIdProviders.keys()); + + return registeredPresentationProviders.reduce((acc, id) => { + const value = this._getPresentationId(id, { + viewport, + viewports, + }); + if (value !== null) { + acc[id] = value; + } + return acc; + }, {}); + } + + public setServiceImplementation({ + getState: getStateImplementation, + setActiveViewportId: setActiveViewportIdImplementation, + setDisplaySetsForViewports: setDisplaySetsForViewportsImplementation, + setLayout: setLayoutImplementation, + reset: resetImplementation, + onModeExit: onModeExitImplementation, + set: setImplementation, + getNumViewportPanes: getNumViewportPanesImplementation, + setViewportIsReady: setViewportIsReadyImplementation, + }): void { + if (getStateImplementation) { + this.serviceImplementation._getState = getStateImplementation; + } + if (setActiveViewportIdImplementation) { + this.serviceImplementation._setActiveViewport = setActiveViewportIdImplementation; + } + if (setDisplaySetsForViewportsImplementation) { + this.serviceImplementation._setDisplaySetsForViewports = + setDisplaySetsForViewportsImplementation; + } + if (setLayoutImplementation) { + this.serviceImplementation._setLayout = setLayoutImplementation; + } + if (resetImplementation) { + this.serviceImplementation._reset = resetImplementation; + } + if (onModeExitImplementation) { + this.serviceImplementation._onModeExit = onModeExitImplementation; + } + if (setImplementation) { + this.serviceImplementation._set = setImplementation; + } + if (getNumViewportPanesImplementation) { + this.serviceImplementation._getNumViewportPanes = getNumViewportPanesImplementation; + } + + if (setViewportIsReadyImplementation) { + this.serviceImplementation._setViewportIsReady = setViewportIsReadyImplementation; + } + } + + public publishViewportsReady() { + this._broadcastEvent(this.EVENTS.VIEWPORTS_READY, {}); + } + + public setActiveViewportId(id: string) { + this.serviceImplementation._setActiveViewport(id); + + // Use queueMicrotask to delay the event broadcast + setTimeout(() => { + this._broadcastEvent(this.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED, { + viewportId: id, + }); + }, 0); + } + + public getState(): AppTypes.ViewportGrid.State { + return this.serviceImplementation._getState(); + } + + public getViewportState(viewportId: string) { + const state = this.getState(); + return state.viewports.get(viewportId); + } + + public setViewportIsReady(viewportId, callback) { + this.serviceImplementation._setViewportIsReady(viewportId, callback); + } + + public getActiveViewportId() { + const state = this.getState(); + return state.activeViewportId; + } + + public setViewportGridSizeChanged() { + const state = this.getState(); + this._broadcastEvent(this.EVENTS.GRID_SIZE_CHANGED, { + state, + }); + } + + public setDisplaySetsForViewport(props) { + // Just update a single viewport, but use the multi-viewport update for it. + this.setDisplaySetsForViewports([props]); + } + + public async setDisplaySetsForViewports(props) { + await this.serviceImplementation._setDisplaySetsForViewports(props); + const state = this.getState(); + const updatedViewports = []; + + const removedViewportIds = []; + + for (const viewport of props) { + const updatedViewport = state.viewports.get(viewport.viewportId); + + if (updatedViewport) { + updatedViewports.push(updatedViewport); + + const updatedDisplaySetUIDs = updatedViewport.displaySetInstanceUIDs || []; + + const isCleared = updatedDisplaySetUIDs.length === 0; + + if (isCleared) { + removedViewportIds.push(viewport.viewportId); + } + } else { + removedViewportIds.push(viewport.viewportId); + } + } + + setTimeout(() => { + this._broadcastEvent(ViewportGridService.EVENTS.GRID_STATE_CHANGED, { + state, + viewports: updatedViewports, + removedViewportIds, + }); + }); + } + + /** + * Retrieves the display set instance UIDs for a given viewport. + * @param viewportId The ID of the viewport. + * @returns An array of display set instance UIDs. + */ + public getDisplaySetsUIDsForViewport(viewportId: string) { + const state = this.getState(); + const viewport = state.viewports.get(viewportId); + return viewport?.displaySetInstanceUIDs; + } + + /** + * + * @param numCols, numRows - the number of columns and rows to apply + * @param findOrCreateViewport is a function which takes the + * index position of the viewport, the position id, and a set of + * options that is initially provided as {} (eg to store intermediate state) + * The function returns a viewport object to use at the given position. + */ + public async setLayout({ + numCols, + numRows, + layoutOptions, + layoutType = 'grid', + activeViewportId = undefined, + findOrCreateViewport = undefined, + isHangingProtocolLayout = false, + }) { + // Get the previous state before the layout change + const prevState = this.getState(); + const prevViewportIds = new Set(prevState.viewports.keys()); + + await this.serviceImplementation._setLayout({ + numCols, + numRows, + layoutOptions, + layoutType, + activeViewportId, + findOrCreateViewport, + isHangingProtocolLayout, + }); + + // Use queueMicrotask to ensure the layout changed event is published after + setTimeout(() => { + // Get the new state after the layout change + const state = this.getState(); + const currentViewportIds = new Set(state.viewports.keys()); + + // Determine which viewport IDs have been removed + const removedViewportIds = [...prevViewportIds].filter(id => !currentViewportIds.has(id)); + + this._broadcastEvent(this.EVENTS.LAYOUT_CHANGED, { + numCols, + numRows, + }); + + this._broadcastEvent(this.EVENTS.GRID_STATE_CHANGED, { + state, + removedViewportIds, + }); + }, 0); + } + + public reset() { + this.serviceImplementation._reset(); + } + + /** + * The onModeExit must set the state of the viewport grid to a standard/clean + * state. To implement store/recover of the viewport grid, perform + * a state store in the mode or extension onModeExit, and recover that + * data if appropriate in the onModeEnter of the mode or extension. + */ + public onModeExit(): void { + this.serviceImplementation._onModeExit(); + } + + public set(newState) { + const prevState = this.getState(); + const prevViewportIds = new Set(prevState.viewports.keys()); + + this.serviceImplementation._set(newState); + + const state = this.getState(); + const currentViewportIds = new Set(state.viewports.keys()); + + const removedViewportIds = [...prevViewportIds].filter(id => !currentViewportIds.has(id)); + + setTimeout(() => { + this._broadcastEvent(this.EVENTS.GRID_STATE_CHANGED, { + state, + removedViewportIds, + }); + }, 0); + } + + public getNumViewportPanes() { + return this.serviceImplementation._getNumViewportPanes(); + } + + public getLayoutOptionsFromState( + state: any + ): { x: number; y: number; width: number; height: number }[] { + return Array.from(state.viewports.entries()).map(([_, viewport]) => { + return { + x: viewport.x, + y: viewport.y, + width: viewport.width, + height: viewport.height, + }; + }); + } +} + +export default ViewportGridService; diff --git a/platform/core/src/services/ViewportGridService/index.ts b/platform/core/src/services/ViewportGridService/index.ts new file mode 100644 index 0000000..47d0835 --- /dev/null +++ b/platform/core/src/services/ViewportGridService/index.ts @@ -0,0 +1,3 @@ +import ViewportGridService from './ViewportGridService'; + +export default ViewportGridService; diff --git a/platform/core/src/services/WorkflowStepsService/WorkflowStepsService.ts b/platform/core/src/services/WorkflowStepsService/WorkflowStepsService.ts new file mode 100644 index 0000000..2c023fd --- /dev/null +++ b/platform/core/src/services/WorkflowStepsService/WorkflowStepsService.ts @@ -0,0 +1,243 @@ +import { CommandsManager } from '../../classes'; +import { ExtensionManager } from '../../extensions'; +import { PubSubService } from '../_shared/pubSubServiceInterface'; + +export const EVENTS = { + ACTIVE_STEP_CHANGED: 'event::workflowStepsService:activateStepChanged', + STEPS_CHANGED: 'event::workflowStepsService:stepsChanged', +}; + +/* + A mode may define a workflow and each workflow may have one or more steps. + Each step may define a different set of tools, hanging protocol and panels + layout that will be applied to the viewer once it gets activated making the + viewer work in a more dynamic way. + + Example: + All keys inside brackets are optionals. + + workflow: { + [initialStepId]: 'step1', + steps: [ + { + id: 'firstStep', + name: 'First Step', + [toolbar]: { + buttons: firstStepToolbarButtons, + sections: [ + { + key: 'primary', + buttons: [ 'MeasurementTools', 'Zoom', ... ], + }, + ], + }, + [layout]: { + [panels]: { + left: ['firstLeftPanelId', 'secondLeftPanelId'], + right: ['firstRightPanelId'], + }, + }, + [hangingProtocol]: { + protocolId: 'default', + [stepId]: 'firstStep', + }, + }, + { + id: 'secondStep', + name: 'Second Step', + ... + }, + ] + } + + If workflow steps are defined but `initialStepId` is not set then the first + step is set as active during mode initialization. +*/ + +type CommandCallback = { + commandName: string; + options: Record; +}; + +export type WorkflowStep = { + id: string; + name: string; + toolbarButtons?: { + buttonSection: string; + buttons: string[]; + }[]; + hangingProtocol?: { + protocolId: string; + stageId?: string; + }; + layout?: { + panels: { + left?: string[]; + right?: string[]; + }; + }; + onEnter: () => void | CommandCallback[]; + onExit: () => void | CommandCallback[]; +}; + +class WorkflowStepsService extends PubSubService { + private _extensionManager: ExtensionManager; + private _servicesManager: AppTypes.ServicesManager; + private _commandsManager: CommandsManager; + private _workflowSteps: WorkflowStep[]; + private _activeWorkflowStep: WorkflowStep; + + constructor( + extensionManager: ExtensionManager, + commandsManager: CommandsManager, + servicesManager: AppTypes.ServicesManager + ) { + super(EVENTS); + this._workflowSteps = []; + this._activeWorkflowStep = null; + this._extensionManager = extensionManager; + this._commandsManager = commandsManager; + this._servicesManager = servicesManager; + } + + public get workflowSteps(): WorkflowStep[] { + return [...this._workflowSteps]; + } + + public get activeWorkflowStep(): WorkflowStep { + return this._activeWorkflowStep; + } + + public addWorkflowSteps(workflowSteps: WorkflowStep[]): void { + let workflowStepAdded = false; + + workflowSteps.forEach(newWorkflowStep => { + const workflowStepExists = this._workflowSteps.some( + workflowStep => workflowStep.id === newWorkflowStep.id + ); + + if (workflowStepExists) { + throw new Error(`Duplicated workflow step id (${newWorkflowStep.id})`); + } + + this._workflowSteps.push(newWorkflowStep); + workflowStepAdded = true; + }); + + if (workflowStepAdded) { + this._broadcastEvent(EVENTS.STEPS_CHANGED, {}); + } + } + + private _updateToolBar(workflowStep: WorkflowStep) { + const { toolbarService } = this._servicesManager.services; + const { toolbarButtons } = workflowStep; + + const toUse = Array.isArray(toolbarButtons) ? toolbarButtons : [toolbarButtons]; + + toUse.forEach(({ buttonSection, buttons }) => { + toolbarService.clearButtonSection(buttonSection); + toolbarService.createButtonSection(buttonSection, buttons); + }); + } + + private _updatePanels(workflowStep: WorkflowStep) { + const { panelService } = this._servicesManager.services; + const panels = workflowStep?.layout?.panels; + + if (!panels) { + return; + } + + panelService.setPanels(panels, workflowStep?.layout?.options); + } + + private _updateHangingProtocol(workflowStep: WorkflowStep) { + const { hangingProtocol } = workflowStep; + + if (!hangingProtocol) { + return; + } + + this._commandsManager.runCommand('setHangingProtocol', { + protocolId: hangingProtocol.protocolId, + stageId: hangingProtocol.stageId, + stageIndex: hangingProtocol.stageIndex, + }); + } + + private _invokeCallbacks(callbacks) { + if (!callbacks) { + return; + } + + const commandsManager = this._commandsManager; + + if (!Array.isArray(callbacks)) { + callbacks = [callbacks]; + } + + // Invoke all callbacks which may be a function or an object like + // { commandName: string, options?: object } + callbacks.forEach(callback => { + let fn = callback; + + if (callback?.commandName) { + const { commandName, options } = callback; + fn = () => commandsManager.runCommand(commandName, options); + } + + fn(); + }); + } + + public setActiveWorkflowStep(workflowStepId: string): void { + const previousWorkflowStep = this._activeWorkflowStep; + + if (workflowStepId === previousWorkflowStep?.id) { + return; + } + + const newWorkflowStep = this._workflowSteps.find(step => step.id === workflowStepId); + + if (!newWorkflowStep) { + throw new Error(`Invalid workflowStepId (${workflowStepId})`); + } + + if (this._activeWorkflowStep) { + this._invokeCallbacks(previousWorkflowStep.onExit); + } + + // onEnter needs to be called before updating the Hanging Protocol because + // some displaySets need to be created before moving to the next HP stage + // (eg: convert segmentations into a chart displaySet). If needed we can + // change it to onBeforeEnter and onAfterEnter in the future. + this._invokeCallbacks(newWorkflowStep.onEnter); + + this._activeWorkflowStep = newWorkflowStep; + this._updateToolBar(newWorkflowStep); + this._updatePanels(newWorkflowStep); + this._updateHangingProtocol(newWorkflowStep); + this._broadcastEvent(EVENTS.ACTIVE_STEP_CHANGED, { + activeWorkflowStep: newWorkflowStep, + }); + } + + public reset(): void { + this._activeWorkflowStep = null; + this._workflowSteps = []; + } + + public onModeEnter(): void { + this.reset(); + } + + public static REGISTRATION = { + name: 'workflowStepsService', + create: ({ extensionManager, commandsManager, servicesManager }): WorkflowStepsService => { + return new WorkflowStepsService(extensionManager, commandsManager, servicesManager); + }, + }; +} + +export { WorkflowStepsService as default, WorkflowStepsService }; diff --git a/platform/core/src/services/WorkflowStepsService/index.ts b/platform/core/src/services/WorkflowStepsService/index.ts new file mode 100644 index 0000000..39db00b --- /dev/null +++ b/platform/core/src/services/WorkflowStepsService/index.ts @@ -0,0 +1,3 @@ +import WorkflowStepsService from './WorkflowStepsService'; + +export default WorkflowStepsService; diff --git a/platform/core/src/services/_shared/index.js b/platform/core/src/services/_shared/index.js new file mode 100644 index 0000000..3ae9a14 --- /dev/null +++ b/platform/core/src/services/_shared/index.js @@ -0,0 +1,3 @@ +import pubSubServiceInterface from './pubSubServiceInterface'; + +export { pubSubServiceInterface }; diff --git a/platform/core/src/services/_shared/pubSubServiceInterface.ts b/platform/core/src/services/_shared/pubSubServiceInterface.ts new file mode 100644 index 0000000..a286b97 --- /dev/null +++ b/platform/core/src/services/_shared/pubSubServiceInterface.ts @@ -0,0 +1,132 @@ +import guid from '../../utils/guid'; + +/** + * Consumer must implement: + * this.listeners = {} + * this.EVENTS = { "EVENT_KEY": "EVENT_VALUE" } + */ +export default { + subscribe, + _broadcastEvent, + _unsubscribe, + _isValidEvent, +}; + +/** + * Subscribe to updates. + * + * @param {string} eventName The name of the event + * @param {Function} callback Events callback + * @return {Object} Observable object with actions + */ +function subscribe(eventName, callback) { + if (this._isValidEvent(eventName)) { + const listenerId = guid(); + const subscription = { id: listenerId, callback }; + + // console.info(`Subscribing to '${eventName}'.`); + if (Array.isArray(this.listeners[eventName])) { + this.listeners[eventName].push(subscription); + } else { + this.listeners[eventName] = [subscription]; + } + + return { + unsubscribe: () => this._unsubscribe(eventName, listenerId), + }; + } else { + throw new Error(`Event ${eventName} not supported.`); + } +} + +/** + * Unsubscribe to measurement updates. + * + * @param {string} eventName The name of the event + * @param {string} listenerId The listeners id + * @return void + */ +function _unsubscribe(eventName, listenerId) { + if (!this.listeners[eventName]) { + return; + } + + const listeners = this.listeners[eventName]; + if (Array.isArray(listeners)) { + this.listeners[eventName] = listeners.filter(({ id }) => id !== listenerId); + } else { + this.listeners[eventName] = undefined; + } +} + +/** + * Check if a given event is valid. + * + * @param {string} eventName The name of the event + * @return {boolean} Event name validation + */ +function _isValidEvent(eventName) { + return Object.values(this.EVENTS).includes(eventName); +} + +/** + * Broadcasts changes. + * + * @param {string} eventName - The event name + * @param {func} callbackProps - Properties to pass callback + * @return void + */ +function _broadcastEvent(eventName, callbackProps) { + const hasListeners = Object.keys(this.listeners).length > 0; + const hasCallbacks = Array.isArray(this.listeners[eventName]); + + const event = new CustomEvent(eventName, { detail: callbackProps }); + document.body.dispatchEvent(event); + + if (hasListeners && hasCallbacks) { + this.listeners[eventName].forEach(listener => { + listener.callback(callbackProps); + }); + } +} + +/** Export a PubSubService class to be used instead of the individual items */ +export class PubSubService { + EVENTS: any; + subscribe: (eventName: string, callback: Function) => { unsubscribe: () => any }; + _broadcastEvent: (eventName: string, callbackProps: any) => void; + _unsubscribe: (eventName: string, listenerId: string) => void; + _isValidEvent: (eventName: string) => boolean; + listeners: {}; + unsubscriptions: any[]; + constructor(EVENTS) { + this.EVENTS = EVENTS; + this.subscribe = subscribe; + this._broadcastEvent = _broadcastEvent; + this._unsubscribe = _unsubscribe; + this._isValidEvent = _isValidEvent; + this.listeners = {}; + this.unsubscriptions = []; + } + + reset() { + this.unsubscriptions.forEach(unsub => unsub()); + this.unsubscriptions = []; + } + + /** + * Creates an event that records whether or not someone + * has consumed it. Call eventData.consume() to consume the event. + * Check eventData.isConsumed to see if it is consumed or not. + * @param props - to include in the event + */ + protected createConsumableEvent(props) { + return { + ...props, + isConsumed: false, + consume: function Consume() { + this.isConsumed = true; + }, + }; + } +} diff --git a/platform/core/src/services/index.ts b/platform/core/src/services/index.ts new file mode 100644 index 0000000..8ddd794 --- /dev/null +++ b/platform/core/src/services/index.ts @@ -0,0 +1,47 @@ +import MeasurementService from './MeasurementService'; +import ServicesManager from './ServicesManager'; +import ServiceProvidersManager from './ServiceProvidersManager'; +import UIDialogService from './UIDialogService'; +import UIModalService from './UIModalService'; +import UINotificationService from './UINotificationService'; +import UIViewportDialogService from './UIViewportDialogService'; +import DicomMetadataStore from './DicomMetadataStore'; +import DisplaySetService from './DisplaySetService'; +import ToolbarService from './ToolBarService'; +import ViewportGridService from './ViewportGridService'; +import CineService from './CineService'; +import HangingProtocolService from './HangingProtocolService'; +import pubSubServiceInterface, { PubSubService } from './_shared/pubSubServiceInterface'; +import UserAuthenticationService from './UserAuthenticationService'; +import CustomizationService from './CustomizationService'; +import PanelService from './PanelService'; +import WorkflowStepsService from './WorkflowStepsService'; +import StudyPrefetcherService from './StudyPrefetcherService'; +import { MultiMonitorService } from './MultiMonitorService'; + +import type Services from '../types/Services'; + +export { + Services, + MeasurementService, + ServicesManager, + ServiceProvidersManager, + CustomizationService, + UIDialogService, + UIModalService, + UINotificationService, + UIViewportDialogService, + DicomMetadataStore, + MultiMonitorService, + DisplaySetService, + ToolbarService, + ViewportGridService, + HangingProtocolService, + CineService, + pubSubServiceInterface, + PubSubService, + UserAuthenticationService, + PanelService, + WorkflowStepsService, + StudyPrefetcherService, +}; diff --git a/platform/core/src/string.js b/platform/core/src/string.js new file mode 100644 index 0000000..0c841a9 --- /dev/null +++ b/platform/core/src/string.js @@ -0,0 +1,62 @@ +function isObject(subject) { + return subject instanceof Object || (typeof subject === 'object' && subject !== null); +} + +function isString(subject) { + return typeof subject === 'string'; +} + +// Search for some string inside any object or array +function search(object, query, property = null, result = []) { + // Create the search pattern + const pattern = new RegExp(query.trim(), 'i'); + + Object.keys(object).forEach(key => { + const item = object[key]; + + // Stop here if item is empty + if (!item) { + return; + } + + // Get the value to be compared + const value = isString(property) ? item[property] : item; + + // Check if the value match the pattern + if (isString(value) && pattern.test(value)) { + // Add the current item to the result + result.push(item); + } + + if (isObject(item)) { + // Search recursively the item if the current item is an object + search(item, query, property, result); + } + }); + + // Return the found items + return result; +} + +// Encode any string into a safe format for HTML id attribute +function encodeId(input) { + const string = input && input.toString ? input.toString() : input; + + // Return an underscore if the given string is empty or if it's not a string + if (string === '' || typeof string !== 'string') { + return '_'; + } + + // Create a converter to replace non accepted chars + const converter = match => '_' + match[0].charCodeAt(0).toString(16) + '_'; + + // Encode the given string and return it + return string.replace(/[^a-zA-Z0-9-]/g, converter); +} + +const string = { + search, + encodeId, +}; + +export default string; diff --git a/platform/core/src/types/AppTypes.ts b/platform/core/src/types/AppTypes.ts new file mode 100644 index 0000000..cf38365 --- /dev/null +++ b/platform/core/src/types/AppTypes.ts @@ -0,0 +1,262 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +import HangingProtocolServiceType from '../services/HangingProtocolService'; +import CustomizationServiceType from '../services/CustomizationService'; +import MeasurementServiceType from '../services/MeasurementService'; +import ViewportGridServiceType from '../services/ViewportGridService'; +import ToolbarServiceType from '../services/ToolBarService'; +import DisplaySetServiceType from '../services/DisplaySetService'; +import UINotificationServiceType from '../services/UINotificationService'; +import UIModalServiceType from '../services/UIModalService'; +import WorkflowStepsServiceType from '../services/WorkflowStepsService'; +import CineServiceType from '../services/CineService'; +import UserAuthenticationServiceType from '../services/UserAuthenticationService'; +import PanelServiceType from '../services/PanelService'; +import UIDialogServiceType from '../services/UIDialogService'; +import UIViewportDialogServiceType from '../services/UIViewportDialogService'; +import StudyPrefetcherServiceType from '../services/StudyPrefetcherService'; +import type { MultiMonitorService } from '../services/MultiMonitorService'; + +import ServicesManagerType from '../services/ServicesManager'; +import CommandsManagerType from '../classes/CommandsManager'; +import ExtensionManagerType from '../extensions/ExtensionManager'; + +import Hotkey from '../classes/Hotkey'; + +import * as CommandTypes from './Command'; +import * as ColorTypes from './Color'; +import * as ConsumerTypes from './Consumer'; +import * as DataSourceTypes from './DataSource'; +import * as DataSourceConfigurationAPITypes from './DataSourceConfigurationAPI'; +import * as DisplaySetTypes from './DisplaySet'; +import * as HangingProtocolTypes from './HangingProtocol'; +import * as IPubSubTypes from './IPubSub'; +import * as PanelModuleTypes from './PanelModule'; +import * as StudyMetadataTypes from './StudyMetadata'; +import * as ViewportGridTypes from './ViewportGridType'; + +import { StepOptions, TourOptions } from 'shepherd.js'; + +declare global { + namespace AppTypes { + export type ServicesManager = ServicesManagerType; + export type CommandsManager = CommandsManagerType; + export type ExtensionManager = ExtensionManagerType; + export type HangingProtocolService = HangingProtocolServiceType; + export type CustomizationService = CustomizationServiceType; + export type MeasurementService = MeasurementServiceType; + export type DisplaySetService = DisplaySetServiceType; + export type ToolbarService = ToolbarServiceType; + export type ViewportGridService = ViewportGridServiceType; + export type UIModalService = UIModalServiceType; + export type UINotificationService = UINotificationServiceType; + export type WorkflowStepsService = WorkflowStepsServiceType; + export type CineService = CineServiceType; + export type UserAuthenticationService = UserAuthenticationServiceType; + export type UIDialogService = UIDialogServiceType; + export type UIViewportDialogService = UIViewportDialogServiceType; + export type PanelService = PanelServiceType; + export type StudyPrefetcherService = StudyPrefetcherServiceType; + export type MultiMonitorService; + + export interface Managers { + servicesManager?: ServicesManager; + commandsManager?: CommandsManager; + extensionManager?: ExtensionManager; + } + + export interface Services { + hangingProtocolService?: HangingProtocolServiceType; + customizationService?: CustomizationServiceType; + measurementService?: MeasurementServiceType; + displaySetService?: DisplaySetServiceType; + toolbarService?: ToolbarServiceType; + viewportGridService?: ViewportGridServiceType; + uiModalService?: UIModalServiceType; + uiNotificationService?: UINotificationServiceType; + workflowStepsService?: WorkflowStepsServiceType; + cineService?: CineServiceType; + userAuthenticationService?: UserAuthenticationServiceType; + uiDialogService?: UIDialogServiceType; + uiViewportDialogService?: UIViewportDialogServiceType; + panelService?: PanelServiceType; + studyPrefetcherService?: StudyPrefetcherServiceType; + multiMonitorService?: MultiMonitorService; + } + + export interface Config { + studyBrowserMode?: 'all' | 'primary'; + routerBasename?: string; + customizationService?: CustomizationServiceType; + extensions?: string[]; + modes?: string[]; + experimentalStudyBrowserSort?: boolean; + defaultDataSourceName?: string; + hotkeys?: Record | Hotkey[]; + preferSizeOverAccuracy?: boolean; + useNorm16Texture?: boolean; + useCPURendering?: boolean; + strictZSpacingForVolumeViewport?: boolean; + useCursors?: boolean; + maxCacheSize?: number; + max3DTextureSize?: number; + showWarningMessageForCrossOrigin?: boolean; + showCPUFallbackMessage?: boolean; + maxNumRequests?: { + interaction?: number; + prefetch?: number; + thumbnail?: number; + compute?: number; + }; + maxNumberOfWebWorkers?: number; + acceptHeader?: string[]; + investigationalUseDialog?: { + option: 'always' | 'never' | 'configure'; + days?: number; + }; + groupEnabledModesFirst?: boolean; + disableConfirmationPrompts?: boolean; + showPatientInfo?: 'visible' | 'visibleCollapsed' | 'disabled' | 'visibleReadOnly'; + requestTransferSyntaxUID?: string; + omitQuotationForMultipartRequest?: boolean; + modesConfiguration?: { + [key: string]: object; + }; + showLoadingIndicator?: boolean; + supportsWildcard?: boolean; + allowMultiSelectExport?: boolean; + activateViewportBeforeInteraction?: boolean; + autoPlayCine?: boolean; + showStudyList?: boolean; + whiteLabeling?: Record; + httpErrorHandler?: (error: Error) => void; + addWindowLevelActionMenu?: boolean; + dangerouslyUseDynamicConfig?: { + enabled: boolean; + regex: RegExp; + }; + onConfiguration?: ( + dicomWebConfig: Record, + options: Record + ) => Record; + dataSources?: Record; + oidc?: Record; + peerImport?: (moduleId: string) => Promise>; + studyPrefetcher?: { + enabled: boolean; + displaySetsCount: number; + maxNumPrefetchRequests: number; + order: 'closest' | 'downward' | 'upward'; + }; + } + + export interface Test { + services?: Services; + commandsManager?: CommandsManager; + extensionManager?: ExtensionManager; + config?: Config; + } + + // Add Command namespace to AppTypes + export namespace Commands { + export type SimpleCommand = CommandTypes.SimpleCommand; + export type ComplexCommand = CommandTypes.ComplexCommand; + export type Command = CommandTypes.Command; + export type RunCommand = CommandTypes.RunCommand; + export interface Commands extends CommandTypes.Commands {} + } + + // Color types + export type RGB = ColorTypes.RGB; + + // Consumer types + export type Consumer = ConsumerTypes.Consumer; + + // DataSource types + export type DataSourceDefinition = DataSourceTypes.DataSourceDefinition; + + // DataSourceConfigurationAPI types + export namespace DataSourceConfiguration { + export type BaseDataSourceConfigurationAPIItem = + DataSourceConfigurationAPITypes.BaseDataSourceConfigurationAPIItem; + export type BaseDataSourceConfigurationAPI = + DataSourceConfigurationAPITypes.BaseDataSourceConfigurationAPI; + } + + // DisplaySet types + export type DisplaySet = DisplaySetTypes.DisplaySet; + export type DisplaySetSeriesMetadataInvalidatedEvent = + DisplaySetTypes.DisplaySetSeriesMetadataInvalidatedEvent; + + // HangingProtocol types + export namespace HangingProtocol { + export type DisplaySetInfo = HangingProtocolTypes.DisplaySetInfo; + export type ViewportMatchDetails = HangingProtocolTypes.ViewportMatchDetails; + export type DisplaySetMatchDetails = HangingProtocolTypes.DisplaySetMatchDetails; + export type DisplaySetAndViewportOptions = HangingProtocolTypes.DisplaySetAndViewportOptions; + export type DisplayArea = HangingProtocolTypes.DisplayArea; + export type SetProtocolOptions = HangingProtocolTypes.SetProtocolOptions; + export type HangingProtocolMatchDetails = HangingProtocolTypes.HangingProtocolMatchDetails; + export type ConstraintValue = HangingProtocolTypes.ConstraintValue; + export type Constraint = HangingProtocolTypes.Constraint; + export type MatchingRule = HangingProtocolTypes.MatchingRule; + export type ViewportLayoutOptions = HangingProtocolTypes.ViewportLayoutOptions; + export type ViewportStructure = HangingProtocolTypes.ViewportStructure; + export type DisplaySetSelector = HangingProtocolTypes.DisplaySetSelector; + export type SyncGroup = HangingProtocolTypes.SyncGroup; + export type CustomOptionAttribute = HangingProtocolTypes.CustomOptionAttribute; + export type CustomOption = HangingProtocolTypes.CustomOption; + export type initialImageOptions = HangingProtocolTypes.initialImageOptions; + export type ViewportOptions = HangingProtocolTypes.ViewportOptions; + export type DisplaySetOptions = HangingProtocolTypes.DisplaySetOptions; + export type Viewport = HangingProtocolTypes.Viewport; + export type StageStatus = HangingProtocolTypes.StageStatus; + export type StageActivation = HangingProtocolTypes.StageActivation; + export type ProtocolStage = HangingProtocolTypes.ProtocolStage; + export type ProtocolNotifications = HangingProtocolTypes.ProtocolNotifications; + export type Protocol = HangingProtocolTypes.Protocol; + export type ProtocolGenerator = HangingProtocolTypes.ProtocolGenerator; + export type HPInfo = HangingProtocolTypes.HPInfo; + } + + // IPubSub types + export namespace PubSub { + export type IPubSub = IPubSubTypes.default; + export type Subscription = IPubSubTypes.Subscription; + } + + // PanelModule types + export namespace PanelModule { + export type Panel = PanelModuleTypes.Panel; + export type ActivatePanelTriggers = PanelModuleTypes.ActivatePanelTriggers; + export type PanelEvent = PanelModuleTypes.PanelEvent; + export type ActivatePanelEvent = PanelModuleTypes.ActivatePanelEvent; + } + + // StudyMetadata types + export namespace StudyMetadata { + export type PatientMetadata = StudyMetadataTypes.PatientMetadata; + export type StudyMetadata = StudyMetadataTypes.StudyMetadata; + export type SeriesMetadata = StudyMetadataTypes.SeriesMetadata; + export type InstanceMetadata = StudyMetadataTypes.InstanceMetadata; + } + + // ViewportGrid types + export namespace ViewportGrid { + export type Viewport = ViewportGridTypes.GridViewport; + export type Layout = ViewportGridTypes.Layout; + export type State = ViewportGridTypes.ViewportGridState; + export type Viewports = ViewportGridTypes.GridViewports; + export type GridViewportOptions = ViewportGridTypes.GridViewportOptions; + } + } + + export interface PresentationIds {} + + export type withAppTypes = T & + AppTypes.Services & + AppTypes.Managers & { + [key: string]: unknown; + } & AppTypes.Config; + + export type withTestTypes = T & AppTypes.Test; +} diff --git a/platform/core/src/types/Color.ts b/platform/core/src/types/Color.ts new file mode 100644 index 0000000..7dfacef --- /dev/null +++ b/platform/core/src/types/Color.ts @@ -0,0 +1,4 @@ +/** + * RGB color type + */ +export type RGB = [number, number, number]; diff --git a/platform/core/src/types/Command.ts b/platform/core/src/types/Command.ts new file mode 100644 index 0000000..965b459 --- /dev/null +++ b/platform/core/src/types/Command.ts @@ -0,0 +1,14 @@ +export type SimpleCommand = string; +export interface ComplexCommand { + commandName: string; + commandOptions?: Record; + context?: string; +} + +export type Command = SimpleCommand | ComplexCommand; +export type RunCommand = Command | Command[]; + +/** A set of commands, typically contained in a tool item or other configuration */ +export interface Commands { + commands: RunCommand; +} diff --git a/platform/core/src/types/Consumer.ts b/platform/core/src/types/Consumer.ts new file mode 100644 index 0000000..8c59ebc --- /dev/null +++ b/platform/core/src/types/Consumer.ts @@ -0,0 +1,4 @@ +/** + * Just a function that consumes a single argument, with no return. + */ +export type Consumer = (props: Record) => void; diff --git a/platform/core/src/types/DataSource.ts b/platform/core/src/types/DataSource.ts new file mode 100644 index 0000000..cc27e16 --- /dev/null +++ b/platform/core/src/types/DataSource.ts @@ -0,0 +1,7 @@ +export type DataSourceDefinition = { + // TODO friendlyName to move to configuration; here now for legacy purposes + friendlyName: string; + namespace: string; + sourceName: string; + configuration: any; +}; diff --git a/platform/core/src/types/DataSourceConfigurationAPI.ts b/platform/core/src/types/DataSourceConfigurationAPI.ts new file mode 100644 index 0000000..146f4a2 --- /dev/null +++ b/platform/core/src/types/DataSourceConfigurationAPI.ts @@ -0,0 +1,68 @@ +export interface BaseDataSourceConfigurationAPIItem { + id: string; + name: string; +} + +/** + * The interface to use to configure an associated data source. Typically an + * instance of this interface is associated with a data source that the instance + * understands and can alter the data source's configuration. + */ +export interface BaseDataSourceConfigurationAPI { + /** + * Gets the i18n labels (i.e. the i18n lookup keys) for each of the configurable items + * of the data source configuration API. + * For example, for the Google Cloud Healthcare API, this would be + * ['Project', 'Location', 'Data set', 'DICOM store']. + * Besides the configurable item labels themselves, several other string look ups + * are used base on EACH of the labels returned by this method. + * For instance, for the label {itemLabel}, the following strings are fetched for + * translation... + * 1. `No {itemLabel} available` + * - used to indicate no such items are available + * - for example, for Google, `No Project available` would be 'No projects available' + * 2. `Select {itemLabel}` + * - used to direct selection of the item + * - for example, for Google, `Select Project` would be 'Select a project' + * 3. `Error fetching {itemLabel} list` + * - used to indicate an error occurred fetching the list of items + * - usually accompanied by the error itself + * - for example, for Google, `Error fetching Project list` would be 'Error fetching projects' + * 4. `Search {itemLabel} list` + * - used as the placeholder text for filtering a list of items + * - for example, for Google, `Search Project list` would be 'Search projects' + */ + getItemLabels(): Array; + + /** + * Initializes the data source configuration API and returns the top-level sub-items + * that can be chosen to begin the process of configuring the data source. + * For example, for the Google Cloud Healthcare API, this would perform the initial request + * to fetch the top level projects for the logged in user account. + */ + initialize(): Promise>; + + /** + * Sets the current path item and returns the sub-items of that item + * that can be further chosen to configure a data source. + * When setting the last configurable item of the data source (path), this method + * returns an empty list AND configures the active data source with the selected + * items path. + * For example, for the Google Cloud Healthcare API, this would take the current item + * (say a data set) and queries and returns its sub-items (i.e. all of the DICOM stores + * contained in that data set). Furthermore, whenever the item to set is a DICOM store, + * the Google Cloud Healthcare API implementation would update the OHIF data source + * associated with this instance to point to that DICOM store. + * @param item the item to set as current + */ + setCurrentItem( + item: BaseDataSourceConfigurationAPIItem + ): Promise>; + + /** + * Gets the list of items currently configured for the data source associated with + * this API instance. The resultant array must be the same length as the result of + * `getItemLabels`. + */ + getConfiguredItems(): Promise>; +} diff --git a/platform/core/src/types/DisplaySet.ts b/platform/core/src/types/DisplaySet.ts new file mode 100644 index 0000000..d81d484 --- /dev/null +++ b/platform/core/src/types/DisplaySet.ts @@ -0,0 +1,29 @@ +import { InstanceMetadata } from './StudyMetadata'; + +export type DisplaySet = { + displaySetInstanceUID: string; + instances: InstanceMetadata[]; + StudyInstanceUID: string; + SeriesInstanceUID?: string; + SeriesNumber?: number; + SeriesDescription?: string; + numImages?: number; + unsupported?: boolean; + Modality?: string; + imageIds?: string[]; + images?: unknown[]; + + // Details about how to display: + /** A URL that can be used to display the thumbnail. Typically a data url */ + thumbnailSrc?: string; + /** A fetch method to get the thumbnail */ + getThumbnailSrc?(imageId?: string): Promise; + SeriesDate?: string; + SeriesTime?: string; + instance?: InstanceMetadata; +}; + +export type DisplaySetSeriesMetadataInvalidatedEvent = { + displaySetInstanceUID: string; + invalidateData: boolean; +}; diff --git a/platform/core/src/types/HangingProtocol.ts b/platform/core/src/types/HangingProtocol.ts new file mode 100644 index 0000000..eeb9142 --- /dev/null +++ b/platform/core/src/types/HangingProtocol.ts @@ -0,0 +1,376 @@ +import { Command } from './Command'; + +export type DisplaySetInfo = { + displaySetInstanceUID?: string; + displaySetOptions: DisplaySetOptions; +}; + +export type ViewportMatchDetails = { + viewportOptions: ViewportOptions; + displaySetsInfo: DisplaySetInfo[]; +}; + +export type DisplaySetMatchDetails = { + StudyInstanceUID?: string; + displaySetInstanceUID: string; + matchDetails?: any; + matchingScores?: DisplaySetMatchDetails[]; + sortingInfo?: any; +}; + +export type DisplaySetAndViewportOptions = { + displaySetInstanceUIDs: string[]; + viewportOptions: ViewportOptions; + displaySetOptions: DisplaySetOptions; +}; + +export type DisplayArea = { + type?: 'SCALE' | 'FIT'; + scale?: number; + interpolationType?: any; + imageArea?: [number, number]; // areaX, areaY + imageCanvasPoint?: { + imagePoint: [number, number]; // imageX, imageY + canvasPoint?: [number, number]; // canvasX, canvasY + }; + storeAsInitialCamera?: boolean; +}; + +export type SetProtocolOptions = { + /** Used to provide a mapping of what keys are provided for which viewport. + * For example, a Chest XRay might use have the display set selector id of + * "ChestXRay", then the user might drag an alternate chest xray from the initially chosen one, + * and then navigate to another stage or protocol. If that new stage/protocol + * uses the name "ChestXRay", then that selection will be used instead of + * matching the display set selectors. That allows remembering the + * user selected views by name. + * Note the keys are not simple display set selector values, but are: + * `${activeStudyUID}:${displaySetSelectorId}:${matchingDisplaySetIndex || 0}` + * This is normally transparent to the user of this, but in order to specify + * specific instances, they can be added like that. + */ + displaySetSelectorMap?: Record; + + /** Used to define the display sets already in view, in order to allow + * filling empty viewports with other instances. + * Only used when the -1 value for matchedDisplaySetsIndex is provided. + * List of display set instance UID's already displayed. + */ + inDisplay?: string[]; + + /** Select the given stage, either by ID or position. + * Don't forget that name is used as the ID if ID not provided. + */ + stageId?: string; + stageIndex?: number; + + /** Indicates to setup the protocol and fire the PROTOCOL_RESTORED event + * but don't fire the protocol changed event. Used to restore the + * HP service to a previous state. + */ + restoreProtocol?: boolean; +}; + +export type HangingProtocolMatchDetails = { + displaySetMatchDetails: Map; + viewportMatchDetails: Map; +}; + +export type ConstraintValue = + | string + | number + | boolean + | [] + | string[] + | { + value: string | number | boolean | []; + }; + +export type Constraint = { + // This value exactly + equals?: ConstraintValue; + notEquals?: ConstraintValue; + // A caseless contains + containsI?: string; + contains?: ConstraintValue; + doesNotContain?: ConstraintValue; + greaterThan?: ConstraintValue; +}; + +export type MatchingRule = { + // No real use for the id + id?: string; + // Defaults to 1 + weight?: number; + attribute: string; + constraint?: Constraint; + // Not required by default + required?: boolean; +}; + +export type ViewportLayoutOptions = { + x: number; + y: number; + width: number; + height: number; +}; + +export type ViewportStructure = { + layoutType: string; + properties: { + rows: number; + columns: number; + layoutOptions?: ViewportLayoutOptions[]; + }; +}; + +/** + * Selects the display sets to apply for a given id. + * This is a set of rules which match the study and display sets + * and then provides an id for them so that they can re-used in different + * viewports. + * The matches are done lazily, so if a stage doesn't need a given match, + * it won't be selected. + */ +export type DisplaySetSelector = { + id?: string; + + /** + * This can be set to true to allow unmatched views to replace a view showing this instance + * This is done at hte display set selector level to ensure that viewports sharing a display set + * don't get different values of allowUnmatchedView + */ + allowUnmatchedView?: boolean; + + // The image matching rule (not currently implemented) selects which image to + // display initially, only for stack views. + imageMatchingRules?: MatchingRule[]; + // The matching rules to choose the display sets at the series level + seriesMatchingRules: MatchingRule[]; + studyMatchingRules?: MatchingRule[]; +}; + +export type OverlaySelector = { + id?: string; + matchingRules: MatchingRule[]; +}; + +export type SyncGroup = { + type: string; + id: string; + source?: boolean; + target?: boolean; + options?: object; +}; + +/** Declares a custom option, that is a computed type value */ +export type CustomOptionAttribute = { + custom: string; + defaultValue?: T; +}; + +export type CustomOption = CustomOptionAttribute | T; + +export type initialImageOptions = { + index?: number; + preset?: string; // todo: type more +}; + +export type ViewportOptions = { + toolGroupId?: CustomOption; + viewportType?: CustomOption; + id?: string; + orientation?: CustomOption; + background?: CustomOption<[number, number, number]>; + viewportId?: string; + displayArea?: DisplayArea; + initialImageOptions?: CustomOption; + syncGroups?: CustomOption[]; + customViewportProps?: Record; + /** + * Set to true to allow non-matching drag and drop or options provided + * from options.displaySetSelectorsMap + * @deprecated Moving to display set selector + */ + allowUnmatchedView?: boolean; +}; + +// The options here includes both the display set selector and matching index +// as well as actual options to apply to the individual viewports. +export type DisplaySetOptions = { + // The id is used to choose which display set selector to apply here + id: string; + /** The offset to allow display secondary series, for example + * to display the second matching series, use `matchedDisplaySetsIndex==1` */ + matchedDisplaySetsIndex?: number; + + // The options to apply to the display set. + options?: Record; +}; + +// some options for overlays +// such as segmentation options +export type OverlayOptions = { + id?: string; + options?: Record; +}; + +export type Viewport = { + viewportOptions: ViewportOptions; + displaySets: DisplaySetOptions[]; + overlays?: OverlayOptions[]; +}; + +/** + * disabled stages are missing display sets required in order to view them. + * enabled stages have all the requiredDisplaySets and at least preferredViewports + * filled. + * passive stages have the requiredDisplaySets and at least requiredViewports filled. + */ +export type StageStatus = 'disabled' | 'enabled' | 'passive'; + +/** Controls whether a stage is activated or not, at the given level, by + * controlling the status of the stage. + */ +export type StageActivation = { + // The minimum number of viewports to be NON-blank to activate this level of the stage + minViewportsMatched?: number; + // The required set of display set selectors to have at least 1 match to activate + displaySetSelectorsMatched?: string[]; +}; + +/** + * Protocol stages are a set of different views which can be applied, for + * example, a 2x1 and a 1x1 view might be both applied (see default extension + * for this example). + */ +export type ProtocolStage = { + /** The id defaults to the name of the protocol if not otherwise specified */ + id?: string; + /** + * The display name used for this stage when shown to the user. This can + * differ from the id, for example, to use the same name for different + * stages, only one of which ends up being active. + */ + name: string; + /** Indicate if the stage can be applied or not */ + status?: StageStatus; + + viewportStructure: ViewportStructure; + stageActivation?: { + // The enabled activation is provided for fully active stages, + // participating in automatic stage selection and navigation + enabled?: StageActivation; + // The passive activation is provided to allow stages to manually + // be activated, but not navigated to by default, or used on initial view + passive?: StageActivation; + }; + + /** A viewport definition used for to fill in manually selected viewports. + * This allows changing the layout definition for additional viewports without + * needing to define layouts for each of the 1x1, 2x2 etc modes. + */ + defaultViewport?: Viewport; + + viewports: Viewport[]; + + // Unused. + createdDate?: string; +}; + +// Add notifications for various types of events. +export type ProtocolNotifications = { + // This set of commands is executed after the protocol is exited and the new one applied + onProtocolExit?: Command[]; + // This set of commands is executed after the protocol is entered and applied + onProtocolEnter?: Command[]; + // This set of commands is executed before the layout change is started. + // If it returns false, the layout change will be aborted. + // The numRows and numCols is included in the command params, so it is possible + // to apply a specific hanging protocol + onLayoutChange?: Command[]; + // This set of commands is executed after the initial viewport grid data is set + // and all viewport data includes a designated display set. This command + // will run on every stage's initial layout. + onViewportDataInitialized?: Command[]; +}; + +/** + * A protocol is the top level definition for a hanging protocol. + * It is a set of rules about when the protocol can be applied at all, + * as well as a set of stages that represent individual views. + * Additionally, the display set selectors are used to choose from the existing + * display sets. The hanging protocol definition here does NOT allow + * redefining the display sets to use, but only selects the views to show. + */ +export type Protocol = { + // Mandatory + id: string; + /** A description of this protocol. Used as a tool tip for the user. */ + description?: string; + /** Maps ids to display set selectors to choose display sets */ + displaySetSelectors: Record; + /** overlay selectors that decide whether an overlay such as segmentation should be shown or not */ + overlaySelectors?: Record; + /** A default viewport to use for any stage to select new viewport layouts. */ + defaultViewport?: Viewport; + stages: ProtocolStage[]; + // Optional + locked?: boolean; + name?: string; + createdDate?: string; + modifiedDate?: string; + availableTo?: Record; + editableBy?: Record; + toolGroupIds?: string[]; + // A set of callbacks relevant to entering and exiting the protocol + callbacks?: ProtocolNotifications; + imageLoadStrategy?: string; // Todo: this should be types specifically + protocolMatchingRules?: MatchingRule[]; + /* The number of priors required for this hanging protocol. + * -1 means that NO priors are referenced, and thus this HP matches + * only the active study, whereas 0 means that an unknown number of + * priors is matched. Positive values mean at least that many priors are + * required. + * Replaces hasUpdatedPriors + */ + numberOfPriorsReferenced?: number; + syncDataForViewports?: boolean; + /** + * Set of minimal conditions necessary to run the hanging protocol. + */ + hpInitiationCriteria?: { + /* If configured, sets the minimum number of series needed to run the hanging + * protocol and start displaying images. Used when OHIF needs to handle studies + * with several series and it is required that the first image should be loaded + * faster. + */ + minSeriesLoaded: number; + }; + + /* + * The icon to use for this protocol. This is used to display the protocol + * in the advanced layout selector. + */ + + icon?: string; + + /** Indicates if the protocol is a preset or not. Useful for setting presets for the layout selector */ + isPreset?: true; +}; + +/** Used to dynamically generate protocols. + * Try to avoid this as it is difficult to provide active/disabled settings + * to the GUI when this is used, and it can be expensive to apply. + * Alternatives include using the custom attributes where possible. + */ +export type ProtocolGenerator = ({ servicesManager, commandsManager }: withAppTypes) => { + protocol: Protocol; +}; + +export type HPInfo = { + protocolId: string; + stageId: string; + stageIndex: number; + activeStudyUID: string; +}; diff --git a/platform/core/src/types/IPubSub.ts b/platform/core/src/types/IPubSub.ts new file mode 100644 index 0000000..733c981 --- /dev/null +++ b/platform/core/src/types/IPubSub.ts @@ -0,0 +1,12 @@ +import Consumer from './Consumer'; + +export default interface IPubSub { + subscribe: (eventName: string, callback: Consumer) => void; + _broadcastEvent: (eventName: string, callbackProps: Record) => void; + _unsubscribe: (eventName: string, listenerId: string) => void; + _isValidEvent: (eventName: string) => boolean; +} + +export type Subscription = { + unsubscribe: () => void; +}; diff --git a/platform/core/src/types/PanelModule.ts b/platform/core/src/types/PanelModule.ts new file mode 100644 index 0000000..eb9d13e --- /dev/null +++ b/platform/core/src/types/PanelModule.ts @@ -0,0 +1,25 @@ +import { PubSubService } from '../services'; + +type Panel = { + id?: string; + name: string; + iconName: string; + iconLabel: string; + label: string; + component: React.FC; +}; + +type ActivatePanelTriggers = { + sourcePubSubService: PubSubService; + sourceEvents: string[]; +}; + +interface PanelEvent { + panelId: string; +} + +interface ActivatePanelEvent extends PanelEvent { + forceActive: boolean; +} + +export type { ActivatePanelEvent, ActivatePanelTriggers, Panel, PanelEvent }; diff --git a/platform/core/src/types/Services.ts b/platform/core/src/types/Services.ts new file mode 100644 index 0000000..3812b80 --- /dev/null +++ b/platform/core/src/types/Services.ts @@ -0,0 +1,41 @@ +import { + HangingProtocolService, + CustomizationService, + MeasurementService, + ViewportGridService, + ToolbarService, + DisplaySetService, + UINotificationService, + UIModalService, + WorkflowStepsService, + CineService, + UserAuthenticationService, + PanelService, + UIDialogService, + UIViewportDialogService, + MultiMonitorService, +} from '../services'; + +/** + * The interface for the services object + */ + +interface Services { + hangingProtocolService?: HangingProtocolService; + customizationService?: CustomizationService; + measurementService?: MeasurementService; + displaySetService?: DisplaySetService; + toolbarService?: ToolbarService; + viewportGridService?: ViewportGridService; + uiModalService?: UIModalService; + uiNotificationService?: UINotificationService; + workflowStepsService: WorkflowStepsService; + cineService?: CineService; + userAuthenticationService?: UserAuthenticationService; + uiDialogService?: UIDialogService; + uiViewportDialogService?: UIViewportDialogService; + panelService?: PanelService; + multiMonitorService?: MultiMonitorService; +} + +export default Services; diff --git a/platform/core/src/types/StudyMetadata.ts b/platform/core/src/types/StudyMetadata.ts new file mode 100644 index 0000000..e06299e --- /dev/null +++ b/platform/core/src/types/StudyMetadata.ts @@ -0,0 +1,24 @@ +/** Defines a typescript interface for study metadata. + * This defines the types for when using study metadata as interfaces. + */ + +export interface PatientMetadata extends Record { + PatientName?: string; + PatientId?: string; +} + +export interface StudyMetadata extends Record { + readonly StudyInstanceUID?: string; + StudyDescription?: string; +} + +export interface SeriesMetadata extends StudyMetadata { + readonly SeriesInstanceUID?: string; + SeriesDescription?: string; + SeriesNumber?: string | number; +} + +export interface InstanceMetadata extends SeriesMetadata { + readonly SOPInstanceUID: string; + InstanceNumber?: string | number; +} diff --git a/platform/core/src/types/ViewportGridType.ts b/platform/core/src/types/ViewportGridType.ts new file mode 100644 index 0000000..1079a8b --- /dev/null +++ b/platform/core/src/types/ViewportGridType.ts @@ -0,0 +1,70 @@ +export interface GridViewportOptions { + id?: string; + viewportId?: string; + viewportType?: string; + toolGroupId?: string; + presentationIds?: AppTypes.PresentationIds; + flipHorizontal?: boolean; + // + orientation?: string; + allowUnmatchedView?: boolean; + needsRerendering?: boolean; + background?: [number, number, number]; + syncGroups?: unknown[]; + rotation?: number; + initialImageOptions?: unknown; + customViewportProps?: Record; + // + displayArea?: unknown; + viewReference?: unknown; +} + +export interface GridViewport { + viewportId: string; + displaySetInstanceUIDs: string[]; + viewportOptions: GridViewportOptions; + displaySetSelectors: unknown[]; + displaySetOptions: unknown[]; + x: number; + y: number; + width: number; + height: number; + viewportLabel: string | null; + isReady: boolean; +} + +export interface Layout { + numRows: number; + numCols: number; + layoutType: string; +} + +export type GridViewports = Map; + +export interface ViewportGridState { + activeViewportId: string | null; + layout: Layout; + isHangingProtocolLayout: boolean; + viewports: GridViewports; +} + +export type SetDisplaySetsForViewportsProps = Array<{ + viewportId: string; + displaySetInstanceUIDs: string[]; + viewportOptions?: AppTypes.ViewportGrid.GridViewportOptions; + displaySetOptions?: Array<{ + id?: string; + voi?: { + windowWidth: number; + windowCenter: number; + }; + voiInverted?: boolean; + blendMode?: string; + slabThickness?: number; + colormap?: { + name: string; + opacity?: number; + }; + displayPreset?: string; + }>; +}>; diff --git a/platform/core/src/types/index.ts b/platform/core/src/types/index.ts new file mode 100644 index 0000000..e01e64f --- /dev/null +++ b/platform/core/src/types/index.ts @@ -0,0 +1,34 @@ +import type * as Extensions from '../extensions/ExtensionManager'; +import type * as HangingProtocol from './HangingProtocol'; +import type Services from './Services'; +import type Hotkey from '../classes/Hotkey'; +import type { DataSourceDefinition } from './DataSource'; +import type { + BaseDataSourceConfigurationAPI, + BaseDataSourceConfigurationAPIItem, +} from './DataSourceConfigurationAPI'; + +export type * from '../services/ViewportGridService'; +export type * from '../services/CustomizationService/types'; +// Separate out some generic types +export type * from './Consumer'; +export type * from './Command'; +export type * from './DisplaySet'; +export type * from './StudyMetadata'; +export type * from './PanelModule'; +export type * from './IPubSub'; +export type * from './Color'; + +/** + * Export the types used within the various services and managers, but + * not the services/managers themselves, which are exported at the top level. + */ +export { + Extensions, + HangingProtocol, + Services, + Hotkey, + DataSourceDefinition, + BaseDataSourceConfigurationAPI, + BaseDataSourceConfigurationAPIItem, +}; diff --git a/platform/core/src/user.js b/platform/core/src/user.js new file mode 100644 index 0000000..9e5df8d --- /dev/null +++ b/platform/core/src/user.js @@ -0,0 +1,13 @@ +// These should be overridden by the implementation +let user = { + userLoggedIn: () => false, + getUserId: () => null, + getName: () => null, + getAccessToken: () => null, + login: () => new Promise((resolve, reject) => reject()), + logout: () => new Promise((resolve, reject) => reject()), + getData: key => null, + setData: (key, value) => null, +}; + +export default user; diff --git a/platform/core/src/utils/Queue.js b/platform/core/src/utils/Queue.js new file mode 100644 index 0000000..34149bc --- /dev/null +++ b/platform/core/src/utils/Queue.js @@ -0,0 +1,64 @@ +export default class Queue { + constructor(limit) { + this.limit = limit; + this.size = 0; + this.awaiting = null; + } + + /** + * Creates a new "proxy" function associated with the current execution queue + * instance. When the returned function is invoked, the queue limit is checked + * to make sure the limit of scheduled tasks is respected (throwing an + * exception when the limit has been reached and before calling the original + * function). The original function is only invoked after all the previously + * scheduled tasks have finished executing (their returned promises have + * resolved/rejected); + * + * @param {function} task The function whose execution will be associated + * with the current Queue instance; + * @returns {function} The "proxy" function bound to the current Queue + * instance; + */ + bind(task) { + return bind(this, task); + } + + bindSafe(task, onError) { + const boundTask = bind(this, task); + return async function safeTask(...args) { + try { + return await boundTask(...args); + } catch (e) { + onError(e); + } + }; + } +} + +/** + * Utils + */ + +function bind(queue, task) { + const cleaner = clean.bind(null, queue); + return async function boundTask(...args) { + if (queue.size >= queue.limit) { + throw new Error('Queue limit reached'); + } + const promise = chain(queue.awaiting, task, args); + queue.awaiting = promise.then(cleaner, cleaner); + queue.size++; + return promise; + }; +} + +function clean(queue) { + if (queue.size > 0 && --queue.size === 0) { + queue.awaiting = null; + } +} + +async function chain(prev, task, args) { + await prev; + return task(...args); +} diff --git a/platform/core/src/utils/Queue.test.js b/platform/core/src/utils/Queue.test.js new file mode 100644 index 0000000..9ea302a --- /dev/null +++ b/platform/core/src/utils/Queue.test.js @@ -0,0 +1,69 @@ +import makeDeferred from './makeDeferred'; +import Queue from './Queue'; + +/** + * Utils + */ + +function timeout(delay) { + const { resolve, promise } = makeDeferred(); + setTimeout(() => void resolve(Date.now()), delay); + return promise; +} + +/** + * Tests + */ + +const threshold = 2400; + +describe('Queue', () => { + // Todo: comment due to wrong implementation + // it('should bind functions to the queue', async () => { + // const queue = new Queue(2); + // const mockedTimeout = jest.fn(timeout); + // const timer = queue.bind(mockedTimeout); + // const start = Date.now(); + // timer(threshold).then(now => { + // const elapsed = now - start; + // expect(elapsed >= threshold && elapsed <= 2 * threshold).toBe(true); + // }); + // const end = await timer(threshold); + // expect(end - start >= 2 * threshold).toBe(true); + // expect(mockedTimeout).toBeCalledTimes(2); + // }); + it('should prevent task execution when queue limit is reached', async () => { + const queue = new Queue(1); + const mockedTimeout = jest.fn(timeout); + const timer = queue.bind(mockedTimeout); + const start = Date.now(); + const promise = timer(threshold).then(time => time - start); + try { + await timer(threshold); + } catch (e) { + expect(Date.now() - start < threshold).toBe(true); + expect(e.message).toBe('Queue limit reached'); + } + const elapsed = await promise; + expect(elapsed >= threshold && elapsed < 2 * threshold).toBe(true); + expect(mockedTimeout).toBeCalledTimes(1); + }); + it('should safely bind tasks to the queue', async () => { + const queue = new Queue(1); + const mockedErrorHandler = jest.fn(); + const mockedTimeout = jest.fn(timeout); + const timer = queue.bindSafe(mockedTimeout, mockedErrorHandler); + const start = Date.now(); + const promise = timer(threshold).then(time => time - start); + await timer(threshold); + expect(Date.now() - start < threshold).toBe(true); + expect(mockedErrorHandler).toBeCalledTimes(1); + expect(mockedErrorHandler).nthCalledWith( + 1, + expect.objectContaining({ message: 'Queue limit reached' }) + ); + const elapsed = await promise; + expect(elapsed >= threshold && elapsed < 2 * threshold).toBe(true); + expect(mockedTimeout).toBeCalledTimes(1); + }); +}); diff --git a/platform/core/src/utils/absoluteUrl.js b/platform/core/src/utils/absoluteUrl.js new file mode 100644 index 0000000..7fb9fd9 --- /dev/null +++ b/platform/core/src/utils/absoluteUrl.js @@ -0,0 +1,22 @@ +const absoluteUrl = path => { + let absolutePath = '/'; + + if (!path) { + return absolutePath; + } + + // TODO: Find another way to get root url + const absoluteUrl = window.location.origin; + const absoluteUrlParts = absoluteUrl.split('/'); + + if (absoluteUrlParts.length > 4) { + const rootUrlPrefixIndex = absoluteUrl.indexOf(absoluteUrlParts[3]); + absolutePath += absoluteUrl.substring(rootUrlPrefixIndex) + path; + } else { + absolutePath += path; + } + + return absolutePath.replace(/\/\/+/g, '/'); +}; + +export default absoluteUrl; diff --git a/platform/core/src/utils/absoluteUrl.test.js b/platform/core/src/utils/absoluteUrl.test.js new file mode 100644 index 0000000..41cc64a --- /dev/null +++ b/platform/core/src/utils/absoluteUrl.test.js @@ -0,0 +1,44 @@ +import absoluteUrl from './absoluteUrl'; + +describe('absoluteUrl', () => { + test('should return /path_1/path_2/path_3/path_to_destination when the window.location.origin is http://dummy.com/path_1/path_2 and the path is /path_3/path_to_destination', () => { + let global = { + window: Object.create(window), + }; + const url = 'http://dummy.com/path_1/path_2'; + Object.defineProperty(window, 'location', { + value: { + origin: url, + }, + writable: true, + }); + const absoluteUrlOutput = absoluteUrl('/path_3/path_to_destination'); + expect(absoluteUrlOutput).toEqual('/path_1/path_2/path_3/path_to_destination'); + }); + + test('should return / when the path is not defined', () => { + const absoluteUrlOutput = absoluteUrl(undefined); + expect(absoluteUrlOutput).toBe('/'); + }); + + test('should return the original path when there path in the window.origin after the domain and port', () => { + delete global.window.location; + const url = 'http://dummy.com'; + global.window.location = { + origin: url, + }; + const absoluteUrlOutput = absoluteUrl('path_1/path_2/path_3'); + expect(absoluteUrlOutput).toEqual('/path_1/path_2/path_3'); + }); + + test('should be able to return the absolute path even when the path contains duplicates', () => { + global.window ||= Object.create(window); + const url = 'http://dummy.com'; + delete global.window.location; + global.window.location = { + origin: url, + }; + const absoluteUrlOutput = absoluteUrl('path_1/path_1/path_1'); + expect(absoluteUrlOutput).toEqual('/path_1/path_1/path_1'); + }); +}); diff --git a/platform/core/src/utils/addAccessors.js b/platform/core/src/utils/addAccessors.js new file mode 100644 index 0000000..c9f2fa5 --- /dev/null +++ b/platform/core/src/utils/addAccessors.js @@ -0,0 +1,66 @@ +const handler = { + /** + * Get a proxied value from the array or property value + * Note that the property value get works even if you update the underlying object. + * Also, return true of proxy.__isProxy in order to distinguish proxies and not double proxy them. + */ + get: (target, prop) => { + if (prop == '__isProxy') { + return true; + } + if (prop in target) { + return target[prop]; + } + return target[0][prop]; + }, + + set: (obj, prop, value) => { + if (typeof prop === 'number' || prop in obj) { + obj[prop] = value; + } else { + obj[0][prop] = value; + } + return true; + }, +}; + +/** + * Add a proxy object for sqZero or the src[0] element if sqZero is unspecified, AND + * src is an array of length 1. + * + * If sqZero isn't passed in, then assume this is a create call on the destination object + * itself, by: + * 1. If not an object, return dest + * 2. If an array of length != 1, return dest + * 3. If an array, use dest[0] as sqZero + * 4. Use dest as sqZero + * + * @example + * src = [{a:5,b:'string', c:null}] + * addAccessors(src) + * src.c = 'outerChange' + * src[0].b='innerChange' + * + * assert src.a===5 + * assert src[0].c === 'outerChange' + * assert src.b === 'innerChange' + */ +const addAccessors = (dest, sqZero) => { + if (dest.__isProxy) { + return dest; + } + let itemZero = sqZero; + if (itemZero === undefined) { + if (typeof dest !== 'object') { + return dest; + } + if (Array.isArray(dest) && dest.length !== 1) { + return dest; + } + itemZero = Array.isArray(dest) ? dest[0] : dest; + } + const ret = [itemZero]; + return new Proxy(ret, handler); +}; + +export default addAccessors; diff --git a/platform/core/src/utils/addServer.test.js b/platform/core/src/utils/addServer.test.js new file mode 100644 index 0000000..dc6a715 --- /dev/null +++ b/platform/core/src/utils/addServer.test.js @@ -0,0 +1,78 @@ +import addServers from './addServers'; + +describe('addServers', () => { + const servers = { + dicomWeb: [ + { + name: 'DCM4CHEE', + wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado', + qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + qidoSupportsIncludeField: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + }, + ], + oidc: [ + { + authority: 'http://127.0.0.1/auth/realms/ohif', + client_id: 'ohif-viewer', + redirect_uri: 'http://127.0.0.1/callback', + response_type: 'code', + scope: 'openid', + post_logout_redirect_uri: '/logout-redirect.html', + }, + ], + }; + + const store = { + dispatch: jest.fn(), + }; + + test('should be able to add a server and dispatch to the store successfuly', () => { + addServers(servers, store); + expect(store.dispatch).toBeCalledWith({ + server: { + authority: 'http://127.0.0.1/auth/realms/ohif', + client_id: 'ohif-viewer', + post_logout_redirect_uri: '/logout-redirect.html', + redirect_uri: 'http://127.0.0.1/callback', + response_type: 'code', + scope: 'openid', + type: 'oidc', + }, + type: 'ADD_SERVER', + }); + expect(store.dispatch).toBeCalledWith({ + server: { + imageRendering: 'wadors', + name: 'DCM4CHEE', + qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + qidoSupportsIncludeField: true, + thumbnailRendering: 'wadors', + type: 'dicomWeb', + wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado', + }, + type: 'ADD_SERVER', + }); + }); + + test('should throw an error if servers list is not defined', () => { + expect(() => addServers(undefined, store)).toThrowError( + new Error('The servers and store must be defined') + ); + }); + + test('should throw an error if store is not defined', () => { + expect(() => addServers(servers, undefined)).toThrowError( + new Error('The servers and store must be defined') + ); + }); + + test('should throw an error when both server and store are not defined', () => { + expect(() => addServers(undefined, undefined)).toThrowError( + new Error('The servers and store must be defined') + ); + }); +}); diff --git a/platform/core/src/utils/addServers.js b/platform/core/src/utils/addServers.js new file mode 100644 index 0000000..c825243 --- /dev/null +++ b/platform/core/src/utils/addServers.js @@ -0,0 +1,21 @@ +// TODO: figure out where else to put this function +const addServers = (servers, store) => { + if (!servers || !store) { + throw new Error('The servers and store must be defined'); + } + + Object.keys(servers).forEach(serverType => { + const endpoints = servers[serverType]; + endpoints.forEach(endpoint => { + const server = Object.assign({}, endpoint); + server.type = serverType; + + store.dispatch({ + type: 'ADD_SERVER', + server, + }); + }); + }); +}; + +export default addServers; diff --git a/platform/core/src/utils/b64toBlob.js b/platform/core/src/utils/b64toBlob.js new file mode 100644 index 0000000..0928ea7 --- /dev/null +++ b/platform/core/src/utils/b64toBlob.js @@ -0,0 +1,22 @@ +/* Enabled JPEG images downloading on IE11. */ +const b64toBlob = (b64Data, contentType = '', sliceSize = 512) => { + const byteCharacters = atob(b64Data); + const byteArrays = []; + + for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { + const slice = byteCharacters.slice(offset, offset + sliceSize); + + const byteNumbers = new Array(slice.length); + for (let i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + + const byteArray = new Uint8Array(byteNumbers); + byteArrays.push(byteArray); + } + + const blob = new Blob(byteArrays, { type: contentType }); + return blob; +}; + +export default b64toBlob; diff --git a/platform/core/src/utils/combineFrameInstance.ts b/platform/core/src/utils/combineFrameInstance.ts new file mode 100644 index 0000000..efb07c0 --- /dev/null +++ b/platform/core/src/utils/combineFrameInstance.ts @@ -0,0 +1,143 @@ +import { vec3 } from 'gl-matrix'; +import { dicomSplit } from './dicomSplit'; + +/** + * Combine the Per instance frame data, the shared frame data + * and the root data objects. + * The data is combined by taking nested sequence objects within + * the functional group sequences. Data that is directly contained + * within the functional group sequences, such as private creators + * will be ignored. + * This can be safely called with an undefined frame in order to handle + * single frame data. (eg frame is undefined is the same as frame===1). + */ +const combineFrameInstance = (frame, instance) => { + const { + PerFrameFunctionalGroupsSequence, + SharedFunctionalGroupsSequence, + NumberOfFrames, + ImageType, + } = instance; + + instance.ImageType = dicomSplit(ImageType); + + if (PerFrameFunctionalGroupsSequence || NumberOfFrames > 1) { + const frameNumber = Number.parseInt(frame || 1); + + // this is to fix NM multiframe datasets with position and orientation + // information inside DetectorInformationSequence + if (!instance.ImageOrientationPatient && instance.DetectorInformationSequence) { + instance.ImageOrientationPatient = + instance.DetectorInformationSequence[0].ImageOrientationPatient; + } + + let ImagePositionPatientToUse = instance.ImagePositionPatient; + + if (!instance.ImagePositionPatient && instance.DetectorInformationSequence) { + let imagePositionPatient = instance.DetectorInformationSequence[0].ImagePositionPatient; + let imageOrientationPatient = instance.ImageOrientationPatient; + + imagePositionPatient = imagePositionPatient?.map(it => Number(it)); + imageOrientationPatient = imageOrientationPatient?.map(it => Number(it)); + const SpacingBetweenSlices = Number(instance.SpacingBetweenSlices); + + // Calculate the position for the current frame + if (imageOrientationPatient && SpacingBetweenSlices) { + const rowOrientation = vec3.fromValues( + imageOrientationPatient[0], + imageOrientationPatient[1], + imageOrientationPatient[2] + ); + + const colOrientation = vec3.fromValues( + imageOrientationPatient[3], + imageOrientationPatient[4], + imageOrientationPatient[5] + ); + + const normalVector = vec3.cross(vec3.create(), rowOrientation, colOrientation); + + const position = vec3.scaleAndAdd( + vec3.create(), + imagePositionPatient, + normalVector, + SpacingBetweenSlices * (frameNumber - 1) + ); + + ImagePositionPatientToUse = [position[0], position[1], position[2]]; + } + } + + // Cache the _parentInstance at the top level as a full copy to prevent + // setting values hard. + if (!instance._parentInstance) { + Object.defineProperty(instance, '_parentInstance', { + value: { ...instance }, + }); + } + const sharedInstance = createCombinedValue( + instance._parentInstance, + SharedFunctionalGroupsSequence?.[0], + '_shared' + ); + const newInstance = createCombinedValue( + sharedInstance, + PerFrameFunctionalGroupsSequence?.[frameNumber - 1], + frameNumber + ); + + newInstance.ImagePositionPatient = ImagePositionPatientToUse ?? + newInstance.ImagePositionPatient ?? [0, 0, frameNumber]; + + Object.defineProperty(newInstance, 'frameNumber', { + value: frameNumber, + writable: true, + enumerable: true, + configurable: true, + }); + return newInstance; + } else { + return instance; + } +}; + +/** + * Creates a combined instance stored in the parent object which + * inherits from the parent instance the attributes in the functional groups. + * The storage key in the parent is in key + */ +function createCombinedValue(parent, functionalGroups, key) { + if (parent[key]) { + return parent[key]; + } + // Exclude any proxying values + const newInstance = Object.create(parent); + Object.defineProperty(parent, key, { + value: newInstance, + writable: false, + enumerable: false, + }); + if (!functionalGroups) { + return newInstance; + } + const shared = functionalGroups + ? Object.values(functionalGroups) + .filter(Boolean) + .map(it => it[0]) + .filter(it => typeof it === 'object') + : []; + + // merge the shared first then the per frame to override + [...shared].forEach(item => { + if (item.SOPInstanceUID) { + // This sub-item is a previous value information item, so don't merge it + return; + } + Object.entries(item).forEach(([key, value]) => { + newInstance[key] = value; + }); + }); + return newInstance; +} + +export default combineFrameInstance; diff --git a/platform/core/src/utils/createStacks.draft-test.js b/platform/core/src/utils/createStacks.draft-test.js new file mode 100644 index 0000000..0cf7272 --- /dev/null +++ b/platform/core/src/utils/createStacks.draft-test.js @@ -0,0 +1,33 @@ +// Leaving here as a starting point +// import createStacks from './createStacks.js'; + +// describe('createStacks.js', () => { +// const seriesMetadatas = [ +// { +// getInstanceCount: jest.fn().mockReturnValue(1), +// getData: jest.fn().mockReturnValue({ +// SeriesDate: '2019-06-04', +// }), +// }, +// { +// getInstanceCount: jest.fn().mockReturnValue(1), +// getData: jest.fn().mockReturnValue({ +// SeriesDate: '2018-06-04', +// }), +// }, +// ]; +// const studyMetadata = { +// getSeriesCount: jest.fn().mockReturnValue(2), +// forEachSeries: jest.fn().mockImplementation(callback => { +// callback(seriesMetadatas[0], 0); +// callback(seriesMetadatas[1], 1); +// }), +// getStudyInstanceUID: jest.fn(), +// }; + +// it('sorts displaySets by SeriesNumber, then by SeriesDate', () => { +// const displaySets = createStacks(studyMetadata); + +// expect(displaySets.length).toBe(2); +// }); +// }); diff --git a/platform/core/src/utils/createStudyBrowserTabs.ts b/platform/core/src/utils/createStudyBrowserTabs.ts new file mode 100644 index 0000000..259558a --- /dev/null +++ b/platform/core/src/utils/createStudyBrowserTabs.ts @@ -0,0 +1,81 @@ +/** + * + * @param {string[]} primaryStudyInstanceUIDs + * @param {object[]} studyDisplayList + * @param {string} studyDisplayList.studyInstanceUid + * @param {string} studyDisplayList.date + * @param {string} studyDisplayList.description + * @param {string} studyDisplayList.modalities + * @param {number} studyDisplayList.numInstances + * @param {object[]} displaySets + * @param {number} recentTimeframe - The number of milliseconds to consider a study recent + * @returns tabs - The prop object expected by the StudyBrowser component + */ + +export function createStudyBrowserTabs( + primaryStudyInstanceUIDs, + studyDisplayList, + displaySets, + recentTimeframeMS = 31536000000 +) { + const primaryStudies = []; + const allStudies = []; + + studyDisplayList.forEach(study => { + const displaySetsForStudy = displaySets.filter( + ds => ds.StudyInstanceUID === study.studyInstanceUid + ); + const tabStudy = Object.assign({}, study, { + displaySets: displaySetsForStudy, + }); + + if (primaryStudyInstanceUIDs.includes(study.studyInstanceUid)) { + primaryStudies.push(tabStudy); + } + allStudies.push(tabStudy); + }); + + const primaryStudiesTimestamps = primaryStudies + .filter(study => study.date) + .map(study => new Date(study.date).getTime()); + + const recentStudies = + primaryStudiesTimestamps.length > 0 + ? allStudies.filter(study => { + const oldestPrimaryTimeStamp = Math.min(...primaryStudiesTimestamps); + + if (!study.date) { + return false; + } + const studyTimeStamp = new Date(study.date).getTime(); + return oldestPrimaryTimeStamp - studyTimeStamp < recentTimeframeMS; + }) + : []; + + // Newest first + const _byDate = (a, b) => { + const dateA = Date.parse(a); + const dateB = Date.parse(b); + + return dateB - dateA; + }; + const tabs = [ + { + name: 'primary', + label: 'Primary', + studies: primaryStudies.sort((studyA, studyB) => _byDate(studyA.date, studyB.date)), + }, + { + name: 'recent', + label: 'Recent', + studies: recentStudies.sort((studyA, studyB) => _byDate(studyA.date, studyB.date)), + }, + { + name: 'all', + label: 'All', + studies: allStudies.sort((studyA, studyB) => _byDate(studyA.date, studyB.date)), + }, + ]; + + return tabs; +} diff --git a/platform/core/src/utils/debounce.js b/platform/core/src/utils/debounce.js new file mode 100644 index 0000000..7a5b2bd --- /dev/null +++ b/platform/core/src/utils/debounce.js @@ -0,0 +1,25 @@ +// Returns a function, that, as long as it continues to be invoked, will not +// be triggered. The function will be called after it stops being called for +// N milliseconds. If `immediate` is passed, trigger the function on the +// leading edge, instead of the trailing. +function debounce(func, wait, immediate) { + var timeout; + return function () { + var context = this, + args = arguments; + var later = function () { + timeout = null; + if (!immediate) { + func.apply(context, args); + } + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + func.apply(context, args); + } + }; +} + +export default debounce; diff --git a/platform/core/src/utils/dicomSplit.ts b/platform/core/src/utils/dicomSplit.ts new file mode 100644 index 0000000..54b20d3 --- /dev/null +++ b/platform/core/src/utils/dicomSplit.ts @@ -0,0 +1,5 @@ +export function dicomSplit(value) { + return ( + (Array.isArray(value) && value) || (typeof value === 'string' && value.split('\\')) || value + ); +} diff --git a/platform/core/src/utils/downloadCSVReport.js b/platform/core/src/utils/downloadCSVReport.js new file mode 100644 index 0000000..70adf9f --- /dev/null +++ b/platform/core/src/utils/downloadCSVReport.js @@ -0,0 +1,106 @@ +import { DicomMetadataStore } from '../services/DicomMetadataStore/DicomMetadataStore'; +import formatPN from './formatPN'; + +export default function downloadCSVReport(measurementData) { + if (measurementData.length === 0) { + // Prevent download of report with no measurements. + return; + } + + const columns = [ + 'Patient ID', + 'Patient Name', + 'StudyInstanceUID', + 'SeriesInstanceUID', + 'SOPInstanceUID', + 'Label', + ]; + + const reportMap = {}; + measurementData.forEach(measurement => { + const { referenceStudyUID, referenceSeriesUID, getReport, uid } = measurement; + + if (!getReport) { + console.warn('Measurement does not have a getReport function'); + return; + } + + const seriesMetadata = DicomMetadataStore.getSeries(referenceStudyUID, referenceSeriesUID); + + const commonRowItems = _getCommonRowItems(measurement, seriesMetadata); + const report = getReport(measurement); + + reportMap[uid] = { + report, + commonRowItems, + }; + }); + + // get columns names inside the report from each measurement and + // add them to the rows array (this way we can add columns for any custom + // measurements that may be added in the future) + Object.keys(reportMap).forEach(id => { + const { report } = reportMap[id]; + report.columns.forEach(column => { + if (!columns.includes(column)) { + columns.push(column); + } + }); + }); + + const results = _mapReportsToRowArray(reportMap, columns); + + let csvContent = 'data:text/csv;charset=utf-8,' + results.map(res => res.join(',')).join('\n'); + + _createAndDownloadFile(csvContent); +} + +function _mapReportsToRowArray(reportMap, columns) { + const results = [columns]; + Object.keys(reportMap).forEach(id => { + const { report, commonRowItems } = reportMap[id]; + const row = []; + // For commonRowItems, find the correct index and add the value to the + // correct row in the results array + Object.keys(commonRowItems).forEach(key => { + const index = columns.indexOf(key); + const value = commonRowItems[key]; + row[index] = value; + }); + + // For each annotation data, find the correct index and add the value to the + // correct row in the results array + report.columns.forEach((column, index) => { + const colIndex = columns.indexOf(column); + const value = report.values[index]; + row[colIndex] = value; + }); + + results.push(row); + }); + + return results; +} + +function _getCommonRowItems(measurement, seriesMetadata) { + const firstInstance = seriesMetadata.instances[0]; + + return { + 'Patient ID': firstInstance.PatientID, // Patient ID + 'Patient Name': formatPN(firstInstance.PatientName) || '', // Patient Name + StudyInstanceUID: measurement.referenceStudyUID, // StudyInstanceUID + SeriesInstanceUID: measurement.referenceSeriesUID, // SeriesInstanceUID + SOPInstanceUID: measurement.SOPInstanceUID, // SOPInstanceUID + Label: measurement.label || '', // Label + }; +} + +function _createAndDownloadFile(csvContent) { + const encodedUri = encodeURI(csvContent); + + const link = document.createElement('a'); + link.setAttribute('href', encodedUri); + link.setAttribute('download', 'MeasurementReport.csv'); + document.body.appendChild(link); + link.click(); +} diff --git a/platform/core/src/utils/formatDate.js b/platform/core/src/utils/formatDate.js new file mode 100644 index 0000000..866a928 --- /dev/null +++ b/platform/core/src/utils/formatDate.js @@ -0,0 +1,14 @@ +import moment from 'moment'; +import i18n from 'i18next'; + +/** + * Format date + * + * @param {string} date Date to be formatted + * @param {string} format Desired date format + * @returns {string} Formatted date + */ +export default (date, format = i18n.t('Common:localDateFormat','DD-MMM-YYYY')) => { + // moment(undefined) returns the current date, so return the empty string instead + return date ? moment(date).format(format) : ''; +}; diff --git a/platform/core/src/utils/formatPN.js b/platform/core/src/utils/formatPN.js new file mode 100644 index 0000000..0427004 --- /dev/null +++ b/platform/core/src/utils/formatPN.js @@ -0,0 +1,23 @@ +/** + * Formats a patient name for display purposes + */ +export default function formatPN(name) { + if (!name) { + return; + } + + let nameToUse = name.Alphabetic ?? name; + if (typeof nameToUse === 'object') { + nameToUse = ''; + } + + // Convert the first ^ to a ', '. String.replace() only affects + // the first appearance of the character. + const commaBetweenFirstAndLast = nameToUse.replace('^', ', '); + + // Replace any remaining '^' characters with spaces + const cleaned = commaBetweenFirstAndLast.replace(/\^/g, ' '); + + // Trim any extraneous whitespace + return cleaned.trim(); +} diff --git a/platform/core/src/utils/formatTime.ts b/platform/core/src/utils/formatTime.ts new file mode 100644 index 0000000..5d46ee0 --- /dev/null +++ b/platform/core/src/utils/formatTime.ts @@ -0,0 +1,12 @@ +import moment from 'moment'; + +/** + * Format time in HHmmss.SSS format (24h time) into HH:mm:ss + * + * @param time - Time to be formatted + * @param format - Desired time format + * @returns Formatted time + */ +export default function formatTime(time: string, format = 'HH:mm:ss') { + return moment(time, 'HH:mm:ss').format(format); +} diff --git a/platform/core/src/utils/generateAcceptHeader.ts b/platform/core/src/utils/generateAcceptHeader.ts new file mode 100644 index 0000000..c384411 --- /dev/null +++ b/platform/core/src/utils/generateAcceptHeader.ts @@ -0,0 +1,63 @@ +const generateAcceptHeader = ( + configAcceptHeader = [], + requestTransferSyntaxUID = '*', //default to accept all transfer syntax + omitQuotationForMultipartRequest = false +): string[] => { + //if acceptedHeader is passed by config use it as it. + if (configAcceptHeader.length > 0) { + return configAcceptHeader; + } + + let acceptHeader = ['multipart/related']; + let hasTransferSyntax = false; + if (requestTransferSyntaxUID && typeForTS[requestTransferSyntaxUID]) { + const type = typeForTS[requestTransferSyntaxUID]; + acceptHeader.push('type=' + type); + acceptHeader.push('transfer-syntax=' + requestTransferSyntaxUID); + hasTransferSyntax = true; + } else { + acceptHeader.push('type=application/octet-stream'); + } + + if (!hasTransferSyntax) { + acceptHeader.push('transfer-syntax=*'); + } + + if (!omitQuotationForMultipartRequest) { + //need to add quotation for each mime type of each accept entry + acceptHeader = acceptHeader.map(mime => { + if (mime.startsWith('type=')) { + const quotedParam = 'type="' + mime.substring(5, mime.length) + '"'; + return quotedParam; + } + if (mime.startsWith('transfer-syntax=')) { + const quotedParam = 'transfer-syntax="' + mime.substring(16, mime.length) + '"'; + return quotedParam; + } else { + return mime; + } + }); + } + + return [acceptHeader.join('; ')]; +}; + +const typeForTS = { + '*': 'application/octet-stream', + '1.2.840.10008.1.2.1': 'application/octet-stream', + '1.2.840.10008.1.2': 'application/octet-stream', + '1.2.840.10008.1.2.2': 'application/octet-stream', + '1.2.840.10008.1.2.4.70': 'image/jpeg', + '1.2.840.10008.1.2.4.50': 'image/jpeg', + '1.2.840.10008.1.2.4.51': 'image/dicom+jpeg', + '1.2.840.10008.1.2.4.57': 'image/jpeg', + '1.2.840.10008.1.2.5': 'image/dicom-rle', + '1.2.840.10008.1.2.4.80': 'image/jls', + '1.2.840.10008.1.2.4.81': 'image/jls', + '1.2.840.10008.1.2.4.90': 'image/jp2', + '1.2.840.10008.1.2.4.91': 'image/jp2', + '1.2.840.10008.1.2.4.92': 'image/jpx', + '1.2.840.10008.1.2.4.93': 'image/jpx', +}; + +export default generateAcceptHeader; diff --git a/platform/core/src/utils/getImageId.js b/platform/core/src/utils/getImageId.js new file mode 100644 index 0000000..d40f127 --- /dev/null +++ b/platform/core/src/utils/getImageId.js @@ -0,0 +1,55 @@ +import getWADORSImageId from './getWADORSImageId'; + +// https://stackoverflow.com/a/6021027/3895126 +function updateQueryStringParameter(uri, key, value) { + const regex = new RegExp('([?&])' + key + '=.*?(&|$)', 'i'); + const separator = uri.indexOf('?') !== -1 ? '&' : '?'; + if (uri.match(regex)) { + return uri.replace(regex, '$1' + key + '=' + value + '$2'); + } else { + return uri + separator + key + '=' + value; + } +} + +/** + * Obtain an imageId for Cornerstone from an image instance + * + * @param instance + * @param frame + * @param thumbnail + * @returns {string} The imageId to be used by Cornerstone + */ +export default function getImageId(instance, frame, thumbnail = false) { + if (!instance) { + return; + } + + if (instance.imageId && frame === undefined) { + return instance.imageId; + } + + if (typeof instance.getImageId === 'function') { + return instance.getImageId(); + } + + if (instance.url) { + if (frame !== undefined) { + instance.url = updateQueryStringParameter(instance.url, 'frame', frame); + } + + return instance.url; + } + + const renderingAttr = thumbnail ? 'thumbnailRendering' : 'imageRendering'; + + if (!instance[renderingAttr] || instance[renderingAttr] === 'wadouri' || !instance.wadorsuri) { + let imageId = 'dicomweb:' + instance.wadouri; + if (frame !== undefined) { + imageId += '&frame=' + frame; + } + + return imageId; + } else { + return getWADORSImageId(instance, frame, thumbnail); // WADO-RS Retrieve Frame + } +} diff --git a/platform/core/src/utils/getWADORSImageId.js b/platform/core/src/utils/getWADORSImageId.js new file mode 100644 index 0000000..b75615b --- /dev/null +++ b/platform/core/src/utils/getWADORSImageId.js @@ -0,0 +1,37 @@ +function getWADORSImageUrl(instance, frame) { + let wadorsuri = instance.wadorsuri; + + if (!wadorsuri) { + return; + } + + // Use null to obtain an imageId which represents the instance + if (frame === null) { + wadorsuri = wadorsuri.replace(/frames\/(\d+)/, ''); + } else { + // We need to sum 1 because WADO-RS frame number is 1-based + frame = frame ? parseInt(frame) + 1 : 1; + + // Replaces /frame/1 by /frame/{frame} + wadorsuri = wadorsuri.replace(/frames\/(\d+)/, `frames/${frame}`); + } + + return wadorsuri; +} + +/** + * Obtain an imageId for Cornerstone based on the WADO-RS scheme + * + * @param {object} instanceMetada metadata object (InstanceMetadata) + * @param {(string\|number)} [frame] the frame number + * @returns {string} The imageId to be used by Cornerstone + */ +export default function getWADORSImageId(instance, frame) { + const uri = getWADORSImageUrl(instance, frame); + + if (!uri) { + return; + } + + return `wadors:${uri}`; +} diff --git a/platform/core/src/utils/getWADORSImageId.test.js b/platform/core/src/utils/getWADORSImageId.test.js new file mode 100644 index 0000000..6eeb78d --- /dev/null +++ b/platform/core/src/utils/getWADORSImageId.test.js @@ -0,0 +1,65 @@ +import getWADORSImageId from './getWADORSImageId'; + +describe('getWADORSImageId', () => { + it('should always return undefined if the instance has no `wadorsuri` property', () => { + const frame = '42'; + const instance = {}; + + expect(getWADORSImageId(instance)).toBeUndefined(); + expect(getWADORSImageId(instance, frame)).toBeUndefined(); + }); + + it('should always prepend the `wadorsuri` with `wadors:`', () => { + const frame = '42'; + const instance = { + wadorsuri: 'wadorsuri', + }; + + expect(getWADORSImageId(instance)).toEqual('wadors:wadorsuri'); + expect(getWADORSImageId(instance, frame)).toEqual('wadors:wadorsuri'); + }); + + describe('with no frame provided', () => { + it('should replace `frames/:number` with `frames/1`', () => { + const instance = { + wadorsuri: 'frames/42', + }; + + expect(getWADORSImageId(instance)).toEqual('wadors:frames/1'); + }); + + it('should work on a real wadorsuri', () => { + const instance = { + wadorsuri: + 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs/studies/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.1/series/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.2/instances/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.8/frames/22', + }; + + expect(getWADORSImageId(instance)).toEqual( + 'wadors:https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs/studies/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.1/series/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.2/instances/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.8/frames/1' + ); + }); + }); + + describe('with a frame provided', () => { + it('should replace `frames/:number` with the argument frame plus one', () => { + const frame = '42'; + const instance = { + wadorsuri: 'frames/1', + }; + + expect(getWADORSImageId(instance, frame)).toEqual('wadors:frames/43'); + }); + + it('should work on a real wadorsuri', () => { + const frame = '42'; + const instance = { + wadorsuri: + 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs/studies/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.1/series/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.2/instances/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.8/frames/22', + }; + + expect(getWADORSImageId(instance, frame)).toEqual( + 'wadors:https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs/studies/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.1/series/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.2/instances/1.3.6.1.4.1.25403.52237031786.3872.20100510032220.8/frames/43' + ); + }); + }); +}); diff --git a/platform/core/src/utils/guid.js b/platform/core/src/utils/guid.js new file mode 100644 index 0000000..6ec6f82 --- /dev/null +++ b/platform/core/src/utils/guid.js @@ -0,0 +1,28 @@ +/** + * Create a random GUID + * + * @return {string} + */ +const guid = () => { + const getFourRandomValues = () => { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + }; + return ( + getFourRandomValues() + + getFourRandomValues() + + '-' + + getFourRandomValues() + + '-' + + getFourRandomValues() + + '-' + + getFourRandomValues() + + '-' + + getFourRandomValues() + + getFourRandomValues() + + getFourRandomValues() + ); +}; + +export default guid; diff --git a/platform/core/src/utils/guid.test.js b/platform/core/src/utils/guid.test.js new file mode 100644 index 0000000..25d0d75 --- /dev/null +++ b/platform/core/src/utils/guid.test.js @@ -0,0 +1,46 @@ +import guid from './guid'; + +describe('guid', () => { + Math.random = jest.fn(() => 0.4677647565236618); + const guidValue = guid(); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test('should return 77bf77bf-77bf-77bf-77bf-77bf77bf77bf when the random value is fixed on 0.4677647565236618', () => { + expect(guidValue).toBe('77bf77bf-77bf-77bf-77bf-77bf77bf77bf'); + }); + + test('should always return a guid of size 36', () => { + expect(guidValue.length).toBe(36); + }); + + test('should always return a guid with five sequences', () => { + expect(guidValue.split('-').length).toBe(5); + }); + + test('should always return a guid with four dashes', () => { + expect(guidValue.split('-').length - 1).toBe(4); + }); + + test('should return the first sequence with length of eigth', () => { + expect(guidValue.split('-')[0].length).toBe(8); + }); + + test('should return the second sequence with length of four', () => { + expect(guidValue.split('-')[1].length).toBe(4); + }); + + test('should return the third sequence with length of four', () => { + expect(guidValue.split('-')[2].length).toBe(4); + }); + + test('should return the fourth sequence with length of four', () => { + expect(guidValue.split('-')[3].length).toBe(4); + }); + + test('should return the last sequence with length of twelve', () => { + expect(guidValue.split('-')[4].length).toBe(12); + }); +}); diff --git a/platform/core/src/utils/hierarchicalListUtils.js b/platform/core/src/utils/hierarchicalListUtils.js new file mode 100644 index 0000000..ca12c77 --- /dev/null +++ b/platform/core/src/utils/hierarchicalListUtils.js @@ -0,0 +1,195 @@ +/** + * Constants + */ + +const SEPARATOR = '/'; + +/** + * API + */ + +/** + * Add values to a list hierarchically. + * @ For example: + * addToList([], 'a', 'b', 'c'); + * will add the following hierarchy to the list: + * a > b > c + * resulting in the following array: + * [['a', [['b', ['c']]]]] + * @param {Array} list The target list; + * @param {...string} values The values to be hierarchically added to the list; + * @returns {Array} Returns the provided list possibly updated with the given + * values or null when a bad list (not an actual array) is provided + */ +function addToList(list, ...values) { + if (Array.isArray(list)) { + if (values.length > 0) { + addValuesToList(list, values); + } + return list; + } + return null; +} + +/** + * Iterates through the provided hierarchical list executing the callback + * once for each leaf-node of the tree. The ancestors of the leaf-node being + * visited are passed to the callback function along with the leaf-node in + * the exact same order they appear on the tree (from root to leaf); + * @ For example, if the hierarchy `a > b > c` appears on the tree ("a" being + * the root and "c" being the leaf) the callback function will be called as: + * callback('a', 'b', 'c'); + * @param {Array} list The hierarchical list to be iterated + * @param {function} callback The callback which will be executed once for + * each leaf-node of the hierarchical list; + * @returns {Array} Returns the provided list or null for bad arguments; + */ +function forEach(list, callback) { + if (Array.isArray(list)) { + if (typeof callback === 'function') { + forEachValue(list, callback); + } + return list; + } + return null; +} + +/** + * Retrieves an item from the given hierarchical list based on an index (number) + * or a path (string). + * @ For example: + * getItem(list, '1/0/4') + * will retrieve the fourth grandchild, from the first child of the second + * element of the list; + * @param {Array} list The source list; + * @param {string|number} indexOrPath The index of the element inside list + * (number) or the path to reach the desired element (string). The slash "/" + * character is cosidered the path separator; + */ +function getItem(list, indexOrPath) { + if (Array.isArray(list)) { + let subpath = null; + let index = typeof indexOrPath === 'number' ? indexOrPath : -1; + if (typeof indexOrPath === 'string') { + const separator = indexOrPath.indexOf(SEPARATOR); + if (separator > 0) { + index = parseInt(indexOrPath.slice(0, separator), 10); + if (separator + 1 < indexOrPath.length) { + subpath = indexOrPath.slice(separator + 1, indexOrPath.length); + } + } else { + index = parseInt(indexOrPath, 10); + } + } + if (index >= 0 && index < list.length) { + const item = list[index]; + if (isSublist(item)) { + if (subpath !== null) { + return getItem(item[1], subpath); + } + return item[0]; + } + return item; + } + } +} + +/** + * Pretty-print the provided hierarchical list; + * @param {Array} list The source list; + * @returns {string} The textual representation of the hierarchical list; + */ +function print(list) { + let text = ''; + if (Array.isArray(list)) { + let prev = []; + forEachValue(list, function (...args) { + let prevLen = prev.length; + for (let i = 0, l = args.length; i < l; ++i) { + if (i < prevLen && args[i] === prev[i]) { + continue; + } + text += ' '.repeat(i) + args[i] + '\n'; + } + prev = args; + }); + } + return text; +} + +/** + * Utils + */ + +function forEachValue(list, callback) { + for (let i = 0, l = list.length; i < l; ++i) { + let item = list[i]; + if (isSublist(item)) { + if (item[1].length > 0) { + forEachValue(item[1], callback.bind(null, item[0])); + continue; + } + item = item[0]; + } + callback(item); + } +} + +function addValuesToList(list, values) { + let value = values.shift(); + let index = add(list, value); + if (index >= 0) { + if (values.length > 0) { + let sublist = list[index]; + if (!isSublist(sublist)) { + sublist = toSublist(value); + list[index] = sublist; + } + return addValuesToList(sublist[1], values); + } + return true; + } + return false; +} + +function add(list, value) { + let index = find(list, value); + if (index === -2) { + index = list.push(value) - 1; + } + return index; +} + +function find(list, value) { + if (typeof value === 'string') { + for (let i = 0, l = list.length; i < l; ++i) { + let item = list[i]; + if (item === value || (isSublist(item) && item[0] === value)) { + return i; + } + } + return -2; + } + return -1; +} + +function isSublist(subject) { + return ( + Array.isArray(subject) && + subject.length === 2 && + typeof subject[0] === 'string' && + Array.isArray(subject[1]) + ); +} + +function toSublist(value) { + return [value + '', []]; +} + +/** + * Exports + */ + +const hierarchicalListUtils = { addToList, getItem, forEach, print }; +export { addToList, getItem, forEach, print }; +export default hierarchicalListUtils; diff --git a/platform/core/src/utils/hierarchicalListUtils.test.js b/platform/core/src/utils/hierarchicalListUtils.test.js new file mode 100644 index 0000000..fbf41e9 --- /dev/null +++ b/platform/core/src/utils/hierarchicalListUtils.test.js @@ -0,0 +1,96 @@ +import { addToList, forEach, getItem, print } from './hierarchicalListUtils'; + +describe('hierarchicalListUtils', function () { + let sharedList; + + beforeEach(function () { + sharedList = [ + ['1.2.3.1', ['1.2.3.1.1', '1.2.3.1.2']], + '1.2.3.2', + ['1.2.3.3', ['1.2.3.3.1', ['1.2.3.3.2', ['1.2.3.3.2.1', '1.2.3.3.2.2']]]], + ]; + }); + + describe('getItem', function () { + it('should retrieve elements from a list by index', function () { + expect(getItem(sharedList, 0)).toBe('1.2.3.1'); + expect(getItem(sharedList, 1)).toBe('1.2.3.2'); + expect(getItem(sharedList, 2)).toBe('1.2.3.3'); + expect(getItem(sharedList, 3)).toBeUndefined(); + }); + it('should retrieve elements from a list by path', function () { + expect(getItem(sharedList, '0')).toBe('1.2.3.1'); + expect(getItem(sharedList, '0/0')).toBe('1.2.3.1.1'); + expect(getItem(sharedList, '0/1')).toBe('1.2.3.1.2'); + expect(getItem(sharedList, '0/2')).toBeUndefined(); + expect(getItem(sharedList, '1')).toBe('1.2.3.2'); + expect(getItem(sharedList, '2')).toBe('1.2.3.3'); + expect(getItem(sharedList, '2/0')).toBe('1.2.3.3.1'); + expect(getItem(sharedList, '2/1')).toBe('1.2.3.3.2'); + expect(getItem(sharedList, '2/2')).toBeUndefined(); + expect(getItem(sharedList, '2/1/0')).toBe('1.2.3.3.2.1'); + expect(getItem(sharedList, '2/1/1')).toBe('1.2.3.3.2.2'); + expect(getItem(sharedList, '2/1/2')).toBeUndefined(); + expect(getItem(sharedList, '3')).toBeUndefined(); + }); + }); + + describe('addToList', function () { + it('should support adding elements to a list hierarchically', function () { + const list = []; + addToList(list, '1.2.3.1', '1.2.3.1.1'); + addToList(list, '1.2.3.1', '1.2.3.1.2'); + addToList(list, '1.2.3.2'); + addToList(list, '1.2.3.3', '1.2.3.3.1'); + addToList(list, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.1'); + addToList(list, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.2'); + expect(list).toStrictEqual(sharedList); + }); + it('should change leaf nodes into non-leaf nodes', function () { + const listw = []; + const listx = [['x.1', ['x.1.1', 'x.1.2']], 'x.2']; + const listy = [ + ['x.1', [['x.1.1', ['x.1.1.1']], 'x.1.2']], + ['x.2', ['x.2.1']], + ]; + addToList(listw, 'x.1'); + addToList(listw, 'x.1', 'x.1.1'); + addToList(listw, 'x.1', 'x.1.2'); + addToList(listw, 'x.2'); + expect(listw).toStrictEqual(listx); + addToList(listw, 'x.2', 'x.2.1'); + addToList(listw, 'x.1', 'x.1.1', 'x.1.1.1'); + expect(listw).toStrictEqual(listy); + }); + }); + + describe('forEach', function () { + it('should iterate through all leaf nodes of the tree', function () { + const fn = jest.fn(); + forEach(sharedList, fn); + expect(fn).toHaveBeenCalledTimes(6); + expect(fn).nthCalledWith(1, '1.2.3.1', '1.2.3.1.1'); + expect(fn).nthCalledWith(2, '1.2.3.1', '1.2.3.1.2'); + expect(fn).nthCalledWith(3, '1.2.3.2'); + expect(fn).nthCalledWith(4, '1.2.3.3', '1.2.3.3.1'); + expect(fn).nthCalledWith(5, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.1'); + expect(fn).nthCalledWith(6, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.2'); + }); + }); + + describe('print', function () { + it('should pretty-print the hierarchical list', function () { + expect(print(sharedList)).toBe( + '1.2.3.1\n' + + ' 1.2.3.1.1\n' + + ' 1.2.3.1.2\n' + + '1.2.3.2\n' + + '1.2.3.3\n' + + ' 1.2.3.3.1\n' + + ' 1.2.3.3.2\n' + + ' 1.2.3.3.2.1\n' + + ' 1.2.3.3.2.2\n' + ); + }); + }); +}); diff --git a/platform/core/src/utils/hotkeys/index.js b/platform/core/src/utils/hotkeys/index.js new file mode 100644 index 0000000..9cd157a --- /dev/null +++ b/platform/core/src/utils/hotkeys/index.js @@ -0,0 +1,8 @@ +import Mousetrap from 'mousetrap'; +import pausePlugin from './pausePlugin'; +import recordPlugin from './recordPlugin'; + +recordPlugin(Mousetrap); +pausePlugin(Mousetrap); + +export default Mousetrap; diff --git a/platform/core/src/utils/hotkeys/migrateHotkeys.ts b/platform/core/src/utils/hotkeys/migrateHotkeys.ts new file mode 100644 index 0000000..45eb9c0 --- /dev/null +++ b/platform/core/src/utils/hotkeys/migrateHotkeys.ts @@ -0,0 +1,76 @@ +import defaults from '../../defaults'; +const defaultHotkeyBindings = defaults.hotkeyBindings; + +/** + * Migrates old hotkey definitions from localStorage to the new format + * Old format: 'hotkey-definitions' containing full hotkey definitions array + * New format: 'user-preferred-keys' containing hashed command keys with their key bindings + * + * @private + */ +function migrateOldHotkeyDefinitions({ + generateHash, +}: { + generateHash: (definition: Record) => string; +}): void { + try { + const oldHotkeyDefinitions = localStorage.getItem('hotkey-definitions'); + const migrated = localStorage.getItem('hotkeys-migrated'); + + if (!oldHotkeyDefinitions || migrated === 'true') { + return; + } + + const oldDefinitions = JSON.parse(oldHotkeyDefinitions); + + if (!Array.isArray(oldDefinitions) || oldDefinitions.length === 0) { + return; + } + + const defaultBindings = defaultHotkeyBindings || []; + + const userPreferredKeys = JSON.parse(localStorage.getItem('user-preferred-keys') || '{}'); + + oldDefinitions.forEach(oldDefinition => { + if (!oldDefinition.commandName || !oldDefinition.keys) { + return; + } + + const matchingDefault = defaultBindings.find(defaultBinding => { + const sameCommand = defaultBinding.commandName === oldDefinition.commandName; + + const oldOptions = oldDefinition.commandOptions || {}; + const defaultOptions = defaultBinding.commandOptions || {}; + + const sameOptions = JSON.stringify(oldOptions) === JSON.stringify(defaultOptions); + + return sameCommand && sameOptions; + }); + + if ( + !matchingDefault || + JSON.stringify(matchingDefault.keys) !== JSON.stringify(oldDefinition.keys) + ) { + const commandHash = generateHash({ + commandName: oldDefinition.commandName, + commandOptions: oldDefinition.commandOptions || {}, + }); + + userPreferredKeys[commandHash] = oldDefinition.keys; + console.debug(`HotkeysManager: Migrated custom hotkey for ${oldDefinition.commandName}`); + } + }); + + localStorage.setItem('user-preferred-keys', JSON.stringify(userPreferredKeys)); + localStorage.setItem('hotkeys-migrated', 'true'); + localStorage.removeItem('hotkey-definitions'); + + console.debug('HotkeysManager: Successfully migrated hotkey definitions to new format'); + } catch (error) { + console.error('HotkeysManager: Error migrating hotkey definitions', error); + + localStorage.setItem('hotkeys-migrated', 'false'); + } +} + +export default migrateOldHotkeyDefinitions; diff --git a/platform/core/src/utils/hotkeys/pausePlugin.js b/platform/core/src/utils/hotkeys/pausePlugin.js new file mode 100644 index 0000000..82176e0 --- /dev/null +++ b/platform/core/src/utils/hotkeys/pausePlugin.js @@ -0,0 +1,32 @@ +/** + * adds a pause and unpause method to Mousetrap + * this allows you to enable or disable keyboard shortcuts + * without having to reset Mousetrap and rebind everything + * + * https://github.com/ccampbell/mousetrap/blob/master/plugins/pause/mousetrap-pause.js + */ +export default function pausePlugin(Mousetrap) { + var _originalStopCallback = Mousetrap.prototype.stopCallback; + + Mousetrap.prototype.stopCallback = function (e, element, combo) { + var self = this; + + if (self.paused) { + return true; + } + + return _originalStopCallback.call(self, e, element, combo); + }; + + Mousetrap.prototype.pause = function () { + var self = this; + self.paused = true; + }; + + Mousetrap.prototype.unpause = function () { + var self = this; + self.paused = false; + }; + + Mousetrap.init(); +} diff --git a/platform/core/src/utils/hotkeys/recordPlugin.js b/platform/core/src/utils/hotkeys/recordPlugin.js new file mode 100644 index 0000000..a2b7574 --- /dev/null +++ b/platform/core/src/utils/hotkeys/recordPlugin.js @@ -0,0 +1,216 @@ +/** + * This extension allows you to record a sequence using Mousetrap. + * + * @author Dan Tao + */ +export default function recordPlugin(Mousetrap, options = { timeout: 100 }) { + /** + * the sequence currently being recorded + * + * @type {Array} + */ + var _recordedSequence = [], + /** + * a callback to invoke after recording a sequence + * + * @type {Function|null} + */ + _recordedSequenceCallback = null, + /** + * a list of all of the keys currently held down + * + * @type {Array} + */ + _currentRecordedKeys = [], + /** + * temporary state where we remember if we've already captured a + * character key in the current combo + * + * @type {boolean} + */ + _recordedCharacterKey = false, + /** + * a handle for the timer of the current recording + * + * @type {null|number} + */ + _recordTimer = null, + /** + * the original handleKey method to override when Mousetrap.record() is + * called + * + * @type {Function} + */ + _origHandleKey = Mousetrap.prototype.handleKey; + + /** + * handles a character key event + * + * @param {string} character + * @param {Array} modifiers + * @param {Event} e + * @returns void + */ + function _handleKey(character, modifiers, e) { + var self = this; + + if (!self.recording) { + _origHandleKey.apply(self, arguments); + return; + } + + // remember this character if we're currently recording a sequence + if (e.type == 'keydown') { + if (character.length === 1 && _recordedCharacterKey) { + _recordCurrentCombo(); + } + + for (let i = 0; i < modifiers.length; ++i) { + _recordKey(modifiers[i]); + } + _recordKey(character); + + // once a key is released, all keys that were held down at the time + // count as a keypress + } else if (e.type == 'keyup' && _currentRecordedKeys.length > 0) { + _recordCurrentCombo(); + } + } + + /** + * marks a character key as held down while recording a sequence + * + * @param {string} key + * @returns void + */ + function _recordKey(key) { + // one-off implementation of Array.indexOf, since IE6-9 don't support it + for (let i = 0; i < _currentRecordedKeys.length; ++i) { + if (_currentRecordedKeys[i] === key) { + return; + } + } + + _currentRecordedKeys.push(key); + + if (key.length === 1) { + _recordedCharacterKey = true; + } + } + + /** + * marks whatever key combination that's been recorded so far as finished + * and gets ready for the next combo + * + * @returns void + */ + function _recordCurrentCombo() { + _recordedSequence.push(_currentRecordedKeys); + _currentRecordedKeys = []; + _recordedCharacterKey = false; + _restartRecordTimer(); + } + + /** + * ensures each combo in a sequence is in a predictable order and formats + * key combos to be '+'-delimited + * + * modifies the sequence in-place + * + * @param {Array} sequence + * @returns void + */ + function _normalizeSequence(sequence) { + for (let i = 0; i < sequence.length; ++i) { + sequence[i].sort(function (x, y) { + // modifier keys always come first, in alphabetical order + if (x.length > 1 && y.length === 1) { + return -1; + } else if (x.length === 1 && y.length > 1) { + return 1; + } + + // character keys come next (list should contain no duplicates, + // so no need for equality check) + return x > y ? 1 : -1; + }); + + sequence[i] = sequence[i].join('+'); + } + } + + /** + * finishes the current recording, passes the recorded sequence to the stored + * callback, and sets Mousetrap.handleKey back to its original function + * + * @returns void + */ + function _finishRecording() { + if (_recordedSequenceCallback) { + _normalizeSequence(_recordedSequence); + _recordedSequenceCallback(_recordedSequence); + } + + // reset all recorded state + _recordedSequence = []; + _recordedSequenceCallback = null; + _currentRecordedKeys = []; + } + + /** + * called to set a 1 second timeout on the current recording + * + * this is so after each key press in the sequence the recording will wait for + * 1 more second before executing the callback + * + * @returns void + */ + function _restartRecordTimer() { + clearTimeout(_recordTimer); + _recordTimer = setTimeout(_finishRecording, options.timeout); + } + + /** + * records the next sequence and passes it to a callback once it's + * completed + * + * @param {Function} callback + * @returns void + */ + Mousetrap.prototype.record = function (callback) { + var self = this; + self.recording = true; + _recordedSequenceCallback = function () { + self.recording = false; + callback.apply(self, arguments); + }; + }; + + /** + * stop recording + * + * @param {Function} callback + * @returns void + */ + Mousetrap.prototype.stopRecord = function () { + var self = this; + self.recording = false; + }; + + /** + * start recording + * + * @param {Function} callback + * @returns void + */ + Mousetrap.prototype.startRecording = function () { + var self = this; + self.recording = true; + }; + Mousetrap.prototype.handleKey = function () { + var self = this; + _handleKey.apply(self, arguments); + }; + + Mousetrap.init(); +} diff --git a/platform/core/src/utils/imageIdToURI.js b/platform/core/src/utils/imageIdToURI.js new file mode 100644 index 0000000..7be55b0 --- /dev/null +++ b/platform/core/src/utils/imageIdToURI.js @@ -0,0 +1,12 @@ +/** + * Removes the data loader scheme from the imageId + * + * @param {string} imageId Image ID + * @returns {string} imageId without the data loader scheme + * @memberof Cache + */ +export default function imageIdToURI(imageId) { + const colonIndex = imageId.indexOf(':'); + + return imageId.substring(colonIndex + 1); +} diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js new file mode 100644 index 0000000..9e7edfd --- /dev/null +++ b/platform/core/src/utils/index.test.js @@ -0,0 +1,53 @@ +import * as utils from './index'; + +describe('Top level exports', () => { + test('should export the modules ', () => { + const expectedExports = [ + 'guid', + 'ObjectPath', + 'absoluteUrl', + 'seriesSortCriteria', + 'sortBy', + 'sortStudy', + 'sortBySeriesDate', + 'sortStudyInstances', + 'sortStudySeries', + 'sortingCriteria', + 'splitComma', + 'getSplitParam', + 'isLowPriorityModality', + 'writeScript', + 'debounce', + 'downloadCSVReport', + 'imageIdToURI', + 'roundNumber', + 'b64toBlob', + 'sopClassDictionary', + 'createStudyBrowserTabs', + 'formatDate', + 'formatTime', + 'formatPN', + 'generateAcceptHeader', + 'isEqualWithin', + //'loadAndCacheDerivedDisplaySets', + 'isDisplaySetReconstructable', + 'isImage', + 'urlUtil', + 'makeDeferred', + 'makeCancelable', + 'hotkeys', + 'Queue', + 'isDicomUid', + 'resolveObjectPath', + 'hierarchicalListUtils', + 'progressTrackingUtils', + 'uuidv4', + 'addAccessors', + 'MeasurementFilters', + ].sort(); + + const exports = Object.keys(utils.default).sort(); + + expect(exports).toEqual(expectedExports); + }); +}); diff --git a/platform/core/src/utils/index.ts b/platform/core/src/utils/index.ts new file mode 100644 index 0000000..e2e4d03 --- /dev/null +++ b/platform/core/src/utils/index.ts @@ -0,0 +1,125 @@ +import ObjectPath from './objectPath'; +import absoluteUrl from './absoluteUrl'; +import guid from './guid'; +import uuidv4 from './uuidv4'; +import sortBy from './sortBy.js'; +import writeScript from './writeScript.js'; +import b64toBlob from './b64toBlob.js'; +//import loadAndCacheDerivedDisplaySets from './loadAndCacheDerivedDisplaySets.js'; +import urlUtil from './urlUtil'; +import makeDeferred from './makeDeferred'; +import makeCancelable from './makeCancelable'; +import hotkeys from './hotkeys'; +import Queue from './Queue'; +import isDicomUid from './isDicomUid'; +import formatDate from './formatDate'; +import formatTime from './formatTime'; +import formatPN from './formatPN'; +import generateAcceptHeader from './generateAcceptHeader'; +import resolveObjectPath from './resolveObjectPath'; +import hierarchicalListUtils from './hierarchicalListUtils'; +import progressTrackingUtils from './progressTrackingUtils'; +import isLowPriorityModality from './isLowPriorityModality'; +import { isImage } from './isImage'; +import isDisplaySetReconstructable from './isDisplaySetReconstructable'; +import sortInstancesByPosition from './sortInstancesByPosition'; +import imageIdToURI from './imageIdToURI'; +import debounce from './debounce'; +import roundNumber from './roundNumber'; +import downloadCSVReport from './downloadCSVReport'; +import isEqualWithin from './isEqualWithin'; +import addAccessors from './addAccessors'; +import { + sortStudy, + sortStudySeries, + sortStudyInstances, + sortingCriteria, + seriesSortCriteria, +} from './sortStudy'; +import { splitComma, getSplitParam } from './splitComma'; +import { createStudyBrowserTabs } from './createStudyBrowserTabs'; +import { sopClassDictionary } from './sopClassDictionary'; +import * as MeasurementFilters from './measurementFilters'; + +// Commented out unused functionality. +// Need to implement new mechanism for derived displaySets using the displaySetManager. + +const utils = { + guid, + uuidv4, + ObjectPath, + absoluteUrl, + sortBy, + sortBySeriesDate: sortStudySeries, + sortStudy, + sortStudySeries, + sortStudyInstances, + sortingCriteria, + seriesSortCriteria, + writeScript, + formatDate, + formatTime, + formatPN, + b64toBlob, + urlUtil, + imageIdToURI, + //loadAndCacheDerivedDisplaySets, + makeDeferred, + makeCancelable, + hotkeys, + Queue, + isDicomUid, + isEqualWithin, + sopClassDictionary, + addAccessors, + resolveObjectPath, + hierarchicalListUtils, + progressTrackingUtils, + isLowPriorityModality, + isImage, + isDisplaySetReconstructable, + debounce, + roundNumber, + downloadCSVReport, + splitComma, + getSplitParam, + generateAcceptHeader, + createStudyBrowserTabs, + MeasurementFilters, +}; + +export { + guid, + ObjectPath, + absoluteUrl, + sortBy, + formatDate, + writeScript, + b64toBlob, + urlUtil, + //loadAndCacheDerivedDisplaySets, + makeDeferred, + makeCancelable, + hotkeys, + Queue, + isDicomUid, + isEqualWithin, + resolveObjectPath, + hierarchicalListUtils, + progressTrackingUtils, + isLowPriorityModality, + isImage, + isDisplaySetReconstructable, + sortInstancesByPosition, + imageIdToURI, + debounce, + roundNumber, + downloadCSVReport, + splitComma, + getSplitParam, + generateAcceptHeader, + createStudyBrowserTabs, + MeasurementFilters, +}; + +export default utils; diff --git a/platform/core/src/utils/isDicomUid.js b/platform/core/src/utils/isDicomUid.js new file mode 100644 index 0000000..42eda4a --- /dev/null +++ b/platform/core/src/utils/isDicomUid.js @@ -0,0 +1,4 @@ +export default function isDicomUid(subject) { + const regex = /^\d+(?:\.\d+)*$/; + return typeof subject === 'string' && regex.test(subject.trim()); +} diff --git a/platform/core/src/utils/isDicomUid.test.js b/platform/core/src/utils/isDicomUid.test.js new file mode 100644 index 0000000..279a2b4 --- /dev/null +++ b/platform/core/src/utils/isDicomUid.test.js @@ -0,0 +1,16 @@ +import isDicomUid from './isDicomUid'; + +describe('isDicomUid', function () { + it('should return true for valid DICOM UIDs', function () { + expect(isDicomUid('1')).toBe(true); + expect(isDicomUid('1.2')).toBe(true); + expect(isDicomUid('1.2.3')).toBe(true); + expect(isDicomUid('1.2.3.4')).toBe(true); + }); + it('should return false for invalid DICOM UIDs', function () { + expect(isDicomUid('x')).toBe(false); + expect(isDicomUid('1.')).toBe(false); + expect(isDicomUid('1. 2')).toBe(false); + expect(isDicomUid('1.2.n.4')).toBe(false); + }); +}); diff --git a/platform/core/src/utils/isDisplaySetReconstructable.js b/platform/core/src/utils/isDisplaySetReconstructable.js new file mode 100644 index 0000000..00decb8 --- /dev/null +++ b/platform/core/src/utils/isDisplaySetReconstructable.js @@ -0,0 +1,262 @@ +import toNumber from './toNumber'; +import sortInstancesByPosition from './sortInstancesByPosition'; + +// TODO: Is 10% a reasonable spacingTolerance for spacing? +const spacingTolerance = 0.2; +const iopTolerance = 0.01; + +/** + * Checks if a series is reconstructable to a 3D volume. + * + * @param {Object[]} instances An array of `OHIFInstanceMetadata` objects. + */ +export default function isDisplaySetReconstructable(instances, appConfig) { + if (!instances.length) { + return { value: false }; + } + const firstInstance = instances[0]; + + const isMultiframe = firstInstance.NumberOfFrames > 1; + + if (appConfig) { + const rows = toNumber(firstInstance.Rows); + const columns = toNumber(firstInstance.Columns); + + if (rows > appConfig.max3DTextureSize || columns > appConfig.max3DTextureSize) { + return { value: false }; + } + } + // We used to check is reconstructable modalities here, but the logic is removed + // in favor of the calculation by metadata (orientation and positions) + + // Can't reconstruct if we only have one image. + if (!isMultiframe && instances.length === 1) { + return { value: false }; + } + + // Can't reconstruct if all instances don't have the ImagePositionPatient. + if (!isMultiframe && !instances.every(instance => instance.ImagePositionPatient)) { + return { value: false }; + } + + const sortedInstances = sortInstancesByPosition(instances); + + return isMultiframe ? processMultiframe(sortedInstances[0]) : processSingleframe(sortedInstances); +} + +function hasPixelMeasurements(multiFrameInstance) { + const perFrameSequence = multiFrameInstance.PerFrameFunctionalGroupsSequence?.[0]; + const sharedSequence = multiFrameInstance.SharedFunctionalGroupsSequence; + + return ( + Boolean(perFrameSequence?.PixelMeasuresSequence) || + Boolean(sharedSequence?.PixelMeasuresSequence) || + Boolean( + multiFrameInstance.PixelSpacing && + (multiFrameInstance.SliceThickness || multiFrameInstance.SpacingBetweenFrames) + ) + ); +} + +function hasOrientation(multiFrameInstance) { + const sharedSequence = multiFrameInstance.SharedFunctionalGroupsSequence; + const perFrameSequence = multiFrameInstance.PerFrameFunctionalGroupsSequence?.[0]; + + return ( + Boolean(sharedSequence?.PlaneOrientationSequence) || + Boolean(perFrameSequence?.PlaneOrientationSequence) || + Boolean( + multiFrameInstance.ImageOrientationPatient || + multiFrameInstance.DetectorInformationSequence?.[0]?.ImageOrientationPatient + ) + ); +} + +function hasPosition(multiFrameInstance) { + const perFrameSequence = multiFrameInstance.PerFrameFunctionalGroupsSequence?.[0]; + + return ( + Boolean(perFrameSequence?.PlanePositionSequence) || + Boolean(perFrameSequence?.CTPositionSequence) || + Boolean( + multiFrameInstance.ImagePositionPatient || + multiFrameInstance.DetectorInformationSequence?.[0]?.ImagePositionPatient + ) + ); +} + +function isNMReconstructable(multiFrameInstance) { + const imageSubType = multiFrameInstance.ImageType?.[2]; + return imageSubType === 'RECON TOMO' || imageSubType === 'RECON GATED TOMO'; +} + +function processMultiframe(multiFrameInstance) { + // If we don't have the PixelMeasuresSequence, then the pixel spacing and + // slice thickness isn't specified or is changing and we can't reconstruct + // the dataset. + if (!hasPixelMeasurements(multiFrameInstance)) { + return { value: false }; + } + + if (!hasOrientation(multiFrameInstance)) { + console.log('No image orientation information, not reconstructable'); + return { value: false }; + } + + if (!hasPosition(multiFrameInstance)) { + console.log('No image position information, not reconstructable'); + return { value: false }; + } + + if (multiFrameInstance.Modality.includes('NM') && !isNMReconstructable(multiFrameInstance)) { + return { value: false }; + } + + // TODO - check spacing consistency + return { value: true }; +} + +function processSingleframe(instances) { + const firstImage = instances[0]; + const firstImageRows = toNumber(firstImage.Rows); + const firstImageColumns = toNumber(firstImage.Columns); + const firstImageSamplesPerPixel = toNumber(firstImage.SamplesPerPixel); + const firstImageOrientationPatient = toNumber(firstImage.ImageOrientationPatient); + const firstImagePositionPatient = toNumber(firstImage.ImagePositionPatient); + + // Can't reconstruct if we: + // -- Have a different dimensions within a displaySet. + // -- Have a different number of components within a displaySet. + // -- Have different orientations within a displaySet. + for (let i = 1; i < instances.length; i++) { + const instance = instances[i]; + const { Rows, Columns, SamplesPerPixel, ImageOrientationPatient } = instance; + + const imageOrientationPatient = toNumber(ImageOrientationPatient); + + if ( + Rows !== firstImageRows || + Columns !== firstImageColumns || + SamplesPerPixel !== firstImageSamplesPerPixel || + !_isSameOrientation(imageOrientationPatient, firstImageOrientationPatient) + ) { + return { value: false }; + } + } + + let missingFrames = 0; + let averageSpacingBetweenFrames; + + // Check if frame spacing is approximately equal within a spacingTolerance. + // If spacing is on a uniform grid but we are missing frames, + // Allow reconstruction, but pass back the number of missing frames. + if (instances.length > 2) { + const lastIpp = toNumber(instances[instances.length - 1].ImagePositionPatient); + + // We can't reconstruct if we are missing ImagePositionPatient values + if (!firstImagePositionPatient || !lastIpp) { + return { value: false }; + } + + averageSpacingBetweenFrames = + _getPerpendicularDistance(firstImagePositionPatient, lastIpp) / (instances.length - 1); + + let previousImagePositionPatient = firstImagePositionPatient; + + for (let i = 1; i < instances.length; i++) { + const instance = instances[i]; + // Todo: get metadata from OHIF.MetadataProvider + const imagePositionPatient = toNumber(instance.ImagePositionPatient); + + const spacingBetweenFrames = _getPerpendicularDistance( + imagePositionPatient, + previousImagePositionPatient + ); + const spacingIssue = _getSpacingIssue(spacingBetweenFrames, averageSpacingBetweenFrames); + + if (spacingIssue) { + const issue = spacingIssue.issue; + + if (issue === reconstructionIssues.MISSING_FRAMES) { + missingFrames += spacingIssue.missingFrames; + } else if (issue === reconstructionIssues.IRREGULAR_SPACING) { + return { value: false }; + } + } + + previousImagePositionPatient = imagePositionPatient; + } + } + + return { value: true, averageSpacingBetweenFrames }; +} + +function _isSameOrientation(iop1, iop2) { + if (iop1 === undefined || iop2 === undefined) { + return; + } + + return ( + Math.abs(iop1[0] - iop2[0]) < iopTolerance && + Math.abs(iop1[1] - iop2[1]) < iopTolerance && + Math.abs(iop1[2] - iop2[2]) < iopTolerance && + Math.abs(iop1[3] - iop2[3]) < iopTolerance && + Math.abs(iop1[4] - iop2[4]) < iopTolerance && + Math.abs(iop1[5] - iop2[5]) < iopTolerance + ); +} + +/** + * Checks for spacing issues. + * + * @param {number} spacing The spacing between two frames. + * @param {number} averageSpacing The average spacing between all frames. + * + * @returns {Object} An object containing the issue and extra information if necessary. + */ +function _getSpacingIssue(spacing, averageSpacing) { + const equalWithinTolerance = + Math.abs(spacing - averageSpacing) < averageSpacing * spacingTolerance; + + if (equalWithinTolerance) { + return; + } + + const multipleOfAverageSpacing = spacing / averageSpacing; + + const numberOfSpacings = Math.round(multipleOfAverageSpacing); + + const errorForEachSpacing = + Math.abs(spacing - numberOfSpacings * averageSpacing) / numberOfSpacings; + + if (errorForEachSpacing < spacingTolerance * averageSpacing) { + return { + issue: reconstructionIssues.MISSING_FRAMES, + missingFrames: numberOfSpacings - 1, + }; + } + + return { issue: reconstructionIssues.IRREGULAR_SPACING }; +} + +function _getPerpendicularDistance(a, b) { + return Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2)); +} + +const constructableModalities = ['MR', 'CT', 'PT', 'NM']; +const reconstructionIssues = { + MISSING_FRAMES: 'missingframes', + IRREGULAR_SPACING: 'irregularspacing', +}; + +export { + hasPixelMeasurements, + hasOrientation, + hasPosition, + isNMReconstructable, + _isSameOrientation, + _getSpacingIssue, + _getPerpendicularDistance, + reconstructionIssues, + constructableModalities, +}; diff --git a/platform/core/src/utils/isEqualWithin.ts b/platform/core/src/utils/isEqualWithin.ts new file mode 100644 index 0000000..ed3c298 --- /dev/null +++ b/platform/core/src/utils/isEqualWithin.ts @@ -0,0 +1,27 @@ +/** + * returns equal if the two arrays are identical within the + * given tolerance. + * + * @param v1 - The first array of values + * @param v2 - The second array of values. + * @param tolerance - The acceptable tolerance, the default is 0.00001 + * + * @returns True if the two values are within the tolerance levels. + */ +export default function isEqualWithin( + v1: number[] | Float32Array, + v2: number[] | Float32Array, + tolerance = 1e-5 +): boolean { + if (v1.length !== v2.length) { + return false; + } + + for (let i = 0; i < v1.length; i++) { + if (Math.abs(v1[i] - v2[i]) > tolerance) { + return false; + } + } + + return true; +} diff --git a/platform/core/src/utils/isImage.js b/platform/core/src/utils/isImage.js new file mode 100644 index 0000000..373abce --- /dev/null +++ b/platform/core/src/utils/isImage.js @@ -0,0 +1,65 @@ +import { sopClassDictionary } from './sopClassDictionary'; + +const imagesTypes = [ + sopClassDictionary.ComputedRadiographyImageStorage, + sopClassDictionary.DigitalXRayImageStorageForPresentation, + sopClassDictionary.DigitalXRayImageStorageForProcessing, + sopClassDictionary.DigitalMammographyXRayImageStorageForPresentation, + sopClassDictionary.DigitalMammographyXRayImageStorageForProcessing, + sopClassDictionary.DigitalIntraOralXRayImageStorageForPresentation, + sopClassDictionary.DigitalIntraOralXRayImageStorageForProcessing, + sopClassDictionary.CTImageStorage, + sopClassDictionary.EnhancedCTImageStorage, + sopClassDictionary.LegacyConvertedEnhancedCTImageStorage, + sopClassDictionary.UltrasoundMultiframeImageStorage, + sopClassDictionary.EnhancedUSVolumeStorage, + sopClassDictionary.MRImageStorage, + sopClassDictionary.EnhancedMRImageStorage, + sopClassDictionary.EnhancedMRColorImageStorage, + sopClassDictionary.LegacyConvertedEnhancedMRImageStorage, + sopClassDictionary.UltrasoundImageStorage, + sopClassDictionary.SecondaryCaptureImageStorage, + sopClassDictionary.MultiframeSingleBitSecondaryCaptureImageStorage, + sopClassDictionary.MultiframeGrayscaleByteSecondaryCaptureImageStorage, + sopClassDictionary.MultiframeGrayscaleWordSecondaryCaptureImageStorage, + sopClassDictionary.MultiframeTrueColorSecondaryCaptureImageStorage, + sopClassDictionary.XRayAngiographicImageStorage, + sopClassDictionary.EnhancedXAImageStorage, + sopClassDictionary.XRayRadiofluoroscopicImageStorage, + sopClassDictionary.EnhancedXRFImageStorage, + sopClassDictionary.XRay3DAngiographicImageStorage, + sopClassDictionary.XRay3DCraniofacialImageStorage, + sopClassDictionary.BreastTomosynthesisImageStorage, + sopClassDictionary.BreastProjectionXRayImageStorageForPresentation, + sopClassDictionary.BreastProjectionXRayImageStorageForProcessing, + sopClassDictionary.IntravascularOpticalCoherenceTomographyImageStorageForPresentation, + sopClassDictionary.IntravascularOpticalCoherenceTomographyImageStorageForProcessing, + sopClassDictionary.NuclearMedicineImageStorage, + sopClassDictionary.VLEndoscopicImageStorage, + sopClassDictionary.VideoEndoscopicImageStorage, + sopClassDictionary.VLMicroscopicImageStorage, + sopClassDictionary.VideoMicroscopicImageStorage, + sopClassDictionary.VLSlideCoordinatesMicroscopicImageStorage, + sopClassDictionary.VLPhotographicImageStorage, + sopClassDictionary.VideoPhotographicImageStorage, + sopClassDictionary.OphthalmicPhotography8BitImageStorage, + sopClassDictionary.OphthalmicPhotography16BitImageStorage, + sopClassDictionary.OphthalmicTomographyImageStorage, + sopClassDictionary.VLWholeSlideMicroscopyImageStorage, + sopClassDictionary.PositronEmissionTomographyImageStorage, + sopClassDictionary.EnhancedPETImageStorage, + sopClassDictionary.LegacyConvertedEnhancedPETImageStorage, + sopClassDictionary.RTImageStorage, +]; + +/** + * Checks whether dicom files with specified SOP Class UID have image data + * @param {string} SOPClassUID - SOP Class UID to be checked + * @returns {boolean} - true if it has image data + */ +export const isImage = SOPClassUID => { + if (!SOPClassUID) { + return false; + } + return imagesTypes.indexOf(SOPClassUID) !== -1; +}; diff --git a/platform/core/src/utils/isImage.test.js b/platform/core/src/utils/isImage.test.js new file mode 100644 index 0000000..8826392 --- /dev/null +++ b/platform/core/src/utils/isImage.test.js @@ -0,0 +1,279 @@ +import { sopClassDictionary } from './sopClassDictionary'; +import { isImage } from './isImage'; + +describe('isImage', () => { + test('should return true when the image is of type sopClassDictionary.ComputedRadiographyImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.ComputedRadiographyImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.DigitalXRayImageStorageForPresentation', () => { + const isImageStatus = isImage(sopClassDictionary.DigitalXRayImageStorageForPresentation); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.DigitalXRayImageStorageForProcessing', () => { + const isImageStatus = isImage(sopClassDictionary.DigitalXRayImageStorageForProcessing); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.DigitalMammographyXRayImageStorageForPresentation', () => { + const isImageStatus = isImage( + sopClassDictionary.DigitalMammographyXRayImageStorageForPresentation + ); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.DigitalMammographyXRayImageStorageForProcessing', () => { + const isImageStatus = isImage( + sopClassDictionary.DigitalMammographyXRayImageStorageForProcessing + ); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.DigitalIntraOralXRayImageStorageForPresentation', () => { + const isImageStatus = isImage( + sopClassDictionary.DigitalIntraOralXRayImageStorageForPresentation + ); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.DigitalIntraOralXRayImageStorageForProcessing', () => { + const isImageStatus = isImage(sopClassDictionary.DigitalIntraOralXRayImageStorageForProcessing); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.CTImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.CTImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.EnhancedCTImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.EnhancedCTImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.LegacyConvertedEnhancedCTImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.LegacyConvertedEnhancedCTImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.UltrasoundMultiframeImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.UltrasoundMultiframeImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.MRImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.MRImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.EnhancedMRImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.EnhancedMRImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.EnhancedMRColorImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.EnhancedMRColorImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.LegacyConvertedEnhancedMRImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.LegacyConvertedEnhancedMRImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.UltrasoundImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.UltrasoundImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.SecondaryCaptureImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.SecondaryCaptureImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.MultiframeSingleBitSecondaryCaptureImageStorage', () => { + const isImageStatus = isImage( + sopClassDictionary.MultiframeSingleBitSecondaryCaptureImageStorage + ); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.MultiframeGrayscaleByteSecondaryCaptureImageStorage', () => { + const isImageStatus = isImage( + sopClassDictionary.MultiframeGrayscaleByteSecondaryCaptureImageStorage + ); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.MultiframeGrayscaleWordSecondaryCaptureImageStorage', () => { + const isImageStatus = isImage( + sopClassDictionary.MultiframeGrayscaleWordSecondaryCaptureImageStorage + ); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.MultiframeTrueColorSecondaryCaptureImageStorage', () => { + const isImageStatus = isImage( + sopClassDictionary.MultiframeTrueColorSecondaryCaptureImageStorage + ); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.XRayAngiographicImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.XRayAngiographicImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.EnhancedXAImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.EnhancedXAImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.XRayRadiofluoroscopicImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.XRayRadiofluoroscopicImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.EnhancedXRFImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.EnhancedXRFImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.XRay3DAngiographicImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.XRay3DAngiographicImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.XRay3DCraniofacialImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.XRay3DCraniofacialImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.BreastTomosynthesisImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.BreastTomosynthesisImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.BreastProjectionXRayImageStorageForPresentation', () => { + const isImageStatus = isImage( + sopClassDictionary.BreastProjectionXRayImageStorageForPresentation + ); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.BreastProjectionXRayImageStorageForProcessing', () => { + const isImageStatus = isImage(sopClassDictionary.BreastProjectionXRayImageStorageForProcessing); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.IntravascularOpticalCoherenceTomographyImageStorageForPresentation', () => { + const isImageStatus = isImage( + sopClassDictionary.IntravascularOpticalCoherenceTomographyImageStorageForPresentation + ); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.IntravascularOpticalCoherenceTomographyImageStorageForProcessing', () => { + const isImageStatus = isImage( + sopClassDictionary.IntravascularOpticalCoherenceTomographyImageStorageForProcessing + ); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.NuclearMedicineImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.NuclearMedicineImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.VLEndoscopicImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.VLEndoscopicImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.VideoEndoscopicImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.VideoEndoscopicImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.VLMicroscopicImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.VLMicroscopicImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.VideoMicroscopicImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.VideoMicroscopicImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.VLSlideCoordinatesMicroscopicImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.VLSlideCoordinatesMicroscopicImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.VLPhotographicImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.VLPhotographicImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.VideoPhotographicImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.VideoPhotographicImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.OphthalmicPhotography8BitImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.OphthalmicPhotography8BitImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.OphthalmicPhotography16BitImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.OphthalmicPhotography16BitImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.OphthalmicTomographyImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.OphthalmicTomographyImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.VLWholeSlideMicroscopyImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.VLWholeSlideMicroscopyImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.PositronEmissionTomographyImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.PositronEmissionTomographyImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.EnhancedPETImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.EnhancedPETImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.LegacyConvertedEnhancedPETImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.LegacyConvertedEnhancedPETImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return true when the image is of type sopClassDictionary.RTImageStorage', () => { + const isImageStatus = isImage(sopClassDictionary.RTImageStorage); + expect(isImageStatus).toBe(true); + }); + + test('should return false when the image is of type sopClassDictionary.SpatialFiducialsStorage', () => { + const isImageStatus = isImage(sopClassDictionary.SpatialFiducialsStorage); + expect(isImageStatus).toBe(false); + }); + + test('should return false when the image is undefined', () => { + const isImageStatus = isImage(undefined); + expect(isImageStatus).toBe(false); + }); + + test('should return false when the image is null', () => { + const isImageStatus = isImage(null); + expect(isImageStatus).toBe(false); + }); +}); diff --git a/platform/core/src/utils/isLowPriorityModality.ts b/platform/core/src/utils/isLowPriorityModality.ts new file mode 100644 index 0000000..cfcee34 --- /dev/null +++ b/platform/core/src/utils/isLowPriorityModality.ts @@ -0,0 +1,5 @@ +const LOW_PRIORITY_MODALITIES = Object.freeze(['SEG', 'KO', 'PR', 'SR', 'RTSTRUCT', 'RTDOSE', 'RTPLAN', 'RTRECORD', 'REG']); + +export default function isLowPriorityModality(Modality) { + return LOW_PRIORITY_MODALITIES.includes(Modality); +} diff --git a/platform/core/src/utils/makeCancelable.js b/platform/core/src/utils/makeCancelable.js new file mode 100644 index 0000000..f64af59 --- /dev/null +++ b/platform/core/src/utils/makeCancelable.js @@ -0,0 +1,23 @@ +export default function makeCancelable(thenable) { + let isCanceled = false; + const promise = Promise.resolve(thenable).then( + function (result) { + if (isCanceled) { + throw Object.freeze({ isCanceled }); + } + return result; + }, + function (error) { + if (isCanceled) { + throw Object.freeze({ isCanceled, error }); + } + throw error; + } + ); + return Object.assign(Object.create(promise), { + then: promise.then.bind(promise), + cancel() { + isCanceled = true; + }, + }); +} diff --git a/platform/core/src/utils/makeDeferred.js b/platform/core/src/utils/makeDeferred.js new file mode 100644 index 0000000..86a25ec --- /dev/null +++ b/platform/core/src/utils/makeDeferred.js @@ -0,0 +1,9 @@ +export default function makeDeferred() { + let reject, + resolve, + promise = new Promise(function (res, rej) { + resolve = res; + reject = rej; + }); + return Object.freeze({ promise, resolve, reject }); +} diff --git a/platform/core/src/utils/makeDeferred.test.js b/platform/core/src/utils/makeDeferred.test.js new file mode 100644 index 0000000..db27db0 --- /dev/null +++ b/platform/core/src/utils/makeDeferred.test.js @@ -0,0 +1,14 @@ +import makeDeferred from './makeDeferred'; + +describe('makeDeferred', () => { + it('should provide a promise to be resolved externally', () => { + const deferred = makeDeferred(); + setTimeout(() => void deferred.resolve('Yay!')); + return deferred.promise.then(result => void expect(result).toBe('Yay!')); + }); + it('should provide a promise to be rejected externally', () => { + const deferred = makeDeferred(); + setTimeout(() => void deferred.reject('Oops...')); + return deferred.promise.catch(error => void expect(error).toBe('Oops...')); + }); +}); diff --git a/platform/core/src/utils/measurementFilters.ts b/platform/core/src/utils/measurementFilters.ts new file mode 100644 index 0000000..db36a30 --- /dev/null +++ b/platform/core/src/utils/measurementFilters.ts @@ -0,0 +1,110 @@ +/** + * Returns a filter function which filters for measurements belonging to both + * the study and series. + */ +export function filterMeasurementsBySeriesUID(selectedSeries: string[]) { + return measurement => selectedSeries.includes(measurement.referenceSeriesUID); +} + +/** + * @returns true for measurements include referencedImageId (coplanar with an image) + */ +export function filterPlanarMeasurement(measurement) { + return measurement?.referencedImageId; +} + +/** A filter that always returns true */ +export function filterAny(_measurement) { + return true; +} + +/** A filter that excludes everything */ +export function filterNone(_measurement) { + return false; +} + +/** + * Filters the measurements which are found in any of the specified + * filters. Strings will be looked up by name. + */ +export function filterOr(...filters) { + return function (item) { + for (let filter of filters) { + if (typeof filter === 'string') { + filter = this[filter]; + } + if (typeof filter !== 'function') { + continue; + } + if (filter.call(this, item)) { + return true; + } + } + return false; + }; +} + +/** + * Filters for additional findings, that is, measurements with + * a value of type point, and having a referenced image + */ +export function filterAdditionalFindings(measurementService) { + const { POINT } = measurementService.VALUE_TYPES; + return dm => dm.type === POINT && dm.referencedImageId; +} + +/** + * Returns a filter that applies the second filter unless the first filter would + * include the given measurement. + * That is, (!filterUnless) && filterThen + */ +export function filterUnless(filterUnless, filterThen) { + return item => (filterUnless(item) ? false : filterThen(item)); +} + +const isString = s => typeof s === 'string' || s instanceof String; + +/** + * Returns true if all the filters return true. + * Any filter can be a string name of a filter on the "this" object + * called on the final filter call. + */ +export function filterAnd(...filters) { + return function (item) { + for (const filter of filters) { + if (isString(filter)) { + if (!this[filter](item)) { + return false; + } + } else if (!filter.call(this, item)) { + return false; + } + } + return true; + }; +} + +/** + * Returns a filter that returns true if none of the filters supplied return true. + * Any filter supplied can be a name, in which case hte filter will be retrieved + * from "this" object on the call. + * + * For example, for filterNot("otherFilterName"), if that is called on + * `{ otherFilterName: filterNone }` + * then otherFilterName will be called, returning false in this case and + * filterNot will return true. + * + * + */ +export function filterNot(...filters) { + if (filters.length !== 1) { + return filterAnd.apply(null, filters.map(filterNot)); + } + const [filter] = filters; + if (isString(filter)) { + return function (item) { + return !this[filter](item); + }; + } + return item => !filter(item); +} diff --git a/platform/core/src/utils/metadataProvider/fetchPaletteColorLookupTableData.js b/platform/core/src/utils/metadataProvider/fetchPaletteColorLookupTableData.js new file mode 100644 index 0000000..a3d5a7a --- /dev/null +++ b/platform/core/src/utils/metadataProvider/fetchPaletteColorLookupTableData.js @@ -0,0 +1,71 @@ +/** + * Gets the palette color data for the specified tag - red/green/blue, + * either from the given UID or from the tag itself. + * Returns an array if the data is immediately available, or a promise + * which resolves to the data if the data needs to be loaded. + * Returns undefined if the palette isn't specified. + * + * @param {*} item containing the palette colour data and description + * @param {*} tag is the tag for the palette data + * @param {*} descriptorTag is the tag for the descriptor + * @returns Array view containing the palette data, or a promise to return one. + * Returns undefined if the palette data is absent. + */ +export default function fetchPaletteColorLookupTableData(item, tag, descriptorTag) { + const { PaletteColorLookupTableUID } = item; + const paletteData = item[tag]; + if (paletteData === undefined && PaletteColorLookupTableUID === undefined) { + return; + } + // performance optimization - read UID and cache by UID + return _getPaletteColor(item[tag], item[descriptorTag]); +} + +function _getPaletteColor(paletteColorLookupTableData, lutDescriptor) { + const numLutEntries = lutDescriptor[0]; + const bits = lutDescriptor[2]; + + if (!paletteColorLookupTableData) { + return undefined; + } + + const arrayBufferToPaletteColorLUT = arraybuffer => { + const lut = []; + + if (bits === 16) { + let j = 0; + for (let i = 0; i < numLutEntries; i++) { + lut[i] = (arraybuffer[j++] + arraybuffer[j++]) << 8; + } + } else { + for (let i = 0; i < numLutEntries; i++) { + lut[i] = arraybuffer[i]; + } + } + return lut; + }; + + if (paletteColorLookupTableData.palette) { + return paletteColorLookupTableData.palette; + } + + if (paletteColorLookupTableData.InlineBinary) { + try { + const arraybuffer = Uint8Array.from(atob(paletteColorLookupTableData.InlineBinary), c => + c.charCodeAt(0) + ); + return (paletteColorLookupTableData.palette = arrayBufferToPaletteColorLUT(arraybuffer)); + } catch (e) { + console.log("Couldn't decode", paletteColorLookupTableData.InlineBinary, e); + return undefined; + } + } + + if (paletteColorLookupTableData.retrieveBulkData) { + return paletteColorLookupTableData + .retrieveBulkData() + .then(val => (paletteColorLookupTableData.palette = arrayBufferToPaletteColorLUT(val))); + } + + console.error(`No data found for ${paletteColorLookupTableData} palette`); +} diff --git a/platform/core/src/utils/metadataProvider/getPixelSpacingInformation.js b/platform/core/src/utils/metadataProvider/getPixelSpacingInformation.js new file mode 100644 index 0000000..0941136 --- /dev/null +++ b/platform/core/src/utils/metadataProvider/getPixelSpacingInformation.js @@ -0,0 +1,105 @@ +import log from '../../log'; + +export default function getPixelSpacingInformation(instance) { + // See http://gdcm.sourceforge.net/wiki/index.php/Imager_Pixel_Spacing + + // TODO: Add manual calibration + + // TODO: Use ENUMS from dcmjs + const projectionRadiographSOPClassUIDs = [ + '1.2.840.10008.5.1.4.1.1.1', // CR Image Storage + '1.2.840.10008.5.1.4.1.1.1.1', // Digital X-Ray Image Storage โ€“ for Presentation + '1.2.840.10008.5.1.4.1.1.1.1.1', // Digital X-Ray Image Storage โ€“ for Processing + '1.2.840.10008.5.1.4.1.1.1.2', // Digital Mammography X-Ray Image Storage โ€“ for Presentation + '1.2.840.10008.5.1.4.1.1.1.2.1', // Digital Mammography X-Ray Image Storage โ€“ for Processing + '1.2.840.10008.5.1.4.1.1.1.3', // Digital Intra โ€“ oral X-Ray Image Storage โ€“ for Presentation + '1.2.840.10008.5.1.4.1.1.1.3.1', // Digital Intra โ€“ oral X-Ray Image Storage โ€“ for Processing + '1.2.840.10008.5.1.4.1.1.12.1', // X-Ray Angiographic Image Storage + '1.2.840.10008.5.1.4.1.1.12.1.1', // Enhanced XA Image Storage + '1.2.840.10008.5.1.4.1.1.12.2', // X-Ray Radiofluoroscopic Image Storage + '1.2.840.10008.5.1.4.1.1.12.2.1', // Enhanced XRF Image Storage + '1.2.840.10008.5.1.4.1.1.12.3', // X-Ray Angiographic Bi-plane Image Storage Retired + ]; + + const { + PixelSpacing, + ImagerPixelSpacing, + SOPClassUID, + PixelSpacingCalibrationType, + PixelSpacingCalibrationDescription, + EstimatedRadiographicMagnificationFactor, + } = instance; + const isProjection = projectionRadiographSOPClassUIDs.includes(SOPClassUID); + + const TYPES = { + NOT_APPLICABLE: 'NOT_APPLICABLE', + UNKNOWN: 'UNKNOWN', + CALIBRATED: 'CALIBRATED', + DETECTOR: 'DETECTOR', + }; + + if (isProjection && !ImagerPixelSpacing) { + // If only Pixel Spacing is present, and this is a projection radiograph, + // PixelSpacing should be used, but the user should be informed that + // what it means is unknown + return { + PixelSpacing, + type: TYPES.UNKNOWN, + isProjection, + }; + } else if (PixelSpacing && ImagerPixelSpacing && PixelSpacing === ImagerPixelSpacing) { + // If Imager Pixel Spacing and Pixel Spacing are present and they have the same values, + // then the user should be informed that the measurements are at the detector plane + return { + PixelSpacing, + type: TYPES.DETECTOR, + isProjection, + }; + } else if (PixelSpacing && ImagerPixelSpacing && PixelSpacing !== ImagerPixelSpacing) { + // If Imager Pixel Spacing and Pixel Spacing are present and they have different values, + // then the user should be informed that these are "calibrated" + // (in some unknown manner if Pixel Spacing Calibration Type and/or + // Pixel Spacing Calibration Description are absent) + return { + PixelSpacing, + type: TYPES.CALIBRATED, + isProjection, + PixelSpacingCalibrationType, + PixelSpacingCalibrationDescription, + }; + } else if (!PixelSpacing && ImagerPixelSpacing) { + let CorrectedImagerPixelSpacing = ImagerPixelSpacing; + if (EstimatedRadiographicMagnificationFactor) { + // Note that in IHE Mammo profile compliant displays, the value of Imager Pixel Spacing is required to be corrected by + // Estimated Radiographic Magnification Factor and the user informed of that. + // TODO: should this correction be done before all of this logic? + CorrectedImagerPixelSpacing = ImagerPixelSpacing.map( + pixelSpacing => pixelSpacing / EstimatedRadiographicMagnificationFactor + ); + } else { + if (!instance._loggedSpacingMessage) { + log.info( + 'EstimatedRadiographicMagnificationFactor was not present. Unable to correct ImagerPixelSpacing.' + ); + instance._loggedSpacingMessage = true; + } + } + + return { + PixelSpacing: CorrectedImagerPixelSpacing, + isProjection, + }; + } else if (isProjection === false && !ImagerPixelSpacing) { + // If only Pixel Spacing is present, and this is not a projection radiograph, + // we can stop here + return { + PixelSpacing, + type: TYPES.NOT_APPLICABLE, + isProjection, + }; + } + + log.info( + 'Unknown combination of PixelSpacing and ImagerPixelSpacing identified. Unable to determine spacing.' + ); +} diff --git a/platform/core/src/utils/metadataProvider/unpackOverlay.js b/platform/core/src/utils/metadataProvider/unpackOverlay.js new file mode 100644 index 0000000..3876874 --- /dev/null +++ b/platform/core/src/utils/metadataProvider/unpackOverlay.js @@ -0,0 +1,12 @@ +export default function unpackOverlay(arrayBuffer) { + const bitArray = new Uint8Array(arrayBuffer); + const byteArray = new Uint8Array(8 * bitArray.length); + + for (let byteIndex = 0; byteIndex < byteArray.length; byteIndex++) { + const bitIndex = byteIndex % 8; + const bitByteIndex = Math.floor(byteIndex / 8); + byteArray[byteIndex] = 1 * ((bitArray[bitByteIndex] & (1 << bitIndex)) >> bitIndex); + } + + return byteArray; +} diff --git a/platform/core/src/utils/objectPath.js b/platform/core/src/utils/objectPath.js new file mode 100644 index 0000000..feae26d --- /dev/null +++ b/platform/core/src/utils/objectPath.js @@ -0,0 +1,96 @@ +export class ObjectPath { + /** + * Set an object property based on "path" (namespace) supplied creating + * ... intermediary objects if they do not exist. + * @param object {Object} An object where the properties specified on path should be set. + * @param path {String} A string representing the property to be set, e.g. "user.study.series.timepoint". + * @param value {Any} The value of the property that will be set. + * @return {Boolean} Returns "true" on success, "false" if any intermediate component of the supplied path + * ... is not a valid Object, in which case the property cannot be set. No exceptions are thrown. + */ + static set(object, path, value) { + let components = ObjectPath.getPathComponents(path), + length = components !== null ? components.length : 0, + result = false; + + if (length > 0 && ObjectPath.isValidObject(object)) { + let i = 0, + last = length - 1, + currentObject = object; + + while (i < last) { + let field = components[i]; + + if (field in currentObject) { + if (!ObjectPath.isValidObject(currentObject[field])) { + break; + } + } else { + currentObject[field] = {}; + } + + currentObject = currentObject[field]; + i++; + } + + if (i === last) { + currentObject[components[last]] = value; + result = true; + } + } + + return result; + } + + /** + * Get an object property based on "path" (namespace) supplied traversing the object + * ... tree as necessary. + * @param object {Object} An object where the properties specified might exist. + * @param path {String} A string representing the property to be searched for, e.g. "user.study.series.timepoint". + * @return {Any} The value of the property if found. By default, returns the special type "undefined". + */ + static get(object, path) { + let found, // undefined by default + components = ObjectPath.getPathComponents(path), + length = components !== null ? components.length : 0; + + if (length > 0 && ObjectPath.isValidObject(object)) { + let i = 0, + last = length - 1, + currentObject = object; + + while (i < last) { + let field = components[i]; + + const isValid = ObjectPath.isValidObject(currentObject[field]); + if (field in currentObject && isValid) { + currentObject = currentObject[field]; + i++; + } else { + break; + } + } + + if (i === last && components[last] in currentObject) { + found = currentObject[components[last]]; + } + } + + return found; + } + + /** + * Check if the supplied argument is a real JavaScript Object instance. + * @param object {Any} The subject to be tested. + * @return {Boolean} Returns "true" if the object is a real Object instance and "false" otherwise. + */ + static isValidObject(object) { + return typeof object === 'object' && object !== null && object instanceof Object; + } + + static getPathComponents(path) { + return typeof path === 'string' ? path.split('.') : null; + } +} + +export default ObjectPath; diff --git a/platform/core/src/utils/objectPath.test.js b/platform/core/src/utils/objectPath.test.js new file mode 100644 index 0000000..49cd4cd --- /dev/null +++ b/platform/core/src/utils/objectPath.test.js @@ -0,0 +1,99 @@ +import objectPath from './objectPath'; + +describe('objectPath', () => { + test('should return false when the supplied argument is not a real JavaScript Object instance such as undefined', () => { + expect(objectPath.isValidObject(undefined)).toBe(false); + }); + + test('should return false when the supplied argument is not a real JavaScript Object instance such as null', () => { + expect(objectPath.isValidObject(null)).toBe(false); + }); + + test('should return true when the supplied argument is a real JavaScript Object instance', () => { + expect(objectPath.isValidObject({})).toBe(true); + }); + + test('should return [path1, path2, path3] when the path is path1.path2.path3', () => { + const path = 'path1.path2.path3'; + const expectedPathComponents = objectPath.getPathComponents(path); + expect(expectedPathComponents).toEqual(['path1', 'path2', 'path3']); + }); + + test('should return null when the path is not a string', () => { + const path = 20; + const expectedPathComponents = objectPath.getPathComponents(path); + expect(expectedPathComponents).toEqual(null); + }); + + test('should return [path1path2path3] when the path is path1path2path3', () => { + const path = 'path1path2path3'; + const expectedPathComponents = objectPath.getPathComponents(path); + expect(expectedPathComponents).toEqual(['path1path2path3']); + }); + + test('should return the property obj.myProperty when the object contains myProperty', () => { + const searchObject = { + obj: { + myProperty: 'MOCK_VALUE', + }, + }; + const path = 'obj.myProperty'; + const expectedPathComponents = objectPath.get(searchObject, path); + expect(expectedPathComponents).toEqual(searchObject.obj.myProperty); + }); + + test('should return undefined when the object does not contain a property', () => { + const searchObject = { + obj: { + myProperty: 'MOCK_VALUE', + }, + }; + const path = 'obj.unknownProperty'; + const expectedPathComponents = objectPath.get(searchObject, path); + expect(expectedPathComponents).toEqual(undefined); + }); + + test('should return undefined when the object is not a valid object', () => { + const searchObject = undefined; + const path = 'obj.unknownProperty'; + const expectedPathComponents = objectPath.get(searchObject, path); + expect(expectedPathComponents).toEqual(undefined); + }); + + test('should return undefined when the inner object is not a valid object', () => { + const searchObject = { + obj: { + myProperty: null, + }, + }; + const path = 'obj.unknownProperty'; + const expectedPathComponents = objectPath.get(searchObject, path); + expect(expectedPathComponents).toEqual(undefined); + }); + + test('should set the property obj.myProperty when the object does not contain myProperty', () => { + const searchObject = { + obj: { + anyProperty: 'MOCK_VALUE', + }, + }; + const newValue = 'NEW_VALUE'; + const path = 'obj.myProperty'; + const output = objectPath.set(searchObject, path, newValue); + expect(output).toBe(true); + expect(searchObject.obj.myProperty).toEqual(newValue); + }); + + test('should return false when the object which is being set is not in a valid path', () => { + const searchObject = { + obj: { + myProperty: 'MOCK_VALUE', + }, + }; + const path = undefined; + const newValue = 'NEW_VALUE'; + + const output = objectPath.set(searchObject, path, newValue); + expect(output).toEqual(false); + }); +}); diff --git a/platform/core/src/utils/progressTrackingUtils.js b/platform/core/src/utils/progressTrackingUtils.js new file mode 100644 index 0000000..7ff2f82 --- /dev/null +++ b/platform/core/src/utils/progressTrackingUtils.js @@ -0,0 +1,326 @@ +import makeDeferred from './makeDeferred'; + +/** + * Constants + */ + +const TYPE = Symbol('Type'); +const TASK = Symbol('Task'); +const LIST = Symbol('List'); + +/** + * Public Methods + */ + +/** + * Creates an instance of a task list + * @returns {Object} A task list object + */ +function createList() { + return objectWithType(LIST, { + head: null, + named: Object.create(null), + observers: [], + }); +} + +/** + * Checks if the given argument is a List instance + * @param {any} subject The value to be tested + * @returns {boolean} true if a valid List instance is given, false otherwise + */ +function isList(subject) { + return isOfType(LIST, subject); +} + +/** + * Creates an instance of a task + * @param {Object} list The List instance related to this task + * @param {Object} next The next Task instance to link to + * @returns {Object} A task object + */ +function createTask(list, next) { + return objectWithType(TASK, { + list: isList(list) ? list : null, + next: isTask(next) ? next : null, + failed: false, + awaiting: null, + progress: 0.0, + }); +} + +/** + * Checks if the given argument is a Task instance + * @param {any} subject The value to be tested + * @returns {boolean} true if a valid Task instance is given, false otherwise + */ +function isTask(subject) { + return isOfType(TASK, subject); +} + +/** + * Appends a new Task to the given List instance and notifies the list observers + * @param {Object} list A List instance + * @returns {Object} The new Task instance appended to the List or null if the + * given List instanc is not valid + */ +function increaseList(list) { + if (isList(list)) { + const task = createTask(list, list.head); + list.head = task; + notify(list, getOverallProgress(list)); + return task; + } + return null; +} + +/** + * Updates the internal progress value of the given Task instance and notifies + * the observers of the associated list. + * @param {Object} task The Task instance to be updated + * @param {number} value A number between 0 (inclusive) and 1 (exclusive) + * indicating the progress of the task; + * @returns {void} Nothing is returned + */ +function update(task, value) { + if (isTask(task) && isValidProgress(value) && value < 1.0) { + if (task.progress !== value) { + task.progress = value; + if (isList(task.list)) { + notify(task.list, getOverallProgress(task.list)); + } + } + } +} + +/** + * Sets a Task instance as finished (progress = 1.0), freezes it in order to + * prevent further modifications and notifies the observers of the associated + * list. + * @param {Object} task The Task instance to be finalized + * @returns {void} Nothing is returned + */ +function finish(task) { + if (isTask(task)) { + task.progress = 1.0; + task.awaiting = null; + Object.freeze(task); + if (isList(task.list)) { + notify(task.list, getOverallProgress(task.list)); + } + } +} + +/** + * Generate a summarized snapshot of the current status of the given task List + * @param {Object} list The List instance to be scanned + * @returns {Object} An object representing the summarized status of the list + */ +function getOverallProgress(list) { + const status = createStatus(); + if (isList(list)) { + let task = list.head; + while (isTask(task)) { + status.total++; + if (isValidProgress(task.progress)) { + status.partial += task.progress; + if (task.progress === 1.0 && task.failed) { + status.failures++; + } + } + task = task.next; + } + } + if (status.total > 0) { + status.progress = status.partial / status.total; + } + return Object.freeze(status); +} + +/** + * Adds a Task instance to the given list that waits on a given "thenable". When + * the thenable resolves the "finish" method is called on the newly created + * instance thus notifying the observers of the list. + * @param {Object} list The List instance to which the new task will be added + * @param {Object|Promise} thenable The thenable to be waited on + * @returns {Object} A reference to the newly created Task; + */ +function waitOn(list, thenable) { + const task = increaseList(list); + if (isTask(task)) { + task.awaiting = Promise.resolve(thenable).then( + function () { + finish(task); + }, + function () { + task.failed = true; + finish(task); + } + ); + return task; + } + return null; +} + +/** + * Adds a Task instance to the given list using a deferred (a Promise that can + * be externally resolved) notifying the observers of the list. + * @param {Object} list The List instance to which the new task will be added + * @returns {Object} An object with references to the created deferred and task + */ +function addDeferred(list) { + const deferred = makeDeferred(); + const task = waitOn(list, deferred.promise); + return Object.freeze({ + deferred, + task, + }); +} + +/** + * Assigns a name to a specific task of the list + * @param {Object} list The List instance whose task will be named + * @param {Object} task The specified Task instance + * @param {string} name The name of the task + * @returns {boolean} Returns true on success, false otherwise + */ +function setTaskName(list, task, name) { + if ( + contains(list, task) && + list.named !== null && + typeof list.named === 'object' && + typeof name === 'string' + ) { + list.named[name] = task; + return true; + } + return false; +} + +/** + * Retrieves a task by name + * @param {Object} list The List instance whose task will be retrieved + * @param {string} name The name of the task to be retrieved + * @returns {Object} The Task instance or null if not found + */ +function getTaskByName(list, name) { + if ( + isList(list) && + list.named !== null && + typeof list.named === 'object' && + typeof name === 'string' + ) { + const task = list.named[name]; + if (isTask(task)) { + return task; + } + } + return null; +} + +/** + * Adds an observer (callback function) to a given List instance + * @param {Object} list The List instance to which the observer will be appended + * @param {Function} observer The observer (function) that will be executed + * every time a change happens within the list + * @returns {boolean} Returns true on success and false otherwise + */ +function addObserver(list, observer) { + if (isList(list) && Array.isArray(list.observers) && typeof observer === 'function') { + list.observers.push(observer); + return true; + } + return false; +} + +/** + * Removes an observer (callback function) from a given List instance + * @param {Object} list The instance List from which the observer will removed + * @param {Function} observer The observer function to be removed + * @returns {boolean} Returns true on success and false otherwise + */ +function removeObserver(list, observer) { + if (isList(list) && Array.isArray(list.observers) && list.observers.length > 0) { + const index = list.observers.indexOf(observer); + if (index >= 0) { + list.observers.splice(index, 1); + return true; + } + } + return false; +} + +/** + * Utils + */ + +function createStatus() { + return Object.seal({ + total: 0, + partial: 0.0, + progress: 0.0, + failures: 0, + }); +} + +function objectWithType(type, object) { + return Object.seal(Object.defineProperty(object, TYPE, { value: type })); +} + +function isOfType(type, subject) { + return subject !== null && typeof subject === 'object' && subject[TYPE] === type; +} + +function isValidProgress(value) { + return typeof value === 'number' && value >= 0.0 && value <= 1.0; +} + +function contains(list, task) { + if (isList(list) && isTask(task)) { + let item = list.head; + while (isTask(item)) { + if (item === task) { + return true; + } + item = item.next; + } + } + return false; +} + +function notify(list, data) { + if (isList(list) && Array.isArray(list.observers) && list.observers.length > 0) { + list.observers.slice().forEach(function (observer) { + if (typeof observer === 'function') { + try { + observer(data, list); + } catch (e) { + /* Oops! */ + } + } + }); + } +} + +/** + * Exports + */ + +const progressTrackingUtils = { + createList, + isList, + createTask, + isTask, + increaseList, + update, + finish, + getOverallProgress, + waitOn, + addDeferred, + setTaskName, + getTaskByName, + addObserver, + removeObserver, +}; + +export default progressTrackingUtils; diff --git a/platform/core/src/utils/progressTrackingUtils.test.js b/platform/core/src/utils/progressTrackingUtils.test.js new file mode 100644 index 0000000..3a74c8c --- /dev/null +++ b/platform/core/src/utils/progressTrackingUtils.test.js @@ -0,0 +1,171 @@ +import utils from './progressTrackingUtils'; + +describe('progressTrackingUtils', () => { + describe('Creation of lists of tasks to be tracked', () => { + it('should support creation of task lists', () => { + expect(utils.createList()).toBeInstanceOf(Object); + }); + it('should support validation of task lists', () => { + const list = utils.createList(); + expect(utils.isList(list)).toBe(true); + expect(utils.isList(JSON.parse(JSON.stringify(list)))).toBe(false); + }); + }); + + describe('Usage of lists of tasks to be tracked', () => { + let context; + + // Mock for download + function fakeRequest(callback) { + return new Promise(resolve => { + let progress = 0.0; + setTimeout(function step() { + if (progress < 1.0) { + progress += 1 / 4; + callback(progress); + setTimeout(step); + return; + } + resolve(true); + }); + }); + } + + beforeEach(() => { + const list = utils.createList(); + const observer = jest.fn(); + utils.addObserver(list, observer); + context = { list, observer }; + }); + + it('should call observer twice for each task', () => { + const { list, observer } = context; + const promises = [Promise.resolve('A'), Promise.resolve('B'), Promise.resolve('C')]; + promises.forEach(promise => void utils.waitOn(list, promise)); + return Promise.all(promises).then(() => { + expect(observer).toBeCalledTimes(6); + [ + { + failures: 0, + partial: 0, + progress: 0, + total: 1, + }, + { + failures: 0, + partial: 0, + progress: 0, + total: 2, + }, + { + failures: 0, + partial: 0, + progress: 0, + total: 3, + }, + { + failures: 0, + partial: 1.0, + progress: 1 / 3, + total: 3, + }, + { + failures: 0, + partial: 2.0, + progress: 2 / 3, + total: 3, + }, + { + failures: 0, + partial: 3.0, + progress: 1.0, + total: 3, + }, + ].forEach((item, i) => { + const result = expect.objectContaining(item); + expect(observer).nthCalledWith(i + 1, result, list); + }); + expect(utils.getOverallProgress(list)).toStrictEqual({ + failures: 0, + partial: 3.0, + progress: 1.0, + total: 3, + }); + }); + }); + + it('should support tasks with internal progress updates', () => { + const { list, observer } = context; + const download = utils.addDeferred(list); + const processing = download.deferred.promise.then(result => result); + const update = jest.fn(p => void utils.update(download.task, p)); + download.deferred.resolve(fakeRequest(update)); + utils.waitOn(list, processing); + return processing.then(() => { + expect(update).toBeCalledTimes(4); + [0.25, 0.5, 0.75, 1.0].forEach( + (value, i) => void expect(update).nthCalledWith(i + 1, value) + ); + expect(observer).toBeCalledTimes(7); + [ + { + failures: 0, + partial: 0, + progress: 0, + total: 1, + }, + { + failures: 0, + partial: 0, + progress: 0, + total: 2, + }, + { + failures: 0, + partial: 0.25, + progress: 0.125, + total: 2, + }, + { + failures: 0, + partial: 0.5, + progress: 0.25, + total: 2, + }, + { + failures: 0, + partial: 0.75, + progress: 0.375, + total: 2, + }, + { + failures: 0, + partial: 1.0, + progress: 0.5, + total: 2, + }, + { + failures: 0, + partial: 2.0, + progress: 1.0, + total: 2, + }, + ].forEach((item, i) => { + const result = expect.objectContaining(item); + expect(observer).nthCalledWith(i + 1, result, list); + }); + }); + }); + }); + + describe('Naming of specific tasks', () => { + it('should support naming specific tasks', () => { + const list = utils.createList(); + const tasks = [utils.increaseList(list), utils.increaseList(list)]; + expect(utils.setTaskName(list, tasks[0], 'firstTask')).toBe(true); + expect(utils.setTaskName(list, tasks[1], 'secondTask')).toBe(true); + expect(utils.getTaskByName(list, 'secondTask')).toBe(tasks[1]); + expect(utils.getTaskByName(list, 'firstTask')).toBe(tasks[0]); + }); + }); +}); diff --git a/platform/core/src/utils/reconstructableModalities.js b/platform/core/src/utils/reconstructableModalities.js new file mode 100644 index 0000000..049cf4d --- /dev/null +++ b/platform/core/src/utils/reconstructableModalities.js @@ -0,0 +1,3 @@ +const reconstructableModalities = ['MR', 'CT', 'PT', 'NM']; + +export default reconstructableModalities; diff --git a/platform/core/src/utils/resolveObjectPath.js b/platform/core/src/utils/resolveObjectPath.js new file mode 100644 index 0000000..c541732 --- /dev/null +++ b/platform/core/src/utils/resolveObjectPath.js @@ -0,0 +1,15 @@ +export default function resolveObjectPath(root, path, defaultValue) { + if (root !== null && typeof root === 'object' && typeof path === 'string') { + let value, + separator = path.indexOf('.'); + if (separator >= 0) { + return resolveObjectPath( + root[path.slice(0, separator)], + path.slice(separator + 1, path.length), + defaultValue + ); + } + value = root[path]; + return value === undefined && defaultValue !== undefined ? defaultValue : value; + } +} diff --git a/platform/core/src/utils/resolveObjectPath.test.js b/platform/core/src/utils/resolveObjectPath.test.js new file mode 100644 index 0000000..838341f --- /dev/null +++ b/platform/core/src/utils/resolveObjectPath.test.js @@ -0,0 +1,35 @@ +import resolveObjectPath from './resolveObjectPath'; + +describe('resolveObjectPath', function () { + let config; + + beforeEach(function () { + config = { + active: { + user: { + name: { + first: 'John', + last: 'Doe', + }, + }, + servers: [ + { + ipv4: '10.0.0.1', + }, + ], + }, + }; + }); + + it('should safely return deeply nested values from an object', function () { + expect(resolveObjectPath(config, 'active.user.name.first')).toBe('John'); + expect(resolveObjectPath(config, 'active.user.name.last')).toBe('Doe'); + expect(resolveObjectPath(config, 'active.servers.0.ipv4')).toBe('10.0.0.1'); + }); + + it('should silently return undefined when intermediate values are not valid objects', function () { + expect(resolveObjectPath(config, 'active.usr.name.first')).toBeUndefined(); + expect(resolveObjectPath(config, 'active.name.last')).toBeUndefined(); + expect(resolveObjectPath(config, 'active.servers.7.ipv4')).toBeUndefined(); + }); +}); diff --git a/platform/core/src/utils/roundNumber.js b/platform/core/src/utils/roundNumber.js new file mode 100644 index 0000000..3747e51 --- /dev/null +++ b/platform/core/src/utils/roundNumber.js @@ -0,0 +1,43 @@ +/** + * Truncates decimal points to that there is at least 1+precision significant + * digits. + * + * For example, with the default precision 2 (3 significant digits) + * * Values larger than 100 show no information after the decimal point + * * Values between 10 and 99 show 1 decimal point + * * Values between 1 and 9 show 2 decimal points + * + * @param value - to return a fixed measurement value from + * @param precision - defining how many digits after 1..9 are desired + */ + +function roundNumber(value, precision = 2) { + if (Array.isArray(value)) { + return value.map(v => roundNumber(v, precision)).join(', '); + } + if (value === undefined || value === null || value === '') { + return 'NaN'; + } + value = Number(value); + const absValue = Math.abs(value); + if (absValue < 0.0001) { + return `${value}`; + } + const fixedPrecision = + absValue >= 100 + ? precision - 2 + : absValue >= 10 + ? precision - 1 + : absValue >= 1 + ? precision + : absValue >= 0.1 + ? precision + 1 + : absValue >= 0.01 + ? precision + 2 + : absValue >= 0.001 + ? precision + 3 + : precision + 4; + return value.toFixed(fixedPrecision); +} + +export default roundNumber; diff --git a/platform/core/src/utils/sopClassDictionary.js b/platform/core/src/utils/sopClassDictionary.js new file mode 100644 index 0000000..cb9712e --- /dev/null +++ b/platform/core/src/utils/sopClassDictionary.js @@ -0,0 +1,119 @@ +// TODO: Deprecate since we have the same thing in dcmjs? +export const sopClassDictionary = { + ComputedRadiographyImageStorage: '1.2.840.10008.5.1.4.1.1.1', + DigitalXRayImageStorageForPresentation: '1.2.840.10008.5.1.4.1.1.1.1', + DigitalXRayImageStorageForProcessing: '1.2.840.10008.5.1.4.1.1.1.1.1', + DigitalMammographyXRayImageStorageForPresentation: '1.2.840.10008.5.1.4.1.1.1.2', + DigitalMammographyXRayImageStorageForProcessing: '1.2.840.10008.5.1.4.1.1.1.2.1', + DigitalIntraOralXRayImageStorageForPresentation: '1.2.840.10008.5.1.4.1.1.1.3', + DigitalIntraOralXRayImageStorageForProcessing: '1.2.840.10008.5.1.4.1.1.1.3.1', + CTImageStorage: '1.2.840.10008.5.1.4.1.1.2', + EnhancedCTImageStorage: '1.2.840.10008.5.1.4.1.1.2.1', + LegacyConvertedEnhancedCTImageStorage: '1.2.840.10008.5.1.4.1.1.2.2', + UltrasoundMultiframeImageStorage: '1.2.840.10008.5.1.4.1.1.3.1', + MRImageStorage: '1.2.840.10008.5.1.4.1.1.4', + EnhancedMRImageStorage: '1.2.840.10008.5.1.4.1.1.4.1', + MRSpectroscopyStorage: '1.2.840.10008.5.1.4.1.1.4.2', + EnhancedMRColorImageStorage: '1.2.840.10008.5.1.4.1.1.4.3', + LegacyConvertedEnhancedMRImageStorage: '1.2.840.10008.5.1.4.1.1.4.4', + UltrasoundImageStorage: '1.2.840.10008.5.1.4.1.1.6.1', + EnhancedUSVolumeStorage: '1.2.840.10008.5.1.4.1.1.6.2', + SecondaryCaptureImageStorage: '1.2.840.10008.5.1.4.1.1.7', + MultiframeSingleBitSecondaryCaptureImageStorage: '1.2.840.10008.5.1.4.1.1.7.1', + MultiframeGrayscaleByteSecondaryCaptureImageStorage: '1.2.840.10008.5.1.4.1.1.7.2', + MultiframeGrayscaleWordSecondaryCaptureImageStorage: '1.2.840.10008.5.1.4.1.1.7.3', + MultiframeTrueColorSecondaryCaptureImageStorage: '1.2.840.10008.5.1.4.1.1.7.4', + Sop12LeadECGWaveformStorage: '1.2.840.10008.5.1.4.1.1.9.1.1', + GeneralECGWaveformStorage: '1.2.840.10008.5.1.4.1.1.9.1.2', + AmbulatoryECGWaveformStorage: '1.2.840.10008.5.1.4.1.1.9.1.3', + HemodynamicWaveformStorage: '1.2.840.10008.5.1.4.1.1.9.2.1', + CardiacElectrophysiologyWaveformStorage: '1.2.840.10008.5.1.4.1.1.9.3.1', + BasicVoiceAudioWaveformStorage: '1.2.840.10008.5.1.4.1.1.9.4.1', + GeneralAudioWaveformStorage: '1.2.840.10008.5.1.4.1.1.9.4.2', + ArterialPulseWaveformStorage: '1.2.840.10008.5.1.4.1.1.9.5.1', + RespiratoryWaveformStorage: '1.2.840.10008.5.1.4.1.1.9.6.1', + GrayscaleSoftcopyPresentationStateStorage: '1.2.840.10008.5.1.4.1.1.11.1', + ColorSoftcopyPresentationStateStorage: '1.2.840.10008.5.1.4.1.1.11.2', + PseudoColorSoftcopyPresentationStateStorage: '1.2.840.10008.5.1.4.1.1.11.3', + BlendingSoftcopyPresentationStateStorage: '1.2.840.10008.5.1.4.1.1.11.4', + XAXRFGrayscaleSoftcopyPresentationStateStorage: '1.2.840.10008.5.1.4.1.1.11.5', + XRayAngiographicImageStorage: '1.2.840.10008.5.1.4.1.1.12.1', + EnhancedXAImageStorage: '1.2.840.10008.5.1.4.1.1.12.1.1', + XRayRadiofluoroscopicImageStorage: '1.2.840.10008.5.1.4.1.1.12.2', + EnhancedXRFImageStorage: '1.2.840.10008.5.1.4.1.1.12.2.1', + XRay3DAngiographicImageStorage: '1.2.840.10008.5.1.4.1.1.13.1.1', + XRay3DCraniofacialImageStorage: '1.2.840.10008.5.1.4.1.1.13.1.2', + BreastTomosynthesisImageStorage: '1.2.840.10008.5.1.4.1.1.13.1.3', + BreastProjectionXRayImageStorageForPresentation: '1.2.840.10008.5.1.4.1.1.13.1.4', + BreastProjectionXRayImageStorageForProcessing: '1.2.840.10008.5.1.4.1.1.13.1.5', + IntravascularOpticalCoherenceTomographyImageStorageForPresentation: + '1.2.840.10008.5.1.4.1.1.14.1', + IntravascularOpticalCoherenceTomographyImageStorageForProcessing: '1.2.840.10008.5.1.4.1.1.14.2', + NuclearMedicineImageStorage: '1.2.840.10008.5.1.4.1.1.20', + RawDataStorage: '1.2.840.10008.5.1.4.1.1.66', + SpatialRegistrationStorage: '1.2.840.10008.5.1.4.1.1.66.1', + SpatialFiducialsStorage: '1.2.840.10008.5.1.4.1.1.66.2', + DeformableSpatialRegistrationStorage: '1.2.840.10008.5.1.4.1.1.66.3', + SegmentationStorage: '1.2.840.10008.5.1.4.1.1.66.4', + SurfaceSegmentationStorage: '1.2.840.10008.5.1.4.1.1.66.5', + RealWorldValueMappingStorage: '1.2.840.10008.5.1.4.1.1.67', + SurfaceScanMeshStorage: '1.2.840.10008.5.1.4.1.1.68.1', + SurfaceScanPointCloudStorage: '1.2.840.10008.5.1.4.1.1.68.2', + VLEndoscopicImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.1', + VideoEndoscopicImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.1.1', + VLMicroscopicImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.2', + VideoMicroscopicImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.2.1', + VLSlideCoordinatesMicroscopicImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.3', + VLPhotographicImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.4', + VideoPhotographicImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.4.1', + OphthalmicPhotography8BitImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.5.1', + OphthalmicPhotography16BitImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.5.2', + StereometricRelationshipStorage: '1.2.840.10008.5.1.4.1.1.77.1.5.3', + OphthalmicTomographyImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.5.4', + VLWholeSlideMicroscopyImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.6', + LensometryMeasurementsStorage: '1.2.840.10008.5.1.4.1.1.78.1', + AutorefractionMeasurementsStorage: '1.2.840.10008.5.1.4.1.1.78.2', + KeratometryMeasurementsStorage: '1.2.840.10008.5.1.4.1.1.78.3', + SubjectiveRefractionMeasurementsStorage: '1.2.840.10008.5.1.4.1.1.78.4', + VisualAcuityMeasurementsStorage: '1.2.840.10008.5.1.4.1.1.78.5', + SpectaclePrescriptionReportStorage: '1.2.840.10008.5.1.4.1.1.78.6', + OphthalmicAxialMeasurementsStorage: '1.2.840.10008.5.1.4.1.1.78.7', + IntraocularLensCalculationsStorage: '1.2.840.10008.5.1.4.1.1.78.8', + MacularGridThicknessandVolumeReport: '1.2.840.10008.5.1.4.1.1.79.1', + OphthalmicVisualFieldStaticPerimetryMeasurementsStorage: '1.2.840.10008.5.1.4.1.1.80.1', + OphthalmicThicknessMapStorage: '1.2.840.10008.5.1.4.1.1.81.1', + CornealTopographyMapStorage: '1.2.840.10008.5.1.4.1.1.82.1', + BasicTextSR: '1.2.840.10008.5.1.4.1.1.88.11', + EnhancedSR: '1.2.840.10008.5.1.4.1.1.88.22', + ComprehensiveSR: '1.2.840.10008.5.1.4.1.1.88.33', + Comprehensive3DSR: '1.2.840.10008.5.1.4.1.1.88.34', + ProcedureLog: '1.2.840.10008.5.1.4.1.1.88.40', + MammographyCADSR: '1.2.840.10008.5.1.4.1.1.88.50', + KeyObjectSelection: '1.2.840.10008.5.1.4.1.1.88.59', + ChestCADSR: '1.2.840.10008.5.1.4.1.1.88.65', + XRayRadiationDoseSR: '1.2.840.10008.5.1.4.1.1.88.67', + RadiopharmaceuticalRadiationDoseSR: '1.2.840.10008.5.1.4.1.1.88.68', + ColonCADSR: '1.2.840.10008.5.1.4.1.1.88.69', + ImplantationPlanSRDocumentStorage: '1.2.840.10008.5.1.4.1.1.88.70', + EncapsulatedPDFStorage: '1.2.840.10008.5.1.4.1.1.104.1', + EncapsulatedCDAStorage: '1.2.840.10008.5.1.4.1.1.104.2', + PositronEmissionTomographyImageStorage: '1.2.840.10008.5.1.4.1.1.128', + EnhancedPETImageStorage: '1.2.840.10008.5.1.4.1.1.130', + LegacyConvertedEnhancedPETImageStorage: '1.2.840.10008.5.1.4.1.1.128.1', + BasicStructuredDisplayStorage: '1.2.840.10008.5.1.4.1.1.131', + RTImageStorage: '1.2.840.10008.5.1.4.1.1.481.1', + RTDoseStorage: '1.2.840.10008.5.1.4.1.1.481.2', + RTStructureSetStorage: '1.2.840.10008.5.1.4.1.1.481.3', + RTBeamsTreatmentRecordStorage: '1.2.840.10008.5.1.4.1.1.481.4', + RTPlanStorage: '1.2.840.10008.5.1.4.1.1.481.5', + RTBrachyTreatmentRecordStorage: '1.2.840.10008.5.1.4.1.1.481.6', + RTTreatmentSummaryRecordStorage: '1.2.840.10008.5.1.4.1.1.481.7', + RTIonPlanStorage: '1.2.840.10008.5.1.4.1.1.481.8', + RTIonBeamsTreatmentRecordStorage: '1.2.840.10008.5.1.4.1.1.481.9', + RTBeamsDeliveryInstructionStorage: '1.2.840.10008.5.1.4.34.7', + GenericImplantTemplateStorage: '1.2.840.10008.5.1.4.43.1', + ImplantAssemblyTemplateStorage: '1.2.840.10008.5.1.4.44.1', + ImplantTemplateGroupStorage: '1.2.840.10008.5.1.4.45.1', +}; + +export default sopClassDictionary; diff --git a/platform/core/src/utils/sortBy.js b/platform/core/src/utils/sortBy.js new file mode 100644 index 0000000..8809b0f --- /dev/null +++ b/platform/core/src/utils/sortBy.js @@ -0,0 +1,40 @@ +// Return the array sorting function for its object's properties +export default function sortBy() { + var fields = [].slice.call(arguments), + n_fields = fields.length; + + return function (A, B) { + var a, b, field, key, reverse, result, i; + + for (i = 0; i < n_fields; i++) { + result = 0; + field = fields[i]; + + key = typeof field === 'string' ? field : field.name; + + a = A[key]; + b = B[key]; + + if (typeof field.primer !== 'undefined') { + a = field.primer(a); + b = field.primer(b); + } + + reverse = field.reverse ? -1 : 1; + + if (a < b) { + result = reverse * -1; + } + + if (a > b) { + result = reverse * 1; + } + + if (result !== 0) { + break; + } + } + + return result; + }; +} diff --git a/platform/core/src/utils/sortInstancesByPosition.test.js b/platform/core/src/utils/sortInstancesByPosition.test.js new file mode 100644 index 0000000..9183ac2 --- /dev/null +++ b/platform/core/src/utils/sortInstancesByPosition.test.js @@ -0,0 +1,61 @@ +import sortInstances from './sortInstancesByPosition'; + +describe('sortInstances', () => { + it('should sort instances based on their imagePositionPatient', () => { + const instances = [ + { + ImagePositionPatient: [0, 0, 2], + ImageOrientationPatient: [1, 0, 0, 0, 1, 0], + }, + { + ImagePositionPatient: [0, 0, 1], + ImageOrientationPatient: [1, 0, 0, 0, 1, 0], + }, + { + ImagePositionPatient: [0, 0, 0], + ImageOrientationPatient: [1, 0, 0, 0, 1, 0], + }, + ]; + + const sortedInstances = sortInstances(instances); + + expect(sortedInstances).toEqual([ + { + ImagePositionPatient: [0, 0, 0], + ImageOrientationPatient: [1, 0, 0, 0, 1, 0], + }, + { + ImagePositionPatient: [0, 0, 1], + ImageOrientationPatient: [1, 0, 0, 0, 1, 0], + }, + { + ImagePositionPatient: [0, 0, 2], + ImageOrientationPatient: [1, 0, 0, 0, 1, 0], + }, + ]); + }); + + it('should return the same instances if there is only one instance', () => { + const instances = [ + { + ImagePositionPatient: [0, 0, 0], + }, + ]; + + const sortedInstances = sortInstances(instances); + + expect(sortedInstances).toEqual([ + { + ImagePositionPatient: [0, 0, 0], + }, + ]); + }); + + it('should return the same instances if there are no instances', () => { + const instances = []; + + const sortedInstances = sortInstances(instances); + + expect(sortedInstances).toEqual([]); + }); +}); diff --git a/platform/core/src/utils/sortInstancesByPosition.ts b/platform/core/src/utils/sortInstancesByPosition.ts new file mode 100644 index 0000000..4fd2483 --- /dev/null +++ b/platform/core/src/utils/sortInstancesByPosition.ts @@ -0,0 +1,65 @@ +import { vec3 } from 'gl-matrix'; + +/** + * Given an array of imageIds, sort them based on their imagePositionPatient, and + * also returns the spacing between images and the origin of the reference image + * + * @param imageIds - array of imageIds + * @param scanAxisNormal - [x, y, z] array or gl-matrix vec3 + * + * @returns The sortedImageIds, zSpacing, and origin of the first image in the series. + */ +export default function sortInstances(instances: Array) { + // Return if only one instance e.g., multiframe + if (instances.length <= 1) { + return instances; + } + + const { ImagePositionPatient: referenceImagePositionPatient, ImageOrientationPatient } = + instances[Math.floor(instances.length / 2)]; // this prevents getting scout image as test image + + if (!referenceImagePositionPatient || !ImageOrientationPatient) { + return instances; + } + + const rowCosineVec = vec3.fromValues( + ImageOrientationPatient[0], + ImageOrientationPatient[1], + ImageOrientationPatient[2] + ); + const colCosineVec = vec3.fromValues( + ImageOrientationPatient[3], + ImageOrientationPatient[4], + ImageOrientationPatient[5] + ); + + const scanAxisNormal = vec3.cross(vec3.create(), rowCosineVec, colCosineVec); + + const refIppVec = vec3.set( + vec3.create(), + referenceImagePositionPatient[0], + referenceImagePositionPatient[1], + referenceImagePositionPatient[2] + ); + + const distanceInstancePairs = instances.map(instance => { + const imagePositionPatient = instance.ImagePositionPatient; + + const positionVector = vec3.create(); + + vec3.sub(positionVector, referenceImagePositionPatient, imagePositionPatient); + + const distance = vec3.dot(positionVector, scanAxisNormal); + + return { + distance, + instance, + }; + }); + + distanceInstancePairs.sort((a, b) => b.distance - a.distance); + + const sortedInstances = distanceInstancePairs.map(a => a.instance); + + return sortedInstances; +} diff --git a/platform/core/src/utils/sortStudy.ts b/platform/core/src/utils/sortStudy.ts new file mode 100644 index 0000000..24fe90e --- /dev/null +++ b/platform/core/src/utils/sortStudy.ts @@ -0,0 +1,119 @@ +import isLowPriorityModality from './isLowPriorityModality'; + +const compareSeriesDateTime = (a, b) => { + const seriesDateA = Date.parse(`${a.seriesDate ?? a.SeriesDate} ${a.seriesTime ?? a.SeriesTime}`); + const seriesDateB = Date.parse(`${a.seriesDate ?? a.SeriesDate} ${a.seriesTime ?? a.SeriesTime}`); + return seriesDateA - seriesDateB; +}; + +const defaultSeriesSort = (a, b) => { + const seriesNumberA = a.SeriesNumber ?? a.seriesNumber; + const seriesNumberB = b.SeriesNumber ?? b.seriesNumber; + if (seriesNumberA === seriesNumberB) { + return compareSeriesDateTime(a, b); + } + return seriesNumberA - seriesNumberB; +}; + +/** + * Series sorting criteria: series considered low priority are moved to the end + * of the list and series number is used to break ties + * @param {Object} firstSeries + * @param {Object} secondSeries + */ +function seriesInfoSortingCriteria(firstSeries, secondSeries) { + const aLowPriority = isLowPriorityModality(firstSeries.Modality ?? firstSeries.modality); + const bLowPriority = isLowPriorityModality(secondSeries.Modality ?? secondSeries.modality); + + if (aLowPriority) { + // Use the reverse sort order for low priority modalities so that the + // most recent one comes up first as usually that is the one of interest. + return bLowPriority ? defaultSeriesSort(secondSeries, firstSeries) : 1; + } else if (bLowPriority) { + return -1; + } + + return defaultSeriesSort(firstSeries, secondSeries); +} + +const seriesSortCriteria = { + default: seriesInfoSortingCriteria, + seriesInfoSortingCriteria, +}; + +const instancesSortCriteria = { + default: (a, b) => parseInt(a.InstanceNumber) - parseInt(b.InstanceNumber), +}; + +const sortingCriteria = { + seriesSortCriteria, + instancesSortCriteria, +}; + +/** + * Sorts given series (given param is modified) + * The default criteria is based on series number in ascending order. + * + * @param {Array} series List of series + * @param {function} seriesSortingCriteria method for sorting + * @returns {Array} sorted series object + */ +const sortStudySeries = ( + series, + seriesSortingCriteria = seriesSortCriteria.default, + sortFunction = null +) => { + if (typeof sortFunction === 'function') { + return sortFunction(series); + } else { + return series.sort(seriesSortingCriteria); + } +}; + +/** + * Sorts given instancesList (given param is modified) + * The default criteria is based on instance number in ascending order. + * + * @param {Array} instancesList List of series + * @param {function} instancesSortingCriteria method for sorting + * @returns {Array} sorted instancesList object + */ +const sortStudyInstances = ( + instancesList, + instancesSortingCriteria = instancesSortCriteria.default +) => { + return instancesList.sort(instancesSortingCriteria); +}; + +/** + * Sorts the series and instances (by default) inside a study instance based on sortingCriteria (given param is modified) + * The default criteria is based on series and instance numbers in ascending order. + * + * @param {Object} study The study instance + * @param {boolean} [deepSort = true] to sort instance also + * @param {function} [seriesSortingCriteria = seriesSortCriteria.default] method for sorting series + * @param {function} [instancesSortingCriteria = instancesSortCriteria.default] method for sorting instances + * @returns {Object} sorted study object + */ +export default function sortStudy( + study, + deepSort = true, + seriesSortingCriteria = seriesSortCriteria.default, + instancesSortingCriteria = instancesSortCriteria.default +) { + if (!study || !study.series) { + throw new Error('Insufficient study data was provided to sortStudy'); + } + + sortStudySeries(study.series, seriesSortingCriteria); + + if (deepSort) { + study.series.forEach(series => { + sortStudyInstances(series.instances, instancesSortingCriteria); + }); + } + + return study; +} + +export { sortStudy, sortStudySeries, sortStudyInstances, sortingCriteria, seriesSortCriteria }; diff --git a/platform/core/src/utils/splitComma.ts b/platform/core/src/utils/splitComma.ts new file mode 100644 index 0000000..922a944 --- /dev/null +++ b/platform/core/src/utils/splitComma.ts @@ -0,0 +1,33 @@ +/** Splits a list of strings by commas within the strings */ +const splitComma = (strings: string[]): string[] => { + if (!strings) { + return null; + } + for (let i = 0; i < strings.length; i++) { + const comma = strings[i].indexOf(','); + if (comma !== -1) { + const splits = strings[i].split(/,/); + strings.splice(i, 1, ...splits); + } + } + return strings; +}; + +/** + * Returns an array of the comma split parameters from the given URL search params + * @param lowerCaseKey - lower case search parameter value + * @param params - URLSearchParams + * @returns Array of comma split items matching, or null + */ +const getSplitParam = ( + lowerCaseKey: string, + params = new URLSearchParams(window.location.search) +): string[] => { + const sourceKey = [...params.keys()].find(it => it.toLowerCase() === lowerCaseKey); + if (!sourceKey) { + return; + } + return splitComma(params.getAll(sourceKey)); +}; + +export { splitComma, getSplitParam }; diff --git a/platform/core/src/utils/str2ab.js b/platform/core/src/utils/str2ab.js new file mode 100644 index 0000000..f804b55 --- /dev/null +++ b/platform/core/src/utils/str2ab.js @@ -0,0 +1,7 @@ +/** + * Convert String to ArrayBuffer + * + * @param {String} str Input String + * @return {ArrayBuffer} Output converted ArrayBuffer + */ +export default str => Uint8Array.from(atob(str), c => c.charCodeAt(0)); diff --git a/platform/core/src/utils/toNumber.js b/platform/core/src/utils/toNumber.js new file mode 100644 index 0000000..b0d3bd0 --- /dev/null +++ b/platform/core/src/utils/toNumber.js @@ -0,0 +1,13 @@ +/** + * Returns the values as an array of javascript numbers + * + * @param val - The javascript object for the specified element in the metadata + * @returns {*} + */ +export default function toNumber(val) { + if (Array.isArray(val)) { + return [...val].map(v => (v !== undefined ? Number(v) : v)); + } else { + return val !== undefined ? Number(val) : val; + } +} diff --git a/platform/core/src/utils/unravelIndex.ts b/platform/core/src/utils/unravelIndex.ts new file mode 100644 index 0000000..8bd57f1 --- /dev/null +++ b/platform/core/src/utils/unravelIndex.ts @@ -0,0 +1,11 @@ +/** + * Given the flatten index, and rows and column, it returns the + * row and column index + */ +const unravelIndex = (index, numRows, numCols) => { + const row = Math.floor(index / numCols); + const col = index % numCols; + return { row, col }; +}; + +export default unravelIndex; diff --git a/platform/core/src/utils/urlUtil.js b/platform/core/src/utils/urlUtil.js new file mode 100644 index 0000000..633b656 --- /dev/null +++ b/platform/core/src/utils/urlUtil.js @@ -0,0 +1,75 @@ +import lib from 'query-string'; + +const PARAM_SEPARATOR = ';'; +const PARAM_PATTERN_IDENTIFIER = ':'; + +function toLowerCaseFirstLetter(word) { + return word[0].toLowerCase() + word.slice(1); +} +const getQueryFilters = (location = {}) => { + const { search } = location; + + if (!search) { + return; + } + + const searchParameters = parse(search); + const filters = {}; + + Object.entries(searchParameters).forEach(([key, value]) => { + filters[toLowerCaseFirstLetter(key)] = value; + }); + + return filters; +}; + +const decode = (strToDecode = '') => { + try { + const decoded = window.atob(strToDecode); + return decoded; + } catch (e) { + return strToDecode; + } +}; + +const parse = toParse => { + if (toParse) { + return lib.parse(toParse); + } + + return {}; +}; +const parseParam = paramStr => { + const _paramDecoded = decode(paramStr); + if (_paramDecoded && typeof _paramDecoded === 'string') { + return _paramDecoded.split(PARAM_SEPARATOR); + } +}; + +const replaceParam = (path = '', paramKey, paramValue) => { + const paramPattern = `${PARAM_PATTERN_IDENTIFIER}${paramKey}`; + if (paramValue) { + return path.replace(paramPattern, paramValue); + } + + return path; +}; + +const isValidPath = path => { + const paramPatternPiece = `/${PARAM_PATTERN_IDENTIFIER}`; + return path.indexOf(paramPatternPiece) < 0; +}; + +const queryString = { + getQueryFilters, +}; + +const paramString = { + isValidPath, + parseParam, + replaceParam, +}; + +const urlUtil = { parse, queryString, paramString }; + +export default urlUtil; diff --git a/platform/core/src/utils/uuidv4.ts b/platform/core/src/utils/uuidv4.ts new file mode 100644 index 0000000..3c239f4 --- /dev/null +++ b/platform/core/src/utils/uuidv4.ts @@ -0,0 +1,13 @@ +// prettier-ignore +// @ts-nocheck +/** + * Generates a unique id that has limited chance of collision + * + * @see {@link https://stackoverflow.com/a/2117523/1867984|StackOverflow: Source} + * @returns a v4 compliant GUID + */ +export default function uuidv4(): string { + return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); +} diff --git a/platform/core/src/utils/writeScript.js b/platform/core/src/utils/writeScript.js new file mode 100644 index 0000000..6bd5b0d --- /dev/null +++ b/platform/core/src/utils/writeScript.js @@ -0,0 +1,14 @@ +/* jshint -W060 */ +import absoluteUrl from './absoluteUrl'; + +export default function writeScript(fileName, callback) { + const script = document.createElement('script'); + script.src = absoluteUrl(fileName); + script.onload = () => { + if (typeof callback === 'function') { + callback(script); + } + }; + + document.body.appendChild(script); +} diff --git a/platform/docs/.gitignore b/platform/docs/.gitignore new file mode 100644 index 0000000..b2d6de3 --- /dev/null +++ b/platform/docs/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +/node_modules + +# Production +/build + +# Generated files +.docusaurus +.cache-loader + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/platform/docs/CHANGELOG.md b/platform/docs/CHANGELOG.md new file mode 100644 index 0000000..7c82a05 --- /dev/null +++ b/platform/docs/CHANGELOG.md @@ -0,0 +1,3231 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + + +### Features + +* **hotkeys:** Migrate hotkeys to customization service and fix issues with overrides ([#4777](https://github.com/OHIF/Viewers/issues/4777)) ([3e6913b](https://github.com/OHIF/Viewers/commit/3e6913b097569280a5cc2fa5bbe4add52f149305)) + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + + +### Bug Fixes + +* **core:** Address 3D reconstruction and Android compatibility issues and clean up 4D data mode ([#4762](https://github.com/OHIF/Viewers/issues/4762)) ([149d6d0](https://github.com/OHIF/Viewers/commit/149d6d049cd333b9e5846576b403ff387558a66f)) + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + + +### Features + +* **ui:** Add support for Custom Modal component in Modal Service ([#4752](https://github.com/OHIF/Viewers/issues/4752)) ([2c183aa](https://github.com/OHIF/Viewers/commit/2c183aa4a777d7b5a0417ebcc8576a0fc2631ad2)) + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + + +### Features + +* **ui:** customization option for viewport notification ([#4638](https://github.com/OHIF/Viewers/issues/4638)) ([8acbd76](https://github.com/OHIF/Viewers/commit/8acbd760d801dcaf624c5d9fb636a029201b91e1)) + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + + +### Features + +* Add customization support for more UI components ([#4634](https://github.com/OHIF/Viewers/issues/4634)) ([f15eb44](https://github.com/OHIF/Viewers/commit/f15eb44b4cf49de1b73a22512571cec02effaef3)) + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + + +### Features + +* **customization:** enable custom onDropHandler for viewportGrid ([#4641](https://github.com/OHIF/Viewers/issues/4641)) ([054b262](https://github.com/OHIF/Viewers/commit/054b262e9cbeb0f44de65d05641efe1e8944a4f5)) + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + + +### Bug Fixes + +* **dependencies:** Update dcmjs library and improve documentation links ([#4741](https://github.com/OHIF/Viewers/issues/4741)) ([d554f02](https://github.com/OHIF/Viewers/commit/d554f02f7cdb876e4132fb94e3b3df8d11b7bb5c)) + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + + +### Features + +* **static-wado:** add support for case-insensitive searching ([#4603](https://github.com/OHIF/Viewers/issues/4603)) ([ac6e674](https://github.com/OHIF/Viewers/commit/ac6e674b4d094f942556d045178011bbf3f81796)) + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + + +### Bug Fixes + +* **docs:** image in customization ([#4735](https://github.com/OHIF/Viewers/issues/4735)) ([28fb921](https://github.com/OHIF/Viewers/commit/28fb92108988c3304344690792947c847bad72a6)) + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + + +### Features + +* **customization:** new customization service api ([#4688](https://github.com/OHIF/Viewers/issues/4688)) ([55ad8ef](https://github.com/OHIF/Viewers/commit/55ad8efbabc3fabd8031fc08927b2f92ae5aec69)) + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + + +### Bug Fixes + +* **seg:** sphere scissor on stack and cpu rendering reset properties was broken ([#4721](https://github.com/OHIF/Viewers/issues/4721)) ([f00d182](https://github.com/OHIF/Viewers/commit/f00d18292f02e8910215d913edfc994850a68d88)) + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + + +### Bug Fixes + +* **ui:** Update dependencies and add missing icons ([#4699](https://github.com/OHIF/Viewers/issues/4699)) ([cf97fa9](https://github.com/OHIF/Viewers/commit/cf97fa9b7b9687a9b73c1cf6926bc9fbc39b6512)) + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + + +### Features + +* **multimonitor:** Add simple multi-monitor support to open another study([#4178](https://github.com/OHIF/Viewers/issues/4178)) ([07c628e](https://github.com/OHIF/Viewers/commit/07c628e689b28f831317a7c28d712509b69c6b13)) + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + + +### Features + +* **dev:** move to rsbuild for dev - faster ([#4674](https://github.com/OHIF/Viewers/issues/4674)) ([d4a4267](https://github.com/OHIF/Viewers/commit/d4a4267429c02916dd51f6aefb290d96dd1c3b04)) + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + + +### Bug Fixes + +* Docker build time was very slow on a tiny change ([#4559](https://github.com/OHIF/Viewers/issues/4559)) ([7e43b2f](https://github.com/OHIF/Viewers/commit/7e43b2f768cfc3e08ecde9dfdae275194daece2b)) + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + + +### Bug Fixes + +* Documentation and default enabled for bulkdata load ([#4607](https://github.com/OHIF/Viewers/issues/4607)) ([d0ccdbd](https://github.com/OHIF/Viewers/commit/d0ccdbd68db1dcb190b5a288dd455f573eddc280)) + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + + +### Features + +* migrate icons to ui-next ([#4606](https://github.com/OHIF/Viewers/issues/4606)) ([4e2ae32](https://github.com/OHIF/Viewers/commit/4e2ae328744ed95589c2cdf7a531454a25bf88b5)) + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + + +### Features + +* **node:** move to node 20 ([#4594](https://github.com/OHIF/Viewers/issues/4594)) ([1f04d6c](https://github.com/OHIF/Viewers/commit/1f04d6c1be729a26fe7bcda923770a1cd461053c)) + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + + +### Bug Fixes + +* **multiframe:** metadata handling of NM studies and loading order ([#4554](https://github.com/OHIF/Viewers/issues/4554)) ([7624ccb](https://github.com/OHIF/Viewers/commit/7624ccb5e495c0a151227a458d8d5bfb8babb22c)) + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + + +### Bug Fixes + +* Make the commands ordering the registration order of hte mode ([#4492](https://github.com/OHIF/Viewers/issues/4492)) ([edfaf72](https://github.com/OHIF/Viewers/commit/edfaf7248d217707e90d24642361a40c6f1a03ff)) + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + + +### Bug Fixes + +* Have an addIcon that adds to both ui and ui-next ([#4490](https://github.com/OHIF/Viewers/issues/4490)) ([4a12523](https://github.com/OHIF/Viewers/commit/4a125236ddbf8a4a95fb9c5820f511d0224e663f)) +* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed)) + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + + +### Bug Fixes + +* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30)) + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + + +### Bug Fixes + +* **tmtv:** fix toggle one up weird behaviours ([#4473](https://github.com/OHIF/Viewers/issues/4473)) ([aa2b649](https://github.com/OHIF/Viewers/commit/aa2b649444eb4fe5422e72ea7830a709c4d24a90)) + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + + +### Features + +* **tours:** freeze versions and add licensings doc ([#4407](https://github.com/OHIF/Viewers/issues/4407)) ([60a8d51](https://github.com/OHIF/Viewers/commit/60a8d5154a5d6d2b121bd93aeacf12d97ef9f8cb)) + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + + +### Features + +* Add CS3D WSI and Video Viewports and add annotation navigation for MPR ([#4182](https://github.com/OHIF/Viewers/issues/4182)) ([7599ec9](https://github.com/OHIF/Viewers/commit/7599ec9421129dcade94e6fa6ec7908424ab3134)) + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + + +### Features + +* **auth:** Add Authorization Code Flow and new Keycloak recipes with new video tutorials ([#4234](https://github.com/OHIF/Viewers/issues/4234)) ([aefa6d9](https://github.com/OHIF/Viewers/commit/aefa6d94dff82d34fa8358933fb1d5dec3f8246d)) + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + + +### Bug Fixes + +* webpack import bugs showing warnings on import ([#4265](https://github.com/OHIF/Viewers/issues/4265)) ([24c511f](https://github.com/OHIF/Viewers/commit/24c511f4bc04c4143bbd3d0d48029f41f7f36014)) + + +### Features + +* Add interleaved HTJ2K and volume progressive loading ([#4276](https://github.com/OHIF/Viewers/issues/4276)) ([a2084f3](https://github.com/OHIF/Viewers/commit/a2084f319b731d98b59485799fb80357094f8c38)) + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + + +### Features + +* **studyPrefetcher:** Study Prefetcher ([#4206](https://github.com/OHIF/Viewers/issues/4206)) ([2048b19](https://github.com/OHIF/Viewers/commit/2048b19484c0b1fae73f993cfaa814f861bbd230)) + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + + +### Bug Fixes + +* **cli:** Fix the cli utilities which require full paths ([d09f8b5](https://github.com/OHIF/Viewers/commit/d09f8b5ba2dcc0c02beb405b8cfa79fbae5bdde8)), closes [#4267](https://github.com/OHIF/Viewers/issues/4267) + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) +* **CustomViewportOverlay:** pass accurate data to Custom Viewport Functions ([#4224](https://github.com/OHIF/Viewers/issues/4224)) ([aef00e9](https://github.com/OHIF/Viewers/commit/aef00e91d63e9bc2de289cc6f35975e36547fb20)) + + +### Features + +* customization service append and customize functionality should run once ([#4238](https://github.com/OHIF/Viewers/issues/4238)) ([e462fd3](https://github.com/OHIF/Viewers/commit/e462fd31f7944acfee34f08cfbc28cfd9de16169)) +* **sort:** custom series sort in study panel ([#4214](https://github.com/OHIF/Viewers/issues/4214)) ([a433d40](https://github.com/OHIF/Viewers/commit/a433d406e2cac13f644203996c682260b54e8865)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + + +### Bug Fixes + +* **cli:** version txt had a new line which it should not ([#4233](https://github.com/OHIF/Viewers/issues/4233)) ([097ef76](https://github.com/OHIF/Viewers/commit/097ef7665559a672d73e1babfc42afccc3cdd41d)) + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + + +### Features + +* **hp:** Add displayArea option for Hanging protocols and example with Mamo([#3808](https://github.com/OHIF/Viewers/issues/3808)) ([18ac08e](https://github.com/OHIF/Viewers/commit/18ac08ed860d119721c52e4ffc270332259100b6)) + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + + +### Features + +* **test:** Playwright testing integration ([#4146](https://github.com/OHIF/Viewers/issues/4146)) ([fe1a706](https://github.com/OHIF/Viewers/commit/fe1a706446cc33670bf5fab8451e8281b487fcd6)) + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + + +### Bug Fixes + +* **bugs:** enhancements and bugs in several areas ([#4086](https://github.com/OHIF/Viewers/issues/4086)) ([730f434](https://github.com/OHIF/Viewers/commit/730f4349100f21b4489a21707dbb2dca9dbfbba2)) + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + + +### Bug Fixes + +* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f)) + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + + +### Features + +* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb)) + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + + +### Bug Fixes + +* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af)) + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + + +### Bug Fixes + +* **bugs:** and replace seriesInstanceUID and seriesInstanceUIDs URL with seriesInstanceUIDs ([#4049](https://github.com/OHIF/Viewers/issues/4049)) ([da7c1a5](https://github.com/OHIF/Viewers/commit/da7c1a5d8c54bfa1d3f97bbc500386bf76e7fd9d)) + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + + +### Bug Fixes + +* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025)) + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637)) + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + + +### Features + +* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2)) + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + + +### Features + +* **measurement:** Add support measurement label autocompletion ([#3855](https://github.com/OHIF/Viewers/issues/3855)) ([56b1eae](https://github.com/OHIF/Viewers/commit/56b1eae6356a6534960df1196bdd1e95b0a9a470)) + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + + +### Features + +* **segmentation:** Enhanced segmentation panel design for TMTV ([#3988](https://github.com/OHIF/Viewers/issues/3988)) ([9f3235f](https://github.com/OHIF/Viewers/commit/9f3235ff096636aafa88d8a42859e8dc85d9036d)) + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + + +### Features + +* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1)) + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + + +### Features + +* **layout:** new layout selector with 3D volume rendering ([#3923](https://github.com/OHIF/Viewers/issues/3923)) ([617043f](https://github.com/OHIF/Viewers/commit/617043fe0da5de91fbea4ac33a27f1df16ae1ca6)) + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + + +### Features + +* **worklist:** new investigational use text ([#3999](https://github.com/OHIF/Viewers/issues/3999)) ([45b68e8](https://github.com/OHIF/Viewers/commit/45b68e841dcb9e28a2ea991c37ee7ac4a8c5b71e)) + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + + +### Bug Fixes + +* **docs:** Minor typos in hpModule.md ([#3962](https://github.com/OHIF/Viewers/issues/3962)) ([4cdfdae](https://github.com/OHIF/Viewers/commit/4cdfdae8149166cf9dc91a55c0d7f2a224e55d8f)) + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + + +### Features + +* **transferSyntax:** prefer server transcoded transfer syntax for all images ([#3883](https://github.com/OHIF/Viewers/issues/3883)) ([1456a49](https://github.com/OHIF/Viewers/commit/1456a493d66c90c787b022256c9f2846afb115fc)) + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + + +### Bug Fixes + +* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6)) + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + + +### Features + +* Add on mode init hook ([#3882](https://github.com/OHIF/Viewers/issues/3882)) ([f58725c](https://github.com/OHIF/Viewers/commit/f58725ce40685f7297181ef98d81bc28420c8291)) + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + + +### Features + +* **config:** Add activateViewportBeforeInteraction parameter for viewport interaction customization ([#3847](https://github.com/OHIF/Viewers/issues/3847)) ([f707b4e](https://github.com/OHIF/Viewers/commit/f707b4ebc996f379cd30337badc06b07e6e35ac5)) + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + + +### Bug Fixes + +* **auth:** fix the issue with oauth at a non root path ([#3840](https://github.com/OHIF/Viewers/issues/3840)) ([6651008](https://github.com/OHIF/Viewers/commit/6651008fbb35dabd5991c7f61128e6ef324012df)) + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + + +### Features + +* Merge Data Source ([#3788](https://github.com/OHIF/Viewers/issues/3788)) ([c4ff2c2](https://github.com/OHIF/Viewers/commit/c4ff2c2f09546ce8b72eab9c5e7beed611e3cab0)) + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + + +### Features + +* **docs:** Added various training videos to support the OHIF CLI tools ([#3794](https://github.com/OHIF/Viewers/issues/3794)) ([d83beb7](https://github.com/OHIF/Viewers/commit/d83beb7c62c1d5be19c54e08d23883f112147fe1)) + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + + +### Features + +* **url:** Add SeriesInstanceUIDs wado query param ([#3746](https://github.com/OHIF/Viewers/issues/3746)) ([b694228](https://github.com/OHIF/Viewers/commit/b694228dd535e4b97cb86a1dc085b6e8716bdaf3)) + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + + +### Features + +* **dicomJSON:** Add Loading Other Display Sets and JSON Metadata Generation script ([#3777](https://github.com/OHIF/Viewers/issues/3777)) ([43b1c17](https://github.com/OHIF/Viewers/commit/43b1c17209502e4876ad59bae09ed9442eda8024)) + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + + +### Bug Fixes + +* **path:** upgrade docusaurus for security ([#3780](https://github.com/OHIF/Viewers/issues/3780)) ([8bbcd0e](https://github.com/OHIF/Viewers/commit/8bbcd0e692e25917c1b6dd94a39fac834c812fca)) + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + + +### Features + +* **hp callback:** Add viewport ready callback ([#3772](https://github.com/OHIF/Viewers/issues/3772)) ([bf252bc](https://github.com/OHIF/Viewers/commit/bf252bcec2aae3a00479fdcb732110b344bcf2c0)) + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + + +### Bug Fixes + +* **toolbar:** allow customizable toolbar for active viewport and allow active tool to be deactivated via a click ([#3608](https://github.com/OHIF/Viewers/issues/3608)) ([dd6d976](https://github.com/OHIF/Viewers/commit/dd6d9768bbca1d3cc472e8c1e6d85822500b96ef)) + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + + +### Bug Fixes + +* **sr:** dcm4chee requires the patient name for an SR to match what is in the original study ([#3739](https://github.com/OHIF/Viewers/issues/3739)) ([d98439f](https://github.com/OHIF/Viewers/commit/d98439fe7f3825076dbc87b664a1d1480ff414d3)) + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + + +### Bug Fixes + +* **cine:** Use the frame rate specified in DICOM and optionally auto play cine ([#3735](https://github.com/OHIF/Viewers/issues/3735)) ([d9258ec](https://github.com/OHIF/Viewers/commit/d9258eca70587cf4dc18be4e56c79b16bae73d6d)) + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + + +### Bug Fixes + +* **export:** wrong export for the tmtv RT function ([#3715](https://github.com/OHIF/Viewers/issues/3715)) ([a3f2a1a](https://github.com/OHIF/Viewers/commit/a3f2a1a7b0d16bfcc0ecddc2ab731e54c5e377c8)) + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + + +### Bug Fixes + +* **segmentation:** Various fixes for segmentation mode and other ([#3709](https://github.com/OHIF/Viewers/issues/3709)) ([a9a6ad5](https://github.com/OHIF/Viewers/commit/a9a6ad50eae67b43b8b34efc07182d788cacdcfe)) + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + + +### Bug Fixes + +* **bugs:** fixing lots of bugs regarding release candidate ([#3700](https://github.com/OHIF/Viewers/issues/3700)) ([8bc12a3](https://github.com/OHIF/Viewers/commit/8bc12a37d0353160ae5ea4624dc0b244b7d59c07)) + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + + +### Features + +* **displayArea:** add display area to hanging protocol ([#3691](https://github.com/OHIF/Viewers/issues/3691)) ([5e7fe91](https://github.com/OHIF/Viewers/commit/5e7fe91617d7399f85702d82e7bfa028b8010a89)) + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + + +### Bug Fixes + +* **config:** support more values for the useSharedArrayBuffer ([#3688](https://github.com/OHIF/Viewers/issues/3688)) ([1129c15](https://github.com/OHIF/Viewers/commit/1129c155d2c7d46c98a5df7c09879aa3d459fa7e)) + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + + +### Bug Fixes + +* **no sab:** should work when shared array buffer is not required ([#3686](https://github.com/OHIF/Viewers/issues/3686)) ([a67d72d](https://github.com/OHIF/Viewers/commit/a67d72de85238b369a18c010bf6d147daefc6df5)) + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + + +### Bug Fixes + +* **cli:** various fixes for adding custom modes and extensions ([#3683](https://github.com/OHIF/Viewers/issues/3683)) ([dc73b18](https://github.com/OHIF/Viewers/commit/dc73b187484da029a2664bb1302f30137c973b8c)) + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + + +### Performance Improvements + +* **memory:** add 16 bit texture via configuration - reduces memory by half ([#3662](https://github.com/OHIF/Viewers/issues/3662)) ([2bd3b26](https://github.com/OHIF/Viewers/commit/2bd3b26a6aa54b211ef988f3ad64ef1fe5648bab)) + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + + +### Bug Fixes + +* **keyCloak:** fix openresty keycloak deployment recipe ([#3655](https://github.com/OHIF/Viewers/issues/3655)) ([2d7721c](https://github.com/OHIF/Viewers/commit/2d7721cb581f55dc49e3baeca2411b18dd78ad74)) + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + + +### Bug Fixes + +* **nginx archive recipe:** Fixes to various configuration files. ([#3624](https://github.com/OHIF/Viewers/issues/3624)) ([3ce7225](https://github.com/OHIF/Viewers/commit/3ce72254b390f32c9aa207a0589e688805e2659d)) + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + + +### Features + +* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd)) + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package ohif-docs + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + + +### Features + +* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1)) + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + +**Note:** Version bump only for package ohif-docs diff --git a/platform/docs/README.md b/platform/docs/README.md new file mode 100644 index 0000000..231a499 --- /dev/null +++ b/platform/docs/README.md @@ -0,0 +1,33 @@ +# Website + +This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. + +## Installation + +```console +yarn install +``` + +## Local Development + +```console +yarn start +``` + +This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. + +## Build + +```console +yarn build +``` + +This command generates static content into the `build` directory and can be served using any static contents hosting service. + +## Deployment + +```console +GIT_USER= USE_SSH=true yarn deploy +``` + +If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. diff --git a/platform/docs/bun.lockb b/platform/docs/bun.lockb new file mode 100755 index 0000000..e765701 Binary files /dev/null and b/platform/docs/bun.lockb differ diff --git a/platform/docs/docs/README.md b/platform/docs/docs/README.md new file mode 100644 index 0000000..f7d7e0a --- /dev/null +++ b/platform/docs/docs/README.md @@ -0,0 +1,133 @@ +--- +id: Introduction +slug: / +sidebar_position: 1 +--- + +The [Open Health Imaging Foundation][ohif-org] (OHIF) Viewer is an open source, +web-based, medical imaging platform. It aims to provide a core framework for +building complex imaging applications. + +Key features: + +- Designed to load large radiology studies as quickly as possible. Retrieves + metadata ahead of time and streams in imaging pixel data as needed. +- Leverages [Cornerstone3D](https://github.com/cornerstonejs/cornerstone3D-beta) for decoding, + rendering, and annotating medical images. +- Works out-of-the-box with Image Archives that support [DICOMWeb][dicom-web]. + Offers a Data Source API for communicating with archives over proprietary API + formats. +- Provides a plugin framework for creating task-based workflow modes which can + reuse core functionality. +- Beautiful user interface (UI) designed with extensibility in mind. UI + components available in a reusable component library built with React.js and + Tailwind CSS + + + + +
+
+ + + +| | | | +| :-: | :--- | :--- | +| Measurement tracking | Measurement Tracking | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5) | +| Segmentations | Labelmap Segmentations | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046) | +| Hanging Protocols | Fusion and Custom Hanging protocols | [Demo](https://viewer.ohif.org/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463) | +| Volume Rendering | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) | +| PDF | PDF | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.317377619501274872606137091638706705333) | +| RTSTRUCT | RT STRUCT | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.5962.99.1.2968617883.1314880426.1493322302363.3.0) | +| 4D | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) | +| VIDEO | Video | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.96975534054447904995905761963464388233) | +| microscopy | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.141277760791347900862109212450152067508) | + + + +## Where to next? + +The Open Health Imaging Foundation intends to provide an imaging viewer +framework which can be easily extended for specific uses. If you find yourself +unable to extend the viewer for your purposes, please reach out via our [GitHub +issues][gh-issues]. We are actively seeking feedback on ways to improve our +integration and extension points. + +Check out these helpful links: + +- Ready to dive into some code? Check out our + [Getting Started Guide](./development/getting-started.md). +- We're an active, vibrant community. + [Learn how you can be more involved.](./development/contributing.md) +- Feeling lost? Read our [help page](/help). + +## Citing OHIF + +To cite the OHIF Viewer in an academic publication, please cite + +> _Open Health Imaging Foundation Viewer: An Extensible Open-Source Framework +> for Building Web-Based Imaging Applications to Support Cancer Research_ +> +> Erik Ziegler, Trinity Urban, Danny Brown, James Petts, Steve D. Pieper, Rob +> Lewis, Chris Hafey, and Gordon J. Harris _JCO Clinical Cancer Informatics_, no. 4 (2020), 336-345, DOI: +> [10.1200/CCI.19.00131](https://www.doi.org/10.1200/CCI.19.00131) + +This article is freely available on Pubmed Central: https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7259879/ + + +or, for Lesion Tracker of OHIF v1, please cite: + +> _LesionTracker: Extensible Open-Source Zero-Footprint Web Viewer for Cancer +> Imaging Research and Clinical Trials_ +> +> Trinity Urban, Erik Ziegler, Rob Lewis, Chris Hafey, Cheryl Sadow, Annick D. +> Van den Abbeele and Gordon J. Harris _Cancer Research_, November 1 2017 (77) (21) e119-e122 DOI: +> [10.1158/0008-5472.CAN-17-0334](https://www.doi.org/10.1158/0008-5472.CAN-17-0334) + +This article is freely available on Pubmed Central. +https://pubmed.ncbi.nlm.nih.gov/29092955/ + + +**Note:** If you use or find this repository helpful, please take the time to +star this repository on Github. This is an easy way for us to assess adoption, +and it can help us obtain future funding for the project. + +## License + +MIT ยฉ [OHIF](https://github.com/OHIF) + +  + + + + +[ohif-org]: https://www.ohif.org +[ohif-demo]: http://viewer.ohif.org/ +[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb +[gh-issues]: https://github.com/OHIF/Viewers/issues + diff --git a/platform/docs/docs/assets/designs/architecture-diagram b/platform/docs/docs/assets/designs/architecture-diagram new file mode 100644 index 0000000..bbf6cf5 Binary files /dev/null and b/platform/docs/docs/assets/designs/architecture-diagram differ diff --git a/platform/docs/docs/assets/designs/canny-full.fig b/platform/docs/docs/assets/designs/canny-full.fig new file mode 100644 index 0000000..8756e9f Binary files /dev/null and b/platform/docs/docs/assets/designs/canny-full.fig differ diff --git a/platform/docs/docs/assets/designs/cloud.svg b/platform/docs/docs/assets/designs/cloud.svg new file mode 100644 index 0000000..ad04389 --- /dev/null +++ b/platform/docs/docs/assets/designs/cloud.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/platform/docs/docs/assets/designs/embedded-viewer-diagram b/platform/docs/docs/assets/designs/embedded-viewer-diagram new file mode 100644 index 0000000..182ad23 Binary files /dev/null and b/platform/docs/docs/assets/designs/embedded-viewer-diagram differ diff --git a/platform/docs/docs/assets/designs/nginx-image-archive.fig b/platform/docs/docs/assets/designs/nginx-image-archive.fig new file mode 100644 index 0000000..460ae95 Binary files /dev/null and b/platform/docs/docs/assets/designs/nginx-image-archive.fig differ diff --git a/platform/docs/docs/assets/designs/npm-logo-red.svg b/platform/docs/docs/assets/designs/npm-logo-red.svg new file mode 100644 index 0000000..8e4aac5 --- /dev/null +++ b/platform/docs/docs/assets/designs/npm-logo-red.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/platform/docs/docs/assets/designs/scope-of-project.fig b/platform/docs/docs/assets/designs/scope-of-project.fig new file mode 100644 index 0000000..5eb82e5 Binary files /dev/null and b/platform/docs/docs/assets/designs/scope-of-project.fig differ diff --git a/platform/docs/docs/assets/designs/user-access-control-request-flow.fig b/platform/docs/docs/assets/designs/user-access-control-request-flow.fig new file mode 100644 index 0000000..8982a8f Binary files /dev/null and b/platform/docs/docs/assets/designs/user-access-control-request-flow.fig differ diff --git a/platform/docs/docs/assets/img/Loading-Indicator.png b/platform/docs/docs/assets/img/Loading-Indicator.png new file mode 100644 index 0000000..d559db4 Binary files /dev/null and b/platform/docs/docs/assets/img/Loading-Indicator.png differ diff --git a/platform/docs/docs/assets/img/OHIF-e2e-test-studies.png b/platform/docs/docs/assets/img/OHIF-e2e-test-studies.png new file mode 100644 index 0000000..4a58a18 Binary files /dev/null and b/platform/docs/docs/assets/img/OHIF-e2e-test-studies.png differ diff --git a/platform/docs/docs/assets/img/SR-exported.png b/platform/docs/docs/assets/img/SR-exported.png new file mode 100644 index 0000000..fc477ad Binary files /dev/null and b/platform/docs/docs/assets/img/SR-exported.png differ diff --git a/platform/docs/docs/assets/img/WORKFLOW_DEPLOY.png b/platform/docs/docs/assets/img/WORKFLOW_DEPLOY.png new file mode 100644 index 0000000..3e562a7 Binary files /dev/null and b/platform/docs/docs/assets/img/WORKFLOW_DEPLOY.png differ diff --git a/platform/docs/docs/assets/img/WORKFLOW_PR_CHECKS.png b/platform/docs/docs/assets/img/WORKFLOW_PR_CHECKS.png new file mode 100644 index 0000000..f9c4a56 Binary files /dev/null and b/platform/docs/docs/assets/img/WORKFLOW_PR_CHECKS.png differ diff --git a/platform/docs/docs/assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png b/platform/docs/docs/assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png new file mode 100644 index 0000000..54b0aa3 Binary files /dev/null and b/platform/docs/docs/assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png differ diff --git a/platform/docs/docs/assets/img/WORKFLOW_RELEASE.png b/platform/docs/docs/assets/img/WORKFLOW_RELEASE.png new file mode 100644 index 0000000..f3c2a80 Binary files /dev/null and b/platform/docs/docs/assets/img/WORKFLOW_RELEASE.png differ diff --git a/platform/docs/docs/assets/img/add-extension.png b/platform/docs/docs/assets/img/add-extension.png new file mode 100644 index 0000000..bb4955e Binary files /dev/null and b/platform/docs/docs/assets/img/add-extension.png differ diff --git a/platform/docs/docs/assets/img/add-mode.png b/platform/docs/docs/assets/img/add-mode.png new file mode 100644 index 0000000..6f1a162 Binary files /dev/null and b/platform/docs/docs/assets/img/add-mode.png differ diff --git a/platform/docs/docs/assets/img/azure1.png b/platform/docs/docs/assets/img/azure1.png new file mode 100644 index 0000000..754c7f6 Binary files /dev/null and b/platform/docs/docs/assets/img/azure1.png differ diff --git a/platform/docs/docs/assets/img/azure10.png b/platform/docs/docs/assets/img/azure10.png new file mode 100644 index 0000000..8286a7f Binary files /dev/null and b/platform/docs/docs/assets/img/azure10.png differ diff --git a/platform/docs/docs/assets/img/azure2.png b/platform/docs/docs/assets/img/azure2.png new file mode 100644 index 0000000..4d09923 Binary files /dev/null and b/platform/docs/docs/assets/img/azure2.png differ diff --git a/platform/docs/docs/assets/img/azure3.png b/platform/docs/docs/assets/img/azure3.png new file mode 100644 index 0000000..f56218d Binary files /dev/null and b/platform/docs/docs/assets/img/azure3.png differ diff --git a/platform/docs/docs/assets/img/azure4.png b/platform/docs/docs/assets/img/azure4.png new file mode 100644 index 0000000..81dbb42 Binary files /dev/null and b/platform/docs/docs/assets/img/azure4.png differ diff --git a/platform/docs/docs/assets/img/azure5.png b/platform/docs/docs/assets/img/azure5.png new file mode 100644 index 0000000..235f4ae Binary files /dev/null and b/platform/docs/docs/assets/img/azure5.png differ diff --git a/platform/docs/docs/assets/img/azure6.png b/platform/docs/docs/assets/img/azure6.png new file mode 100644 index 0000000..f860599 Binary files /dev/null and b/platform/docs/docs/assets/img/azure6.png differ diff --git a/platform/docs/docs/assets/img/azure7.png b/platform/docs/docs/assets/img/azure7.png new file mode 100644 index 0000000..24b4863 Binary files /dev/null and b/platform/docs/docs/assets/img/azure7.png differ diff --git a/platform/docs/docs/assets/img/azure8.png b/platform/docs/docs/assets/img/azure8.png new file mode 100644 index 0000000..1ab7679 Binary files /dev/null and b/platform/docs/docs/assets/img/azure8.png differ diff --git a/platform/docs/docs/assets/img/azure9.png b/platform/docs/docs/assets/img/azure9.png new file mode 100644 index 0000000..3825f11 Binary files /dev/null and b/platform/docs/docs/assets/img/azure9.png differ diff --git a/platform/docs/docs/assets/img/browser-console-non-secure-context.png b/platform/docs/docs/assets/img/browser-console-non-secure-context.png new file mode 100644 index 0000000..3fb4f1b Binary files /dev/null and b/platform/docs/docs/assets/img/browser-console-non-secure-context.png differ diff --git a/platform/docs/docs/assets/img/cli-search-no-verbose.png b/platform/docs/docs/assets/img/cli-search-no-verbose.png new file mode 100644 index 0000000..40b5113 Binary files /dev/null and b/platform/docs/docs/assets/img/cli-search-no-verbose.png differ diff --git a/platform/docs/docs/assets/img/cli-search-with-verbose.png b/platform/docs/docs/assets/img/cli-search-with-verbose.png new file mode 100644 index 0000000..b15713b Binary files /dev/null and b/platform/docs/docs/assets/img/cli-search-with-verbose.png differ diff --git a/platform/docs/docs/assets/img/clock-mode.png b/platform/docs/docs/assets/img/clock-mode.png new file mode 100644 index 0000000..68ea6dc Binary files /dev/null and b/platform/docs/docs/assets/img/clock-mode.png differ diff --git a/platform/docs/docs/assets/img/clock-mode1.png b/platform/docs/docs/assets/img/clock-mode1.png new file mode 100644 index 0000000..7b0375d Binary files /dev/null and b/platform/docs/docs/assets/img/clock-mode1.png differ diff --git a/platform/docs/docs/assets/img/colorbarImage.png b/platform/docs/docs/assets/img/colorbarImage.png new file mode 100644 index 0000000..f862211 Binary files /dev/null and b/platform/docs/docs/assets/img/colorbarImage.png differ diff --git a/platform/docs/docs/assets/img/context-menu.jpg b/platform/docs/docs/assets/img/context-menu.jpg new file mode 100644 index 0000000..4fef53f Binary files /dev/null and b/platform/docs/docs/assets/img/context-menu.jpg differ diff --git a/platform/docs/docs/assets/img/cornerstone-tools-link.gif b/platform/docs/docs/assets/img/cornerstone-tools-link.gif new file mode 100644 index 0000000..22fde7a Binary files /dev/null and b/platform/docs/docs/assets/img/cornerstone-tools-link.gif differ diff --git a/platform/docs/docs/assets/img/cors-browser-console-errors.png b/platform/docs/docs/assets/img/cors-browser-console-errors.png new file mode 100644 index 0000000..0b8d062 Binary files /dev/null and b/platform/docs/docs/assets/img/cors-browser-console-errors.png differ diff --git a/platform/docs/docs/assets/img/cors-network-panel-errors.png b/platform/docs/docs/assets/img/cors-network-panel-errors.png new file mode 100644 index 0000000..3820b76 Binary files /dev/null and b/platform/docs/docs/assets/img/cors-network-panel-errors.png differ diff --git a/platform/docs/docs/assets/img/create-extension.png b/platform/docs/docs/assets/img/create-extension.png new file mode 100644 index 0000000..a682649 Binary files /dev/null and b/platform/docs/docs/assets/img/create-extension.png differ diff --git a/platform/docs/docs/assets/img/create-mode.png b/platform/docs/docs/assets/img/create-mode.png new file mode 100644 index 0000000..3ca4e26 Binary files /dev/null and b/platform/docs/docs/assets/img/create-mode.png differ diff --git a/platform/docs/docs/assets/img/custom-logo.png b/platform/docs/docs/assets/img/custom-logo.png new file mode 100644 index 0000000..ea3c9ac Binary files /dev/null and b/platform/docs/docs/assets/img/custom-logo.png differ diff --git a/platform/docs/docs/assets/img/customizable-overlay.jpeg b/platform/docs/docs/assets/img/customizable-overlay.jpeg new file mode 100644 index 0000000..e166f24 Binary files /dev/null and b/platform/docs/docs/assets/img/customizable-overlay.jpeg differ diff --git a/platform/docs/docs/assets/img/data-source-configuration-ui.png b/platform/docs/docs/assets/img/data-source-configuration-ui.png new file mode 100644 index 0000000..f04956e Binary files /dev/null and b/platform/docs/docs/assets/img/data-source-configuration-ui.png differ diff --git a/platform/docs/docs/assets/img/dcm4chee-upload.gif b/platform/docs/docs/assets/img/dcm4chee-upload.gif new file mode 100644 index 0000000..e0e94f1 Binary files /dev/null and b/platform/docs/docs/assets/img/dcm4chee-upload.gif differ diff --git a/platform/docs/docs/assets/img/demo-4d.webp b/platform/docs/docs/assets/img/demo-4d.webp new file mode 100644 index 0000000..0d00c2c Binary files /dev/null and b/platform/docs/docs/assets/img/demo-4d.webp differ diff --git a/platform/docs/docs/assets/img/demo-measurements.webp b/platform/docs/docs/assets/img/demo-measurements.webp new file mode 100644 index 0000000..d6aaf88 Binary files /dev/null and b/platform/docs/docs/assets/img/demo-measurements.webp differ diff --git a/platform/docs/docs/assets/img/demo-pdf.webp b/platform/docs/docs/assets/img/demo-pdf.webp new file mode 100644 index 0000000..3119a12 Binary files /dev/null and b/platform/docs/docs/assets/img/demo-pdf.webp differ diff --git a/platform/docs/docs/assets/img/demo-ptct.webp b/platform/docs/docs/assets/img/demo-ptct.webp new file mode 100644 index 0000000..ec0a528 Binary files /dev/null and b/platform/docs/docs/assets/img/demo-ptct.webp differ diff --git a/platform/docs/docs/assets/img/demo-rtstruct.webp b/platform/docs/docs/assets/img/demo-rtstruct.webp new file mode 100644 index 0000000..7a985ff Binary files /dev/null and b/platform/docs/docs/assets/img/demo-rtstruct.webp differ diff --git a/platform/docs/docs/assets/img/demo-segmentation.webp b/platform/docs/docs/assets/img/demo-segmentation.webp new file mode 100644 index 0000000..2f798c3 Binary files /dev/null and b/platform/docs/docs/assets/img/demo-segmentation.webp differ diff --git a/platform/docs/docs/assets/img/demo-video.webp b/platform/docs/docs/assets/img/demo-video.webp new file mode 100644 index 0000000..f3b3cdb Binary files /dev/null and b/platform/docs/docs/assets/img/demo-video.webp differ diff --git a/platform/docs/docs/assets/img/demo-volume-rendering.webp b/platform/docs/docs/assets/img/demo-volume-rendering.webp new file mode 100644 index 0000000..c74915f Binary files /dev/null and b/platform/docs/docs/assets/img/demo-volume-rendering.webp differ diff --git a/platform/docs/docs/assets/img/demo-volumeRendering.png b/platform/docs/docs/assets/img/demo-volumeRendering.png new file mode 100644 index 0000000..4c508ab Binary files /dev/null and b/platform/docs/docs/assets/img/demo-volumeRendering.png differ diff --git a/platform/docs/docs/assets/img/dicom-json-public.png b/platform/docs/docs/assets/img/dicom-json-public.png new file mode 100644 index 0000000..2d77daf Binary files /dev/null and b/platform/docs/docs/assets/img/dicom-json-public.png differ diff --git a/platform/docs/docs/assets/img/dicom-json.png b/platform/docs/docs/assets/img/dicom-json.png new file mode 100644 index 0000000..8eed743 Binary files /dev/null and b/platform/docs/docs/assets/img/dicom-json.png differ diff --git a/platform/docs/docs/assets/img/docker-pacs.png b/platform/docs/docs/assets/img/docker-pacs.png new file mode 100644 index 0000000..ad33ebe Binary files /dev/null and b/platform/docs/docs/assets/img/docker-pacs.png differ diff --git a/platform/docs/docs/assets/img/e2e-cypress-final.png b/platform/docs/docs/assets/img/e2e-cypress-final.png new file mode 100644 index 0000000..49b3a41 Binary files /dev/null and b/platform/docs/docs/assets/img/e2e-cypress-final.png differ diff --git a/platform/docs/docs/assets/img/e2e-cypress.png b/platform/docs/docs/assets/img/e2e-cypress.png new file mode 100644 index 0000000..89ccc3e Binary files /dev/null and b/platform/docs/docs/assets/img/e2e-cypress.png differ diff --git a/platform/docs/docs/assets/img/embedded-viewer-diagram.png b/platform/docs/docs/assets/img/embedded-viewer-diagram.png new file mode 100644 index 0000000..426cb7a Binary files /dev/null and b/platform/docs/docs/assets/img/embedded-viewer-diagram.png differ diff --git a/platform/docs/docs/assets/img/filtering-worklist.png b/platform/docs/docs/assets/img/filtering-worklist.png new file mode 100644 index 0000000..47ab317 Binary files /dev/null and b/platform/docs/docs/assets/img/filtering-worklist.png differ diff --git a/platform/docs/docs/assets/img/github-readme-branches-Jun2024.png b/platform/docs/docs/assets/img/github-readme-branches-Jun2024.png new file mode 100644 index 0000000..4129cc4 Binary files /dev/null and b/platform/docs/docs/assets/img/github-readme-branches-Jun2024.png differ diff --git a/platform/docs/docs/assets/img/google-create-credentials.png b/platform/docs/docs/assets/img/google-create-credentials.png new file mode 100644 index 0000000..e6534fe Binary files /dev/null and b/platform/docs/docs/assets/img/google-create-credentials.png differ diff --git a/platform/docs/docs/assets/img/google-enable-apis.png b/platform/docs/docs/assets/img/google-enable-apis.png new file mode 100644 index 0000000..434bf77 Binary files /dev/null and b/platform/docs/docs/assets/img/google-enable-apis.png differ diff --git a/platform/docs/docs/assets/img/google-healthcare-service-agent-warning.png b/platform/docs/docs/assets/img/google-healthcare-service-agent-warning.png new file mode 100644 index 0000000..c98ddca Binary files /dev/null and b/platform/docs/docs/assets/img/google-healthcare-service-agent-warning.png differ diff --git a/platform/docs/docs/assets/img/google-manually-add-scopes.png b/platform/docs/docs/assets/img/google-manually-add-scopes.png new file mode 100644 index 0000000..9500b85 Binary files /dev/null and b/platform/docs/docs/assets/img/google-manually-add-scopes.png differ diff --git a/platform/docs/docs/assets/img/google-oauth-consent-steps.png b/platform/docs/docs/assets/img/google-oauth-consent-steps.png new file mode 100644 index 0000000..67d4a42 Binary files /dev/null and b/platform/docs/docs/assets/img/google-oauth-consent-steps.png differ diff --git a/platform/docs/docs/assets/img/google-projects-drop-down.png b/platform/docs/docs/assets/img/google-projects-drop-down.png new file mode 100644 index 0000000..fb20310 Binary files /dev/null and b/platform/docs/docs/assets/img/google-projects-drop-down.png differ diff --git a/platform/docs/docs/assets/img/google-provided-accounts-checkbox.png b/platform/docs/docs/assets/img/google-provided-accounts-checkbox.png new file mode 100644 index 0000000..e129854 Binary files /dev/null and b/platform/docs/docs/assets/img/google-provided-accounts-checkbox.png differ diff --git a/platform/docs/docs/assets/img/hangingProtocolExample.png b/platform/docs/docs/assets/img/hangingProtocolExample.png new file mode 100644 index 0000000..ce9e18c Binary files /dev/null and b/platform/docs/docs/assets/img/hangingProtocolExample.png differ diff --git a/platform/docs/docs/assets/img/iframe-basic.png b/platform/docs/docs/assets/img/iframe-basic.png new file mode 100644 index 0000000..5934a94 Binary files /dev/null and b/platform/docs/docs/assets/img/iframe-basic.png differ diff --git a/platform/docs/docs/assets/img/iframe-headers.png b/platform/docs/docs/assets/img/iframe-headers.png new file mode 100644 index 0000000..27d649d Binary files /dev/null and b/platform/docs/docs/assets/img/iframe-headers.png differ diff --git a/platform/docs/docs/assets/img/jwt-explained.png b/platform/docs/docs/assets/img/jwt-explained.png new file mode 100644 index 0000000..f26509a Binary files /dev/null and b/platform/docs/docs/assets/img/jwt-explained.png differ diff --git a/platform/docs/docs/assets/img/keycloak-default-theme.png b/platform/docs/docs/assets/img/keycloak-default-theme.png new file mode 100644 index 0000000..0ea77f9 Binary files /dev/null and b/platform/docs/docs/assets/img/keycloak-default-theme.png differ diff --git a/platform/docs/docs/assets/img/keycloak-ohif-theme.png b/platform/docs/docs/assets/img/keycloak-ohif-theme.png new file mode 100644 index 0000000..ad060f2 Binary files /dev/null and b/platform/docs/docs/assets/img/keycloak-ohif-theme.png differ diff --git a/platform/docs/docs/assets/img/labelling-flow.png b/platform/docs/docs/assets/img/labelling-flow.png new file mode 100644 index 0000000..d16f461 Binary files /dev/null and b/platform/docs/docs/assets/img/labelling-flow.png differ diff --git a/platform/docs/docs/assets/img/large-pt-ct.jpeg b/platform/docs/docs/assets/img/large-pt-ct.jpeg new file mode 100644 index 0000000..9999e24 Binary files /dev/null and b/platform/docs/docs/assets/img/large-pt-ct.jpeg differ diff --git a/platform/docs/docs/assets/img/layoutSelectorAdvancedPresetGeneratorImage.png b/platform/docs/docs/assets/img/layoutSelectorAdvancedPresetGeneratorImage.png new file mode 100644 index 0000000..6a912d7 Binary files /dev/null and b/platform/docs/docs/assets/img/layoutSelectorAdvancedPresetGeneratorImage.png differ diff --git a/platform/docs/docs/assets/img/layoutSelectorCommonPresetsImage.png b/platform/docs/docs/assets/img/layoutSelectorCommonPresetsImage.png new file mode 100644 index 0000000..57bf60a Binary files /dev/null and b/platform/docs/docs/assets/img/layoutSelectorCommonPresetsImage.png differ diff --git a/platform/docs/docs/assets/img/loading-indicator-icon.png b/platform/docs/docs/assets/img/loading-indicator-icon.png new file mode 100644 index 0000000..934d03a Binary files /dev/null and b/platform/docs/docs/assets/img/loading-indicator-icon.png differ diff --git a/platform/docs/docs/assets/img/loading-indicator-percent.png b/platform/docs/docs/assets/img/loading-indicator-percent.png new file mode 100644 index 0000000..b4f466b Binary files /dev/null and b/platform/docs/docs/assets/img/loading-indicator-percent.png differ diff --git a/platform/docs/docs/assets/img/locizeSponsor.svg b/platform/docs/docs/assets/img/locizeSponsor.svg new file mode 100644 index 0000000..1139aa2 --- /dev/null +++ b/platform/docs/docs/assets/img/locizeSponsor.svg @@ -0,0 +1,187 @@ + + + + Custom Preset 2 Copy + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/docs/docs/assets/img/locked-sr.png b/platform/docs/docs/assets/img/locked-sr.png new file mode 100644 index 0000000..3c6ba7d Binary files /dev/null and b/platform/docs/docs/assets/img/locked-sr.png differ diff --git a/platform/docs/docs/assets/img/measurement-labels-auto.png b/platform/docs/docs/assets/img/measurement-labels-auto.png new file mode 100644 index 0000000..6196084 Binary files /dev/null and b/platform/docs/docs/assets/img/measurement-labels-auto.png differ diff --git a/platform/docs/docs/assets/img/measurement-panel-1.png b/platform/docs/docs/assets/img/measurement-panel-1.png new file mode 100644 index 0000000..cafeae7 Binary files /dev/null and b/platform/docs/docs/assets/img/measurement-panel-1.png differ diff --git a/platform/docs/docs/assets/img/measurement-panel-prompt.png b/platform/docs/docs/assets/img/measurement-panel-prompt.png new file mode 100644 index 0000000..12d21a0 Binary files /dev/null and b/platform/docs/docs/assets/img/measurement-panel-prompt.png differ diff --git a/platform/docs/docs/assets/img/measurement-panel-tracked.png b/platform/docs/docs/assets/img/measurement-panel-tracked.png new file mode 100644 index 0000000..9075869 Binary files /dev/null and b/platform/docs/docs/assets/img/measurement-panel-tracked.png differ diff --git a/platform/docs/docs/assets/img/measurement-temporary.png b/platform/docs/docs/assets/img/measurement-temporary.png new file mode 100644 index 0000000..9d46fd3 Binary files /dev/null and b/platform/docs/docs/assets/img/measurement-temporary.png differ diff --git a/platform/docs/docs/assets/img/measurements-prevNext.png b/platform/docs/docs/assets/img/measurements-prevNext.png new file mode 100644 index 0000000..d4bd71b Binary files /dev/null and b/platform/docs/docs/assets/img/measurements-prevNext.png differ diff --git a/platform/docs/docs/assets/img/memory-profiling-regular.png b/platform/docs/docs/assets/img/memory-profiling-regular.png new file mode 100644 index 0000000..dd87a34 Binary files /dev/null and b/platform/docs/docs/assets/img/memory-profiling-regular.png differ diff --git a/platform/docs/docs/assets/img/microscopy.webp b/platform/docs/docs/assets/img/microscopy.webp new file mode 100644 index 0000000..e348e38 Binary files /dev/null and b/platform/docs/docs/assets/img/microscopy.webp differ diff --git a/platform/docs/docs/assets/img/migration-modes.png b/platform/docs/docs/assets/img/migration-modes.png new file mode 100644 index 0000000..2e51605 Binary files /dev/null and b/platform/docs/docs/assets/img/migration-modes.png differ diff --git a/platform/docs/docs/assets/img/migration-split-button.png b/platform/docs/docs/assets/img/migration-split-button.png new file mode 100644 index 0000000..3bfc15b Binary files /dev/null and b/platform/docs/docs/assets/img/migration-split-button.png differ diff --git a/platform/docs/docs/assets/img/mode-archs.png b/platform/docs/docs/assets/img/mode-archs.png new file mode 100644 index 0000000..f931818 Binary files /dev/null and b/platform/docs/docs/assets/img/mode-archs.png differ diff --git a/platform/docs/docs/assets/img/mode-clock.png b/platform/docs/docs/assets/img/mode-clock.png new file mode 100644 index 0000000..d855edb Binary files /dev/null and b/platform/docs/docs/assets/img/mode-clock.png differ diff --git a/platform/docs/docs/assets/img/mode-template.png b/platform/docs/docs/assets/img/mode-template.png new file mode 100644 index 0000000..9442411 Binary files /dev/null and b/platform/docs/docs/assets/img/mode-template.png differ diff --git a/platform/docs/docs/assets/img/nginx-image-archive.png b/platform/docs/docs/assets/img/nginx-image-archive.png new file mode 100644 index 0000000..f1ac061 Binary files /dev/null and b/platform/docs/docs/assets/img/nginx-image-archive.png differ diff --git a/platform/docs/docs/assets/img/ohif-cli-list.png b/platform/docs/docs/assets/img/ohif-cli-list.png new file mode 100644 index 0000000..a992f01 Binary files /dev/null and b/platform/docs/docs/assets/img/ohif-cli-list.png differ diff --git a/platform/docs/docs/assets/img/ohif-non-secure-context.png b/platform/docs/docs/assets/img/ohif-non-secure-context.png new file mode 100644 index 0000000..b4ff6d0 Binary files /dev/null and b/platform/docs/docs/assets/img/ohif-non-secure-context.png differ diff --git a/platform/docs/docs/assets/img/ohif-pacs-keycloak.png b/platform/docs/docs/assets/img/ohif-pacs-keycloak.png new file mode 100644 index 0000000..e95d2af Binary files /dev/null and b/platform/docs/docs/assets/img/ohif-pacs-keycloak.png differ diff --git a/platform/docs/docs/assets/img/open-graph.png b/platform/docs/docs/assets/img/open-graph.png new file mode 100644 index 0000000..5b881ab Binary files /dev/null and b/platform/docs/docs/assets/img/open-graph.png differ diff --git a/platform/docs/docs/assets/img/overview.png b/platform/docs/docs/assets/img/overview.png new file mode 100644 index 0000000..d504f5b Binary files /dev/null and b/platform/docs/docs/assets/img/overview.png differ diff --git a/platform/docs/docs/assets/img/panel-module-left-right.png b/platform/docs/docs/assets/img/panel-module-left-right.png new file mode 100644 index 0000000..fdbb558 Binary files /dev/null and b/platform/docs/docs/assets/img/panel-module-left-right.png differ diff --git a/platform/docs/docs/assets/img/panel-module-v3.png b/platform/docs/docs/assets/img/panel-module-v3.png new file mode 100644 index 0000000..83f9e19 Binary files /dev/null and b/platform/docs/docs/assets/img/panel-module-v3.png differ diff --git a/platform/docs/docs/assets/img/panelmodule-icon.png b/platform/docs/docs/assets/img/panelmodule-icon.png new file mode 100644 index 0000000..b1e4c53 Binary files /dev/null and b/platform/docs/docs/assets/img/panelmodule-icon.png differ diff --git a/platform/docs/docs/assets/img/preferSizeOverAccuracy.png b/platform/docs/docs/assets/img/preferSizeOverAccuracy.png new file mode 100644 index 0000000..253414c Binary files /dev/null and b/platform/docs/docs/assets/img/preferSizeOverAccuracy.png differ diff --git a/platform/docs/docs/assets/img/progressDropdown.png b/platform/docs/docs/assets/img/progressDropdown.png new file mode 100644 index 0000000..263e99a Binary files /dev/null and b/platform/docs/docs/assets/img/progressDropdown.png differ diff --git a/platform/docs/docs/assets/img/reference-lines-from-start.png b/platform/docs/docs/assets/img/reference-lines-from-start.png new file mode 100644 index 0000000..6e63ee4 Binary files /dev/null and b/platform/docs/docs/assets/img/reference-lines-from-start.png differ diff --git a/platform/docs/docs/assets/img/restore-exported-sr.png b/platform/docs/docs/assets/img/restore-exported-sr.png new file mode 100644 index 0000000..7aeca26 Binary files /dev/null and b/platform/docs/docs/assets/img/restore-exported-sr.png differ diff --git a/platform/docs/docs/assets/img/scope-of-project.png b/platform/docs/docs/assets/img/scope-of-project.png new file mode 100644 index 0000000..6daac8b Binary files /dev/null and b/platform/docs/docs/assets/img/scope-of-project.png differ diff --git a/platform/docs/docs/assets/img/segDisplayEditingFalse.png b/platform/docs/docs/assets/img/segDisplayEditingFalse.png new file mode 100644 index 0000000..a6d1306 Binary files /dev/null and b/platform/docs/docs/assets/img/segDisplayEditingFalse.png differ diff --git a/platform/docs/docs/assets/img/segDisplayEditingTrue.png b/platform/docs/docs/assets/img/segDisplayEditingTrue.png new file mode 100644 index 0000000..993f23f Binary files /dev/null and b/platform/docs/docs/assets/img/segDisplayEditingTrue.png differ diff --git a/platform/docs/docs/assets/img/segmentationShowAddSegmentImage.png b/platform/docs/docs/assets/img/segmentationShowAddSegmentImage.png new file mode 100644 index 0000000..f53c75a Binary files /dev/null and b/platform/docs/docs/assets/img/segmentationShowAddSegmentImage.png differ diff --git a/platform/docs/docs/assets/img/segmentationTableModeImage.png b/platform/docs/docs/assets/img/segmentationTableModeImage.png new file mode 100644 index 0000000..22f9526 Binary files /dev/null and b/platform/docs/docs/assets/img/segmentationTableModeImage.png differ diff --git a/platform/docs/docs/assets/img/segmentationTableModeImage2.png b/platform/docs/docs/assets/img/segmentationTableModeImage2.png new file mode 100644 index 0000000..c0de2b7 Binary files /dev/null and b/platform/docs/docs/assets/img/segmentationTableModeImage2.png differ diff --git a/platform/docs/docs/assets/img/self-signed-cert-advanced-warning.png b/platform/docs/docs/assets/img/self-signed-cert-advanced-warning.png new file mode 100644 index 0000000..6f0c98b Binary files /dev/null and b/platform/docs/docs/assets/img/self-signed-cert-advanced-warning.png differ diff --git a/platform/docs/docs/assets/img/self-signed-cert-warning.png b/platform/docs/docs/assets/img/self-signed-cert-warning.png new file mode 100644 index 0000000..41df65f Binary files /dev/null and b/platform/docs/docs/assets/img/self-signed-cert-warning.png differ diff --git a/platform/docs/docs/assets/img/seriesSort.png b/platform/docs/docs/assets/img/seriesSort.png new file mode 100644 index 0000000..f325392 Binary files /dev/null and b/platform/docs/docs/assets/img/seriesSort.png differ diff --git a/platform/docs/docs/assets/img/services-data.png b/platform/docs/docs/assets/img/services-data.png new file mode 100644 index 0000000..e5251ed Binary files /dev/null and b/platform/docs/docs/assets/img/services-data.png differ diff --git a/platform/docs/docs/assets/img/services-measurements.png b/platform/docs/docs/assets/img/services-measurements.png new file mode 100644 index 0000000..900419a Binary files /dev/null and b/platform/docs/docs/assets/img/services-measurements.png differ diff --git a/platform/docs/docs/assets/img/services-ui.png b/platform/docs/docs/assets/img/services-ui.png new file mode 100644 index 0000000..34c3bf1 Binary files /dev/null and b/platform/docs/docs/assets/img/services-ui.png differ diff --git a/platform/docs/docs/assets/img/services.png b/platform/docs/docs/assets/img/services.png new file mode 100644 index 0000000..569c046 Binary files /dev/null and b/platform/docs/docs/assets/img/services.png differ diff --git a/platform/docs/docs/assets/img/static-dicom-web.png b/platform/docs/docs/assets/img/static-dicom-web.png new file mode 100644 index 0000000..fab41fc Binary files /dev/null and b/platform/docs/docs/assets/img/static-dicom-web.png differ diff --git a/platform/docs/docs/assets/img/studyMenuItemsImage.png b/platform/docs/docs/assets/img/studyMenuItemsImage.png new file mode 100644 index 0000000..b70a44d Binary files /dev/null and b/platform/docs/docs/assets/img/studyMenuItemsImage.png differ diff --git a/platform/docs/docs/assets/img/surge-deploy.gif b/platform/docs/docs/assets/img/surge-deploy.gif new file mode 100644 index 0000000..545f068 Binary files /dev/null and b/platform/docs/docs/assets/img/surge-deploy.gif differ diff --git a/platform/docs/docs/assets/img/template-extension-files.png b/platform/docs/docs/assets/img/template-extension-files.png new file mode 100644 index 0000000..465c2a9 Binary files /dev/null and b/platform/docs/docs/assets/img/template-extension-files.png differ diff --git a/platform/docs/docs/assets/img/template-mode-files.png b/platform/docs/docs/assets/img/template-mode-files.png new file mode 100644 index 0000000..0c44ca9 Binary files /dev/null and b/platform/docs/docs/assets/img/template-mode-files.png differ diff --git a/platform/docs/docs/assets/img/template-mode-ui.png b/platform/docs/docs/assets/img/template-mode-ui.png new file mode 100644 index 0000000..c63d172 Binary files /dev/null and b/platform/docs/docs/assets/img/template-mode-ui.png differ diff --git a/platform/docs/docs/assets/img/thumbnailMenuItemsImage.png b/platform/docs/docs/assets/img/thumbnailMenuItemsImage.png new file mode 100644 index 0000000..1354337 Binary files /dev/null and b/platform/docs/docs/assets/img/thumbnailMenuItemsImage.png differ diff --git a/platform/docs/docs/assets/img/toolbar-module.png b/platform/docs/docs/assets/img/toolbar-module.png new file mode 100644 index 0000000..5753669 Binary files /dev/null and b/platform/docs/docs/assets/img/toolbar-module.png differ diff --git a/platform/docs/docs/assets/img/toolbarModule-layout.png b/platform/docs/docs/assets/img/toolbarModule-layout.png new file mode 100644 index 0000000..8190b5f Binary files /dev/null and b/platform/docs/docs/assets/img/toolbarModule-layout.png differ diff --git a/platform/docs/docs/assets/img/toolbarModule-nested-buttons.png b/platform/docs/docs/assets/img/toolbarModule-nested-buttons.png new file mode 100644 index 0000000..1a85837 Binary files /dev/null and b/platform/docs/docs/assets/img/toolbarModule-nested-buttons.png differ diff --git a/platform/docs/docs/assets/img/toolbarModule-zoom.png b/platform/docs/docs/assets/img/toolbarModule-zoom.png new file mode 100644 index 0000000..00acfca Binary files /dev/null and b/platform/docs/docs/assets/img/toolbarModule-zoom.png differ diff --git a/platform/docs/docs/assets/img/toolbox-modal.png b/platform/docs/docs/assets/img/toolbox-modal.png new file mode 100644 index 0000000..ce97f73 Binary files /dev/null and b/platform/docs/docs/assets/img/toolbox-modal.png differ diff --git a/platform/docs/docs/assets/img/tracked-not-tracked.png b/platform/docs/docs/assets/img/tracked-not-tracked.png new file mode 100644 index 0000000..d61b36d Binary files /dev/null and b/platform/docs/docs/assets/img/tracked-not-tracked.png differ diff --git a/platform/docs/docs/assets/img/tracking-workflow1.png b/platform/docs/docs/assets/img/tracking-workflow1.png new file mode 100644 index 0000000..d5b5959 Binary files /dev/null and b/platform/docs/docs/assets/img/tracking-workflow1.png differ diff --git a/platform/docs/docs/assets/img/tracking-workflow2.png b/platform/docs/docs/assets/img/tracking-workflow2.png new file mode 100644 index 0000000..988e56d Binary files /dev/null and b/platform/docs/docs/assets/img/tracking-workflow2.png differ diff --git a/platform/docs/docs/assets/img/tracking-workflow3.png b/platform/docs/docs/assets/img/tracking-workflow3.png new file mode 100644 index 0000000..c62fde7 Binary files /dev/null and b/platform/docs/docs/assets/img/tracking-workflow3.png differ diff --git a/platform/docs/docs/assets/img/ui-modal.gif b/platform/docs/docs/assets/img/ui-modal.gif new file mode 100644 index 0000000..599964e Binary files /dev/null and b/platform/docs/docs/assets/img/ui-modal.gif differ diff --git a/platform/docs/docs/assets/img/ui-services.png b/platform/docs/docs/assets/img/ui-services.png new file mode 100644 index 0000000..dd53063 Binary files /dev/null and b/platform/docs/docs/assets/img/ui-services.png differ diff --git a/platform/docs/docs/assets/img/uploader.gif b/platform/docs/docs/assets/img/uploader.gif new file mode 100644 index 0000000..69aff80 Binary files /dev/null and b/platform/docs/docs/assets/img/uploader.gif differ diff --git a/platform/docs/docs/assets/img/user-access-control-request-flow.png b/platform/docs/docs/assets/img/user-access-control-request-flow.png new file mode 100644 index 0000000..573c835 Binary files /dev/null and b/platform/docs/docs/assets/img/user-access-control-request-flow.png differ diff --git a/platform/docs/docs/assets/img/user-hotkeys-default.png b/platform/docs/docs/assets/img/user-hotkeys-default.png new file mode 100644 index 0000000..7621c4f Binary files /dev/null and b/platform/docs/docs/assets/img/user-hotkeys-default.png differ diff --git a/platform/docs/docs/assets/img/user-hotkeys.png b/platform/docs/docs/assets/img/user-hotkeys.png new file mode 100644 index 0000000..389aa31 Binary files /dev/null and b/platform/docs/docs/assets/img/user-hotkeys.png differ diff --git a/platform/docs/docs/assets/img/user-measurement-export.png b/platform/docs/docs/assets/img/user-measurement-export.png new file mode 100644 index 0000000..1ce6174 Binary files /dev/null and b/platform/docs/docs/assets/img/user-measurement-export.png differ diff --git a/platform/docs/docs/assets/img/user-open-viewer.png b/platform/docs/docs/assets/img/user-open-viewer.png new file mode 100644 index 0000000..5e2b29c Binary files /dev/null and b/platform/docs/docs/assets/img/user-open-viewer.png differ diff --git a/platform/docs/docs/assets/img/user-study-filter.png b/platform/docs/docs/assets/img/user-study-filter.png new file mode 100644 index 0000000..05d0c4b Binary files /dev/null and b/platform/docs/docs/assets/img/user-study-filter.png differ diff --git a/platform/docs/docs/assets/img/user-study-list.png b/platform/docs/docs/assets/img/user-study-list.png new file mode 100644 index 0000000..4d58959 Binary files /dev/null and b/platform/docs/docs/assets/img/user-study-list.png differ diff --git a/platform/docs/docs/assets/img/user-study-next.png b/platform/docs/docs/assets/img/user-study-next.png new file mode 100644 index 0000000..b082eed Binary files /dev/null and b/platform/docs/docs/assets/img/user-study-next.png differ diff --git a/platform/docs/docs/assets/img/user-study-panel.png b/platform/docs/docs/assets/img/user-study-panel.png new file mode 100644 index 0000000..42db7c2 Binary files /dev/null and b/platform/docs/docs/assets/img/user-study-panel.png differ diff --git a/platform/docs/docs/assets/img/user-study-summary.png b/platform/docs/docs/assets/img/user-study-summary.png new file mode 100644 index 0000000..e5a5aad Binary files /dev/null and b/platform/docs/docs/assets/img/user-study-summary.png differ diff --git a/platform/docs/docs/assets/img/user-studyist-modespecific.png b/platform/docs/docs/assets/img/user-studyist-modespecific.png new file mode 100644 index 0000000..bf878bc Binary files /dev/null and b/platform/docs/docs/assets/img/user-studyist-modespecific.png differ diff --git a/platform/docs/docs/assets/img/user-toolbar-download-icon.png b/platform/docs/docs/assets/img/user-toolbar-download-icon.png new file mode 100644 index 0000000..ca7eef5 Binary files /dev/null and b/platform/docs/docs/assets/img/user-toolbar-download-icon.png differ diff --git a/platform/docs/docs/assets/img/user-toolbar-extra.png b/platform/docs/docs/assets/img/user-toolbar-extra.png new file mode 100644 index 0000000..15632fd Binary files /dev/null and b/platform/docs/docs/assets/img/user-toolbar-extra.png differ diff --git a/platform/docs/docs/assets/img/user-toolbar-preset.png b/platform/docs/docs/assets/img/user-toolbar-preset.png new file mode 100644 index 0000000..5a24014 Binary files /dev/null and b/platform/docs/docs/assets/img/user-toolbar-preset.png differ diff --git a/platform/docs/docs/assets/img/user-toolbarDownload.png b/platform/docs/docs/assets/img/user-toolbarDownload.png new file mode 100644 index 0000000..d39c9cb Binary files /dev/null and b/platform/docs/docs/assets/img/user-toolbarDownload.png differ diff --git a/platform/docs/docs/assets/img/user-viewer-layout.png b/platform/docs/docs/assets/img/user-viewer-layout.png new file mode 100644 index 0000000..7111088 Binary files /dev/null and b/platform/docs/docs/assets/img/user-viewer-layout.png differ diff --git a/platform/docs/docs/assets/img/user-viewer-main.png b/platform/docs/docs/assets/img/user-viewer-main.png new file mode 100644 index 0000000..1ef3e76 Binary files /dev/null and b/platform/docs/docs/assets/img/user-viewer-main.png differ diff --git a/platform/docs/docs/assets/img/user-viewer-toolbar-measurements.png b/platform/docs/docs/assets/img/user-viewer-toolbar-measurements.png new file mode 100644 index 0000000..2cb7fd7 Binary files /dev/null and b/platform/docs/docs/assets/img/user-viewer-toolbar-measurements.png differ diff --git a/platform/docs/docs/assets/img/user-viewer-toolbar.png b/platform/docs/docs/assets/img/user-viewer-toolbar.png new file mode 100644 index 0000000..fc36c58 Binary files /dev/null and b/platform/docs/docs/assets/img/user-viewer-toolbar.png differ diff --git a/platform/docs/docs/assets/img/user-viewer.png b/platform/docs/docs/assets/img/user-viewer.png new file mode 100644 index 0000000..c79dce1 Binary files /dev/null and b/platform/docs/docs/assets/img/user-viewer.png differ diff --git a/platform/docs/docs/assets/img/viewport-action-corners.png b/platform/docs/docs/assets/img/viewport-action-corners.png new file mode 100644 index 0000000..887bfe0 Binary files /dev/null and b/platform/docs/docs/assets/img/viewport-action-corners.png differ diff --git a/platform/docs/docs/assets/img/viewport-notification.png b/platform/docs/docs/assets/img/viewport-notification.png new file mode 100644 index 0000000..cef7c46 Binary files /dev/null and b/platform/docs/docs/assets/img/viewport-notification.png differ diff --git a/platform/docs/docs/assets/img/viewportModule-layout.png b/platform/docs/docs/assets/img/viewportModule-layout.png new file mode 100644 index 0000000..58af0ad Binary files /dev/null and b/platform/docs/docs/assets/img/viewportModule-layout.png differ diff --git a/platform/docs/docs/assets/img/viewportModule.png b/platform/docs/docs/assets/img/viewportModule.png new file mode 100644 index 0000000..0542337 Binary files /dev/null and b/platform/docs/docs/assets/img/viewportModule.png differ diff --git a/platform/docs/docs/assets/img/viewportOverlay-customization.png b/platform/docs/docs/assets/img/viewportOverlay-customization.png new file mode 100644 index 0000000..7f7245c Binary files /dev/null and b/platform/docs/docs/assets/img/viewportOverlay-customization.png differ diff --git a/platform/docs/docs/assets/img/webgl-int16.png b/platform/docs/docs/assets/img/webgl-int16.png new file mode 100644 index 0000000..1a9abd8 Binary files /dev/null and b/platform/docs/docs/assets/img/webgl-int16.png differ diff --git a/platform/docs/docs/assets/img/webgl-report-norm16.png b/platform/docs/docs/assets/img/webgl-report-norm16.png new file mode 100644 index 0000000..766938b Binary files /dev/null and b/platform/docs/docs/assets/img/webgl-report-norm16.png differ diff --git a/platform/docs/docs/assets/img/windowLevelActionMenu.png b/platform/docs/docs/assets/img/windowLevelActionMenu.png new file mode 100644 index 0000000..1a1b43b Binary files /dev/null and b/platform/docs/docs/assets/img/windowLevelActionMenu.png differ diff --git a/platform/docs/docs/assets/img/windowLevelPresets.png b/platform/docs/docs/assets/img/windowLevelPresets.png new file mode 100644 index 0000000..f98a282 Binary files /dev/null and b/platform/docs/docs/assets/img/windowLevelPresets.png differ diff --git a/platform/docs/docs/configuration/_category_.json b/platform/docs/docs/configuration/_category_.json new file mode 100644 index 0000000..0aea174 --- /dev/null +++ b/platform/docs/docs/configuration/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Configuration", + "position": 4 +} diff --git a/platform/docs/docs/configuration/configurationFiles.md b/platform/docs/docs/configuration/configurationFiles.md new file mode 100644 index 0000000..db24e8d --- /dev/null +++ b/platform/docs/docs/configuration/configurationFiles.md @@ -0,0 +1,365 @@ +--- +sidebar_position: 1 +sidebar_label: Configuration Files +--- + +# Config files + +After following the steps outlined in +[Getting Started](./../development/getting-started.md), you'll notice that the +OHIF Viewer has data for several studies and their images. You didn't add this +data, so where is it coming from? + +By default, the viewer is configured to connect to a Amazon S3 bucket that is hosting +a Static WADO server (see [Static WADO DICOMWeb](https://github.com/RadicalImaging/static-dicomweb)). +By default we use `default.js` for the configuration file. You can change this by setting the `APP_CONFIG` environment variable +and select other options such as `config/local_orthanc.js` or `config/google.js`. + + +## Configuration Files + +The configuration for our viewer is in the `platform/app/public/config` +directory. Our build process knows which configuration file to use based on the +`APP_CONFIG` environment variable. By default, its value is +[`config/default.js`][default-config]. The majority of the viewer's features, +and registered extension's features, are configured using this file. + +The simplest way is to update the existing default config: + +```js title="platform/app/public/config/default.js" +window.config = { + routerBasename: '/', + extensions: [], + modes: [], + showStudyList: true, + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'dcmjs DICOMWeb Server', + name: 'DCM4CHEE', + wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado', + qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + qidoSupportsIncludeField: true, + supportsReject: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: true, + omitQuotationForMultipartRequest: true, + }, + }, + ], + defaultDataSourceName: 'dicomweb', +}; +``` + +> As you can see a new change in `OHIF-v3` is the addition of `dataSources`. You +> can build your own datasource and map it to the internal data structure of +> OHIFโ€™s > metadata and enjoy using other peoples developed mode on your own +> data! +> +> You can read more about data sources at +> [Data Source section in Modes](../platform/modes/index.md) + +The configuration can also be written as a JS Function in case you need to +inject dependencies like external services: + +```js +window.config = ({ servicesManager } = {}) => { + const { UIDialogService } = servicesManager.services; + return { + cornerstoneExtensionConfig: { + tools: { + ArrowAnnotate: { + configuration: { + getTextCallback: (callback, eventDetails) => UIDialogService.create({... + } + } + }, + }, + routerBasename: '/', + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'dcmjs DICOMWeb Server', + name: 'DCM4CHEE', + wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado', + qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + qidoSupportsIncludeField: true, + supportsReject: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: true, + omitQuotationForMultipartRequest: true, + }, + }, + ], + defaultDataSourceName: 'dicomweb', + }; +}; +``` + + + + + +## Configuration Options + + +Here are a list of some options available: +- `disableEditing`: If true, it disables editing in OHIF, hiding edit buttons in segmentation + panel and locking already stored measurements. +- `maxNumberOfWebWorkers`: The maximum number of web workers to use for + decoding. Defaults to minimum of `navigator.hardwareConcurrency` and + what is specified by `maxNumberOfWebWorkers`. Some windows machines require smaller values. +- `acceptHeader` : accept header to request specific dicom transfer syntax ex : [ 'multipart/related; type=image/jls; q=1', 'multipart/related; type=application/octet-stream; q=0.1' ] +- `investigationalUseDialog`: This should contain an object with `option` value, it can be either `always` which always shows the dialog once per session, `never` which never shows the dialog, or `configure` which shows the dialog once and won't show it again until a set number of days defined by the user, if it's set to configure, you are required to add an additional property `days` which is the number of days to wait before showing the dialog again. +- `groupEnabledModesFirst`: boolean, if set to true, all valid modes for the study get grouped together first, then the rest of the modes. If false, all modes are shown in the order they are defined in the configuration. +- `experimentalStudyBrowserSort`: boolean, if set to true, you will get the experimental StudyBrowserSort component in the UI, which displays a list of sort functions that the displaySets can be sorted by, the sort reflects in all part of the app including the thumbnail/study panel. These sort functions are defined in the customizationModule and can be expanded by users. +- `disableConfirmationPrompts`: boolean, if set to true, it skips confirmation prompts for measurement tracking and hydration. +- `showPatientInfo`: string, if set to 'visible', the patient info header will be shown and its initial state is expanded. If set to 'visibleCollapsed', the patient info header will be shown but it's initial state is collapsed. If set to 'disabled', the patient info header will never be shown, and if set to 'visibleReadOnly', the patient info header will be shown and always expanded. +- `requestTransferSyntaxUID` : Request a specific Transfer syntax from dicom web server ex: 1.2.840.10008.1.2.4.80 (applied only if acceptHeader is not set) +- `omitQuotationForMultipartRequest`: Some servers (e.g., .NET) require the `multipart/related` request to be sent without quotation marks. Defaults to `false`. If your server doesn't require this, then setting this flag to `true` might improve performance (by removing the need for preflight requests). Also note that +if auth headers are used, a preflight request is required. +- `maxNumRequests`: The maximum number of requests to allow in parallel. It is an object with keys of `interaction`, `thumbnail`, and `prefetch`. You can specify a specific number for each type. +- `modesConfiguration`: Allows overriding modes configuration. + - Example config: + ```js + modesConfiguration: { + '@ohif/mode-longitudinal': { + displayName: 'Custom Name', + routeName: 'customRouteName', + routes: [ + { + path: 'customPath', + layoutTemplate: () => { + /** Custom Layout */ + return { + id: ohif.layout, + props: { + leftPanels: [tracked.thumbnailList], + rightPanels: [dicomSeg.panel, tracked.measurements], + rightPanelClosed: true, + viewports: [ + { + namespace: tracked.viewport, + displaySetsToDisplay: [ohif.sopClassHandler], + }, + ], + }, + }; + }, + }, + ], + } + }, + ``` + Note: Although the mode configuration is passed to the mode factory function, it is up to the particular mode itself if its going to use it to allow overwriting its original configuration e.g. + ```js + function modeFactory({ modeConfiguration }) { + return { + id, + routeName: 'viewer', + displayName: 'Basic Viewer', + ... + onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => { + ... + }, + /** + * This mode allows its configuration to be overwritten by + * destructuring the modeConfiguration value from the mode fatory function + * at the end of the mode configuration definition. + */ + ...modeConfiguration, + }; + } + ``` +- `showLoadingIndicator`: (default to true), if set to false, the loading indicator will not be shown when navigating between studies. +- `useNorm16Texture`: (default to false), if set to true, it will use 16 bit data type for the image data wherever possible which has + significant impact on reducing the memory usage. However, the 16Bit textures require EXT_texture_norm16 extension in webGL 2.0 (you can check if you have it here https://webglreport.com/?v=2). In addition to the extension, there are reported problems for Intel Macs that might cause the viewer to crash. In summary, it is great a configuration if you have support for it. +- `useSharedArrayBuffer` (default to 'TRUE', options: 'AUTO', 'FALSE', 'TRUE', note that these are strings), for volume loading we use sharedArrayBuffer to be able to + load the volume progressively as the data arrives (each webworker has the shared buffer and can write to it). However, there might be certain environments that do not support sharedArrayBuffer. In that case, you can set this flag to false and the viewer will use the regular arrayBuffer which might be slower for large volume loading. +- `supportsWildcard`: (default to false), if set to true, the datasource will support wildcard matching for patient name and patient id. +- `allowMultiSelectExport`: (default to false), if set to true, the user will be able to select the datasource to export the report to. +- `activateViewportBeforeInteraction`: (default to true), if set to false, tools can be used directly without the need to click and activate the viewport. +- `autoPlayCine`: (default to false), if set to true, data sets with the DICOM frame time tag (i.e. (0018,1063)) will auto play when displayed +- `addWindowLevelActionMenu`: (default to true), if set to false, the window level action menu item is NOT added to the viewport action corners +- `dangerouslyUseDynamicConfig`: Dynamic config allows user to pass `configUrl` query string. This allows to load config without recompiling application. If the `configUrl` query string is passed, the worklist and modes will load from the referenced json rather than the default .env config. If there is no `configUrl` path provided, the default behaviour is used and there should not be any deviation from current user experience.
+Points to consider while using `dangerouslyUseDynamicConfig`:
+ - User have to enable this feature by setting `dangerouslyUseDynamicConfig.enabled:true`. By default it is `false`. + - Regex helps to avoid easy exploit. Default is `/.*/`. Setup your own regex to choose a specific source of configuration only. + - System administrators can return `cross-origin: same-origin` with OHIF files to disallow any loading from other origin. It will block read access to resources loaded from a different origin to avoid potential attack vector. + - Example config: + ```js + dangerouslyUseDynamicConfig: { + enabled: false, + regex: /.*/ + } + ``` + > Example 1, to allow numbers and letters in an absolute or sub-path only.
+`regex: /(0-9A-Za-z.]+)(\/[0-9A-Za-z.]+)*/`
+Example 2, to restricts to either hosptial.com or othersite.com.
+`regex: /(https:\/\/hospital.com(\/[0-9A-Za-z.]+)*)|(https:\/\/othersite.com(\/[0-9A-Za-z.]+)*)/`
+Example usage:
+`http://localhost:3000/?configUrl=http://localhost:3000/config/example.json`
+- `onConfiguration`: Currently only available for DicomWebDataSource, this option allows the interception of the data source configuration for dynamic values e.g. values coming from url params or query params. Here is an example of building the dicomweb datasource configuration object with values that are based on the route url params: + ``` + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'gcpdicomweb', + configuration: { + friendlyName: 'GCP DICOMWeb Server', + name: 'gcpdicomweb', + qidoSupportsIncludeField: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: false, + singlepart: 'bulkdata,video,pdf', + onConfiguration: (dicomWebConfig, options) => { + const { params } = options; + const { project, location, dataset, dicomStore } = params; + const pathUrl = `https://healthcare.googleapis.com/v1/projects/${project}/locations/${location}/datasets/${dataset}/dicomStores/${dicomStore}/dicomWeb`; + return { + ...dicomWebConfig, + wadoRoot: pathUrl, + qidoRoot: pathUrl, + wadoUri: pathUrl, + wadoUriRoot: pathUrl, + }; + }, + }, + }, + ``` +This configuration would allow the user to build a dicomweb configuration from a GCP healthcare api path e.g. http://localhost:3000/projects/your-gcp-project/locations/us-central1/datasets/your-dataset/dicomStores/your-dicom-store/study/1.3.6.1.4.1.1234.5.2.1.1234.1234.123123123123123123123123123123 + + +:::note +You can stack multiple panel components on top of each other by providing an array of panel components in the `rightPanels` or `leftPanels` properties. + +For instance we can use + +``` +rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.measurements]] +``` + +This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other. + +::: + +### Study Prefetcher + +You can enable the study prefetcher so that OHIF loads the next/previous series/display sets +based on the proximity to the current series/display set. This can be useful to improve the user experience + + +```js + studyPrefetcher: { + /* Enable/disable study prefetching service (default: false) */ + enabled: true, + /* Number of displaysets to be prefetched (default: 2)*/ + displaySetCount: 2, + /** + * Max number of concurrent prefetch requests (default: 10) + * High numbers may impact on the time to load a new dropped series because + * the browser will be busy with all prefetching requests. As soon as the + * prefetch requests get fulfilled the new ones from the new dropped series + * are sent to the server. + * + * TODO: abort all prefetch requests when a new series is loaded on a viewport. + * (need to add support for `AbortController` on Cornerstone) + * */ + maxNumPrefetchRequests: 10, + /* Display sets loading order (closest (deafult), downward or upward) */ + order: 'closest', + }, + +``` + +### More on Accept Header Configuration +In the previous section we showed that you can modify the `acceptHeader` +configuration to request specific dicom transfer syntax. By default +we use `acceptHeader: ['multipart/related; type=application/octet-stream; transfer-syntax=*']` for the following +reasons: + +- **Ensures Optimal Transfer Syntax**: By allowing the server to select the transfer syntax, + the client is more likely to receive the image in a syntax that's well-suited for fast transmission + and rendering. This might be the original syntax the image was stored in or another syntax that the server deems efficient. + +- **Avoids Transcoding**: Transcoding (converting from one transfer syntax to another) can be a resource-intensive process. + Since the OHIF Viewer supports all transfer syntaxes, it is fine to accept any transfer syntax (transfer-syntax=*). + This allows the server to send the images in their stored syntax, avoiding the need for costly on-the-fly conversions. + This approach not only saves server resources but also reduces response times by leveraging the viewer's capability to handle various syntaxes directly. + +- **Faster Data Transfer**: Compressed transfer syntaxes generally result in smaller file sizes compared + to uncompressed ones. Smaller files transmit faster over the network, leading to quicker load + times for the end-user. By accepting any syntax, the client can take advantage of compression when available. + +However, if you would like to get compressed data in a specific transfer syntax, you can modify the `acceptHeader` configuration or +`requestTransferSyntaxUID` configuration. + +## Environment Variables + +We use environment variables at build and dev time to change the Viewer's +behavior. We can update the `HTML_TEMPLATE` to easily change which extensions +are registered, and specify a different `APP_CONFIG` to connect to an +alternative data source (or even specify different default hotkeys). + +| Environment Variable | Description | Default | +| -------------------- | -------------------------------------------------------------------------------------------------- | ------------------- | +| `HTML_TEMPLATE` | Which [HTML template][html-templates] to use as our web app's entry point. Specific to PWA builds. | `index.html` | +| `PUBLIC_URL` | The route relative to the host that the app will be served from. Specific to PWA builds. | `/` | +| `APP_CONFIG` | Which [configuration file][config-file] to copy to output as `app-config.js` | `config/default.js` | +| `PROXY_TARGET` | When developing, proxy requests that match this pattern to `PROXY_DOMAIN` | `undefined` | +| `PROXY_DOMAIN` | When developing, proxy requests from `PROXY_TARGET` to `PROXY_DOMAIN` | `undefined` | +| `OHIF_PORT` | The port to run the webpack server on for PWA builds. | `3000` | + +You can also create a new config file and specify its path relative to the build +output's root by setting the `APP_CONFIG` environment variable. You can set the +value of this environment variable a few different ways: + +- ~[Add a temporary environment variable in your shell](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-temporary-environment-variables-in-your-shell)~ + - Previous `react-scripts` functionality that we need to duplicate with + `dotenv-webpack` +- ~[Add environment specific variables in `.env` file(s)](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-development-environment-variables-in-env)~ + - Previous `react-scripts` functionality that we need to duplicate with + `dotenv-webpack` +- Using the `cross-env` package in a npm script: + - `"build": "cross-env APP_CONFIG=config/my-config.js react-scripts build"` + +After updating the configuration, `yarn run build` to generate updated build +output. + + + + +[dcmjs-org]: https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado +[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb +[storescu]: https://support.dcmtk.org/docs/storescu.html +[webpack-proxy]: https://webpack.js.org/configuration/dev-server/#devserverproxy +[orthanc-docker-compose]: https://github.com/OHIF/Viewers/tree/master/platform/app/.recipes/Nginx-Orthanc + +[dcm4chee]: https://github.com/dcm4che/dcm4chee-arc-light +[dcm4chee-docker]: https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker +[orthanc]: https://www.orthanc-server.com/ +[orthanc-docker]: https://book.orthanc-server.com/users/docker.html +[dicomcloud]: https://github.com/DICOMcloud/DICOMcloud +[dicomcloud-install]: https://github.com/DICOMcloud/DICOMcloud#running-the-code +[osirix]: https://www.osirix-viewer.com/ +[horos]: https://www.horosproject.org/ +[default-config]: https://github.com/OHIF/Viewers/blob/master/platform/app/public/config/default.js +[html-templates]: https://github.com/OHIF/Viewers/tree/master/platform/app/public/html-templates +[config-files]: https://github.com/OHIF/Viewers/tree/master/platform/app/public/config + diff --git a/platform/docs/docs/configuration/dataSources/_category_.json b/platform/docs/docs/configuration/dataSources/_category_.json new file mode 100644 index 0000000..fe1e3e8 --- /dev/null +++ b/platform/docs/docs/configuration/dataSources/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Data Sources", + "position": 2 +} diff --git a/platform/docs/docs/configuration/dataSources/configuration-ui.md b/platform/docs/docs/configuration/dataSources/configuration-ui.md new file mode 100644 index 0000000..200fc16 --- /dev/null +++ b/platform/docs/docs/configuration/dataSources/configuration-ui.md @@ -0,0 +1,177 @@ +--- +sidebar_position: 6 +sidebar_label: Configuration UI +--- + +# Configuration UI + +OHIF provides for a generic mechanism for configuring a data source. This is +most useful for those organizations with several data sources +that share common (path) hierarchies. For example, an organization may have several DICOM stores +in the Google Cloud Healthcare realm where each is organized into various projects, +location, data sets and DICOM stores. + +By implementing the `BaseDataSourceConfigurationAPI` and +`BaseDataSourceConfigurationAPIItem` in an [OHIF extension](../../platform/extensions/index.md), a data source can +be made configurable via the generic UI as is depicted below for a +Google Cloud Healthcare data source. + +![Data source configuration UI](../../assets/img/data-source-configuration-ui.png) + +:::tip +A datasource root URI can be [fully or partially specified](../../deployment/google-cloud-healthcare.md#configuring-google-cloud-healthcare-as-a-datasource-in-ohif) +in the OHIF configuration file. +::: + +## `BaseDataSourceConfigurationAPIItem` interface + +Each (path) item of a data source is represented by an instance of this interface. +At the very least each of these items must expose two properties: + +|Property |Description| +|---------|-----------| +|id|a string that uniquely identifies the item| +|name|a human readable name for the item| + +Note that information such as where in the path hierarchy the item exists +has been omitted, but can be added in any concrete class that might implement this +interface. For example, the the Google Cloud Healthcare implementation of this +interface (`GoogleCloudDataSourceConfigurationAPIItem`) adds an `itemType` +(i.e. projects, locations, datasets, or dicomStores) and `url`. + +## `BaseDataSourceConfigurationAPI` interface + +The implementation of this interface is at the heart of the configuration process. +It possesses several methods for building up a data source path based on various +`BaseDataSourceConfigurationAPIItem` objects that are set via calls to the `setCurrentItem` +method. + +The constructor for the concrete class implementation should accept whatever +parameters are necessary for configuring the data source. One argument +to the constructor must be the string identifying the name of the data source +to be configured. Furthermore, considering that the `ExtensionManager` possesses +API to configure and update data sources, it too will likely be an argument to +the constructor. See [Creation via Customization Module](#creation-via-customization-module) +for more information on how the constructor is invoked via a factory method. + +For an example implementation of this interface see `GoogleCloudDataSourceConfigurationAPI`. + +### Interface Methods + +Each of the following subsections lists a method of the interface with a description +detailing what the method should do. + +#### `getItemLabels` + +Gets the i18n labels (i.e. the i18n lookup keys) for each of the configurable items +of the data source configuration API. For example, for the Google Cloud Healthcare +API, this would be `['Project', 'Location', 'Data set', 'DICOM store']`. + +Besides the configurable item labels themselves, several other string look ups +are used base on EACH of the labels returned by this method. +For instance, for the label `{itemLabel}`, the following strings are fetched for +translation... +1. No `{itemLabel}` available + - used to indicate no such items are available + - for example, for Google, No Project available would be 'No projects available' +2. Select `{itemLabel}` + - used to direct selection of the item + - for example, for Google, Select Project would be 'Select a project' +3. Error fetching `{itemLabel}` list + - used to indicate an error occurred fetching the list of items + - usually accompanied by the error itself + - for example, for Google, Error fetching Project list would be 'Error fetching projects' +4. Search `{itemLabel}` list + - used as the placeholder text for filtering a list of items + - for example, for Google, Search Project list would be 'Search projects' + +#### `initialize` + +Initializes the cloud server API and returns the top-level sub-items +that can be chosen to begin the process of configuring a data source. +For example, for the Google Cloud Healthcare API, this would perform the initial request +to fetch the top level projects for the logged in user account. + +#### `setCurrentItem` + +Sets the current path item that is passed as an argument to the method and +returns the sub-items of that item +that can be further chosen to configure a data source. +When setting the last configurable item of the data source (path), this method +returns an empty list AND configures the active data source with the selected +items path. + +For example, for the Google Cloud Healthcare API, this would take the current item +(say a data set) and queries and returns its sub-items (i.e. all of the DICOM stores +contained in that data set). Furthermore, whenever the item to set is a DICOM store, +the Google Cloud Healthcare API implementation would update the OHIF data source +associated with this instance to point to that DICOM store. + +#### `getConfiguredItems` + +Gets the list of items currently configured for the data source associated with +this API instance. The resultant array must be the same length as the result of +`getItemLabels`. Furthermore the items returned should correspond (index-wise) +with the labels returned from `getItemLabels`. + +## Creation via Customization Module + +The generic UI (i.e. `DataSourceConfigurationComponent`) uses the +[OHIF UI customization service](../../platform/services/customization-service/customizationService.md) to +instantiate the `BaseDataSourceConfigurationAPI` instance to configure a data source. + +A UI configurable data source should have a `configurationAPI` field as part of +its `configuration` in the OHIF config file. The `configurationAPI` value is the +customization id of the customization module that provides the factory method +to instantiate the `BaseDataSourceConfigurationAPI` instance. + +For example, the following is a snippet of a Google Cloud Healthcare data source configuration. + +```js + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'google-dicomweb', + configuration: { + name: 'GCP', + wadoUriRoot: 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/...', + ... + configurationAPI: 'ohif.dataSourceConfigurationAPI.google', + ... + }, + }, + ] +``` + +This suggests that the factory method is provided by the `'ohif.dataSourceConfigurationAPI.google'` +customization module. That customization module is provided by the `default` extension's +`getCustomizationModule` and looks something like the following snippet of code. Notice that +the factory method's name MUST be `factory` and accept one argument - the data source name. +Furthermore note how the constructor is invoked with anything required by the concrete configuration +API class. + +```js +export default function getCustomizationModule({ + servicesManager, + extensionManager, +}) { + return [ + { + name: 'default', + value: [ + { + // The factory for creating an instance of a BaseDataSourceConfigurationAPI for Google Cloud Healthcare + id: 'ohif.dataSourceConfigurationAPI.google', + factory: (dataSourceName: string) => + new GoogleCloudDataSourceConfigurationAPI( + dataSourceName, + servicesManager, + extensionManager + ), + }, + ], + }, + ]; +} + +``` diff --git a/platform/docs/docs/configuration/dataSources/dicom-json.md b/platform/docs/docs/configuration/dataSources/dicom-json.md new file mode 100644 index 0000000..84e7be2 --- /dev/null +++ b/platform/docs/docs/configuration/dataSources/dicom-json.md @@ -0,0 +1,194 @@ +--- +sidebar_position: 3 +sidebar_label: DICOM JSON +--- + +# DICOM JSON + +You can launch the OHIF Viewer with a JSON file which points to a DICOMWeb +server as well as a list of study and series instance UIDs along with metadata. + +An example would look like + +`https://viewer.ohif.org/viewer/dicomjson?url=https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001.json` + +As you can see the url to the location of the JSON file is passed in the query +after the `dicomjson` string, which is +`https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001.json` (this +json file has been generated by OHIF team and stored in an amazon s3 bucket for +the purpose of the guide). + +## DICOM JSON sample + +Here we are using the LIDC-IDRI-0001 case which is a sample of the LIDC-IDRI +dataset. Let's have a look at the JSON file: + +### Metadata + +JSON file stores the metadata for the study level, series level and instance +level. A JSON launch file should follow the same structure as the one below. + +:::tip +You can use our script to generate the JSON file from a hosted endpoint. See +`.scripts/dicom-json-generator.js` + +You could run it like this: + +```bash +node .scripts/dicom-json-generator.js '/path/to/study/folder' 'url/to/dicom/server/folder' 'json/output/file.json' +``` + +Some modalities require additional metadata to be added to the JSON file. You can read more about the minimum amount of metadata required for the viewer to work [here](../../faq/technical#what-are-the-list-of-required-metadata-for-the-ohif-viewer-to-work). We will handle this in the script. For example, the script will add the CodeSequences for SR in order to display the measurements in the viewer. +::: + + +Note that at the instance level metadata we are storing both the `metadata` and +also the `url` for the dicom file on the dicom server. In this case we are +referring to +`dicomweb:https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm` +which is stored in another directory in our s3. (You can actually try +downloading the dicom file by opening the url in your browser). + +The URL to the script in the given example is `https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178`. This URL serves as the parent directory that contains all the series within their respective folders. + +```json +{ + "studies": [ + // first study metadata + { + "StudyInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.298806137288633453246975630178", + "StudyDate": "20000101", + "StudyTime": "", + "PatientName": "", + "PatientID": "LIDC-IDRI-0001", + "AccessionNumber": "", + "PatientAge": "", + "PatientSex": "", + "series": [ + // first series metadata + { + "SeriesInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.179049373636438705059720603192", + "SeriesNumber": 3000566, + "Modality": "CT", + "SliceThickness": 2.5, + "instances": [ + // first instance metadata + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 1, + "SOPClassUID": "1.2.840.10008.5.1.4.1.1.2", + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.703125, 0.703125], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 1, 0], + "ImagePositionPatient": [-166, -171.699997, -10], + "FrameOfReferenceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.229925374658226729607867499499", + "ImageType": ["ORIGINAL", "PRIMARY", "AXIAL"], + "Modality": "CT", + "SOPInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.262721256650280657946440242654", + "SeriesInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.179049373636438705059720603192", + "StudyInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.298806137288633453246975630178", + "WindowCenter": -600, + "WindowWidth": 1600, + "SeriesDate": "20000101" + }, + "url": "dicomweb:https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm" + }, + // second instance metadata + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 2, + "SOPClassUID": "1.2.840.10008.5.1.4.1.1.2", + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.703125, 0.703125], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 1, 0], + "ImagePositionPatient": [-166, -171.699997, -12.5], + "FrameOfReferenceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.229925374658226729607867499499", + "ImageType": ["ORIGINAL", "PRIMARY", "AXIAL"], + "Modality": "CT", + "SOPInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.512235483218154065970649917292", + "SeriesInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.179049373636438705059720603192", + "StudyInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.298806137288633453246975630178", + "WindowCenter": -600, + "WindowWidth": 1600, + "SeriesDate": "20000101" + }, + "url": "dicomweb:https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-002.dcm" + } + // ..... other instances metadata + ] + } + // ... other series metadata + ], + "NumInstances": 133, + "Modalities": "CT" + } + // second study metadata + ] +} +``` + +![](../../assets/img/dicom-json.png) + +### Local Demo + +You can run OHIF with a JSON data source against you local datasets (given that +their JSON metadata is extracted). + +First you need to put the JSON file and the folder containing the dicom files +inside your `public` folder. Since files are served from your local server the +`url` for the JSON file will be `http://localhost:3000/LIDC-IDRI-0001.json` and +the dicom files will be +`dicomweb:http://localhost:3000/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm`. + +After `yarn install` and running `yarn dev` and opening the browser at +`http://localhost:3000/viewer/dicomjson?url=http://localhost:3000/LIDC-IDRI-0001.json` +will display the viewer. + +Download JSON file from +[here](https://www.dropbox.com/sh/zvkv6mrhpdze67x/AADLGK46WuforD2LopP99gFXa?dl=0) + +Sample DICOM files can be downloaded from +[TCIA](https://wiki.cancerimagingarchive.net/display/Public/LIDC-IDRI) or +directly from +[here](https://www.dropbox.com/sh/zvkv6mrhpdze67x/AADLGK46WuforD2LopP99gFXa?dl=0) + +Your public folder should look like this: + +![](../../assets/img/dicom-json-public.png) + +:::tip +It is important to URL encode the `url` query parameter especially if the `url` +parameter itself also contains query parameters. So for example, + +`http://localhost:3000/viewer/dicomjson?url=http://localhost:3000/LIDC-IDRI-0001.json?key0=val0&key1=val1` + +should be... + +`http://localhost:3000/viewer/dicomjson?url=http://localhost:3000/LIDC-IDRI-0001.json?key0=val0%26key1=val1` + +Notice the ampersand (`&`) is encoded as `%26`. +::: + +:::note +When hosting the DICOM JSON files, it is important to be aware that certain providers +do not automatically handle the 404 error and fallback to index.html. For example, Netlify +handles this, but Azure does not. Consequently, when you attempt to access a link with a +specific URL, a 404 error will be displayed. + +This issue also occurs locally, where the http-server does not handle it. However, +if you utilize the `serve` package (npx serve ./dist -c ../public/serve.json), it effectively addresses this problem. +::: diff --git a/platform/docs/docs/configuration/dataSources/dicom-web-proxy.md b/platform/docs/docs/configuration/dataSources/dicom-web-proxy.md new file mode 100644 index 0000000..faeea85 --- /dev/null +++ b/platform/docs/docs/configuration/dataSources/dicom-web-proxy.md @@ -0,0 +1,54 @@ +--- +sidebar_position: 4 +sidebar_label: DICOMweb Proxy +--- + +# DICOMweb Proxy + +You can launch the OHIF Viewer with a url that returns a JSON file which +contains a DICOMWeb configuration. The DICOMweb Proxy constructs a DICOMweb +datasource and delegates subsequent requests for metadata and images to that. + +Usage is similar to that of the [DICOM JSON](./dicom-json.md) datasource and +might look like + +`https://viewer.ohif.org/viewer/dicomwebproxy?url=https://ohif-dicom-json-example.s3.amazonaws.com/dicomweb.json` + +The url to the location of the JSON file is passed in the query +after the `dicomwebproxy` string, which is +`https://ohif-dicom-json-example.s3.amazonaws.com/dicomweb.json` (this json file +does not exist at the moment of this writing). + +## DICOMweb JSON configuration sample + +The json returned by the url in this example contains a dicomweb configuration +(see [DICOMweb](dicom-web.md)), in a "servers" object, which is then used to +construct a dynamic DICOMweb datasource to delegate requests to. Here is an +example configuration that might be returned using the url parameter. + +```json +{ + "servers": { + "dicomWeb": [ + { + "name": "DCM4CHEE", + "wadoUriRoot": "https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado", + "qidoRoot": "https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs", + "wadoRoot": "https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs", + "qidoSupportsIncludeField": true, + "supportsReject": true, + "imageRendering": "wadors", + "thumbnailRendering": "wadors", + "enableStudyLazyLoad": true, + "supportsFuzzyMatching": true, + "supportsWildcard": true + } + ] + } +} +``` + +The DICOMweb Proxy expects the json returned by the url parameter it is invoked +with to include a servers object which contains a "dicomWeb" configuration array +as above. It will only consider the first array item in the dicomWeb +configuration. diff --git a/platform/docs/docs/configuration/dataSources/dicom-web.md b/platform/docs/docs/configuration/dataSources/dicom-web.md new file mode 100644 index 0000000..82fb703 --- /dev/null +++ b/platform/docs/docs/configuration/dataSources/dicom-web.md @@ -0,0 +1,254 @@ +--- +sidebar_position: 2 +sidebar_label: DICOMweb +--- + +# DICOMweb + +## Set up a local DICOM server + +ATTENTION! Already have a remote or local server? Skip to the +[configuration section](#configuration-learn-more) below. + +While the OHIF Viewer can work with any data source, the easiest to configure +are the ones that follow the [DICOMWeb][dicom-web] spec. + +1. Choose and install an Image Archive +2. Upload data to your archive (e.g. with DCMTK's [storescu][storescu] or your + archive's web interface) +3. Keep the server running + +For our purposes, we will be using `Orthanc`, but you can see a list of +[other Open Source options](#open-source-dicom-image-archives) below. + +### Requirements + +- Docker + - [Docker for Mac](https://docs.docker.com/docker-for-mac/) + - [Docker for Windows (recommended)](https://docs.docker.com/docker-for-windows/) + - [Docker Toolbox for Windows](https://docs.docker.com/toolbox/toolbox_install_windows/) + +_Not sure if you have `docker` installed already? Try running `docker --version` +in command prompt or terminal_ + +> If you are using `Docker Toolbox` you need to change the _PROXY_DOMAIN_ +> parameter in _platform/app/package.json_ to http://192.168.99.100:8042 or +> the ip docker-machine ip throws. This is the value [`WebPack`][webpack-proxy] +> uses to proxy requests + +## Open Source DICOM Image Archives + +There are a lot of options available to you to use as a local DICOM server. Here +are some of the more popular ones: + +| Archive | Installation | +| --------------------------------------------- | ---------------------------------- | +| [DCM4CHEE Archive 5.x][dcm4chee] | [W/ Docker][dcm4chee-docker] | +| [Orthanc][orthanc] | [W/ Docker][orthanc-docker] | +| [DICOMcloud][dicomcloud] (**DICOM Web only**) | [Installation][dicomcloud-install] | +| [OsiriX][osirix] (**Mac OSX only**) | Desktop Client | +| [Horos][horos] (**Mac OSX only**) | Desktop Client | + +_Feel free to make a Pull Request if you want to add to this list._ + +Below, we will focus on `DCM4CHEE` and `Orthanc` usage: + +### Running Orthanc + +_Start Orthanc:_ + +```bash +# Runs orthanc so long as window remains open +yarn run orthanc:up +``` + +_Upload your first Study:_ + +1. Navigate to + [Orthanc's web interface](http://localhost:8042/ui/app/index.html#/) at + `http://localhost:8042/ui/app/index.html#/` in a web browser. +2. In the left you can see the upload button where you can drag and drop your DICOM files + +#### Orthanc: Learn More + +You can see the `docker-compose.yml` file this command runs at +[`/platform/app/.recipes/Nginx-Orthanc`][orthanc-docker-compose], and more on +Orthanc for Docker in [Orthanc's documentation][orthanc-docker]. + +#### Connecting to Orthanc + +Now that we have a local Orthanc instance up and running, we need to configure +our web application to connect to it. Open a new terminal window, navigate to +this repository's root directory, and run: + +```bash +# If you haven't already, enable yarn workspaces +yarn config set workspaces-experimental true + +# Restore dependencies +yarn install + +# Run our dev command, but with the local orthanc config +yarn run dev:orthanc +``` + +#### Configuration: Learn More + +> For more configuration fun, check out the +> [Essentials Configuration](../configurationFiles.md) guide. + +Let's take a look at what's going on under the hood here. `yarn run dev:orthanc` +is running the `dev:orthanc` script in our project's `package.json` (inside +`platform/app`). That script is: + +```js +cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker-nginx-orthanc.js webpack-dev-server --config .webpack/webpack.pwa.js -w +``` + +- `cross-env` sets three environment variables + - PROXY_TARGET: `/dicom-web` + - PROXY_DOMAIN: `http://localhost:8042` + - APP_CONFIG: `config/docker-nginx-orthanc.js` +- `webpack-dev-server` runs using the `.webpack/webpack.pwa.js` configuration + file. It will watch for changes and update as we develop. + +`PROXY_TARGET` and `PROXY_DOMAIN` tell our development server to proxy requests +to `Orthanc`. This allows us to bypass CORS issues that normally occur when +requesting resources that live at a different domain. + +The `APP_CONFIG` value tells our app which file to load on to `window.config`. +By default, our app uses the file at +`/platform/app/public/config/default.js`. Here is what that +configuration looks like: + +```js +window.config = { + routerBasename: '/', + extensions: [], + modes: [], + showStudyList: true, + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'dcmjs DICOMWeb Server', + name: 'DCM4CHEE', + wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado', + qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + qidoSupportsIncludeField: true, + supportsReject: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: true, + }, + }, + ], + defaultDataSourceName: 'dicomweb', +}; +``` + +### Data Source Configuration Options + +The following properties can be added to the `configuration` property of each data source. + +##### `dicomUploadEnabled` +A boolean indicating if the DICOM upload to the data source is permitted/accepted or not. A value of true provides a link on the OHIF work list page that allows for DICOM files from the local file system to be uploaded to the data source + +:::tip +The [OHIF plugin for Orthanc](https://book.orthanc-server.com/plugins/ohif.html) by default utilizes the DICOM JSON data +source and it has been discovered that only those studies uploaded to Orthanc AFTER the plugin has been installed are +available as DICOM JSON. As such, if the OHIF plugin for Orthanc is desired for studies uploaded prior to installing the plugin, +then consider switching to using [DICOMweb instead](https://book.orthanc-server.com/plugins/ohif.html#using-dicomweb). +::: + +![toolbarModule-layout](../../assets/img/uploader.gif) + +Don't forget to add the customization to the config as well + +```js +customizationService: { + dicomUploadComponent: + '@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent', +}, +``` + + +#### `singlepart` +A comma delimited string specifying which payloads the data source responds with as single part. Those not listed are considered multipart. Values that can be included here are `pdf`, `video`, `bulkdata`, `thumbnail` and `image`. + +For DICOM video and PDF it has been found that Orthanc delivers multipart, while DCM4CHEE delivers single part. Consult the DICOM conformance statement for your particular data source to determine which payload types it delivers. + +To learn more about how you can configure the OHIF Viewer, check out our +[Configuration Guide](../configurationFiles.md). + + +### DICOM PDF +See the [`singlepart`](#singlepart) data source configuration option. + +### DICOM Video +See the [`singlepart`](#singlepart) data source configuration option. + +### BulkDataURI + +The `bulkDataURI` configuration option alters how the datasource uses the +bulkdata end points for retrieving metadata if the data was originally not included in the +response from the server. This is useful for the metadata information that +are big and can/should be retrieved in a separate request. In case the bulkData URI +is relative (instead of absolute) the `relativeResolution` option can be used to +specify the resolution of the relative URI. The possible values are `studies`, `series`. + +The default value is shown below (this will be added if not included in the config). + +```js +bulkDataURI: { + enabled: true, + relativeResolution: 'series', +}, +``` + +The other options allowed are: + +* transform - to take the string and return an updated string +* startsWith and prefixWith - to remove a standard prefix and add an optional prefix + * Used primarily for a reverse proxy or change in URL naming +* relativeResolution - used to set bulkdata paths to studies resolution for incorrect bulkdata paths + +### Running DCM4CHEE + +dcm4che is a collection of open source applications for healthcare enterprise +written in Java programming language which implements DICOM standard. dcm4chee +(extra 'e' at the end) is dcm4che project for an Image Manager/Image Archive +which provides storage, retrieval and other functionalities. You can read more +about dcm4chee in their website [here](https://www.dcm4che.org/) + +DCM4chee installation is out of scope for these tutorials and can be found +[here](https://github.com/dcm4che/dcm4chee-arc-light/wiki/Run-minimum-set-of-archive-services-on-a-single-host) + +An overview of steps for running OHIF Viewer using a local DCM4CHEE is shown +below: + +
+ +
+ +[dcm4chee]: https://github.com/dcm4che/dcm4chee-arc-light +[dcm4chee-docker]: + https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker +[orthanc]: https://www.orthanc-server.com/ +[orthanc-docker]: http://book.orthanc-server.com/users/docker.html +[dicomcloud]: https://github.com/DICOMcloud/DICOMcloud +[dicomcloud-install]: https://github.com/DICOMcloud/DICOMcloud#running-the-code +[osirix]: http://www.osirix-viewer.com/ +[horos]: https://www.horosproject.org/ +[default-config]: + https://github.com/OHIF/Viewers/blob/master/platform/app/public/config/default.js +[html-templates]: + https://github.com/OHIF/Viewers/tree/master/platform/app/public/html-templates +[config-files]: + https://github.com/OHIF/Viewers/tree/master/platform/app/public/config +[storescu]: http://support.dcmtk.org/docs/storescu.html +[webpack-proxy]: https://webpack.js.org/configuration/dev-server/#devserverproxy diff --git a/platform/docs/docs/configuration/dataSources/introduction.md b/platform/docs/docs/configuration/dataSources/introduction.md new file mode 100644 index 0000000..2a0add5 --- /dev/null +++ b/platform/docs/docs/configuration/dataSources/introduction.md @@ -0,0 +1,19 @@ +--- +sidebar_position: 1 +sidebar_label: Introduction +--- + +# Data Source + +The internal data structure of OHIFโ€™s metadata follows naturalized DICOM JSON, a +format pioneered by `dcmjs`. In short DICOM metadata headers with DICOM Keywords +instead of tags and sequences as arrays, for easy development and clear code. + +Here in this section we will discuss couple of data sources that are commonly used +and OHIF has provided the implementation for them. + +## Custom Data Source +Do you have a custom data source? or a custom data that you want to use in OHIF? +You can easily write a data source to map your data to OHIFโ€™s native format. + +You can read more in the [Data Source Module](../../platform/extensions/modules/data-source.md) diff --git a/platform/docs/docs/configuration/dataSources/static-files.md b/platform/docs/docs/configuration/dataSources/static-files.md new file mode 100644 index 0000000..ec460db --- /dev/null +++ b/platform/docs/docs/configuration/dataSources/static-files.md @@ -0,0 +1,100 @@ +--- +sidebar_position: 5 +sidebar_label: Static Files +--- + + +# Static DICOMweb Files for Enhanced Performance + +This section describes how to generate and serve static DICOMweb files, significantly improving the performance of your OHIF Viewer setup. These files are pre-processed and compressed, minimizing storage space and reducing serving time to the bare minimum (disk read and HTTP stream write). + +## Static-DICOMWeb Project + +The core tool for this process is the `static-wado` project, available on GitHub: + +[static-wado]: https://github.com/RadicalImaging/Static-DICOMWeb + +This project contains two main components: + +* **`static-wado-creator`:** Converts raw DICOM files into a DICOMweb-compliant directory structure, optimizing them for efficient serving. +* **`static-wado-webserver`:** A simple web server specifically designed to serve the generated static DICOMweb files. + +## Prerequisites + +- Node.js and npm (or yarn) installed on your system. + +## Installation + +1. **Clone the Repository:** + + ```bash + git clone https://github.com/RadicalImaging/Static-DICOMWeb + cd Static-DICOMWeb + ``` + +2. **Install Dependencies:** + + ```bash + yarn install + ``` + +## Generating Static DICOMweb Files + +1. **Prepare your DICOM data:** Organize your DICOM files into a directory. For example `/Users/alireza/dicom/test-static-script/ACRIN-CT`. + +2. **Convert to DICOMweb Structure:** + Use the `mkdicomweb.js` script from the `static-wado-creator` package to create the DICOMweb directory: + + ```bash + node packages/static-wado-creator/bin/mkdicomweb.js '/Users/alireza/dicom/test-static-script/ACRIN-CT' -o '/Users/alireza/dicom/test-static-script/output' + ``` + + * **Replace:** + * `/Users/alireza/dicom/test-static-script/ACRIN-CT` with the path to your directory of DICOM files. + * `/Users/alireza/dicom/test-static-script/output` with your desired output directory for the DICOMweb structure. + + This command will generate a directory structure similar to this in your output location: + + ![alt text](../../assets/img/static-dicom-web.png) + +## Serving Static Files with the Web Server + +1. **Start the Server:** + + Run the `dicomwebserver.mjs` script from the `static-wado-webserver` package, specifying the port and the DICOMweb directory: + + ```bash + node packages/static-wado-webserver/bin/dicomwebserver.mjs -p 3001 -o /Users/alireza/dicom/test-static-script/output + ``` + + * **`-p 3001`:** Sets the server to listen on port 3001. You can change this if needed. + * **`-o /Users/alireza/dicom/test-static-script/output`:** Specifies the path to your generated DICOMweb directory. + + :::info + The `-p` (port) and `-o` (output directory) flags are used to configure the server. + ::: + +## Running OHIF Viewer with Static Data + +1. **Use the `local_static.js` Configuration:** + + Start the OHIF Viewer in development mode using the provided `local_static.js` configuration file: + + ```bash + yarn dev:static + ``` + +2. **Configuration Details:** + + The `local_static.js` configuration file is pre-configured to point to: + + ```js + qidoRoot: 'http://localhost:3001/dicomweb', + wadoRoot: 'http://localhost:3001/dicomweb', + ``` + + This matches the default port (3001) used by the `static-wado-webserver`. + + :::info + If you change the port or output directory when running the `static-wado-webserver`, you **must** also update the `qidoRoot` and `wadoRoot` values in your `local_static.js` configuration file accordingly to ensure the OHIF Viewer can access the data. + ::: diff --git a/platform/docs/docs/configuration/tour-demo.gif b/platform/docs/docs/configuration/tour-demo.gif new file mode 100644 index 0000000..f351f75 Binary files /dev/null and b/platform/docs/docs/configuration/tour-demo.gif differ diff --git a/platform/docs/docs/configuration/tours.md b/platform/docs/docs/configuration/tours.md new file mode 100644 index 0000000..3408000 --- /dev/null +++ b/platform/docs/docs/configuration/tours.md @@ -0,0 +1,133 @@ +--- +sidebar_position: 3 +sidebar_label: Tours +--- + +# Configuring Tours in OHIF with Shepherd.js + +In OHIF, you can configure guided tours for users by leveraging [Shepherd.js](https://shepherdjs.dev/), a JavaScript library for building feature tours. This page explains how you can define and customize these tours within your app configuration file. + +## Overview + +Tours allow you to provide step-by-step guidance to users, explaining different features of your mode/extension or the viewer. Each tour is associated with a route and consists of several steps, each guiding the user through specific interactions in the viewer. + +### Adding a Tour to your Configuration + +Here's how you can add a tour to your configuration file: + +```javascript +window.config = { + customizationService: { + 'ohif.tours': { + $set: [ + { + id: 'basicViewerTour', + route: '/viewer', + steps: [ + { + id: 'zoom', + title: 'Zooming In and Out', + text: 'You can zoom the images using the right click.', + attachTo: { + element: '.viewport-element', + on: 'left', + }, + advanceOn: { + selector: '.cornerstone-viewport-element', + event: 'CORNERSTONE_TOOLS_MOUSE_UP', + }, + }, + ], + }, + ], + }, + }, +}; +``` + + +## Explanation of Parameters + +### `tours` Array + +Each item in the `tours` array defines a specific tour for a particular route. The object contains the following properties: + +- **`id`**: A unique identifier for the tour. This helps in tracking whether the tour has been shown. +- **`route`**: The route in the application where the tour is applicable. When the user navigates to this route, the tour can automatically trigger if it hasn't been shown before. +- **`steps`**: An array of steps that define the individual guide elements in the tour. Each step corresponds to a UI element and guides the user through interactions. +- **`tourOptions`**: An object that allows you to configure the overall behavior of the tour, such as using a modal overlay or defining default step options. + +### `steps` Array + +Each step defines a part of the tour. Here's a breakdown of the properties you can define: + +- **`id`**: A unique identifier for the step within the tour. +- **`title`**: The title of the step, which appears at the top of the tooltip for the step. +- **`text`**: The content or description of the step, explaining what the user needs to do or understand. +- **`attachTo`**: Specifies where the step should be attached in the DOM. It includes: + - `element`: A string selector or a DOM element that the step should attach to. + - `on`: Specifies the position of the tooltip relative to the element (e.g., 'top', 'left', 'bottom', 'right'). +- **`advanceOn`**: Defines an event that will automatically advance the tour to the next step. This is useful for actions like clicking a button or scrolling. + - `selector`: The CSS selector for the element that triggers the advance. + - `event`: The event name that advances the step, this can be a OHIF service event, or a cornerstone event, or any native JS event (e.g., 'click', 'CORNERSTONE_TOOLS_MOUSE_WHEEL'). +- **`beforeShowPromise`**: A function that returns a promise. When the promise resolves, the rest of the show logic for the step will execute. You can use this to ensure that the target element is ready before the step shows. + +### `tourOptions` + +The `tourOptions` object allows you to configure the overall behavior of the tour. Here's a breakdown of the available properties: + +- **`useModalOverlay`**: A boolean that, if set to `true`, places the tour steps above a darkened modal overlay. The overlay creates an opening around the target element so it can remain interactive. +- **`defaultStepOptions`**: Default options that apply to all steps in the tour. You can override these in individual steps. The following are some options available: + - `buttons`: An array of button objects that appear in the footer of each step. Each button can trigger actions like advancing the tour or skipping it. For example: + - **`text`**: The label text on the button. + - **`action`**: A function to execute when the button is clicked. You can advance the tour using `this.next()`, or complete it using `this.complete()`. + - **`secondary`**: A boolean that, when set to `true`, styles the button as secondary (often for actions like skipping). + +### `floatingUIOptions` + +You can define positioning options for the steps using **Floating UI** middleware. This helps control how the steps are positioned, especially near the browser edges. + +For example, you can ensure that the steps maintain a margin of 24px from the viewport edges by configuring `preventOverflow` middleware: + +```javascript +floatingUIOptions: { + middleware: [ + preventOverflow({ padding: 24 }), + flip(), // Allows the step to flip if it is overflowing + ] +} +``` + +### Shepherd.js Lifecycle Events + +Each step and tour can have lifecycle events like `show`, `hide`, `complete`, or `cancel`. These events allow you to hook into the tourโ€™s lifecycle to perform actions when certain events are triggered. + +For example: + +```javascript +when: { + show() { + console.log('Step shown!'); + }, + hide() { + console.log('Step hidden.'); + } +} +``` + +## Customizing Your Tour + +Once you have a basic tour in place, you can extend it with more advanced features like custom scrolling behavior, dynamic elements, and event-based step advancement. For more details, check out the [Shepherd.js documentation](https://shepherdjs.dev/). + +## Licensing +All versions below 14.0 for Shepherd.JS is under the MIT license, if you wish to use any version above 14.0, you can visit the ShepherdJS website to learn about their pricing and plans [Shepherd.js](https://www.shepherdjs.dev/) + +[LICENSE](https://github.com/shipshapecode/shepherd?tab=License-1-ov-file#readme) + +## Demo + +![Tour Demo]() + +## Conclusion + +By leveraging **Shepherd.js**, you can provide users with interactive and informative guided tours of the viewer. This can greatly improve the user experience and help users understand how to use key features. diff --git a/platform/docs/docs/configuration/url.md b/platform/docs/docs/configuration/url.md new file mode 100644 index 0000000..cac7e5a --- /dev/null +++ b/platform/docs/docs/configuration/url.md @@ -0,0 +1,208 @@ +--- +sidebar_position: 3 +sidebar_label: URL +--- + +# URL + +You can modify the URL at any state of the app to get the desired result. Here +are different part of the APP that you can modify: + + +## WorkList + +The WorkList can be modified by adding the following query parameters: + +### PatientName + +The patient name can be modified by adding the `PatientName` query parameter. + +```js +/?patientName=myQuery +``` + +### MRN + +The MRN can be modified by adding the `MRN` query parameter. + +```js +/?mrn=myQuery +``` + +### Description + +The description can be modified by adding the `Description` query parameter. + +```js +/?description=myQuery +``` + +### Modality + +The modality can be modified by adding the `modalities` query parameter. + +```js +/?modalities=MG +``` + +### Accession Number + +The accession number can be modified by adding the `accession` query parameter. + +```js +/?accession=myQuery +``` + +### DataSources + +If you happen to have multiple data sources configured, you can filter the +WorkList by adding the `dataSources` query parameter. + +```js +/?dataSources=orthanc +``` + +Note1: You should pass the `sourceName` of the data source in the configuration file (not the friendly name nor the name) +Note2: Make sure that the configuration file you are using actually includes that data source. You cannot use a data source from another configuration file. + + +:::tip + +You can add `sortBy` and `sortDirection` query parameters to sort the WorkList + +```js +/?patientName=myquery&sortBy=studyDate&sortDirection=ascending +``` + +::: + + +## Viewer + +The Viewer can be modified by adding the following query parameters: + + +### Mode + +As you have seen before, the Viewer can be configured to be in different modes. +Each mode registers their `id` in the URL. + +For instance + +```js +/viewer?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339 +``` + +will open the viewer in the basic (longitudinal) mode with the StudyInstanceUID +1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339. + +And if configured, the same study can be opened in the `tmtv` mode + +```js +/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339 +``` + +### StudyInstanceUIDs + +You can open more than one study in the Viewer by adding the `StudyInstanceUIDs` + + +```js +/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095722.1&StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095258.1 +``` + +:::tip + +You can use this feature to open a current and prior study in the Viewer. +Read more in the [Hanging Protocol Module](../platform/extensions/modules/hpModule.md#matching-on-prior-study-with-uid) section. You can also use commas to separate +values. + +::: + + +### SeriesInstanceUIDs + +Sometimes you need to only retrieve a specific series in a study, you can do +that by providing series level QIDO query parameters in the URL such as +SeriesInstanceUIDs. This does NOT work with instance or study +level parameters. For example: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8 +``` + +This will only open the viewer with one series (one displaySet) loaded, and no +queries made for any other series. + +Sometimes you need to only retrieve a subset of series in a study, you can do +that by providing more than one series, separated by commas. For example: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8,1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10 +``` + +This will only open the viewer with two series (two displaySets) loaded, and no +queries made for any other series. + +### initialSeriesInstanceUID + +Alternatively, sometimes you want to just open the study on a specified series, but allowing other +series to be present too. This is the same behavior can be +achieved by using the `initialSeriesInstanceUID` parameter. For example: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&initialSeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8 +``` + +This will open all the series in the study, but the viewer will start with the +series specified by the `initialSeriesInstanceUID` parameter. + + +Note that you can combine these, if you want to load a specific set of series +plus show an initial one as the first one selected, for example: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8,1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10&initialSeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10 +``` + +### initialSopInstanceUID + +You can also specify the initial SOP Instance to be displayed by using the +`initialSopInstanceUID` parameter. For example: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8&initialSopInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095501.9 +``` + +This will open the study with the filtered series, and navigate to the slice 101 +which happens to be the SOP Instance specified by the `initialSopInstanceUID` + +Note: again you can mix and match + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8,1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10&initialSeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10&initialSopInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095510.8 +``` + +You can even load the whole study and only specify the initial SOP Instance to be displayed. Although +it will take more time to match, but it works as expected. + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&initialSopInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095510.8 +``` + +### hangingProtocolId + +You can select the initial hanging protocol to apply by using the +hangingProtocolId parameter. The selected parameter must be available in a +hangingProtocolModule registration, but does not have to be active. + +For instance for loading a specific study in mpr mode from start you can use: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingProtocolId=@ohif/mnGrid +``` + +### token + +Although not recommended, you can use the token param in the URL which will inject +the token into the Authorization header of the request. diff --git a/platform/docs/docs/conformance.md b/platform/docs/docs/conformance.md new file mode 100644 index 0000000..ac34b46 --- /dev/null +++ b/platform/docs/docs/conformance.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 12 +sidebar_label: DICOM Conformance Statement +title: DICOM Conformance Statement +--- + +You can find a version that has been open sourced by Radical Imaging [in this link](https://docs.google.com/document/d/1hbDlUApX4svX33gAUGxGfD7fXXZNaBsX0hSePbc-hNA/edit?usp=sharing) diff --git a/platform/docs/docs/deployment/_category_.json b/platform/docs/docs/deployment/_category_.json new file mode 100644 index 0000000..534be1d --- /dev/null +++ b/platform/docs/docs/deployment/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Deployment", + "position": 3 +} diff --git a/platform/docs/docs/deployment/authorization.md b/platform/docs/docs/deployment/authorization.md new file mode 100644 index 0000000..b3589b1 --- /dev/null +++ b/platform/docs/docs/deployment/authorization.md @@ -0,0 +1,95 @@ +--- +sidebar_position: 6 +sidebar_label: Auth +--- + +# Authorization and Authentication +The OHIF Viewer can be configured to work with authorization servers that support one or more of the OpenID-Connect authorization flows. The Viewer finds it's OpenID-Connect settings on the oidc configuration key. You can set these values in your configuration files. For instance you can take a look at our +`google.js` configuration file. + + +```js +oidc: [ + { + // ~ REQUIRED + authority: 'https://accounts.google.com', + client_id: '723928408739-k9k9r3i44j32rhu69vlnibipmmk9i57p.apps.googleusercontent.com', + redirect_uri: '/callback', + response_type: 'id_token token', + scope: 'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid + // ~ OPTIONAL + post_logout_redirect_uri: '/logout-redirect.html', + revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=', + automaticSilentRenew: true, + revokeAccessTokenOnSignout: true, + }, +], +``` + +You need to provide the following information: +- authority: The URL of the authorization server. +- client_id: The client id of your application (provided by the authorization server). +- redirect_uri: The callback URL of your application. +- response_type: The response type of the authorization flow (e.g. id_token token, [learn more about different flows](https://darutk.medium.com/diagrams-of-all-the-openid-connect-flows-6968e3990660)). +- scope: The scopes that your application needs to access +- post_logout_redirect_uri: The URL that the user will be redirected to after logout. +- revoke_uri: The URL that the user will be redirected to after logout. +- automaticSilentRenew: If true, the user will be automatically logged in after the token expires. +- revokeAccessTokenOnSignout: If true, the access token will be revoked on logout. + + + +## How it works +The Viewer uses the `userAuthenticationService` to set the OpenID-Connect settings. The `userAuthenticationService` is a singleton service that is responsible for authentication and authorization. It is initialized by the app and you can grab it +from the `servicesManager` + +```js +const userAuthenticationService = servicesManager.services.userAuthenticationService; +``` + +Then the userAuthenticationService will inject the token as Authorization header in the requests that are sent to the server (both metadata +and pixelData). + +## Token based authentication in URL +Sometimes (although not recommended), some servers like to send the token +in the query string. In this case, the viewer will automatically grab the token from the query string +and add it to the userAuthenticationService and remove it from the query string (to prevent it from being logged in the console +in future requests). + +and example would be + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.2.3.4.5.6.6.7&token=e123125jsdfahsdf +``` + + + +## Implicit Flow vs Authorization Code Flow + +The Viewer supports both the Implicit Flow and the Authorization Code Flow. The Implicit Flow is the default currently, as it is easier to set up and use. However, you can opt for better security by using the Authorization Code Flow. To do so, add `useAuthorizationCodeFlow` to the configuration and change the `response_type` from `id_token token` to `code`. + +Read more about Implicit Flow vs Authorization Code Flow [here](https://documentation.openiddict.com/guides/choosing-the-right-flow.html#:~:text=The%20implicit%20flow%20is%20similar,when%20using%20response_mode%3Dform_post%20) and [here](https://medium.com/@alysachan830/the-basics-of-oauth-2-0-authorization-code-implicit-flow-state-and-pkce-ed95d3478e1c) + +```js +oidc: [ + { + authority: 'https://accounts.google.com', + client_id: '723928408739-k9k9r3i44j32rhu69vlnibipmmk9i57p.apps.googleusercontent.com', + redirect_uri: '/callback', + scope: 'email profile openid', + post_logout_redirect_uri: '/logout-redirect.html', + revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=', + revokeAccessTokenOnSignout: true, + automaticSilentRenew: true, + // CHANGE THESE ***************************** + response_type: 'code', + useAuthorizationCodeFlow: true, + }, +], +``` + +In fact, since browsers are blocking third-party cookies, the Implicit Flow will cease functioning in the future (not specific to OHIF). Read more [here](https://support.okta.com/help/s/article/FAQ-How-Blocking-Third-Party-Cookies-Can-Potentially-Impact-Your-Okta-Environment?language=en_US). It is recommended to use the Authorization Code Flow and begin migrating to it. + +:::note +For the Authorization Code Flow, when authenticating against Google, you must add the `client_secret` to the configuration as well. Unfortunately, this seems to occur only with Google. +::: diff --git a/platform/docs/docs/deployment/azure.md b/platform/docs/docs/deployment/azure.md new file mode 100644 index 0000000..219532d --- /dev/null +++ b/platform/docs/docs/deployment/azure.md @@ -0,0 +1,176 @@ +--- +sidebar_position: 12 +--- + +# Microsoft Azure + +This guide explains how to configure a DICOM datasource in OHIF using Azure Healthcare APIs. It focuses on the configuration details and parameters necessary for integration. + +--- + +## Configuring Azure Healthcare APIs as a DICOMweb Data Source + +Follow these steps to set up Azure as a DICOM datasource for the OHIF Viewer. + +--- + +### Azure AD Registration: + +1. Navigate to the Azure Portal. +2. Select **"Azure Active Directory"** > **"App registrations"** > **"New registration"**. +3. Name your application. +4. Under **"Supported account types"**, select **"Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)"**. +5. Enter the following values in your redirect URI tab: + + ![Redirect URI](../assets/img/azure4.png) + +--- + +### API Permissions: + +1. Under your registered application, go to **"API permissions"**. +2. Click **"Add a permission"**. +3. Choose the Azure API for DICOM (**Dicom.ReadWrite**). If you can't find it, refer to the "Configure Azure DICOMWEB Service" section and then return to this step. + + ![API Permissions](../assets/img/azure1.png) + +--- + +### Authentication: + +1. Under **"Authentication"**, check the **"ID tokens"** box since we are using OpenID Connect. + +--- + +### App Client ID and Tenant ID: + +1. Copy your app client ID and tenant ID to prepare for use in configuring an OHIF datasource. + +--- + +### Consent: + +1. The first time a user logs in, they will be prompted to consent to the permissions your application has requested. +2. Once they grant consent, your application can use the obtained access token to call the specific Microsoft API on behalf of the user. + + ![Consent](../assets/img/azure5.png) + +--- + +### Configure Azure DICOMWEB Service: + +1. **Create a Health Data Services workspace**: + + ![Create Workspace](../assets/img/azure6.png) + +2. Visit the newly created workspace and press **"Deploy DICOM Service"**: + + ![Deploy DICOM Service](../assets/img/azure7.png) + +3. After the DICOM service is deployed, visit the **"CORS headers"** section: + + ![CORS Headers](../assets/img/azure8.png) + +4. Set the headers and origins to `*` and specify the HTTP methods you'd like to use: + + ![Set Headers](../assets/img/azure9.png) + +5. Save the changes. + +6. Add the Microsoft emails of the users you'd like to grant access to your DICOM service in the **"Access control"** section and assign them the **"DICOM Data Owner"** role (or other roles depending on your requirements): + + ![Access Control](../assets/img/azure10.png) + +7. Copy your DICOM service URL to prepare it for usage in OHIF as a datasource: + + ![DICOM Service URL](../assets/img/azure3.png) + +8. Upload your DICOM files to your service. + +--- + +## 1. Configure OIDC Authentication + +Azure uses OpenID Connect (OIDC) for authentication. Update the OIDC section in your configuration file with the following parameters: + +```json +"oidc": [ + { + "redirect_uri": "/callback", + "response_type": "id_token token", + "scope": "openid https://dicom.healthcareapis.azure.com/Dicom.ReadWrite", + "post_logout_redirect_uri": "/logout-redirect.html", + "automaticSilentRenew": false, + "revokeAccessTokenOnSignout": true, + "loadUserInfo": false, + "authority": "https://login.microsoftonline.com/{tenant-id}/v2.0/", + "client_id": "{client-id}" + } +] +``` + +#### Parameters: +- **redirect_uri**: The URL where users are redirected after successful authentication. +- **response_type**: Specifies the authentication response type (id_token and token). +- **scope**: Defines the level of access. Use `Dicom.ReadWrite` to allow read and write access to DICOM data. +- **post_logout_redirect_uri**: The URL users are redirected to after logout. +- **automaticSilentRenew**: Automatically renews tokens without user interaction. Set to `false` for manual renewal. +- **revokeAccessTokenOnSignout**: Revokes access tokens upon logout for added security. +- **loadUserInfo**: Disables loading additional user information; set to `false` for Azure as it is not supported. +- **authority**: The Azure AD tenant URL for OIDC authorization. +- **client_id**: The applicationโ€™s client ID from Azure AD. + +--- + +## 2. Add the Data Source Configuration + +Update the data source configuration file with your Azure Healthcare APIs details: + +```json +{ + "namespace": "@ohif/extension-default.dataSourcesModule.dicomweb", + "sourceName": "ohif_azure", + "friendlyName": "ohif_azure", + "configuration": { + "singlepart": "bulkdata,pdf,video", + "imageRendering": "wadors", + "thumbnailRendering": "wadors", + "supportsWildcard": true, + "enableStudyLazyLoad": true, + "supportsFuzzyMatching": false, + "supportsStow": true, + "qidoRoot": "https://{your-dicom-instance}.dicom.azurehealthcareapis.com/v2", + "wadoUriRoot": "https://{your-dicom-instance}.dicom.azurehealthcareapis.com/v2", + "wadoRoot": "https://{your-dicom-instance}.dicom.azurehealthcareapis.com/v2" + } +} +``` + +#### Parameters: +- **qidoRoot**: Base URL for QIDO-RS queries. +- **wadoUriRoot**: Base URL for WADO-URI requests. +- **wadoRoot**: Base URL for WADO-RS requests. + +--- + +## 3. Running the Viewer with Azure Configuration + +1. Save the above configurations in your OHIF Viewer configuration file. +2. Run the viewer: + + ```bash + cd OHIFViewer + yarn install + APP_CONFIG=config/azure.js yarn run dev + ``` + + Replace `config/azure.js` with the path to your configuration file. + +--- + +### Additional Notes +- Ensure that the Azure Healthcare API is enabled for your subscription and that the necessary permissions (e.g., `Dicom.ReadWrite`) are assigned to the OIDC client. +- The `qidoRoot`, `wadoUriRoot`, and `wadoRoot` should point to your Azure DICOM service URL. Replace `{your-dicom-instance}` with your actual instance name. + +This setup allows OHIF to interact seamlessly with Azure's Healthcare APIs, enabling robust DICOM management and visualization. + diff --git a/platform/docs/docs/deployment/build-for-production.md b/platform/docs/docs/deployment/build-for-production.md new file mode 100644 index 0000000..dbd89a8 --- /dev/null +++ b/platform/docs/docs/deployment/build-for-production.md @@ -0,0 +1,130 @@ +--- +sidebar_position: 2 +--- + +# Build for Production + +### Build Machine Requirements + +- [Node.js & NPM](https://nodejs.org/en/download/) +- [Yarn](https://yarnpkg.com/lang/en/docs/install/) +- [Git](https://www.atlassian.com/git/tutorials/install-git) + +### Getting the Code + +_With Git:_ + +```bash +# Clone the remote repository to your local machine +git clone https://github.com/OHIF/Viewers.git +``` + +More on: _[`git clone`](https://git-scm.com/docs/git-clone), +[`git checkout`](https://git-scm.com/docs/git-checkout)_ + +_From .zip:_ + +[OHIF/Viewers: master.zip](https://github.com/OHIF/Viewers/archive/master.zip) + +### Restore Dependencies & Build + +Open your terminal, and navigate to the directory containing the source files. +Next run these commands: + +```bash +# If you haven't already, enable yarn workspaces +yarn config set workspaces-experimental true + +# Restore dependencies +yarn install + +# Build source code for production +yarn run build +``` + +If everything worked as expected, you should have a new `dist/` directory in the +`platform/app/dist` folder. It should roughly resemble the following: + +```bash title="platform/app/dist/" +โ”œโ”€โ”€ app-config.js +โ”œโ”€โ”€ app.bundle.js +โ”œโ”€โ”€ app.css +โ”œโ”€โ”€ index.html +โ”œโ”€โ”€ manifest.json +โ”œโ”€โ”€ service-worker.js +โ””โ”€โ”€ ... +``` + +By default, the build output will connect to OHIF's publicly accessible PACS. If +this is your first time setting up the OHIF Viewer, it is recommended that you +test with these default settings. After testing, you can find instructions on +how to configure the project for your own imaging archive below. + +### Configuration + +The configuration for our viewer is in the `platform/app/public/config` +directory. Our build process knows which configuration file to use based on the +`APP_CONFIG` environment variable. By default, its value is +[`config/default.js`][default-config]. The majority of the viewer's features, +and registered extension's features, are configured using this file. + +The easiest way to apply your own configuration is to modify the `default.js` +file. For more advanced configuration options, check out our +[configuration essentials guide](../configuration/configurationFiles.md). + +## Next Steps + +### Deploying Build Output + +_Drag-n-drop_ + +- [Netlify: Drop](./static-assets#netlify-drop) + +_Easy_ + +- [Surge.sh](./static-assets#surgesh) +- [GitHub Pages](./static-assets#github-pages) + +_Advanced_ + +- [AWS S3 + Cloudfront](./static-assets#aws-s3--cloudfront) +- [GCP + Cloudflare](./static-assets#gcp--cloudflare) +- [Azure](./static-assets#azure) + +### Testing Build Output Locally + +A quick way to test your build output locally is to spin up a small webserver. +You can do this by running the following commands in the `dist/` output +directory: + +```bash +# Install http-server as a globally available package +yarn global add http-server + +# Change the directory to the platform/app +cd platform/app + +# Serve the files in our current directory +npx serve ./dist -c ../public/serve.json +``` + + + +### Automating Builds and Deployments + +If you found setting up your environment and running all of these steps to be a +bit tedious, then you are in good company. Thankfully, there are a large number +of tools available to assist with automating tasks like building and deploying +web application. For a starting point, check out this repository's own use of: + +- [CircleCI][circleci]: [config.yaml][circleci-config] +- [Netlify][netlify]: [netlify.toml][netlify.toml] | + [build-deploy-preview.sh][build-deploy-preview.sh] + + +[circleci]: https://circleci.com/gh/OHIF/Viewers +[circleci-config]: https://github.com/OHIF/Viewers/blob/master/.circleci/config.yml +[netlify]: https://app.netlify.com/sites/ohif/deploys +[netlify.toml]: https://github.com/OHIF/Viewers/blob/master/platform/app/netlify.toml +[build-deploy-preview.sh]: https://github.com/OHIF/Viewers/blob/master/.netlify/build-deploy-preview.sh + diff --git a/platform/docs/docs/deployment/cors.md b/platform/docs/docs/deployment/cors.md new file mode 100644 index 0000000..7035ab2 --- /dev/null +++ b/platform/docs/docs/deployment/cors.md @@ -0,0 +1,144 @@ +--- +sidebar_position: 8 +--- + +# Cross-Origin Information for OHIF + +This document describes various security configurations, settings and environments/contexts needed to fully leverage OHIFโ€™s capabilities. One may need some configurations while others might need ALL of them - it all depends on the environment OHIF is expected to run in. + +In particular, three of OHIFโ€™s features depend on these configurations: +- [Embedding OHIF in an iframe](#embedding-ohif-in-an-iframe) +- [XMLHttpRequests to fetch data from data sources](#cors-in-ohif) + + +## Embedding OHIF in an iframe + +As described [here](./iframe.md), there are cases where OHIF will be embedded in an iframe. The following links provide more information for setting up and configuring OHIF to work in an iframe: + +- [OHIF iframe documentation](./iframe.md#static-build) +- [OHIF as a Cross-origin Resource in an iframe](#ohif-as-a-cross-origin-resource-in-an-iframe) + +## Secure Context + +MDN defines a secure context as [โ€œa Window or Worker for which certain minimum standards of authentication and confidentiality are met.โ€œ](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) + +Any local URL is considered secure. The following are some examples of local URLs that are considered secureโ€ฆ +- http://localhost +- http://127.0.0.1:3000 + +URLs that are NOT local must be delivered over `https://` or `wss://` (i.e. TLS) to be considered secure. See [When is a context considered secure](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure) in MDN for more information. + +### iframes + +A page embedded in an iframe is considered secure if it itself and every one of its embedding ancestors are delivered securely. Otherwise it is deemed insecure. + + +### Configuring/setting up a secure context + +[Local URLs are considered secure](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure), and as such whenever OHIF is accessed via a local URL (e.g. http://localhost:3000) it is running in a secure context. For example, in a development environment using the default webpack setup, OHIF can be deployed and accessed in a secure context at http://localhost:3000. + +The best alternative is to host OHIF over HTTPS. + +:::tip +OHIF can be served over HTTPS in a variety of ways (these are just some examples). +- Website hosting services that offer HTTPS deployment (e.g,. Netlify) or offer HTTPS load balancers (AWS, Google Cloud etc.) +- Setting up a reverse proxy (e.g. `nginx`) with a self-signed certificate that forwards requests to the OHIF server + - [An OHIF Docker image can be set up this way](./docker/docker.md#ssl). +::: + +## Origin Definition + +According to [MDN](https://developer.mozilla.org/en-US/docs/Glossary/Origin), a Web contentโ€™s origin is defined by the scheme (protocol), hostname (domain), and port of the URL used to access it. Two objects have the same origin only when the scheme, hostname, and port all match. + +## CORS - Cross-Origin Resource Sharing + +A cross-origin resource is a resource (e.g. image, JSON, etc) that is served by one origin and used/referenced by a different origin. + +CORS is the protocol utilized by web servers and browsers whereby a server of one origin identifies and/or restricts which of its resources that other origins (i.e. other than its own) a browser should allow access to. By default a browser does not permit cross-origin resource sharing. + +The CORS mechanism relies on the HTTP response headers from the server to indicate if a resource can be shared with a different origin. + +See the [MDN CORS article](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) for more information. + +### CORS HTTP Headers + +The header that mostly concerns OHIF is listed below and should be configured accordingly on the DICOMweb server or any data source that OHIF would make XMLHttpRequests to for its data. + +```http +Access-Control-Allow-Origin: `` | * +``` + +:::tip +The `Access-Control-Allow-Origin` header specifies which origins can access the served resource embedded in the response. + +Either a single, specific origin (i.e. ``) can be specified or ALL origins (i.e. *) + +See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-allow-origin) for more information. +::: + +### CORS in OHIF + +OHIF fetches and displays data and images from data sources. It invokes XMLHttpRequests to some data sources such as DICOMweb data sources to fetch the information to render. Typically, a DICOMweb server is hosted on a completely different origin than the one serving OHIF. As such, those XMLHttpRequests use CORS. + +### Troubleshooting CORS in OHIF + +The following is an example screenshot of the browser console when one of OHIFโ€™s DICOMweb data source servers is not configured for CORS. + +![CORS browser console errors](../assets/img/cors-browser-console-errors.png) + +And the following is what is in the accompanying network tab. + +![CORS browser network panel errors](../assets/img/cors-network-panel-errors.png) + +:::info +Setting the appropriate CORS header varies per server or service that is hosting the data source. What follows below is just one example to remedy the problem. +::: + +:::tip +If Orthanc is the data source running in a Docker container composed with/behind nginx. And OHIF is being served at localhost:3000. The issue can be remedied by adding either of the following to Orthancโ€™s Docker container nginx.conf file. + +```nginx +add_header 'Access-Control-Allow-Origin' 'http://localhost:3000' always; +``` + +Or + +```nginx +add_header 'Access-Control-Allow-Origin' '*' always; +``` +::: + + + + +### Header Values (see [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy#usage) for more information) + +|Value|Description| +|-----|-----------| +|same-site|Only requests from the same site can read the resource.| +|same-origin|Only requests from the same origin can read the resource.| +|cross-origin|Requests from any origin can read the resource. The value is useful and [exists](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy#relationship_to_cross-origin_embedder_policy_coep) primarily for letting documents with the [COEP require-corp value](#header-values-pertinent-to-ohif-see-mdn-for-more-information-1) know that the resource is ok to be embedded| + +### OHIF and CORP + + +#### PDF from a Cross Origin DICOMweb Data Source + +There are some DICOMweb data sources (e.g. dcm4chee) whereby OHIF uses the data sourceโ€™s `/rendered` endpoint to embed a DICOM PDF document in the OHIF DOM using an `` tag. + +As specified for the [COEP require-corp value](#header-values-pertinent-to-ohif-see-mdn-for-more-information-1), a page like OHIF with COEP header `require-corp` can embed cross-origin resources in DOM elements that have the [`crossorigin` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin) OR the resource is delivered with an appropriate CORP header. The `` tag does NOT support the `crossorigin` attribute. As such, the PDF must be delivered with a CORP header. + +:::tip +Setting the CORP header varies per server or service that is hosting the data source. The following is just one example. + +For a dcm4chee DICOMweb data source composed in Docker behind nginx, the CORP header can be configured in the nginx.conf file as such: + +```nginx +add_header 'Cross-Origin-Resource-Policy' 'cross-origin' always; +``` + +If the dcm4chee server and the OHIF server are hosted on the same site, then the following would also work: +```nginx +add_header 'Cross-Origin-Resource-Policy' 'same-site' always; +``` +::: diff --git a/platform/docs/docs/deployment/custom-url-access.md b/platform/docs/docs/deployment/custom-url-access.md new file mode 100644 index 0000000..dfb1ddd --- /dev/null +++ b/platform/docs/docs/deployment/custom-url-access.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 4 +title: Custom URL Access/Build +--- + + +## Build for non-root path + +Sometimes it is desired to access the viewer from a non-root path (e.g., `/my-awesome-viewer` instead of `/`). +You can achieve so by using the `PUBLIC_URL` environment variable AND the `routerBasename` configuration option. + +1. use a config (e.g. `config/myConfig.js`) file that is using the `routerBasename` of your choice `/my-awesome-viewer` (note there is only one / - it is not /my-awesome-viewer/). +2. build the viewer with `PUBLIC_URL=/my-awesome-viewer/` (note there are two / - it is not /my-awesome-viewer). + + +:::tip +The PUBLIC_URL tells the application where to find the static assets and the routerBasename will tell the application how to handle the routes +::: + + +### Testing in Development +For testing the build locally, you can use the following command: + +```bash +# we use default config file, so we assume you have already set the routerBasename to /my-awesome-viewer in the default config as an example +PUBLIC_URL=/my-awesome-viewer/ APP_CONFIG=config/default.js yarn dev +``` + + +### Testing in Build (production) +You need to build the viewer with the following command: + +```bash +PUBLIC_URL=/my-awesome-viewer/ APP_CONFIG=config/default.js yarn build +``` + +We can use the `npx serve` to serve the build folder. There are two things you need to consider however, +1. You need to change the `public/serve.json` file to reflect the new routerBasename in the destination (see the example below) + + +```json +// final serve.json +{ + "rewrites": [{ "source": "*", "destination": "my-awesome-viewer/index.html" }], +} +``` + +```bash +cd platform/app; + +# rename the dist folder to my-awesome-viewer +mv dist my-awesome-viewer + +# serve the folder with custom json, note that we are using ../public/serve.json and NOT public/serve.json +npx serve -c ./public/serve.json +``` + + +:::note +When you want to authenticate against a sub path, there are a few things you should keep in mind: + +1. Set the `routerBasename` to the sub path and also update the `PUBLIC_URL` to match the sub path. +2. Don't forget to modify the `serve.json` file as mentioned earlier. +3. Ensure that the sub path is included in the list of allowed callback URLs. For example, in the Google Cloud dashboard, you can set it in the `Authorized redirect URIs` field under the `Credentials` section of the `APIs & Services` menu. +::: diff --git a/platform/docs/docs/deployment/docker/docker.md b/platform/docs/docs/deployment/docker/docker.md new file mode 100644 index 0000000..e9c7e21 --- /dev/null +++ b/platform/docs/docs/deployment/docker/docker.md @@ -0,0 +1,210 @@ +--- +sidebar_position: 4 +--- + +# Docker + +The OHIF source code provides a [Dockerfile](https://github.com/OHIF/Viewers/blob/master/Dockerfile) to create and run a Docker image that containerizes an [nginx](https://www.nginx.com/) web server serving the OHIF Viewer. + +:::info +This Dockerfile is the same used to generate the [OHIF image(s) on Docker Hub](https://hub.docker.com/r/ohif/app/tags). +::: + + +## Running the Docker Container with our pre-built images from Docker Hub + + +To run the Docker container, use the following command based on whether you're targeting a release or beta version. (Learn more about versioning [here](../../development/getting-started.md#branches).) + +```sh +# beta version +docker run -d -p 3000:80 ohif/app:v3.10.0-beta.33 + +# release version +docker run -d -p 3000:80 ohif/app:v3.9.2 +``` + +This will run the Docker container and serve the OHIF Viewer at `http://localhost:3000`. You can name the container anything you want by adding the `--name` flag (e.g., `docker run -d -p 3000:80 --name ohif-viewer-container ohif/app:v3.10.0-beta.33`). + + +## Building the Docker Image From Source + +:::tip +Building a Docker image comes in handy when OHIF has been customized (e.g. with custom extensions, modes, hanging protocols, etc.). For convenience, there are basic OHIF images built in Docker Hub. Find the latest [release](https://hub.docker.com/r/ohif/app/tags?page=1&name=latest) and [dev](https://hub.docker.com/r/ohif/app/tags?page=1&name=beta) images all in Docker Hub. +::: + +### Prerequisites +The machine on which to build and run the Docker container must have: +1. All of the [requirements](../build-for-production.md#build-for-production) for building a production version of OHIF. +2. A checked out branch of the OHIF Viewer. +3. [Docker](https://docs.docker.com/get-docker/) installed. + +### Building the Docker Image + +:::info +In this tutorial, we will build the Docker image for the OHIF Viewer and OHIF server as defined in the `default.js` config which points to our server and our studies. + +If you need the Viewer to show your own server studies, you need to build the viewer with a custom configuration that points to your server and your studies. + +You can set build arguments to point to your custom configuration file. For more information on data sources, see [here](../../platform/extensions/modules/data-source.md). + +::: + + + + +To build the Docker image from the terminal: + +- Navigate to the OHIF Viewer code root directory (base of the monorepo). +- Run a basic Docker build command: + + ```sh + docker build . -t ohif-viewer-image + ``` + + *Note*: The name `ohif-viewer-image` is an example. You can replace it with any name and tag of your choice by changing the `-t` value (e.g., `-t my-image:latest`). This naming is arbitrary for local Docker images. + +- To customize the build, you can include optional build arguments to set defaults for the app configuration, public path, or port: + + ```sh + docker build . -t ohif-viewer-image \ + --build-arg APP_CONFIG=config/e2e.js \ + --build-arg PUBLIC_URL=/ohif/ \ + --build-arg PORT=6000 + ``` + +#### Available Build Arguments (Optional) +You can use the following build arguments to customize the Docker image: + +- `APP_CONFIG`: (Optional) Sets the default app configuration (e.g., `config/e2e.js`). This value can be overridden later by setting an environment variable (you can set it in the docker run command). +- `PUBLIC_URL`: (Optional) Specifies the public path for serving the OHIF Viewer (e.g., `/ohif/`). This value is baked into the build and cannot be changed without rebuilding the image. +- `PORT`: (Optional) Sets the applicationโ€™s port. + +#### Examples of Using Build Arguments +Here are examples of how to use the `--build-arg` option: + +- Set the public path: + + ```sh + docker build . --build-arg PUBLIC_URL=/ohif/ + ``` + +- Set a custom app configuration: + + ```sh + docker build . --build-arg APP_CONFIG=config/kheops.js + ``` + +- Specify a port: + + ```sh + docker build . --build-arg PORT=6000 + ``` + +- Combine multiple arguments: + + ```sh + docker build . --build-arg PUBLIC_URL=/ohif/ --build-arg APP_CONFIG=config/kheops.js --build-arg PORT=6000 + ``` + +:::info PUBLIC_URL Explanation +The `PUBLIC_URL` build argument sets the public path for serving the OHIF Viewer. For example, using `--build-arg PUBLIC_URL=/ohif/` will serve the worklist at `http://host/ohif/` and the viewer at `http://host/ohif/viewer`. While the worklist is also accessible at `http://host/`, it redirects to the `PUBLIC_URL`. +::: + +--- + +## Running the Docker Container + +After building the Docker image, you can run it as a container using the following command. The name of the Docker image (`ohif-viewer-image`) is specified at the end, while the flags control various runtime settings. + +```sh +docker run -d -p 3000:80/tcp --name ohif-viewer-container ohif-viewer-image +``` + +- `-d`: Runs the container in the background and prints the container ID. + +- `-p {host-port}:{nginx-port}/tcp`: Maps the container's `nginx` port to a port on the host machine. For example, `3000:80` maps host port 3000 to container port 80. + +- `--name`: Assigns an arbitrary name to the container for easy identification (e.g., `ohif-viewer-container`). + + + +### Configuring the `nginx` Listen Port + +The `nginx` server uses the `{PORT}` environment variable to determine the listening port inside the container. By default, this is set to `80`. You can override it during runtime or build: + +#### Setting the Port at Runtime + +Use the `-e PORT={container-port}` flag to set the listening port and publish it with `-p`. For example, the following command sets the container port to `8080` and maps it to host port `3000`: + +```sh +docker run -d -e PORT=8080 -p 3000:8080/tcp --name ohif-viewer-container ohif-viewer-image +``` + +#### Setting the Port During Build + +To bake the port configuration into the Docker image, use the `--build-arg PORT={container-port}` flag when building the image: + +```sh +docker build . --build-arg PORT=8080 +``` + +then you can run the container with the following command: + +```sh +docker run -d -p 3000:8080/tcp --name ohif-viewer-container ohif-viewer-image +``` + +--- + +### Specifying the OHIF Configuration File + +You can specify the OHIF configuration file for the container in three ways: + +1. **[Build Default](#build-default)**: Set the default configuration file during the build process. +2. **[Volume Mounting](#volume-mounting)**: Mount a local configuration file into the container. +3. **[Environment Variable](#environment-variable)**: Pass the configuration file contents directly as an environment variable. + +#### Build Default + +Set the configuration file during the build process using the `--build-arg APP_CONFIG={config-path}` flag. For example: + +```sh +docker build . --build-arg APP_CONFIG=config/kheops +``` + +--- + +#### Volume Mounting + +To use a local configuration file, mount it as a volume during runtime. For example, to use a file located at `/path/to/config/file.js`, use the `-v` flag: + +```sh +docker run -d -p 3000:80/tcp -v /path/to/config/file.js:/usr/share/nginx/html/app-config.js --name ohif-viewer-container ohif-viewer-image +``` + +:::tip +Ensure the path to the local configuration file is absolute, as some Docker versions require it. +::: + +--- + +#### Environment Variable + +Alternatively, you can specify the configuration file contents directly as an environment variable (`APP_CONFIG`). This method is useful in environments like Google Cloud. + +**Important**: The `APP_CONFIG` variable must contain the file's contents, not its file path. Use the `cat` command to read the file and pass its contents as the environment variable: + +```sh +docker run -d -p 3000:80/tcp -e APP_CONFIG="$(cat /path/to/the/config/file)" --name ohif-viewer-container ohif-viewer-image +``` + +:::tip +- Remove single-line comments (`//`) from the configuration file to prevent issues when serving the file to the OHIF client. +- As an alternative to the `cat` command, you can convert the file to a single line and copy-paste it directly. Tools like [Visual Studio Code](https://stackoverflow.com/questions/46491061/shortcut-for-joining-two-lines) and [Notepad++](https://superuser.com/questions/518229/how-do-i-remove-linebreaks-in-notepad) offer "Join Lines" commands to help with this. +- If both the [Volume Mounting](#volume-mounting) and [Environment Variable](#environment-variable) methods are used, the Volume Mounting method takes precedence. +::: + +--- + +This rewrite improves readability by reorganizing information into smaller, clear sections and providing consistent formatting for examples and tips. diff --git a/platform/docs/docs/deployment/docker/ssl.md b/platform/docs/docs/deployment/docker/ssl.md new file mode 100644 index 0000000..1942468 --- /dev/null +++ b/platform/docs/docs/deployment/docker/ssl.md @@ -0,0 +1,127 @@ +--- +sidebar_position: 2 +title: SSL +--- + +# SSL + +:::caution +We make no claims or guarantees regarding this section concerning security. If in doubt, enlist the help of an expert and conduct proper audits. +::: + + +If OHIF is not deployed over SSL, this means information transferred to/from OHIF is not encrypted. Consideration must be given as to whether OHIF should be deployed in a secure context over SSL. + +### Specifying the SSL Port, Certificate and Private Key + +For convenience, the [built Docker image](#building-the-docker-image) can be run over SSL by +- setting the `{SSL_PORT}` environment variable +- volume mounting the SSL certificate +- volume mounting the SSL private key + +:::info +The volume mounted SSL certificate and private key are mapped to the [`ssl_certificate`](http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate) and [`ssl_certificate_key`](http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate_key) `nginx` directives respectively. +::: + +Similar to the [`nginx` listen port](#configuring-the-nginx-listen-port), the `{SSL_PORT}` environment variable is the internal port that `nginx` listens on to serve the OHIF web server over SSL and has to be likewise published via the `-p` switch. + +The following is an example command running the Docker container over SSL. Note that depending on the version of Docker, an absolute path to the certificate and private key files might be required. + +```sh +docker run -d -e SSL_PORT=443 -p 3003:443/tcp -v /path/to/certificate:/etc/ssl/certs/ssl-certificate.crt -v /path/to/private/key:/etc/ssl/private/ssl-private-key.key --name ohif-viewer-container ohif-viewer-image +``` + +:::caution +The above deploys OHIF over SSL using `nginx`'s default SSL configuration. For further OHIF server hardening and security configuration, consider enlisting an expert and then editing OHIF's `nginx` [SSL template configuration file](https://github.com/OHIF/Viewers/blob/8a8ae237d26faf123abeb073cbf0cd426c3e9ef2/.docker/Viewer-v3.x/default.ssl.conf.template) with further [security settings](https://nginx.org/en/docs/http/ngx_http_ssl_module.html) and [tweaks](http://nginx.org/en/docs/http/configuring_https_servers.html) and then [build a new Docker image](#building-the-docker-image) from there. +::: + +:::caution +The private key is a secure entity and should have restricted access. Keep it safe! +::: + +:::caution +The presence of the `{SSL_PORT}` environment variable is used to trigger to deploy over SSL as opposed to HTTP. If `{SSL_PORT}` is NOT defined, then HTTP is used even if the certificate and private key volumes are mounted. +::: + +:::tip +The read and write permissions of the source, mounted volumes are preserved in the Docker container. The volume mounted certificate and private key require read permission. + +One way to ensure both are readable is to issue the following on the host system terminal prior to running the Docker container and mounting the certificate and private key volumes. + +```sh +sudo chmod 644 /path/to/certificate /path/to/private/key +``` +::: + +:::tip +The SSL certificate and private key can be either [CA issued](#ca-signed-certificates) or [self-signed](#self-signed-certificates). +::: + + +### CA Signed Certificates + +According to [SSL.com](https://www.ssl.com/faqs/what-is-a-certificate-authority/), a global certificate authority (CA) is a trusted authority and organization that guarantees the identity of other, third-party entities and guarantees the integrity of the electronic information (e.g. web site data) those third-party entities provide and deliver. + +There are many globally trusted CAs. Below is a non-exhaustive list of some CAs including links to some documentation for creating and installing certificates and keys from those authorities to be used with `nginx`. +- [GoDaddy](https://ca.godaddy.com/help/nginx-install-a-certificate-6722) +- [Let's Encrypt](https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/) +- [digicert](https://www.digicert.com/kb/csr-ssl-installation/nginx-openssl.htm) + + +### Self-Signed Certificates + +According to [Entrust](https://www.entrust.com/resources/faq/what-is-a-self-signed-certificate), a self-signed certificate is one that is NOT signed by a trusted, public [CA authority](#ca-signed-certificates), but instead (typically) signed by the developer or individual or organization responsible for a web site. + +Browsers will treat self-signed certificates as not secure because the signer is not publicly recognized and trusted. When visiting a site encrypted with a self-signed certificate, the browser will present a screen similar to the following warning about the potential risk. + +![Self-signed certificate warning](../../assets/img/self-signed-cert-warning.png) + +For a self-signed certificate this is normal and expected. Clicking the `Advanced` button displays further information as well as a link for proceeding to site that the certificate is encrypting. + +![Self-signed certificate warning](../../assets/img/self-signed-cert-advanced-warning.png) + + +Self-signed certificates might be appropriate for testing or perhaps deploying a site within an organization's internal LAN. In any case, consult an expert prior to deploying OHIF over SSL. + +:::tip +A self-signed certificate can be generated using [`openssl`](https://www.openssl.org/) on the command line. +::: + +To create a self-signed certificate: +1. Open a command prompt. +2. Issue the following command: + ```sh + sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /desired/key/directory/self-signed-private.key -out /desired/cert/directory/self-signed.crt + ``` + + The chart below describes each of the items in the command. + + |Command Item|Description| + |------------|-----------| + |sudo|temporarily grant access as the root/super user to run the `openssl` command| + |openssl|the command line tool for creating and managing certificates and keys| + |req|this together with the subsequent `-x509` indicates to request to generate a self-signed certificate| + |-x509|this together with the `req` indicates to request to generate a self-signed certificate| + |-nodes|skip the option to secure the certificate with a passphrase; this allows `nginx` to start up with without intervention to enter a passphrase each time| + |-days 365|the number of days the certificate will be valid for| + |-newkey rsa:2048|create the a new certificate and key together and make an RSA key that is 2048 bits long| + |-keyout|the path and file name where the private key will be written to| + |-out|the path and file name where the certificate will be written to| + +3. Answer the prompts that follow. The table below lists the various prompts. The default value for each prompt is shown within the square brackets. The most important prompt is `Common Name (e.g. server FQDN or YOUR name)`. For this enter the IP address of the OHIF server being secured. + + |Prompt| + |------| + |Country Name (2 letter code) [AU]| + |State or Province Name (full name) [Some-State]| + |Locality Name (eg, city) []| + |Organization Name (eg, company) [Internet Widgets Pty Ltd]| + |Organizational Unit Name (eg, section) []| + |Common Name (e.g. server FQDN or YOUR name) []| + |Email Address []| + +4. Once completed, the self-signed certificate and private key will be in the locations specified by the `-keyout` and `-out` flags and can be [volume mounted](#specifying-the-ssl-port-certificate-and-private-key) accordingly to the OHIF Docker container. + +:::tip +Windows' users can access `openssl` using [Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/en-us/windows/wsl/). +::: diff --git a/platform/docs/docs/deployment/google-cloud-healthcare.md b/platform/docs/docs/deployment/google-cloud-healthcare.md new file mode 100644 index 0000000..54df8bb --- /dev/null +++ b/platform/docs/docs/deployment/google-cloud-healthcare.md @@ -0,0 +1,139 @@ +--- +sidebar_position: 9 +--- + +# Google Cloud Healthcare + +> The [Google Cloud Healthcare API](https://cloud.google.com/healthcare/) is a +> powerful option for storing medical imaging data in the cloud. + +An alternative to deploying your own PACS is to use a software-as-a-service +provider such as Google Cloud. The Cloud Healthcare API promises to be a +scalable, secure, cost effective image storage solution for those willing to +store their data in the cloud. It offers an +[almost-entirely complete DICOMWeb API](https://cloud.google.com/healthcare/docs/dicom) +which requires tokens generated via the +[OAuth 2.0 Sign In flow](https://developers.google.com/identity/protocols/oauth2). +Images can even be transcoded on the fly if this is desired. + +## Setup a Google Cloud Healthcare Project + +1. Create a Google Cloud account +2. Create a project in Google Cloud + + A project in Google Cloud can be created by clicking the projects drop down box. + + ![Google projects drop down](../assets/img/google-projects-drop-down.png) + + And then clicking the `NEW PROJECT` button in the top-right corner of the + dialogue that is displayed. + +3. Enable the [Cloud Healthcare API](https://cloud.google.com/healthcare/) for your project + + :::tip + An API can be enabled through the `APIs & Services > Enabled APIs & Services` + console and clicking the `+ ENABLE APIS AND SERVICES` button. + + ![Google enable apis](../assets/img/google-enable-apis.png) + ::: + + :::tip + The principal (i.e. account) that is enabling the Cloud Healthcare API will require + the following roles that can be set in the `IAM & Admin > IAM` console for the + desired project. + - Service Usage Viewer + - Service Usage Admin + ::: + + :::tip + Roles can be added to a principal in the `IAM & Admin > IAM` console by clicking + the `Edit principal` (i.e. pencil) icon to the right of a principal or by clicking the + `GRANT ACCESS` button at the top of the list of principals. The `GRANT ACCESS` + button is particularly useful if the `Edit principal` icon is disabled. + ::: + +4. (Optional): Create a Dataset and DICOM Data Store for storing your DICOM data + + :::tip + To both list existing datasets as well as create a new dataset for your project, + the principal (i.e. account) must have the following roles enabled + in the `IAM & Admin > IAM` console. + + - Editor + + ::: + +5. Enable the [Cloud Resource Manager API](https://cloud.google.com/resource-manager/) for your project. + + _Note:_ If you are having trouble finding the APIs, use the search box at + the top of the Cloud console. + +6. Go to APIs & Services > OAuth Consent Screen to create an OAuth Consent screen and fill in your application details. + + - Run through the three step process of adding an OAuth Consent Screen, clicking `SAVE AND CONTINUE` at the end of each step. + + ![Google OAuth Consent Screen steps](../assets/img/google-oauth-consent-steps.png) + - For the Scopes step, for Google APIs, click the `ADD OR REMOVE SCOPES` button. + - In the `Update selected scopes` dialogue that flies in from the right, add the + following scopes to the `Manually add scopes` text box. + - `https://www.googleapis.com/auth/cloudplatformprojects.readonly` + - `https://www.googleapis.com/auth/cloud-healthcare` + + ![Google Manually Add Scopes](../assets/img/google-manually-add-scopes.png) + + - Click `ADD TO TABLE` and then click `UPDATE` + + +7. Go to APIs & Services > Credentials to create a new set of credentials: + + - Click `+ CREATE CREDENTIALS` and from the drop down select `OAuth Client ID`. + See [OAuth 2.0 Client ID](https://developers.google.com/identity/protocols/oauth2/) for more information. + + ![Google Create Credentials](../assets/img/google-create-credentials.png) + + - Choose the "Web Application" type + - Add your domain (e.g. `http://localhost:3000`) to the Authorized JavaScript + origins. + - Add your domain, plus `callback` (e.g. `http://localhost:3000/callback`) to the Authorized Redirect URIs. + - Save your Client ID for later. + +8. (Optional): Create a bucket containing DICOM files and import it into a Data Store + + - When importing a bucket into a Data Store, the following warning might be + displayed indicating that the Cloud Healthcare Service Agent service account associated with the + project does not have the `Storage Object Viewer` role. + + ![Google Create Credentials](../assets/img/google-healthcare-service-agent-warning.png) + + - The Cloud Healthcare Service Agent service account can be displayed in the + `IAM & Admin > IAM` console by checking the `Include Google-provided role grants` checkbox. + The `Storage Object Viewer` role can then be granted to the Cloud Healthcare Service Agent service account. + + ![Google Provided Accounts Checkbox](../assets/img/google-provided-accounts-checkbox.png) + + - More information regarding the Cloud Healthcare Service Agent service account can + be found at https://cloud.google.com/healthcare-api/docs/permissions-healthcare-api-gcp-products + +9. (Optional): Enable Public Datasets that are being hosted by Google: + https://cloud.google.com/healthcare/docs/resources/public-datasets/ + +## Run the viewer with your OAuth Client ID + +1. Open the `config/google.js` file and change `YOURCLIENTID` to your Client ID + value. +1. Run the OHIF Viewer using the config/google.js configuration file + +```bash +cd OHIFViewer +yarn install +APP_CONFIG=config/google.js yarn run dev +``` + +## Configuring Google Cloud Healthcare as a datasource in OHIF + +A Google Cloud Healthcare DICOM store can be configured as a DICOMweb datasource +in OHIF. A full or partial path is permitted in the configuration file. For +partial paths, the [data source configuration UI](../configuration/dataSources/configuration-ui.md) +will assist in filling in the missing pieces. For example, a configuration with +empty `wadoUriRoot`, `qidoRoot` and `wadoRoot` will prompt for the entire path +step-by-step starting with the project. diff --git a/platform/docs/docs/deployment/iframe.md b/platform/docs/docs/deployment/iframe.md new file mode 100644 index 0000000..6717569 --- /dev/null +++ b/platform/docs/docs/deployment/iframe.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 7 +sidebar_label: iframe +--- + +# iframe + +With the transition to more advanced visualization, loading, and rendering techniques using WebWorkers, WASM, and WebGL, the script tag usage of the OHIF viewer v3 has been deprecated. +An alternative option for script tag usage is to employ an iframe. You can utilize the iframe element to load the OHIF viewer and establish communication with it using the postMessage API if needed. + +We recommend utilizing modern development practices and incorporating OHIF viewer within your application using a more modular and integrated approach, such as leveraging bundlers, other UI +components, and frameworks. + +## Static Build + +You can use the iframe element to load the OHIF viewer as a child element of your application if you need the +viewer to be embedded within your application. The iframe element can be used as follows (use your own custom styles) + +```html + + + + +### Troubleshooting + +_Exit code 137_ + +This means Docker ran out of memory. Open Docker Desktop, go to the `advanced` +tab, and increase the amount of Memory available. + +_Cannot create container for service X_ + +Use this one with caution: `docker system prune` + +_X is already running_ + +Stop running all containers: + +- Win: `docker ps -a -q | ForEach { docker stop $_ }` +- Linux: `docker stop $(docker ps -a -q)` + + +_Traceback (most recent call last):_ + _File "urllib3/connectionpool.py", line 670, in urlopen_ + _...._ + +Are you sure your docker is running? see explanation [here](https://github.com/docker/compose/issues/7896) + + +### Configuration + +After verifying that everything runs with default configuration values, you will +likely want to update: + +- The domain: `http://127.0.0.1` + +#### OHIF Viewer + +The OHIF Viewer's configuration is imported from a static `.js` file. The +configuration we use is set to a specific file when we build the viewer, and +determined by the env variable: `APP_CONFIG`. You can see where we set its value +in the `dockerfile` for this solution: + +`ENV APP_CONFIG=config/docker-nginx-orthanc.js` + +You can find the configuration we're using here: +`/public/config/docker-nginx-orthanc.js` + +To rebuild the `webapp` image created by our `dockerfile` after updating the +Viewer's configuration, you can run: + +- `docker-compose build` OR +- `docker-compose up --build` + +#### Other + +All other files are found in: `/docker/Nginx-Orthanc/` + +| Service | Configuration | Docs | +| ----------------- | --------------------------------- | ------------------------------------------- | +| OHIF Viewer | [dockerfile][dockerfile] | You're reading them now! | +| Nginx | [`/nginx.conf`][config-nginx] | | +| Orthanc | [`/orthanc.json`][config-orthanc] | [Here][orthanc-docs] | + +## Next Steps + +### OHIF + Dcm4chee + +You can follow the similar steps above to run OHIF Viewer with Dcm4chee PACS. + +The recipe for this setup can be found at `platform/app/.recipes/Nginx-Dcm4chee`. + + +The routes are as follows: +- `127.0.0.1` for the OHIF viewer +- `127.0.0.1/pacs` for the Dcm4chee UI + +:::info +For uploading studies, you can see the following gif for the steps: + +![alt text](../assets/img/dcm4chee-upload.gif) + +::: + +### Deploying to Production + +While you can deploy this solution to production, there is one main caveat: every user can access the app and the patient portal without any authentication. In the next step, we will add authentication with Keycloak to secure the app. + + + + +### Improving This Guide + +Here are some improvements this guide would benefit from, and that we would be +more than happy to accept Pull Requests for: + +- Add Docker caching for faster builds + + + +### Referenced Articles + +For more documentation on the software we've chosen to use, you may find the +following resources helpful: + +- [Orthanc for Docker](http://book.orthanc-server.com/users/docker.html) + +For a different take on this setup, check out the repositories our community +members put together: + +- [mjstealey/ohif-orthanc-dimse-docker](https://github.com/mjstealey/ohif-orthanc-dimse-docker) +- [trypag/ohif-orthanc-postgres-docker](https://github.com/trypag/ohif-orthanc-postgres-docker) + + + + + +[nginx]: https://www.nginx.com/resources/glossary/nginx/ +[understanding-cors]: https://medium.com/@baphemot/understanding-cors-18ad6b478e2b +[orthanc-docs]: http://book.orthanc-server.com/users/configuration.html#configuration +[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc + +[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/app/.recipes/OpenResty-Orthanc/dockerfile +[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/app/.recipes/OpenResty-Orthanc/config/nginx.conf +[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/app/.recipes/OpenResty-Orthanc/config/orthanc.json + diff --git a/platform/docs/docs/deployment/static-assets.md b/platform/docs/docs/deployment/static-assets.md new file mode 100644 index 0000000..4186a1b --- /dev/null +++ b/platform/docs/docs/deployment/static-assets.md @@ -0,0 +1,176 @@ +--- +sidebar_position: 3 +--- + +# Deploy Static Assets + +> WARNING! All of these solutions stand-up a publicly accessible web viewer. Do +> not hook your hosted viewer up to a sensitive source of data without +> implementing authentication. + +There are a lot of options for deploying static assets. Some services, like +`netlify` and `surge.sh`, specialize in static websites. You'll notice that +deploying with them requires much less time and effort, but comes at the cost of +less product offerings. + +While not required, it can simplify things to host your Web Viewer alongside +your image archive. Services with more robust product offerings, like +`Google Cloud`, `Microsoft's Azure`, and `Amazon Web Services (AWS)`, are able +to accommodate this setup. + +_Drag-n-drop_ + +- [Netlify: Drop](#netlify-drop) + +_Easy_ + +- [Surge.sh](#surgesh) +- [GitHub Pages](#github-pages) + +_Advanced_ + +- [Deploy Static Assets](#deploy-static-assets) + - [Drag-n-drop](#drag-n-drop) + - [Netlify Drop](#netlify-drop) + - [Easy](#easy) + - [Surge.sh](#surgesh) + - [GitHub Pages](#github-pages) + - [Advanced](#advanced) + - [AWS S3 + Cloudfront](#aws-s3--cloudfront) + - [GCP + Cloudflare](#gcp--cloudflare) + - [Azure](#azure) + +## Drag-n-drop + +### Netlify Drop + + +
+ +
+ + +_GIF demonstrating deployment with Netlify Drop_ + +1. https://app.netlify.com/drop +2. Drag your `build/` folder on to the drop target +3. ... +4. _annnd you're done_ + +**Features:** + +- Custom domains & HTTPS +- Instant Git integration +- Continuous deployment +- Deploy previews +- Access to add-ons + +(Non-free tiers include identity, FaaS, Forms, etc.) + +Learn more about [Netlify on their website](https://www.netlify.com/) + +## Easy + +### Surge.sh + +> Static web publishing for Front-End Developers. Simple, single-command web +> publishing. Publish HTML, CSS, and JS for free, without leaving the command +> line. + +![surge.sh deploy example](../assets/img/surge-deploy.gif) + +_GIF demonstrating deployment with surge_ + +```shell +# Add surge command +yarn global add surge + +# In the build directory +surge +``` + +**Features:** + +- Free custom domain support +- Free SSL for surge.sh subdomains +- pushState support for single page apps +- Custom 404.html pages +- Barrier-free deployment through the CLI +- Easy integration into your Grunt toolchain +- Cross-origin resource support +- And moreโ€ฆ + +Learn more about [surge.sh on their website](https://surge.sh/) + +### GitHub Pages + +> WARNING! While great for project sites and light use, it is not advised to use +> GitHub Pages for production workloads. Please consider using a different +> service for mission critical applications. + +> Websites for you and your projects. Hosted directly from your GitHub +> repository. Just edit, push, and your changes are live. + +This deployment strategy makes more sense if you intend to maintain your project in +a GitHub repository. It allows you to specify a `branch` or `folder` as the +target for a GitHub Page's website. As you push code changes, the hosted content +updates to reflect those changes. + +1. Head over to GitHub.com and create a new repository, or go to an existing + one. Click on the Settings tab. +2. Scroll down to the GitHub Pages section. Choose the `branch` or `folder` you + would like as the "root" of your website. +3. Fire up a browser and go to `http://username.github.io/repository` + +Configuring Your Site: + +- [Setting up a custom domain](https://help.github.com/en/articles/using-a-custom-domain-with-github-pages) +- [Setting up SSL](https://help.github.com/en/articles/securing-your-github-pages-site-with-https) + +Learn more about [GitHub Pages on its website](https://pages.github.com/) + +## Advanced + +All of these options, while using providers with more service offerings, +demonstrate how to host the viewer with their respective file storage and CDN +offerings. While you can serve your static assets this way, if you're going +through the trouble of using AWS/GCP/Azure, it's more likely you're doing so to +avoid using a proxy or to simplify authentication. + +If that is the case, check out some of our more advanced `docker` deployments +that target these providers from the left-hand sidepanel. + +These guides can be a bit longer and an update more frequently. To provide +accurate documentation, we will link to each provider's own recommended steps: + +### AWS S3 + Cloudfront + +- [Host a Static Website](https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html) +- [Speed Up Your Website with Cloudfront](https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-cloudfront-walkthrough.html) + +### GCP + Cloudflare + +- [Things to Know Before Getting Started](https://code.luasoftware.com/tutorials/google-cloud-storage/things-to-know-before-hosting-static-website-on-google-cloud-storage/) +- [Hosting a Static Website on GCP](https://cloud.google.com/storage/docs/hosting-static-website) + +### Azure + + - Deploying viewer to Azure blob storage as a static website: + Refer to [Host a static website](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website) + High level steps : + 1. Go to Azure portal and create a storage account. + 2. Under Overview->Capabilities, select Static website. + 3. Enable Static website. Set the index document as โ€˜index.htmlโ€™. + 4. Copy the primary endpoint. This will serve as the root URL for the viewer. + 5. Save. A new container named โ€˜$webโ€™ will be created. + 6. Copy OHIF viewerโ€™s build output from โ€˜platform\app\distโ€™ folder to the โ€˜$webโ€™ container. + 7. Open browser and navigate to the viewer root URL copied in the step above. It should display OHIF viewer with data from default data source. + + ![image](https://github.com/OHIF/Viewers/assets/132684122/236a574b-0f05-4d90-a721-df8720d05949) + Special consideration while accessing DicomJson data source : + โ€ข Due to the way routing is handled in react, it may error out in production when trying to display data through dicomJson data source. E.g. https://[Static Website endpoint]/viewer/dicomjson?url= https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001.json + โ€ข Resolution to this is to set error page to โ€˜index.htmlโ€™ at the website level. This will ensure that all errors are redirected to root and requests are further served from root path. + ![image](https://github.com/OHIF/Viewers/assets/132684122/87696c90-c344-489a-af15-b992434555f9) + +- [Add SSL Support](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-https-custom-domain-cdn) +- [Configure a Custom Domain](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-custom-domain-name) diff --git a/platform/docs/docs/deployment/user-account-control.md b/platform/docs/docs/deployment/user-account-control.md new file mode 100644 index 0000000..87f0235 --- /dev/null +++ b/platform/docs/docs/deployment/user-account-control.md @@ -0,0 +1,524 @@ +--- +sidebar_position: 11 +--- +# User Account Control + + +:::danger +DISCLAIMER: We make no claims or guarantees regarding the security of this approach. If you have any doubts, please consult an expert and conduct thorough audits. +::: + +Making a viewer and its medical imaging data accessible on the open web can +provide a lot of benefits, but requires additional security to make sure +sensitive information can only be viewed by authorized individuals. Most image +archives are equipped with basic security measures, but they are not +robust/secure enough for the open web. + +This guide covers one of many potential production setups that secure our +sensitive data. + +## Overview + +This guide builds on top of our +[Nginx + Image Archive guide](./nginx--image-archive.md), +wherein we used a [`reverse proxy`](https://en.wikipedia.org/wiki/Reverse_proxy) +to retrieve resources from our image archive (Orthanc). + +To add support for "User Account Control" we introduce +[Keycloak](https://www.keycloak.org/about.html). Keycloak is an open source +Identity and Access Management solution that makes it easy to secure +applications and services with little to no code. We improve upon our +`reverse proxy` setup by integrating Keycloak and Nginx to create an +`authenticating reverse proxy`. + +> An authenticating reverse proxy is a reverse proxy that only retrieves the +> resources on behalf of a client if the client has been authenticated. If a +> client is not authenticated they can be redirected to a login page. + +This setup allows us to create a setup similar to the one pictured below: + +![userControlFlow](../assets/img/ohif-pacs-keycloak.png) + + + +**Nginx:** + +- Acts as a reverse proxy server that handles incoming requests to the domain (mydomain.com:80) and forwards them to the appropriate backend services. +- It also ensures that all requests go through the OAuth2 Proxy for authentication. + + +**OAuth2 Proxy:** + +- Serves as an intermediary that authenticates users via OAuth2. +- Works in conjunction with Keycloak to manage user sessions and authentication tokens. +- Once the user is authenticated, it allows access to specific routes (/ohif-viewer, /pacs, /pacs-admin). + +**Keycloak:** + +- An open-source identity and access management solution. +- Manages user identities, including authentication and authorization. +- Communicates with the OAuth2 Proxy to validate user credentials and provide tokens for authenticated sessions. + +**OHIF Viewer:** + +- Hosted under the route /ohif-viewer, which serves the static assets of the OHIF Viewer. + +**Orthanc/DCM4chee:** + +- PACS (Picture Archiving and Communication System) for managing medical imaging data. +Exposes two routes: +- /pacs: Accesses the DICOM web services. +- /pacs-admin: Provides administrative and explorer interfaces. + + + +## Getting Started - Orthanc + + +### Requirements + +- Docker + - [Docker for Mac](https://docs.docker.com/docker-for-mac/) + - [Docker for Windows](https://docs.docker.com/docker-for-windows/) + +_Not sure if you have `docker` installed already? Try running `docker --version` +in command prompt or terminal_ + +### Setup 1 - Trying Locally + +Navigate to the Orthanc Keycloak configuration directory: + +`cd platform\app\.recipes\Nginx-Orthanc-Keycloak` + +Due to the increased complexity of this setup, we've introduced a magic word `YOUR_DOMAIN`. Replace this word with your project IP address to follow along more easily. + +Since we are running this locally, we will use `127.0.0.1` as our IP address. + +In the `docker-compose.yml` file, replace `YOUR_DOMAIN` with `127.0.0.1`. + +In the Keycloak service: + + +Before: + +``` +KC_HOSTNAME_ADMIN_URL: http://YOUR_DOMAIN/keycloak/ +KC_HOSTNAME_URL: http://YOUR_DOMAIN/keycloak/ +``` + + +After + +``` +KC_HOSTNAME_ADMIN_URL: http://127.0.0.1/keycloak/ +KC_HOSTNAME_URL: http://127.0.0.1/keycloak/ +``` + +In the Keycloak healthcheck, replace `YOUR_DOMAIN` with `localhost`. + +In the Nginx config, change: + +``` +server_name YOUR_DOMAIN; +``` + +to: + +``` +server_name 127.0.0.1; +``` + +Since we're not using SSL, remove the following lines from the Nginx config file and create one server instead of two: + +Before (two servers one for http and one for https): + +``` +server { + listen 80; + server_name YOUR_DOMAIN; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + server_name YOUR_DOMAIN; + + ssl_certificate /etc/letsencrypt/live/ohifviewer.duckdns.org/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/ohifviewer.duckdns.org/privkey.pem; + + root /var/www/html; +``` + +After (merging both servers into one only http server): + +``` +server { + listen 80; + server_name 127.0.0.1; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + root /var/www/html; +``` + +In OAuth2-proxy configuration at `oauth2-proxy.cfg` + +Before: + +``` +redirect_url="http://YOUR_DOMAIN/oauth2/callback" +oidc_issuer_url="http://YOUR_DOMAIN/keycloak/realms/ohif" +``` + +After: + +``` +redirect_url="http://127.0.0.1/oauth2/callback" +oidc_issuer_url="http://127.0.0.1/keycloak/realms/ohif" +``` + +Finally, in the docker-nginx-orthanc-keycloak config file that lives in `platform/app/public/config/docker-nginx-orthanc-keycloak.js`, replace `YOUR_DOMAIN` with + +Before: + +``` +wadoUriRoot: 'http://YOUR_DOMAIN/pacs', +qidoRoot: 'http://YOUR_DOMAIN/pacs', +wadoRoot: 'http://YOUR_DOMAIN/pacs', +``` + +After: + +``` +wadoUriRoot: 'http://127.0.0.1/pacs', +qidoRoot: 'http://127.0.0.1/pacs', +wadoRoot: 'http://127.0.0.1/pacs', +``` + +:::note +This is the config that is used inside the dockerfile to build the viewer, look at dockerfile + +`ENV APP_CONFIG=config/docker-nginx-orthanc-keycloak.js` +::: + +Run the following command to start the services: + +``` +docker-compose up --build +``` + + +You can watch the following video, which will guide you through the process of setting up Orthanc with keycloak and OHIF locally. + +We have set up two predefined users in Keycloak: + +- `user: admin password: admin` - Has access to keycloak portal for managing users and clients +- `user: viewer password: viewer` - Has access to the OHIF Viewer but not the pacs-admin +- `user: pacsadmin password: pacsadmin` - Has access to both the pacs-admin for uploading and the OHIF Viewer + +You can navigate to: + +- `http://127.0.0.1` - This will redirect you to `http://127.0.0.1/ohif-viewer`, prompting you to log in with Keycloak using either user +- `http://127.0.0.1/pacs-admin` - Only the `pacsadmin` user can access this route, while the `viewer` user cannot +- + +
+ +
+ + +### Step 2 - Trying via a Server + +Now that you have successfully set up Orthanc with Keycloak and OHIF locally, you can deploy it to a server. While you can rent a server from any provider, this tutorial will demonstrate the process using Linode as an example. + +You can watch the following video, which will guide you through the process. + +Some notes: + +- Since this is a remote machine we need to clone the repo +- Typically a Linux machine, you need to download and install Docker on it +- Use the Visual Studio Code Remote SSH extension to connect to the server +- Use docker extension in Visual Studio Code to manage the containers +- The public IP address of the server now becomes the YOUR_DOMAIN and is used in the configuration files. + +Still we have not set up SSL, so we will use HTTP instead of HTTPS. + +We should use the same one server configuration as we did locally for Nginx (but with the new server IP address) + +:::info +Don't forget to change the `docker-ngix-orthanc-keycloak.js` file to use the new server IP address. +::: + +After you run `docker compose up --build` you can navigate to the server IP address and see the viewer will not work... + +We have encountered some strange issues with the Keycloak service not allowing non-HTTPS connections (around 10:00). To resolve this, we need to modify the Keycloak configuration to permit HTTPS. This requires accessing the container and making the necessary changes. + +After accessing the container shell + +``` +cd /opt/keycloak/bin + +./kcadm.sh config credentials --server http://localhost:8080 --realm master --user admin +./kcadm.sh update realms/master -s sslRequired=NONE +``` + +After we need to change some configurations in the Keycloak UI to enable the connection in the server + +Navigate to + +``` +http://IP_ADDRESS/keycloak +``` + +which will redirect you to the Keycloak login page + +0. login with the admin user `admin` and password `admin` +1. From the top left drop down menu, select `ohif` realm +2. Go to `Clients` and select `ohif_viewer` +3. In the `Access Settings` change all instances of `http://127.0.0.1` to `http://IP_ADDRESS` + 1. Root URL: `http://IP_ADDRESS` + 2. Home URL: `http://IP_ADDRESS` + 3. Valid Redirect URIs: `http://IP_ADDRESS/oauth2/callback` + 4. Valid post logout URIs: `*` + 5. Web Origins: `http://IP_ADDRESS` + 6. Admin URL: `http://IP_ADDRESS` + +Now if you navigate to the IP address it should work !! + + +
+ +
+ +### Step 3 - Adding SSL and Deploying to Production + +Now we'll add an SSL certificate to our server to enable HTTPS. We'll use Let's Encrypt to generate the SSL certificate. + +Let's Encrypt requires a domain name, so we'll use a free domain name service like DuckDNS (duckdns.org). Follow these steps: + +1. Visit https://www.duckdns.org/ and create an account. +2. Create a free domain name and point it to your server's IP address. + +You can watch a video guide for this process if needed. + +Replace `YOUR_DOMAIN` with your new domain name in the `docker-compose.yml` file and all other config files, as we did previously. + +Next, we'll add HTTPS support. Add the following lines to the Nginx config file: + +(Note: We'll have both HTTP and HTTPS servers, and the server IP will use HTTPS) +``` +server { + listen 80; + server_name https://IP_ADDRESS; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + server_name https://IP_ADDRESS; + + ssl_certificate /etc/letsencrypt/live/ohifviewer.duckdns.org/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/ohifviewer.duckdns.org/privkey.pem; + + root /var/www/html; +``` + +Don't forget to replace `YOUR_DOMAIN` with the new domain name in the `docker-nginx-orthanc-keycloak.js` file. + +:::info +Remember to include `https://` when adding the domain name to the configurations. +::: + +Now, we need to add a certificate. Let's assume we have the domain name `hospital.duckdns.org` and the email we registered with DuckDNS is `your_email@example.com`. + +``` + docker run -it --rm --name certbot \ + -v ./config/letsencrypt:/etc/letsencrypt \ + -v ./config/certbot:/var/www/certbot \ + -p 80:80 \ + certbot/certbot certonly \ + --standalone \ + --preferred-challenges http \ + --email your_email@example.com \ + --agree-tos \ + --no-eff-email \ + -d hospital.duckdns.org +``` + +:::note +Replace "hospital.duckdns.org" with your domain name and update the email address accordingly. +::: + +:::warning +DuckDNS is suitable for testing and demonstration purposes only. For production environments, use a proper domain name and SSL certificate to ensure security. +::: + +If you follow these steps, you'll encounter the error `invalid parameter: redirect_uri` when attempting to log in to Keycloak. This occurs because the redirect URL isn't set up correctly in the Keycloak client configuration. To resolve this, we need to log in and adjust these settings. + +Navigate to: + +``` +http://IP_ADDRESS/keycloak +``` + +Log in using the admin credentials: +- Username: `admin` +- Password: `admin` + +Replace all IP addresses with the new domain name, using HTTPS. + +
+ +
+ + + + + + +## Getting Started - DCM4CHEE + + + + +You can follow the same steps as above to set up DCM4CHEE. The only difference is that you need to navigate to the correct directory. `platform\app\.recipes\Nginx-Dcm4chee-Keycloak` + +You can watch the following video, which will guide you through the process of setting up DCM4CHEE. + + +
+ +
+ + + +## Troubleshooting + + +_invalid parameter: redirect_uri_ + +This means the redirect URL isn't set up correctly in the Keycloak client configuration. To resolve this, log in to Keycloak and adjust the settings in the correct client (ohif_viewer) and correct realm (ohif). + +_Exit code 137_ + +This means Docker ran out of memory. Open Docker Desktop, go to the `advanced` +tab, and increase the amount of Memory available. + +_Cannot create container for service X_ + +Use this one with caution: `docker system prune` + +_X is already running_ + +Stop running all containers: + +- Win: `docker ps -a -q | ForEach { docker stop $_ }` +- Linux: `docker stop $(docker ps -a -q)` + + +#### OHIF Viewer + +The OHIF Viewer's configuration is imported from a static `.js` file. The +configuration we use is set to a specific file when we build the viewer, and +determined by the env variable: `APP_CONFIG`. You can see where we set its value +in the `dockerfile` for this solution: + +`ENV APP_CONFIG=config/docker-nginx-orthanc-keycloak.js` + +You can find the configuration we're using here: +`/public/config/docker-nginx-orthanc-keycloak.js` + +To rebuild the `webapp` image created by our `dockerfile` after updating the +Viewer's configuration, you can run: + +- `docker-compose build` OR +- `docker-compose up --build` + + + +## Next Steps + +### Keycloak Theming + +The `Login` screen for the `ohif-viewer` client is using a Custom Keycloak +theme. You can find the source files for it in +`platform/app/.recipes/deprecated-recipes/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes`. You can see how +we add it to Keycloak in the `docker-compose` file, and you can read up on how +to leverage custom themes in +[Keycloak's own docs](https://www.keycloak.org/docs/latest/server_development/index.html#_themes). + +| Default Theme | OHIF Theme | +| ---------------------------------------------------------------------- | ---------------------------------------------------------------- | +| ![Keycloak Default Theme](../assets/img/keycloak-default-theme.png) | ![Keycloak OHIF Theme](../assets/img/keycloak-ohif-theme.png) | + + + + + +## Resources + +### Referenced Articles + +The inspiration for our setup was driven largely by these articles: + +- [Securing Nginx with Keycloak](https://edhull.co.uk/blog/2018-06-06/keycloak-nginx) +- [Authenticating Reverse Proxy with Keycloak](https://eclipsesource.com/blogs/2018/01/11/authenticating-reverse-proxy-with-keycloak/) +- [Securing APIs with Kong and Keycloak](https://www.jerney.io/secure-apis-kong-keycloak-1/) + +For more documentation on the software we've chosen to use, you may find the +following resources helpful: + +- [Orthanc for Docker](http://book.orthanc-server.com/users/docker.html) +- [OpenResty Guide](http://www.staticshin.com/programming/definitely-an-open-resty-guide/) +- [Lua Ngx API](https://openresty-reference.readthedocs.io/en/latest/Lua_Nginx_API/) +- [Auth0: Picking a Grant Type](https://auth0.com/docs/api-auth/which-oauth-flow-to-use) + +We chose to use a generic OpenID Connect library on the client, but it's worth +noting that Keycloak comes packaged with its own: + +- [oidc-client-js](https://github.com/IdentityModel/oidc-client-js/wiki) +- [Keycloak JavaScript Adapter](https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter) + +If you're not already drowning in links, here are some good security resources +for OAuth: + +- [Diagrams of OpenID Connect Flows](https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660) +- [KeyCloak: OpenID Connect Flows](https://www.keycloak.org/docs/latest/securing_apps/index.html#authorization-code) + +For a different take on this setup, check out the repositories our community +members put together: + +- [mjstealey/ohif-orthanc-dimse-docker](https://github.com/mjstealey/ohif-orthanc-dimse-docker) +- [trypag/ohif-orthanc-postgres-docker](https://github.com/trypag/ohif-orthanc-postgres-docker) + + + + + +[orthanc-docs]: http://book.orthanc-server.com/users/configuration.html#configuration +[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc + +[config]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/src/config.js +[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/dockerfile +[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/nginx.conf +[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/orthanc.json +[config-keycloak]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/ohif-keycloak-realm.json + diff --git a/platform/docs/docs/development/_category_.json b/platform/docs/docs/development/_category_.json new file mode 100644 index 0000000..8627cac --- /dev/null +++ b/platform/docs/docs/development/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Development", + "position": 5 +} diff --git a/platform/docs/docs/development/android-ios-debugging.md b/platform/docs/docs/development/android-ios-debugging.md new file mode 100644 index 0000000..67c4273 --- /dev/null +++ b/platform/docs/docs/development/android-ios-debugging.md @@ -0,0 +1,80 @@ +--- +sidebar_position: 12 +sidebar_label: Android & iOS Debugging +--- + +# Android & iOS Debugging for OHIF using Emulators + +This guide covers how to debug the OHIF viewer on Android and iOS emulators using Chrome DevTools and Safari Web Inspector, respectively. You can use these tools to inspect elements, debug JavaScript, and view console logs for the web content running on the emulators. + +## Android Emulator Setup with Android Studio + +### Prerequisites: +- Install [Android Studio](https://developer.android.com/studio) +- Ensure you have a recent Android SDK and Emulator installed via Android Studio +- Google Chrome installed on your machine + +### Steps to Run Android Emulator: + +1. **Launch Android Studio:** + - Open Android Studio and create a new project if you don't already have one. + - Once your IDE opens up, click on the **Device Manager** icon in the right-side toolbar. + +2. **Create a Virtual Device (if necessary):** + - If you donโ€™t have an existing virtual device, click **Create Virtual Device**. + - Choose a device model (e.g., Pixel series) and click **Next**. + - Select a system image with the required Android API version and click **Next**. + - Finish the setup by clicking **Finish**. + +3. **Start the Android Emulator:** + - Once the device is created, click the **Play** button next to the virtual device to start the emulator. + +4. **Open a Browser on the Emulator:** + - Once the emulator is running, open the **Chrome** app on the virtual device. + - Navigate to the OHIF Viewer URL to view the application. The URL will be 10.0.2.2:3000, you can read more about it [here](https://developer.android.com/studio/run/emulator-networking). + +5. **Debug Using Chrome DevTools:** + - On your development machine, open Google Chrome. + - Type `chrome://inspect` in the Chrome address bar and hit **Enter**. + - You will see your Android device listed under **Remote Target**. + - Click **Inspect** to open DevTools for the browser on the Android emulator. + +6. **Happy Debugging!:** + - You can now use Chrome DevTools to inspect elements, debug JavaScript, and view console logs directly from the emulatorโ€™s browser. + +### Video Tutorial + + + +--- + +## iOS Emulator Setup with Xcode + +### Prerequisites: +- Install [Xcode](https://developer.apple.com/xcode/) from the Mac App Store. +- Ensure you have the latest iOS SDK. + +### Steps to Run iOS Emulator: + +1. **Launch Xcode:** + - Open Xcode and navigate to **Xcode > Settings**. + - Go to the **Platform** tab and ensure you have an iOS simulator installed for the version of iOS you need. If not you can do so using the + button. + +2. **Start the iOS Simulator:** + - Open Xcode and navigate to **Xcode > Open Developer Tools > Simulator**. + - Select your device from the list of available simulators and click on it. + +3. **Open a Browser on the Simulator:** + - Run the **Safari** browser + +4. **Connect Safari DevTools to the iOS Simulator:** + - On your development machine, open **Safari** on your Mac. + - Click **Develop** in the menu bar and select your simulator under **Devices**. + - You will see the web pages open on the iOS simulator. Select the page to open the inspector. + +5. **Happy Debugging!:** + - You can now use the Safari Web Inspector to inspect elements, debug JavaScript, and view logs for the OHIF Viewer on the iOS simulator. + +### Video Tutorial + + diff --git a/platform/docs/docs/development/architecture.md b/platform/docs/docs/development/architecture.md new file mode 100644 index 0000000..f191f16 --- /dev/null +++ b/platform/docs/docs/development/architecture.md @@ -0,0 +1,205 @@ +--- +sidebar_position: 2 +sidebar_label: Architecture +--- + +# Architecture + +In order to achieve a platform that can support various workflows and be +extensible for the foreseeable future we went through extensive planning of +possible use cases and decided to significantly change and improve the +architecture. + +Below, we aim to demystify that complexity by providing insight into how +`OHIF Platform` is architected, and the role each of its dependent libraries +plays. + +## Overview + +The [OHIF Medical Image Viewing Platform][viewers-project] is maintained as a +[`monorepo`][monorepo]. This means that this repository, instead of containing a +single project, contains many projects. If you explore our project structure, +you'll see the following: + +```bash +โ”‚ +โ”œโ”€โ”€ extensions +โ”‚ โ”œโ”€โ”€ default # default functionalities +โ”‚ โ”œโ”€โ”€ cornerstone # 2D/3D images w/ Cornerstonejs +โ”‚ โ”œโ”€โ”€ cornerstone-dicom-sr # Structured reports +โ”‚ โ”œโ”€โ”€ measurement-tracking # measurement tracking +โ”‚ โ””โ”€โ”€ dicom-pdf # View DICOM wrapped PDFs in viewport +| # and many more ... +โ”‚ +โ”œโ”€โ”€ modes +โ”‚ โ””โ”€โ”€ longitudinal # longitudinal measurement tracking mode +| โ””โ”€โ”€ basic-dev-mode # basic viewer with Cornerstone (a developer focused mode) +| # and many more +โ”‚ +โ”œโ”€โ”€ platform +โ”‚ โ”œโ”€โ”€ core # Business Logic +โ”‚ โ”œโ”€โ”€ i18n # Internationalization Support +โ”‚ โ”œโ”€โ”€ ui # React component library +โ”‚ โ””โ”€โ”€ app # Connects platform and extension projects +โ”‚ +โ”œโ”€โ”€ ... # misc. shared configuration +โ”œโ”€โ”€ lerna.json # MonoRepo (Lerna) settings +โ”œโ”€โ”€ package.json # Shared devDependencies and commands +โ””โ”€โ”€ README.md +``` + +OHIF v3 is composed of the following components, described in detail in further +sections: + +- `@ohif/app`: The core framework that controls extension registration, mode + composition and routing. +- `@ohif/core`: A library of useful and reusable medical imaging functionality + for the web. +- `@ohif/ui`: A library of reusable components to build OHIF-styled applications + with. +- `Extensions`: A set of building blocks for building applications. The OHIF org + maintains a few core libraries. +- `Modes`: Configuration objects that tell @ohif/app how to compose + extensions to build applications on different routes of the platform. + +## Extensions + +The `extensions` directory contains many packages that provide essential +functionalities such as rendering, study/series browsers, measurement tracking +that modes can consume to enable a certain workflow. Extensions have had their +behavior changed in `OHIF-v3` and their api is expanded. In summary: + +> In `OHIF-v3`, extensions no longer automatically hook themselves to the app. +> Now, registering an extension makes its component available to `modes` that +> wish to use them. Basically, extensions in `OHIF-v3` are **building blocks** +> for building applications. + +OHIF team maintains several high value and commonly used functionalities in its +own extensions. For a list of extensions maintained by OHIF, +[check out this helpful table](../platform/extensions/index.md#maintained-extensions). +As an example `default` extension provides a default viewer layout, a +study/series browser and a datasource that maps to a DICOMWeb compliant backend. + +[Click here to read more about extensions!](../platform/extensions/index.md) + +## Modes + +The `modes` directory contains workflows that can be registered with OHIF within +certain `routes`. The mode will get used once the user opens the viewer on the +registered route. + +OHIF extensions were designed to provide certain core functionalities for +building your viewer. However, often in medical imaging we face a specific use +case in which we are using some core functionalities, adding our specific UI, +and use it in our workflows. Previously, to achieve this you had to create an +extension to add have such feature. `OHIF-v3` introduces `Modes` to enable +building such workflows by re-using the core functionalities from the +extensions. + +Some common workflows may include: + +- Measurement tracking for lesions +- Segmentation of brain abnormalities +- AI probe mode for detecting prostate cancer + +In the mentioned modes above, they will share the same core rendering module +that the `default` extension provides. However, segmentation mode will require +segmentation tools which is not needed for the other two. As you can see, modes +are a layer on top of extensions, that you can configure in order to achieve +certain workflows. + +To summarize the difference between extensions and modes in `OHIF-v3` and +extensions in `OHIF-v2` + +> - `Modes` are configuration objects that tell _@ohif/app_ how to compose +> extensions to build applications on different routes of the platform. +> - In v2 extensions are โ€œpluginsโ€ that add functionality to a core viewer. +> - In v3 extensions are building blocks that a mode uses to build an entire +> viewer layout. + +[Click here to read more about modes!](../platform/modes/index.md) + +## Platform + +### `@ohif/app` + +This library is the core library which consumes modes and extensions and builds +an application. Extensions can be passed in as app configuration and will be +consumed and initialized at the appropriate time by the application. Upon +initialization the viewer will consume extensions and modes and build up the +route desired, these can then be accessed via the study list, or directly via +url parameters. + +Upon release modes will also be plugged into the app via configuration, but this +is still an area which is under development/discussion, and they are currently +pulled from the window in beta. + +Future ideas for this framework involve only adding modes and fetching the +required extension versions at either runtime or build time, but this decision +is still up for discussion. + +### `@ohif/core` + +OHIF core is a carefully maintained and tested set of web-based medical imaging +functions and classes. This library includes managers and services used from +within the viewer app. + +OHIF core is largely similar to the @ohif/core library in v2, however a lot of +logic has been moved to extensions: however all logic about DICOMWeb and other +data fetching mechanisms have been pulled out, as these now live in extensions, +described later. + +### `@ohif/ui` + +Firstly, a large time-consumer/barrier for entry we discovered was building new +UI in a timely manner that fit OHIFโ€™s theme. For this reason we have built a new +UI component library which contains all the components one needs to build their +own viewer. + +These components are presentational only, so you can reuse them with whatever +logic you desire. As the components are presentational, you may swap out +@ohif/ui for a custom UI library with conforming API if you wish to white label +the viewer. The UI library is here to make development easier and quicker, but +it is not mandatory for extension components to use. + +[Check out our component library!](https://ui.ohif.org/) + +## Overview of the architecture + +OHIF-v3 architecture can be seen in the following figure. We will explore each +piece in more detail. + +![mode-archs](../assets/img/mode-archs.png) + +## Common Questions + +> Can I create my own Viewer using Vue.js or Angular.js? + +You can, but you will not be able to leverage as much of the existing code and +components. `@ohif/core` could still be used for business logic, and to provide +a model for extensions. `@ohif/ui` would then become a guide for the components +you would need to recreate. + +> When I want to implement a functionality, should it be in the mode or in an +> extension? + +This is a great question. Modes are designed to consume extensions, so you +should implement your functionality in one of the modules of your new extension, +and let the mode consume it. This way, in the future, if you needed another mode +that utilizes the same functionality, you can easily hook the extension to the +new mode as well. + + + + +[monorepo]: https://github.com/OHIF/Viewers/issues/768 +[viewers-project]: https://github.com/OHIF/Viewers +[viewer-npm]: https://www.npmjs.com/package/@ohif/app +[pwa]: https://developers.google.com/web/progressive-web-apps/ +[configuration]: ../configuration/configurationFiles.md +[extensions]: ../platform/extensions/index.md +[core-github]: https://github.com/OHIF/viewers/platform/core +[ui-github]: https://github.com/OHIF/Viewers/tree/master/platform/ui + diff --git a/platform/docs/docs/development/continuous-integration.md b/platform/docs/docs/development/continuous-integration.md new file mode 100644 index 0000000..48ad4c4 --- /dev/null +++ b/platform/docs/docs/development/continuous-integration.md @@ -0,0 +1,90 @@ +--- +sidebar_position: 8 +sidebar_label: Continuous Integration +--- + +# Continuous Integration (CI) + +This repository uses `CircleCI` and `Netlify` for Continuous integration. + +## Deploy Previews + +[Netlify Deploy previews][deploy-previews] are generated for every pull request. +They allow pull request authors and reviewers to "Preview" the OHIF Viewer as if +the changes had been merged. + +Deploy previews can be configured by modifying the `netlify.toml` file in the +root of the repository. Some additional scripts/assets for netlify are included +in the root `.netlify` directory. + +## Workflows + +[CircleCI Workflows][circleci-workflows] are a set of rules for defining a +collection of jobs and their run order. They are self-documenting and their +configuration can be found in our CircleCI configuration file: +`.circleci/config.yml`. + +### Workflow: PR_CHECKS + +The PR_CHECKS workflow (Pull Request Checks) runs our automated unit and +end-to-end tests for every code check-in. These tests must all pass before code +can be merged to our `master` branch. + +![PR_CHECKS](../assets/img/WORKFLOW_PR_CHECKS.png) + +### Workflow: PR_OPTIONAL_DOCKER_PUBLISH + +The PR_OPTIONAL_DOCKER_PUBLISH workflow allows for "manual approval" to publish +the pull request as a tagged docker image. This is helpful when changes need to +be tested with the Google Adapter before merging to `master`. + +![PR_Workflow](../assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png) + +> NOTE: This workflow will fail unless it's for a branch on our `upstream` +> repository. If you need this functionality, but the branch is from a fork, +> merge the changes to a short-lived `feature/` branch on `upstream` + +### Workflow: DEPLOY + +The DEPLOY workflow deploys the OHIF Viewer when changes are merged to master. +It uses the Netlify CLI to deploy assets created as part of the repository's PWA +Build process (`yarn run build`). The workflow allows for "Manual Approval" to +promote the build to `STAGING` and `PRODUCTION` environments. + +![WORKFLOW_DEPLOY](../assets/img/WORKFLOW_DEPLOY.png) + +| Environment | Description | URL | +| ----------- | ---------------------------------------------------------------------------------- | --------------------------------------------- | +| Development | Always reflects latest changes on `master` branch. | [Netlify][netlify-dev] / [OHIF][ohif-dev] | +| Staging | For manual testing before promotion to prod. Keeps development workflow unblocked. | [Netlify][netlify-stage] / [OHIF][ohif-stage] | +| Production | Stable, tested, updated less frequently. | [Netlify][netlify-prod] / [OHIF][ohif-prod] | + +### Workflow: RELEASE + +The RELEASE workflow publishes our `npm` packages, updated documentation, and +`docker` image when changes are merged to master. `Lerna` and "Semantic Commit +Syntax" are used to independently version and publish the many packages in our +monorepository. If a new version is cut/released, a Docker image is created. +Documentation is generated with `gitbook` and pushed to our `gh-pages` branch. +GitHub hosts the `gh-pages` branch with GitHub Pages. + +- Platform Packages: https://github.com/ohif/viewers/#platform +- Extension Packages: https://github.com/ohif/viewers/#extensions +- Documentation: https://docs.ohif.org/ + +![WORKFLOW_RELEASE](../assets/img/WORKFLOW_RELEASE.png) + + + + +[deploy-previews]: https://www.netlify.com/blog/2016/07/20/introducing-deploy-previews-in-netlify/ +[circleci-workflows]: https://circleci.com/docs/2.0/workflows/ +[netlify-dev]: https://ohif-dev.netlify.com +[netlify-stage]: https://ohif-stage.netlify.com +[netlify-prod]: https://ohif-prod.netlify.com +[ohif-dev]: https://viewer-dev.ohif.org +[ohif-stage]: https://viewer-stage.ohif.org +[ohif-prod]: https://viewer-prod.ohif.org + diff --git a/platform/docs/docs/development/contributing.md b/platform/docs/docs/development/contributing.md new file mode 100644 index 0000000..5e513b7 --- /dev/null +++ b/platform/docs/docs/development/contributing.md @@ -0,0 +1,97 @@ +--- +sidebar_position: 5 +sidebar_label: Contributing +--- + +# Contributing + +## How can I help? + +Fork the repository, make your change and submit a pull request. If you would +like to discuss the changes you intend to make to clarify where or how they +should be implemented, please don't hesitate to create a new issue. At a +minimum, you may want to read the following documentation: + +- [Getting Started](/development/getting-started.md) +- [Architecture](./architecture.md) + +Pull requests that are: + +- Small +- [Well tested](./testing.md) +- Decoupled + +Are much more likely to get reviewed and merged in a timely manner. + +## When changes impact multiple repositories + +While this can be tricky, we've tried to reduce how often this situation crops +up this with our [recent switch to a monorepo][monorepo]. Our maintained +extensions, ui components, internationalization library, and business logic can +all be developed by simply running `yarn run dev` from the repository root. + +Testing the viewer with locally developed, unpublished package changes from a +package outside of the monorepo is most common with extension development. Let's +demonstrate how to accomplish this with two commonly forked extension +dependencies: + +#### Other linkage notes + +We're still working out some of the kinks with local package development as +there are a lot of factors that can influence the behavior of our development +server and bundler. If you encounter issues not addressed here, please don't +hesitate to reach out on GitHub. + +Sometimes you might encounter a situation where the linking doesn't work as +expected. This might happen when there are multiple linked packages with the +same name. You can [remove][unlink] the linked packages inside yarn and try +again. + +## Any guidance on submitting changes? + +While we do appreciate code contributions, triaging and integrating contributed +code changes can be very time consuming. Please consider the following tips when +working on your pull requests: + +- Functionality is appropriate for the repository. Consider creating a GitHub + issue to discuss your suggested changes. +- The scope of the pull request is not too large. Please consider separate pull + requests for each feature as big pull requests are very time consuming to + understand. + +We will provide feedback on your pull requests as soon as possible. Following +the tips above will help ensure your changes are reviewed. + + + + + + +[example-url]: https://deploy-preview-237--ohif.netlify.com/viewer/?url=https://s3.eu-central-1.amazonaws.com/ohif-viewer/sampleDICOM.json +[pr-237]: https://github.com/OHIF/Viewers/pull/237 +[monorepo]: https://github.com/OHIF/Viewers/issues/768 +[unlink]: https://stackoverflow.com/questions/58459698/is-there-a-command-to-unlink-all-yarn-packages-yarn-unlink-all + diff --git a/platform/docs/docs/development/getting-started.md b/platform/docs/docs/development/getting-started.md new file mode 100644 index 0000000..32214b6 --- /dev/null +++ b/platform/docs/docs/development/getting-started.md @@ -0,0 +1,133 @@ +--- +sidebar_position: 1 +sidebar_label: Getting Started +--- + +# Getting Started + +## Setup + +### Fork & Clone + +If you intend to contribute back changes, or if you would like to pull updates +we make to the OHIF Viewer, then follow these steps: + +- [Fork][fork-a-repo] the [OHIF/Viewers][ohif-viewers-repo] repository +- [Create a local clone][clone-a-repo] of your fork + - `git clone https://github.com/YOUR-USERNAME/Viewers` +- Add OHIF/Viewers as a [remote repository][add-remote-repo] labeled `upstream` + - Navigate to the cloned project's directory + - `git remote add upstream https://github.com/OHIF/Viewers.git` + +With this setup, you can now [sync your fork][sync-changes] to keep it +up-to-date with the upstream (original) repository. This is called a "Triangular +Workflow" and is common for Open Source projects. The GitHub blog has a [good +graphic that illustrates this setup][triangular-workflow]. + + +### Private + +Alternatively, if you intend to use the OHIF Viewer as a starting point, and you +aren't as concerned with syncing updates, then follow these steps: + +1. Navigate to the [OHIF/Viewers][ohif-viewers] repository +2. Click `Clone or download`, and then `Download ZIP` +3. Use the contents of the `.zip` file as a starting point for your viewer + +> NOTE: It is still possible to sync changes using this approach. However, +> submitting pull requests for fixes and features are best done with the +> separate, forked repository setup described in "Fork & Clone" + +## Developing + + +### Branches + +#### `master` branch - The latest dev (beta) release + +- `master` - The latest dev release + +This is typically where the latest development happens. Code that is in the master branch has passed code reviews and automated tests, but it may not be deemed ready for production. This branch usually contains the most recent changes and features being worked on by the development team. It's often the starting point for creating feature branches (where new features are developed) and hotfix branches (for urgent fixes). + +Each package is tagged with beta version numbers, and published to npm such as `@ohif/ui@3.6.0-beta.1` + +### `release/*` branches - The latest stable releases +Once the `master` branch code reaches a stable, release-ready state, we conduct a comprehensive code review and QA testing. Upon approval, we create a new release branch from `master`. These branches represent the latest stable version considered ready for production. + +For example, `release/3.5` is the branch for version 3.5.0, and `release/3.6` is for version 3.6.0. After each release, we wait a few days to ensure no critical bugs. If any are found, we fix them in the release branch and create a new release with a minor version bump, e.g., 3.5.1 in the `release/3.5` branch. + +Each package is tagged with version numbers and published to npm, such as `@ohif/ui@3.5.0`. Note that `master` is always ahead of the `release` branch. We publish docker builds for both beta and stable releases. + +Here is a schematic representation of our development workflow: + +![alt text](../assets/img/github-readme-branches-Jun2024.png) + + + +### Requirements + +- [Node.js & NPM](https://nodejs.org/en/) +- [Yarn](https://yarnpkg.com/en/) +- Yarn workspaces should be enabled: + - `yarn config set workspaces-experimental true` + +### Kick the tires + +Navigate to the root of the project's directory in your terminal and run the +following commands: + +```bash +# Restore dependencies +yarn install + +# Start local development server +yarn run dev +``` + +You should see the following output: + +```bash +@ohif/app: i ๏ฝขwds๏ฝฃ: Project is running at http://localhost:3000/ +@ohif/app: i ๏ฝขwds๏ฝฃ: webpack output is served from / +@ohif/app: i ๏ฝขwds๏ฝฃ: Content not from webpack is served from D:\code\ohif\Viewers\platform\viewer +@ohif/app: i ๏ฝขwds๏ฝฃ: 404s will fallback to /index.html + +# And a list of all generated files +``` + +### ๐ŸŽ‰ Celebrate ๐ŸŽ‰ + +
+ +
+ +### Building for Production + +> More comprehensive guides for building and publishing can be found in our +> [deployment docs](./../deployment/index.md) + +```bash +# Build static assets to host a PWA +yarn run build +``` + +## Troubleshooting + +- If you receive a _"No Studies Found"_ message and do not see your studies, try + changing the Study Date filters to a wider range. +- If you see a 'Loading' message which never resolves, check your browser's + JavaScript console inside the Developer Tools to identify any errors. + + + + +[fork-a-repo]: https://help.github.com/en/articles/fork-a-repo +[clone-a-repo]: https://help.github.com/en/articles/fork-a-repo#step-2-create-a-local-clone-of-your-fork +[add-remote-repo]: https://help.github.com/en/articles/fork-a-repo#step-3-configure-git-to-sync-your-fork-with-the-original-spoon-knife-repository +[sync-changes]: https://help.github.com/en/articles/syncing-a-fork +[triangular-workflow]: https://github.blog/2015-07-29-git-2-5-including-multiple-worktrees-and-triangular-workflows/#improved-support-for-triangular-workflows +[ohif-viewers-repo]: https://github.com/OHIF/Viewers/ +[ohif-viewers]: https://github.com/OHIF/Viewers + diff --git a/platform/docs/docs/development/link.md b/platform/docs/docs/development/link.md new file mode 100644 index 0000000..5d8882f --- /dev/null +++ b/platform/docs/docs/development/link.md @@ -0,0 +1,11 @@ +--- +sidebar_position: 9 +sidebar_label: Local Linking +--- + +# Introduction + +Local linking allows you to develop and test a library in the context of an application before it's published or when you encounter +a bug that you suspect is related to a library. With Yarn, this can be achieved through the yarn link command. + +You can take a look at the Cornerstonejs tutorial for linking https://www.cornerstonejs.org/docs/contribute/linking diff --git a/platform/docs/docs/development/ohif-cli.md b/platform/docs/docs/development/ohif-cli.md new file mode 100644 index 0000000..038f331 --- /dev/null +++ b/platform/docs/docs/development/ohif-cli.md @@ -0,0 +1,312 @@ +--- +sidebar_position: 3 +sidebar_label: OHIF CLI +--- + +# OHIF Command Line Interface + +OHIF-v3 architecture has been re-designed to enable building applications that +are easily extensible to various use cases (Modes) that behind the scene would +utilize desired functionalities (Extensions) to reach the goal of the use case. +Now, the question is _how to create/remove/install/uninstall an extension and/or +mode?_ + +You can use the `cli` script that comes with the OHIF monorepo to achieve these +goals. + +:::note Info +In the long-term, we envision our `cli` tool to be a separate installable +package that you can invoke anywhere on your local system to achieve the same +goals. In the meantime, `cli` will remain as part of the OHIF monorepo and needs +to be invoked using the `yarn` command. +::: + + +## CLI Installation + +You don't need to install the `cli` currently. You can use `yarn` to invoke its +commands. + +## Commands + +:::note Important +All commands should run from the root of the monorepo. +::: + + +There are various commands that can be used to interact with the OHIF-v3 CLI. If +you run the following command, you will see a list of available commands. + +``` +yarn run cli --help +``` + +which will output + +``` +OHIF CLI + +Options: + -V, --version output the version number + -h, --help display help for command + +Commands: + create-extension Create a new template extension + create-mode Create a new template Mode + add-extension [version] Adds an ohif extension + remove-extension removes an ohif extension + add-mode [version] Removes an ohif mode + remove-mode Removes an ohif mode + link-extension Links a local OHIF extension to the Viewer to be used for development + unlink-extension Unlinks a local OHIF extension from the Viewer + link-mode Links a local OHIF mode to the Viewer to be used for development + unlink-mode Unlinks a local OHIF mode from the Viewer + list List Added Extensions and Modes + search [options] Search NPM for the list of Modes and Extensions + help [command] display help for command +``` + +As seen there are commands for you such as: `create-extension`, `create-mode`, +`add-extension`, `remove-extension`, `add-mode`, `remove-mode`, +`link-extension`, `unlink-extension`, `link-mode`, `unlink-mode`, `list`, +`search`, and `help`. Here we will go through each of the commands and describe +them. + +### create-mode + +If you need to create a new mode, you can use the `create-mode` command. This +command will create a new mode template in the directory that you specify. +The command will ask you couple of information/questions in order +to properly create the mode metadata in the `package.json` file. + +```bash +yarn run cli create-mode +``` + +
+ +![image](../assets/img/create-mode.png) + + +
+ +Note 1: Some questions have a default answer, which is indicated inside the +parenthesis. If you don't want to answer the question, just hit enter. It will +use the default answer. + +Note 2: As you see in the questions, you can initiate a git repository for the +new mode right away by answering `Y` (default) to the question. + +Note 3: Finally, as indicated by the green lines at the end, `create-mode` command only +create the mode template. You will need to link the mode to the Viewer in order +to use it. See the [`link-mode`](#link-mode) command. + +If we take a look at the directory that we created, we will see the following +files: + +
+ +![image](../assets/img/mode-template.png) + +
+ + +### create-extension + +Similar to the `create-mode` command, you can use the `create-extension` +command to create a new extension template. This command will create a new +extension template in the directory that you specify the path. + +```bash +yarn run cli create-extension +``` + + +Note: again similar to the `create-extension` command, you need to manually link +the extension to the Viewer in order to use it. See the +[`link-mode`](#link-mode) command. + + +### link-extension + +`link-extension` command will link a local OHIF extension to the Viewer. This +command will utilize `yarn link` to achieve so. + +```bash +yarn run cli link-extension +``` + +### unlink-extension + +There might be situations where you want to unlink an extension from the Viewer +after some developments. `unlink-extension` command will do so. + +```bash +ohif-cli unlink-extension +``` + + + +### link-mode + +Similar to the `link-extension` command, `link-mode` command will link a local +OHIF mode to the Viewer. + +```bash +yarn run cli link-mode +``` + +### unlink-mode + +Similar to the `unlink-extension` command, `unlink-mode` command will unlink a +local OHIF mode from the Viewer. + +```bash +ohif-cli unlink-mode +``` + +### add-mode + +OHIF is a modular viewer. This means that you can install (add) different modes +to the viewer if they are published online . `add-mode` command will add a new mode to +the viewer. It will look for the mode in the NPM registry and installs it. This +command will also add the extension dependencies that the mode relies on to the +Viewer (if specified in the peerDependencies section of the package.json). + +:::note Important +`cli` will validate the npm package before adding it to the Viewer. An OHIF mode +should have `ohif-mode` as one of its keywords. +::: + +Note: If you don't specify the version, the latest version will be used. + +```bash +yarn run cli add-mode [version] +``` + +For instance `@ohif-test/mode-clock` is an example OHIF mode that we have +published to NPM. This mode basically has a panel that shows the clock :) + +We can add this mode to the Viewer by running the following command: + +```bash +yarn run cli add-mode @ohif-test/mode-clock +``` + +After installation, the Viewer has a new mode! + + +![image](../assets/img/add-mode.png) + + +Note: If the mode has an extension peerDependency (in this case @ohif-test/extension-clock), +`cli` will automatically add the extension to the Viewer too. + +The result + +![image](../assets/img/clock-mode.png) +![image](../assets/img/clock-mode1.png) + +### add-extension + +This command will add an OHIF extension to the Viewer. It will look for the +extension in the NPM registry and install it. + +```bash +yarn run cli add-extension [version] +``` + + +### remove-mode + +This command will remove the mode from the Viewer and also remove the extension +dependencies that the mode relies on from the Viewer. + +```bash +yarn run cli remove-mode +``` + + +### remove-extension + +Similar to the `remove-mode` command, this command will remove the extension +from the Viewer. + +```bash +yarn run cli remove-extension +``` + +### list + +`list` command will list all the installed extensions and modes in +the Viewer. It uses the `PluginConfig.json` file to list the installed +extensions and modes. + +```bash +yarn run cli list +``` + +an output would look like this: + +
+ +![image](../assets/img/ohif-cli-list.png) + +
+ +### search + +Using `search` command, you can search for OHIF extensions and modes +in the NPM registry. This tool can accept a `--verbose` flag to show more +information about the results. + +```bash +yarn run cli search [--verbose] +``` + +
+ +![image](../assets/img/cli-search-no-verbose.png) + +
+ +with the verbose flag `ohif-cli search --verbose` you will achieve the following +output: + +
+ +![image](../assets/img/cli-search-with-verbose.png) + +
+ + +## PluginConfig.json + +To make all the above commands work, we have created a new file called `PluginConfig.json` which contains the +information needed to run the commands. You **don't need to (and should not)** +edit/update/modify this file as it is automatically generated by the CLI. You +can take a look at what this file contains by going to +`platform/app/PluginConfig.json` in your project's root directory. In short, +this file tracks and stores all the extensions/modes and the their version that +are currently being used by the viewer. + +## Private NPM Repos + +For the `yarn cli` to view private NPM repos, create a read-only token with the +following steps and export it as an environmental variable. You may also export +an existing npm token. +``` +npm login +npm token create --read-only +export NPM_TOKEN= +``` + +## External dependencies +The ohif-cli will add the path to the external dependencies to the webpack config, +so that you can install them in your project and use them in your custom +extensions and modes. To achieve this ohif-cli will update the webpack.pwa.js +file in the platform/app directory. + +## Video tutorials +See the [Video Tutorials](./video-tutorials.md) for videos of some the above +commands in action. diff --git a/platform/docs/docs/development/our-process.md b/platform/docs/docs/development/our-process.md new file mode 100644 index 0000000..263135b --- /dev/null +++ b/platform/docs/docs/development/our-process.md @@ -0,0 +1,156 @@ +--- +sidebar_position: 6 +sidebar_label: Issue & PR Triage Process +--- + +# Our Process + +Our process is a living, breathing thing. We strive to have regular +[retrospectives][retrospective] that help us shape and adapt our process to our +team's current needs. This document attempts to capture the broad strokes of +that process in an effort to: + +- Strengthen community member involvement and understanding +- Welcome feedback and helpful suggestions + +## Issue Triage + +[GitHub issues][gh-issues] are the best way to provide feedback, ask questions, +and suggest changes to the OHIF Viewer's core team. Community issues generally +fall into one of three categories, and are marked with a `triage` label when +created. + +| Issue Template Name | Description | +| ---------------------- | ---------------------------------------------------------------------------------------- | +| Community: Report ๐Ÿ› | Describe a new issue; Provide steps to reproduce; Expected versus actual result? | +| Community: Request โœ‹ | Describe a proposed new feature. Why should it be implemented? What is the impact/value? | +| Community: Question โ“ | Seek clarification or assistance relevant to the repository. | + +_table 1. issue template names and descriptions_ + +Issues that require `triage` are akin to support tickets. As this is often our +first contact with would-be adopters and contributors, it's important that we +strive for timely responses and satisfactory resolutions. We attempt to +accomplish this by: + +1. Responding to issue requiring `triage` at least once a week +2. Create new "official issues" from "community issues" +3. Provide clear guidance and next steps (when applicable) +4. Regularly clean up old (stale) issues + +> ๐Ÿ–‹ Less obviously, patterns in the issues being reported can highlight areas +> that need improvement. For example, users often have difficulty navigating +> CORS issues when deploying the OHIF Viewer -- how do we best reduce our ticket +> volume for this issue? + +### Backlogged Issues + +Community issues serve as vehicles of discussion that lead us to "backlogged +issues". Backlogged issues are the distilled and actionable information +extracted from community issues. They contain the scope and requirements +necessary for hand-off to a core-team (or community) contributor ^\_^ + +| Category | Description | Labels | +| -------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Bugs | An issue with steps that produce a bug (an unexpected result). | [Bug: Verified ๐Ÿ›][label-bug] | +| Stories | A feature/enhancement with a clear benefit, boundaries, and requirements. | [Story ๐Ÿ™Œ][label-story] | +| Tasks | Changes that improve [UX], [DX], or test coverage; but don't impact application behavior | [Task: CI/Tooling ๐Ÿค–][label-tooling], [Task: Docs ๐Ÿ“–][label-docs], [Task: Refactor ๐Ÿ› ][label-refactor], [Task: Tests ๐Ÿ”ฌ][label-tests] | + +_table 2. backlogged issue types ([full list of labels][gh-labels])_ + +## Issue Curation (["backlog grooming"][groom-backlog]) + +If a [GitHub issue][gh-issues] has a `bug`, `story`, or `task` label; it's on +our backlog. If an issue is on our backlog, it means we are, at the very least, +committed to reviewing any community drafted Pull Requests to complete the +issue. If you're interested in seeing an issue completed but don't know where to +start, please don't hesitate to leave a comment! + +While we don't yet have a long-term or quarterly road map, we do regularly add +items to our ["Active Development" GitHub Project Board][gh-board]. Items on +this project board are either in active development by Core Team members, or +queued up for development as in-progress items are completed. + +> ๐Ÿ–‹ Want to contribute but not sure where to start? Check out [Up for +> grabs][label-grabs] issues and our [Contributing +> documentation][contributing-docs] + +## Contributions (Pull Requests) + +Incoming Pull Requests (PRs) are triaged using the following labels. Code review +is performed on all PRs where the bug fix or added functionality is deemed +appropriate: + +| Labels | Description | +| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| **Classification** | | +| [PR: Bug Fix][label-bug] | Filed to address a Bug. | +| [PR: Draft][draft] | Filed to gather early feedback from the core team, but which is not intended for merging in the short term. | +| **Review Workflow** | | +| [PR: Awaiting Response ๐Ÿ’ฌ][awaiting-response] | The core team is waiting for additional information from the author. | +| [PR: Awaiting Review ๐Ÿ‘€][awaiting-review] | The core team has not yet performed a code review. | +| [PR: Awaiting Revisions ๐Ÿ–Š][awaiting-revisions] | Following code review, this label is applied until the author has made sufficient changes. | +| **QA** | | +| [PR: Awaiting User Cases ๐Ÿ’ƒ][awaiting-stories] | The PR code changes need common language descriptions of impact to end users before the review can start | +| [PR: No UX Impact ๐Ÿ™ƒ][no-ux-impact] | The PR code changes do not impact the user's experience | + +We rely on GitHub Checks and integrations with third party services to evaluate +changes in code quality and test coverage. Tests must pass and User cases must +be present (when applicable) before a PR can be merged to master, and code +quality and test coverage must not be changed by a significant margin. For some +repositories, visual screenshot-based tests are also included, and video +recordings of end-to-end tests are stored for later review. + +[You can read more about our continuous integration efforts here](/development/continuous-integration.md) + +## Releases + +Releases are made automatically based on the type of commits which have been +merged (major.minor.patch). Releases are automatically pushed to NPM. Release +notes are automatically generated. Users can subscribe to GitHub and NPM +releases. + +We host development, staging, and production environments for the Progressive +Web Application version of the OHIF Viewer. [Development][ohif-dev] always +reflects the latest changes on our master branch. [Staging][ohif-stage] is used +to regression test a release before a bi-weekly deploy to our [Production +environment][ohif-prod]. + +Important announcements are made on GitHub, tagged as Announcement, and pinned +so that they remain at the top of the Issue page. + +The Core team occasionally performs full manual testing to begin the process of +releasing a Stable version. Once testing is complete, the known issues are +addressed and a Stable version is released. + + + + +[groom-backlog]: https://www.agilealliance.org/glossary/backlog-grooming +[retrospective]: https://www.atlassian.com/team-playbook/plays/retrospective +[gh-issues]: https://github.com/OHIF/Viewers/issues/new/choose +[gh-labels]: https://github.com/OHIF/Viewers/labels + +[label-story]: https://github.com/OHIF/Viewers/labels/Story%20%3Araised_hands%3A +[label-tooling]: https://github.com/OHIF/Viewers/labels/Task%3A%20CI%2FTooling%20%3Arobot%3A +[label-docs]: https://github.com/OHIF/Viewers/labels/Task%3A%20Docs%20%3Abook%3A +[label-refactor]: https://github.com/OHIF/Viewers/labels/Task%3A%20Refactor%20%3Ahammer_and_wrench%3A +[label-tests]: https://github.com/OHIF/Viewers/labels/Task%3A%20Tests%20%3Amicroscope%3A +[label-bug]: https://github.com/OHIF/Viewers/labels/Bug%3A%20Verified%20%3Abug%3A + +[draft]: https://github.com/OHIF/Viewers/labels/PR%3A%20Draft +[awaiting-response]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20Response%20%3Aspeech_balloon%3A +[awaiting-review]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20Review%20%3Aeyes%3A +[awaiting-stories]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20UX%20Stories%20%3Adancer%3A +[awaiting-revisions]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20Revisions%20%3Apen%3A +[no-ux-impact]: https://github.com/OHIF/Viewers/labels/PR%3A%20No%20UX%20Impact%20%3Aupside_down_face%3A + +[ohif-dev]: https://viewer-dev.ohif.org +[ohif-stage]: https://viewer-stage.ohif.org +[ohif-prod]: https://viewer.ohif.org +[gh-board]: https://github.com/OHIF/Viewers/projects/4 +[label-grabs]: https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs+%3Araising_hand_woman%3A%22 +[contributing-docs]: ./contributing.md + diff --git a/platform/docs/docs/development/playwright-testing.md b/platform/docs/docs/development/playwright-testing.md new file mode 100644 index 0000000..f85f02d --- /dev/null +++ b/platform/docs/docs/development/playwright-testing.md @@ -0,0 +1,175 @@ +--- +sidebar_position: 11 +sidebar_label: Playwright Testing +--- + + + +:::note +You might need to run the `yarn playwright install ` for the first time if you have not +::: + +# Running the tests + +```bash +# run the tests +yarn test:e2e:ui +``` + + +# Writing PlayWright Tests + +Our Playwright tests are written using the Playwright test framework. We use these tests to test our OHIF Viewer and ensure that it is working as expected. + +In this guide, we will show you how to write Playwright tests for the OHIF Viewer. + + + +## Using a specific study and mode + +If you would like to use a specific study, you can use the `studyInstanceUID` property to reference the study you would like to visit. for example, if you would like to use the study with StudyInstanceUID `2.16.840.1.114362.1.11972228.22789312658.616067305.306.2` and the mode `Basic Viewer`, you can use the following code snippet: + +```ts +import { test } from '@playwright/test'; +import { visitStudy, checkForScreenshot, screenShotPaths } from './utils/index.js'; + +test.beforeEach(async ({ page }) => { + const studyInstanceUID = '2.16.840.1.114362.1.11972228.22789312658.616067305.306.2'; + const mode = 'Basic Viewer'; + await visitStudy(page, studyInstanceUID, mode); +}); + +test.describe('Some Test', async () => { + test('should do something.', async ({ page }) => { + // Your test code here... + }); +}); + +``` + +## Screenshots + +A good way to check your tests is working as expected is to capture screenshots at different stages of the test. You can use our `checkForScreenshot` function located in `tests/utils/checkForScreenshot.ts` to capture screenshots. You should also plan your screenshots in advance, screenshots need to be defined in the `tests/utils/screenshotPaths.ts` file. For example, if you would to capture a screenshot after a measurement is added, you can define a screenshot path like this: + +```ts +const screenShotPaths = { + your_test_name: { + measurementAdded: 'measurementAdded.png', + measurementRemoved: 'measurementRemoved.png', + }, +}; +``` + +It's okay if the screenshot doesn't exist yet, this will be dealt with in the next step. Once you have defined your screenshot path, you can use the `checkForScreenshot` function in your test to capture the screenshot. For example, if you would like to capture a screenshot of the page after a measurement is added, you can use the following code snippet: + +```ts +import { test } from '@playwright/test'; +import { + visitStudy, + checkForScreenshot, + screenshotPath, +} from './utils/index.js'; + +test.beforeEach(async ({ page }) => { + const studyInstanceUID = '2.16.840.1.114362.1.11972228.22789312658.616067305.306.2'; + const mode = 'Basic Viewer'; + await visitStudy(page, studyInstanceUID, mode); +}); + +test.describe('Some test', async () => { + test('should do something', async ({ page }) => { + // Your test code here to add a measurement + await checkForScreenshot( + page, + page, + screenshotPath.your_test_name.measurementAdded + ); + }); +}); +``` + +The test will automatically fail the first time you run it, it will however generate the screenshot for you, you will notice 3 new entries in the `tests/screenshots` folder, under `chromium/your-test.spec.js/measurementAdded.png`, `firefox/your-test.spec.js/measurementAdded.png` and `webkit/your-test.spec.js/measurementAdded.png` folders. You can now run the test again and it will use those screenshots to compare against the current state of the example. Please verify that the ground truth screenshots are correct before committing them or testing against them. + +## Simulating mouse drags + +If you would like to simulate a mouse drag, you can use the `simulateDrag` function located in `tests/utils/simulateDrag.ts`. You can use this function to simulate a mouse drag on an element. For example, if you would like to simulate a mouse drag on the `cornerstone-canvas` element, you can use the following code snippet: + +```ts +import { + visitStudy, + checkForScreenshot, + screenShotPaths, + simulateDrag, +} from './utils/index.js'; + +test.beforeEach(async ({ page }) => { + const studyInstanceUID = '2.16.840.1.114362.1.11972228.22789312658.616067305.306.2'; + const mode = 'Basic Viewer'; + await visitStudy(page, studyInstanceUID, mode); +}); + +test.describe('Some Test', async () => { + test('should do something..', async ({ + page, + }) => { + const locator = page.locator('.cornerstone-canvas'); + await simulateDrag(page, locator); + }); +}); +``` + +Our simulate drag utility can simulate a drag on any element, and avoid going out of bounds. It will calculuate the bounding box of the element and ensure that the drag stays within the bounds of the element. This should be good enough for most tools, and better than providing custom x, and y coordinates which can be error prone and make the code difficult to maintain. + +## Running the tests + +After you have wrote your tests, you can run them by using the following command: + +```bash +yarn test:e2e:ci +``` + +If you want to use headed mode, you can use the following command: + +```bash +yarn test:e2e:headed +``` + +You will see the test results in your terminal, if you want an indepth report, you can use the following command: + +```bash +yarn playwright show-report tests/playwright-report +``` + +## Serving the viewer manually for development + +By default, when you run the tests, it will call the `yarn start` command to serve the viewer first, then run the tests, if you would like to serve the viewer manually, you can use the same command. The viewer will be available at `http://localhost:3000`. This could speed up your development process since playwright will skip this step and use the existing server on port 3000. + +## Accessing services, managers, configs and cornerstone in your tests + +If you would like to access the cornerstone3D, services, or command managers in your tests, you can use the `page.evaluate` function to access them. For example, if you would like to access the `services` so you can show a UI notifcation using the uiNotifcationService, you can use the following code snippet: + +```ts + await page.evaluate(({ services }: AppTypes.Test) => { + const { uiNotificationService } = services; + uiNotificationService.show({ + title: 'Test', + message: 'This is a test', + type: 'info', + }); + }, await page.evaluateHandle('window')); + ``` + +## Playwright VSCode Extension and Recording Tests + +If you are using VSCode, you can use the Playwright extension to help you write your tests. The extension provides a test runner and many great features such as picking a locator using your mouse, recording a new test, and more. You can install the extension by searching for `Playwright` in the extensions tab in VSCode or by visiting the [Playwright extension page](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright). + +
+ +
+ + +
+ +
diff --git a/platform/docs/docs/development/testing.md b/platform/docs/docs/development/testing.md new file mode 100644 index 0000000..23f2397 --- /dev/null +++ b/platform/docs/docs/development/testing.md @@ -0,0 +1,217 @@ +--- +sidebar_position: 7 +sidebar_label: Testing +--- + +# Running Tests for OHIF + +We introduce here various test types that is available for OHIF, and how to run +each test in order to make sure your contribution hasn't broken any existing +functionalities. Idea and philosophy of each testing category is discussed in +the second part of this page. + +## Unit test + +To run the unit test: + +```bash +yarn run test:unit:ci +``` + +Note: You should have already installed all the packages with `yarn install`. + +Running unit test will generate a report at the end showing the successful and +unsuccessful tests with detailed explanations. + +## End-to-end test +For running the OHIF e2e test you need to run the following steps: + +- Open a new terminal, and from the root of the OHIF mono repo, run the following command: + + ```bash + yarn test:data + ``` + + This will download the required data to run the e2e tests (it might take a while). + The `test:data` only needs to be run once and checks the data out. Read more about + test data [below](#test-data). + +- Run the viewer with e2e config + + ```bash + APP_CONFIG=config/e2e.js yarn start + ``` + + You should be able to see test studies in the study list + + ![OHIF-e2e-test-studies](../assets/img/OHIF-e2e-test-studies.png) + +- Open a new terminal inside the OHIF project, and run the e2e cypress test + + ```bash + yarn test:e2e + ``` + + You should be able to see the cypress window open + + ![e2e-cypress](../assets/img/e2e-cypress.png) + + Run the tests by clicking on the `Run #number integration tests` . + + A new window will open, and you will see e2e tests being executed one after + each other. + + ![e2e-cypress-final](../assets/img/e2e-cypress-final.png) + + ## Test Data + The testing data is stored in two OHIF repositories. The first contains the + binary DICOM data, at [viewer-testdata](https://github.com/OHIF/viewer-testdata.git) + while the second module contains data in the DICOMweb format, installed as a submodule + into OHIF in the `testdata` directory. This is retrieved via the command + ```bash + yarn test:data + ``` + or the equivalent command `git submodule update --init` + When adding new data, run: + ``` + npm install -g dicomp10-to-dicomweb + mkdicomweb -d dicomweb dcm + ``` + to update the local dicomweb submodule in viewer-testdata. Then, commit + that data and update the submodules used in OHIF and in the viewer-testdata + parent modules. + + All data MUST be fully anonymized and allowed to be used for open access. + Any attributions should be included in the DCM directory. + +## Testing Philosophy + +> Testing is an opinionated topic. Here is a rough overview of our testing +> philosophy. See something you want to discuss or think should be changed? Open +> a PR and let's discuss. + +You're an engineer. You know how to write code, and writing tests isn't all that +different. But do you know why we write tests? Do you know when to write one, or +what kind of test to write? How do you know if a test is a _"good"_ test? This +document's goal is to give you the tools you need to make those determinations. + +Okay. So why do we write tests? To increase our... **CONFIDENCE** + +- If I do a large refactor, does everything still work? +- If I changed some critical piece of code, is it safe to push to production? + +Gaining the confidence we need to answer these questions after every change is +costly. Good tests allow us to answer them without manual regression testing. +What and how we choose to test to increase that confidence is nuanced. + +## Further Reading: Kinds of Tests + +Test's buy us confidence, but not all tests are created equal. Each kind of test +has a different cost to write and maintain. An expensive test is worth it if it +gives us confidence that a payment is processed, but it may not be the best +choice for asserting an element's border color. + +| Test Type | Example | Speed | Cost | +| ----------- | ------------------------------------------------------------------------ | ---------------- | ------------------------------------------------------------------------ | +| Static | `addNums(1, '2')` called with `string`, expected `int`. | :rocket: Instant | :money_with_wings: | +| Unit | `addNums(1, 2)` returns expected result `3` | :airplane: Fast | :money_with_wings::money_with_wings: | +| Integration | Clicking "Sign In", navigates to the dashboard (mocked network requests) | :running: Okay | :money_with_wings::money_with_wings::money_with_wings: | +| End-to-end | Clicking "Sign In", navigates to the dashboard (no mocks) | :turtle: Slow | :money_with_wings::money_with_wings::money_with_wings::money_with_wings: | + +- :rocket: Speed: How quickly tests run +- :money_with_wings: Cost: Time to write, and to debug when broken (more points + of failure) + +### Static Code Analysis + +Modern tooling gives us this "for free". It can catch invalid regular +expressions, unused variables, and guarantee we're calling methods/functions +with the expected parameter types. + +Example Tooling: + +- [ESLint][eslint-rules] +- [TypeScript][typescript-docs] or [Flow][flow-org] + +### Unit Tests + +The building blocks of our libraries and applications. For these, you'll often +be testing a single function or method. Conceptually, this equates to: + +_Pure Function Test:_ + +- If I call `sum(2, 2)`, I expect the output to be `4` + +_Side Effect Test:_ + +- If I call `resetViewport(viewport)`, I expect `cornerstone.reset` to be called + with `viewport` + +#### When to use + +Anything that is exposed as public API should have unit tests. + +#### When to avoid + +You're actually testing implementation details. You're testing implementation +details if: + +- Your test does something that the consumer of your code would never do. + - IE. Using a private function +- A refactor can break your tests + +### Integration Tests + +We write integration tests to gain confidence that several units work together. +Generally, we want to mock as little as possible for these tests. In practice, +this means only mocking network requests. + +### End-to-End Tests + +These are the most expensive tests to write and maintain. Largely because, when +they fail, they have the largest number of potential points of failure. So why +do we write them? Because they also buy us the most confidence. + +#### When to use + +Mission critical features and functionality, or to cover a large breadth of +functionality until unit tests catch up. Unsure if we should have a test for +feature `X` or scenario `Y`? Open an issue and let's discuss. + +### General + +- [Assert(js) Conf 2018 Talks][assert-js-talks] + - [Write tests. Not too many. Mostly integration.][kent-talk] - Kent C. Dodds + - [I see your point, butโ€ฆ][gleb-talk] - Gleb Bahmutov +- [Static vs Unit vs Integration vs E2E Testing][kent-blog] - Kent C. Dodds + (Blog) + +### End-to-end Testing w/ Cypress + +- [Getting Started](https://docs.cypress.io/guides/overview/why-cypress.html) + - Be sure to check out `Getting Started` and `Core Concepts` +- [Best Practices](https://docs.cypress.io/guides/references/best-practices.html) +- [Example Recipes](https://docs.cypress.io/examples/examples/recipes.html) + + + + +[eslint-rules]: https://eslint.org/docs/rules/ +[mini-pacs]: https://github.com/OHIF/viewer-testdata +[typescript-docs]: https://www.typescriptlang.org/docs/home.html +[flow-org]: https://flow.org/ + +[assert-js-talks]: https://www.youtube.com/playlist?list=PLZ66c9_z3umNSrKSb5cmpxdXZcIPNvKGw +[kent-talk]: https://www.youtube.com/watch?v=Fha2bVoC8SE +[gleb-talk]: https://www.youtube.com/watch?v=5FnalKRjpZk +[kent-blog]: https://kentcdodds.com/blog/unit-vs-integration-vs-e2e-tests + +[testing-trophy]: https://twitter.com/kentcdodds/status/960723172591992832?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E960723172591992832&ref_url=https%3A%2F%2Fkentcdodds.com%2Fblog%2Fwrite-tests +[aaron-square]: https://twitter.com/Carofine247/status/966727489274961920 +[gleb-pyramid]: https://twitter.com/Carofine247/status/966764532046684160/photo/3 +[testing-pyramid]: https://dojo.ministryoftesting.com/dojo/lessons/the-mobile-test-pyramid +[testing-dorito]: https://twitter.com/denvercoder/status/960752578198843392 +[testing-dorito-img]: https://pbs.twimg.com/media/DVVHXycUMAAcN-F?format=jpg&name=4096x4096 + diff --git a/platform/docs/docs/development/types.md b/platform/docs/docs/development/types.md new file mode 100644 index 0000000..a6af5df --- /dev/null +++ b/platform/docs/docs/development/types.md @@ -0,0 +1,85 @@ +--- +sidebar_position: 10 +sidebar_label: Global Types +--- + +# Extending App Types and Services in Your Application + +This documentation provides an overview and examples on how to use and extend `withAppTypes`, integrate custom properties, and add services in the global namespace of the application. This helps in enhancing the application's modularity and extensibility. + +## Overview of `withAppTypes` + +The `withAppTypes` function is a TypeScript utility that extends the base properties of components or modules with the application's core service and manager types. It allows for a more flexible and type-safe way to pass around core functionality and custom properties. + +### Using `withAppTypes` + +`withAppTypes` can be enhanced using generics to include custom properties. This is particularly useful for passing additional data or configurations specific to your component or service. + +### Extending with Custom Properties + +You can extend `withAppTypes` to include custom properties by defining an interface for the props you need. For example: + +```typescript +interface ColorbarProps { + viewportId: string; + displaySets: Array; + colorbarProperties: ColorbarProperties; +} + +export function Colorbar({ + viewportId, + displaySets, + commandsManager, // injected type + servicesManager, // injected type + colorbarProperties, +}: withAppTypes): ReactElement { + // Component logic here +} +``` + +In this example, `ColorbarProps` is a custom interface that extends the application types through `withAppTypes`. + +## Typing the custom extensions's new services + +Extensions can define additional services that integrate seamlessly into the application's global service architecture, and will be available on the ServicesManager for use across the application. + +### Adding the extension's services Types + +Declare your service in the global namespace and use it across your application as demonstrated below: + +`extensions/my-extension/src/types/whatever.ts` + +```typescript +declare global { + namespace AppTypes { + // only add if you need direct access to the service ex. AppTypes.MicroscopyService + export type MicroscopyService = MicroscopyServiceType; + // add to the global Services interface, and to withAppTypes + export interface Services { + microscopyService?: MicroscopyServiceType; + } + } +} +``` + +Doing the above adds the `microscopyService` to the global Services interface, which ServicesManager uses by default `public services: AppTypes.Services = {};` to type services, and is also used by withAppTypes to inject services into components. +You will also get access to the seperate services via `AppTypes.YourServiceName` in your application. + + +```typescript +export function CustomComponent({ + servicesManager, +}: withAppTypes): ReactElement { + const { microscopyService } = servicesManager.services; + microscopyService.someMethod(); // auto completation available + +} +``` + +```typescript +export function CustomComponent2( + microscopyService: AppTypes.MicroscopyService, +): ReactElement { + microscopyService.someMethod(); // auto completation available +} +``` diff --git a/platform/docs/docs/development/video-tutorials.md b/platform/docs/docs/development/video-tutorials.md new file mode 100644 index 0000000..482967e --- /dev/null +++ b/platform/docs/docs/development/video-tutorials.md @@ -0,0 +1,68 @@ +--- +sidebar_position: 4 +sidebar_label: Video Tutorials +--- + +# Video Tutorials + +## Creating, Linking and Publishing OHIF Modes and Extensions + +The [OHIF CLI](./ohif-cli.md) facilitates the creation, linkage and publication +of OHIF modes and extensions. The videos below walk through how to use the CLI for +- creating modes and extensions +- linking local modes and extensions +- publishing modes and extensions to NPM +- adding published modes and extensions to OHIF +- submitting a mode to OHIF + +The videos build on top of one another whereby the mode and extension created +in each of the first two videos are published to NPM and then the published +entities are added to OHIF. + +### Creating and Linking a Mode + +The first video demonstrates the creation and linkage of a mode. +
+ +
+ +### Creating and Linking an Extension + +The second video creates and links an extension. The mode from the first +video is modified to reference the extension. +
+ +
+ +### Publishing an Extension to NPM + +The third video shows how the extension created in the second video can +be published to NPM. +
+ +
+ +### Publishing a Mode to NPM + +The fourth video shows how the mode created in the first video can be +published to NPM. +
+ +
+ +### Adding a Mode from NPM + +The fifth video adds the mode and extension published in NPM to OHIF. Note +that since the mode references the extension both are added with one CLI +command. +
+ +
+ +### Submitting a Mode to OHIF + +The sixth video demonstrates how a mode can be submitted to OHIF to have it +appear in OHIF's mode gallery. +
+ +
diff --git a/platform/docs/docs/development/webWorkers.md b/platform/docs/docs/development/webWorkers.md new file mode 100644 index 0000000..7fa3bed --- /dev/null +++ b/platform/docs/docs/development/webWorkers.md @@ -0,0 +1,191 @@ +--- +sidebar_position: 13 +sidebar_label: Web Workers +--- +# Web Worker Implementation Guide + +## Overview +Web Workers enable running computationally intensive tasks in background threads without blocking the UI. This guide explains how to implement them step by step. + +## Basic Setup + +### 1. Create Your Worker File +First, create a worker file with your background tasks: + +```javascript +// myWorker.js +import { expose } from 'comlink'; + +const obj = { + // Simple task + basicCalculation({ data }) { + // Your computation here + return result; + }, + + // Task with progress updates + longRunningTask({ data }, progressCallback) { + const total = data.length; + + for (let i = 0; i < total; i++) { + // Your processing logic + + if (progressCallback) { + const progress = Math.round((i / total) * 100); + progressCallback(progress); + } + } + + return result; + } +}; + +expose(obj); +``` + +### 2. Register the Worker + +In the main thread, can be your service, commands module, etc. + +```javascript +import { getWebWorkerManager } from '@cornerstonejs/core'; + +const workerManager = getWebWorkerManager(); + +// Define worker creation function +const workerFn = () => { + return new Worker( + new URL('./myWorker.js', import.meta.url), + { name: 'my-worker' } + ); +}; + +// Registration options +const options = { + maxWorkerInstances: 1, // Number of concurrent workers + autoTerminateOnIdle: { + enabled: true, + idleTimeThreshold: 3000, // Terminate after 3s idle + }, +}; + +// Register the worker +workerManager.registerWorker('my-worker', workerFn, options); +``` + +:::info +It is recommended to register the worker in top of the commands module. So that it +gets registered before any commands that need to use the worker. +::: + +### 3. Execute Tasks + +```javascript +// Basic execution +try { + const result = await workerManager.executeTask( + 'my-worker', + 'basicCalculation', + { data: myData } + ); +} catch (error) { + console.error('Task failed:', error); +} + +// Execution with progress callback +try { + const result = await workerManager.executeTask( + 'my-worker', + 'longRunningTask', + { data: myData }, + { + callbacks: [ + (progress) => { + console.log(`Progress: ${progress}%`); + } + ] + } + ); +} catch (error) { + console.error('Task failed:', error); +} +``` + +## Progress Events (Optional) + +If you want to show progress in your UI as a loading spinner, you can implement a progress event system: + +### 1. Publish Progress Events + +```javascript +// Helper to trigger progress events +const publishProgress = (eventTarget, progress, taskId) => { + triggerEvent(eventTarget, 'WEB_WORKER_PROGRESS', { + progress, // number 0-100 + type: 'YOUR_TASK_TYPE', // can be any string identifier + id: taskId, // unique task identifier + }); +}; + +// Usage in your application +async function runTaskWithProgress(data) { + // Start progress + publishProgress(eventTarget, 0, data.id); + + try { + const result = await workerManager.executeTask( + 'my-worker', + 'longRunningTask', + { data }, + { + callbacks: [ + (progress) => { + publishProgress(eventTarget, progress, data.id); + } + ] + } + ); + + // Complete progress + publishProgress(eventTarget, 100, data.id); + + return result; + } catch (error) { + console.error('Task failed:', error); + throw error; + } +} +``` + +Note: Publishing the `WEB_WORKER_PROGRESS` event on Cornerstone's `eventTarget` will automatically trigger the built-in loading spinner. This gives users visual feedback while your worker runs in the background. + + +## Multiple Methods in One Worker + +You can define multiple related methods in a single worker file: + +```javascript +// complexWorker.js +import { expose } from 'comlink'; + +const obj = { + processingMethod1({ data }, progressCallback) { + // Implementation + }, + + processingMethod2({ data }, progressCallback) { + // Implementation + }, + + processingMethod3({ data }, progressCallback) { + // Implementation + }, + + // Shared helper methods + _internalHelper() { + // Helper logic + } +}; + +expose(obj); +``` diff --git a/platform/docs/docs/faq/_category_.json b/platform/docs/docs/faq/_category_.json new file mode 100644 index 0000000..d83af03 --- /dev/null +++ b/platform/docs/docs/faq/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "FAQ", + "position": 13 +} diff --git a/platform/docs/docs/faq/faq-measure-1.png b/platform/docs/docs/faq/faq-measure-1.png new file mode 100644 index 0000000..86d92dd Binary files /dev/null and b/platform/docs/docs/faq/faq-measure-1.png differ diff --git a/platform/docs/docs/faq/faq-measure-2.png b/platform/docs/docs/faq/faq-measure-2.png new file mode 100644 index 0000000..3feef69 Binary files /dev/null and b/platform/docs/docs/faq/faq-measure-2.png differ diff --git a/platform/docs/docs/faq/faq-measure-4.png b/platform/docs/docs/faq/faq-measure-4.png new file mode 100644 index 0000000..4198eda Binary files /dev/null and b/platform/docs/docs/faq/faq-measure-4.png differ diff --git a/platform/docs/docs/faq/faq-measure-5.png b/platform/docs/docs/faq/faq-measure-5.png new file mode 100644 index 0000000..1dcdc42 Binary files /dev/null and b/platform/docs/docs/faq/faq-measure-5.png differ diff --git a/platform/docs/docs/faq/faq-measure3.png b/platform/docs/docs/faq/faq-measure3.png new file mode 100644 index 0000000..3231771 Binary files /dev/null and b/platform/docs/docs/faq/faq-measure3.png differ diff --git a/platform/docs/docs/faq/general.md b/platform/docs/docs/faq/general.md new file mode 100644 index 0000000..2c01de7 --- /dev/null +++ b/platform/docs/docs/faq/general.md @@ -0,0 +1,82 @@ +--- +id: general +--- + + + +# General FAQ + + +## How do I report a bug? + +Navigate to our [GitHub Repository][new-issue], and submit a new bug report. +Follow the steps outlined in the [Bug Report Template][bug-report-template]. + +## How can I request a new feature? + +At the moment we are in the process of defining our roadmap and will do our best +to communicate this to the community. If your requested feature is on the +roadmap, then it will most likely be built at some point. If it is not, you are +welcome to build it yourself and [contribute it](../development/contributing.md). +If you have resources and would like to fund the development of a feature, +please [contact us](https://ohif.org/get-support). + + +## Who should I contact about Academic Collaborations? + +[Gordon J. Harris](https://www.dfhcc.harvard.edu/insider/member-detail/member/gordon-j-harris-phd/) +at Massachusetts General Hospital is the primary contact for any academic +collaborators. We are always happy to hear about new groups interested in using +the OHIF framework, and may be able to provide development support if the +proposed collaboration has an impact on cancer research. + +## Does OHIF offer support? + +yes, you can contact us for more information [here](https://ohif.org/get-support) + + +## Does The OHIF Viewer have [510(k) Clearance][501k-clearance] from the U.S. F.D.A or [CE Marking][ce-marking] from the European Commission? + +**NO.** The OHIF Viewer is **NOT** F.D.A. cleared or CE Marked. It is the users' +responsibility to ensure compliance with applicable rules and regulations. The +[License](https://github.com/OHIF/Viewers/blob/master/LICENSE) for the OHIF +Platform does not prevent your company or group from seeking F.D.A. clearance +for a product built using the platform. + +If you have gone this route (or are going there), please let us know because we +would be interested to hear about your experience. + +## Is there a DICOM Conformance Statement for the OHIF Viewer? + +Yes, check it here [DICOM Conformance Statement](https://docs.google.com/document/d/1hbDlUApX4svX33gAUGxGfD7fXXZNaBsX0hSePbc-hNA/edit?usp=sharing) + +## Is The OHIF Viewer [HIPAA][hipaa-def] Compliant? + +**NO.** The OHIF Viewer **DOES NOT** fulfill all of the criteria to become HIPAA +Compliant. It is the users' responsibility to ensure compliance with applicable +rules and regulations. + +## Could you provide me with a particular study from the OHIF Viewer Demo? + +You can check out the studies that we have put in this [Dropbox link](https://www.dropbox.com/scl/fo/66xidsx13pn0zf3b9cbfq/ADaCgn7aT29WMlnTdT_WRXM?rlkey=rratvx6g4kfxnswjdbupewjye&dl=0) + + + + + + +[general]: general +[technical]: technical +[report-bug]: how-do-i-report-a-bug +[new-feature]: how-can-i-request-a-new-feature +[commercial-support]: does-ohif-offer-commercial-support +[academic]: who-should-i-contact-about-academic-collaborations +[fda-clearance]: does-the-ohif-viewer-have-510k-clearance-from-the-us-fda-or-ce-marking-from-the-european-commission +[hipaa]: is-the-ohif-viewer-hipaa-compliant +[501k-clearance]: https://www.fda.gov/MedicalDevices/DeviceRegulationandGuidance/HowtoMarketYourDevice/PremarketSubmissions/PremarketNotification510k/ +[ce-marking]: https://ec.europa.eu/growth/single-market/ce-marking_en +[hipaa-def]: https://en.wikipedia.org/wiki/Health_Insurance_Portability_and_Accountability_Act +[new-issue]: https://github.com/OHIF/Viewers/issues/new/choose +[bug-report-template]: https://github.com/OHIF/Viewers/issues/new?assignees=&labels=Bug+Report+%3Abug%3A&template=---bug-report.md&title= diff --git a/platform/docs/docs/faq/index.md b/platform/docs/docs/faq/index.md new file mode 100644 index 0000000..e44af0f --- /dev/null +++ b/platform/docs/docs/faq/index.md @@ -0,0 +1,81 @@ +--- +id: index +--- + + +# General FAQ + + +## How do I report a bug? + +Navigate to our [GitHub Repository][new-issue], and submit a new bug report. +Follow the steps outlined in the [Bug Report Template][bug-report-template]. + +## How can I request a new feature? + +At the moment we are in the process of defining our roadmap and will do our best +to communicate this to the community. If your requested feature is on the +roadmap, then it will most likely be built at some point. If it is not, you are +welcome to build it yourself and [contribute it](../development/contributing.md). +If you have resources and would like to fund the development of a feature, +please [contact us](https://ohif.org/get-support). + + +## Who should I contact about Academic Collaborations? + +[Gordon J. Harris](https://www.dfhcc.harvard.edu/insider/member-detail/member/gordon-j-harris-phd/) +at Massachusetts General Hospital is the primary contact for any academic +collaborators. We are always happy to hear about new groups interested in using +the OHIF framework, and may be able to provide development support if the +proposed collaboration has an impact on cancer research. + +## Does OHIF offer support? + +yes, you can contact us for more information [here](https://ohif.org/get-support) + + +## Does The OHIF Viewer have [510(k) Clearance][501k-clearance] from the U.S. F.D.A or [CE Marking][ce-marking] from the European Commission? + +**NO.** The OHIF Viewer is **NOT** F.D.A. cleared or CE Marked. It is the users' +responsibility to ensure compliance with applicable rules and regulations. The +[License](https://github.com/OHIF/Viewers/blob/master/LICENSE) for the OHIF +Platform does not prevent your company or group from seeking F.D.A. clearance +for a product built using the platform. + +If you have gone this route (or are going there), please let us know because we +would be interested to hear about your experience. + +## Is there a DICOM Conformance Statement for the OHIF Viewer? + +Yes, check it here [DICOM Conformance Statement](https://docs.google.com/document/d/1hbDlUApX4svX33gAUGxGfD7fXXZNaBsX0hSePbc-hNA/edit?usp=sharing) + +## Is The OHIF Viewer [HIPAA][hipaa-def] Compliant? + +**NO.** The OHIF Viewer **DOES NOT** fulfill all of the criteria to become HIPAA +Compliant. It is the users' responsibility to ensure compliance with applicable +rules and regulations. + +## Could you provide me with a particular study from the OHIF Viewer Demo? + +You can check out the studies that we have put in this [Dropbox link](https://www.dropbox.com/scl/fo/66xidsx13pn0zf3b9cbfq/ADaCgn7aT29WMlnTdT_WRXM?rlkey=rratvx6g4kfxnswjdbupewjye&dl=0) + + + + + + +[general]: #general +[technical]: #technicalรŸหš +[report-bug]: #how-do-i-report-a-bug +[new-feature]: #how-can-i-request-a-new-feature +[commercial-support]: #does-ohif-offer-commercial-support +[academic]: #who-should-i-contact-about-academic-collaborations +[fda-clearance]: #does-the-ohif-viewer-have-510k-clearance-from-the-us-fda-or-ce-marking-from-the-european-commission +[hipaa]: #is-the-ohif-viewer-hipaa-compliant +[501k-clearance]: https://www.fda.gov/MedicalDevices/DeviceRegulationandGuidance/HowtoMarketYourDevice/PremarketSubmissions/PremarketNotification510k/ +[ce-marking]: https://ec.europa.eu/growth/single-market/ce-marking_en +[hipaa-def]: https://en.wikipedia.org/wiki/Health_Insurance_Portability_and_Accountability_Act +[new-issue]: https://github.com/OHIF/Viewers/issues/new/choose +[bug-report-template]: https://github.com/OHIF/Viewers/issues/new?assignees=&labels=Bug+Report+%3Abug%3A&template=---bug-report.md&title= diff --git a/platform/docs/docs/faq/study-sorting.png b/platform/docs/docs/faq/study-sorting.png new file mode 100644 index 0000000..46d2be1 Binary files /dev/null and b/platform/docs/docs/faq/study-sorting.png differ diff --git a/platform/docs/docs/faq/technical.md b/platform/docs/docs/faq/technical.md new file mode 100644 index 0000000..bee5b8f --- /dev/null +++ b/platform/docs/docs/faq/technical.md @@ -0,0 +1,361 @@ +# Technical FAQ + + + +## Viewer opens but does not show any thumbnails + +Thumbnails may not appear in your DICOMWeb application for various reasons. This guide focuses on one primary scenario, which is you are using +the `supportsWildcard: true` in your configuration file while your sever does not support it. +One + +For instance for the following filtering in the worklist tab we send this request + +![](../assets/img/filtering-worklist.png) + +`https://d33do7qe4w26qo.cloudfront.net/dicomweb/studies?PatientName=*Head*&limit=101&offset=0&fuzzymatching=false&includefield=00081030%2C00080060` + +Which our server can respond properly. If your server does not support this type of filtering, you can disable it by setting `supportsWildcard: false` in your configuration file, +or edit your server code to support it for instance something like + +```js +Pseudocode: +For each filter in filters: + if filter.value contains "*": + Convert "*" to SQL LIKE wildcard ("%") + Add "metadataField LIKE ?" to query + else: + Add "metadataField = ?" to query +``` + + + +## What are the list of required metadata for the OHIF Viewer to work? + + +### Mandatory + +**All Modalities** + +- `StudyInstanceUID`, `SeriesInstanceUID`, `SOPInstanceUID`: Unique identifiers for the study, series, and object. +- `PhotometricInterpretation`: Describes the color space of the image. +- `Rows`, `Columns`: Image dimensions. +- `PixelRepresentation`: Indicates how pixel data should be interpreted. +- `Modality`: Type of modality (e.g., CT, MR, etc.). +- `PixelSpacing`: Spacing between pixels. +- `BitsAllocated`: Number of bits allocated for each pixel sample. +- `SOPClassUID`: Specifies the DICOM service class of the object (though you might be able to render without it for most regular images datasets, but it is pretty normal to have it) + +**Rendering** + +You need to have the following tags for the viewer to render the image properly, otherwise you should +use the windowing tools to adjust the image to your liking: + +- `RescaleIntercept`, `RescaleSlope`: Values used for rescaling pixel values for visualization. +- `WindowCenter`, `WindowWidth`: Windowing parameters for display. + +**Some Datasets** + +- `InstanceNumber`: Useful for sorting instances (without it the instances might be out of order) + +**For MPR (Multi-Planar Reformatting) rendering and tools** + +- `ImagePositionPatient`, `ImageOrientationPatient`: Position and orientation of the image in the patient. + +**SEG (Segmentation)** + +- `FrameOfReferenceUID` for handling segmentation layers. +- sequences + - `ReferencedSeriesSequence` + - `SharedFunctionalGroupsSequence` + - `PerFrameFunctionalGroupsSequence` + +**RTSTRUCT (Radiotherapy Structure)** + +- `FrameOfReferenceUID` for handling segmentation layers. +- sequences + - `ROIContourSequence` + - `StructureSetROISequence` + - `ReferencedFrameOfReferenceSequence` + +**US (Ultrasound)** + +- `NumberOfFrames`: Number of frames in a multi-frame image. +- `SequenceOfUltrasoundRegions`: For measurements. +- `FrameTime`: Time between frames if specified. + +**SR (Structured Reporting)** + +- Various sequences for encoding the report content and template. + - `ConceptNameCodeSequence` + - `ContentSequence` + - `ContentTemplateSequence` + - `CurrentRequestedProcedureEvidenceSequence` + - `ContentTemplateSequence` + - `CodingSchemeIdentificationSequence` + +**PT with SUV Correction (Positron Tomography Standardized Uptake Value)** + +- Sequences and tags related to radiopharmaceuticals, units, corrections, and timing. + - `RadiopharmaceuticalInformationSequence` + - `SeriesDate` + - `SeriesTime` + - `CorrectedImage` + - `Units` + - `DecayCorrection` + - `AcquisitionDate` + - `AcquisitionTime` + - `PatientWeight` + +**PDF** + +- `EncapsulatedDocument`: Contains the PDF document. + +**Video** + +- `NumberOfFrames`: Video frame count . + + +### Optional +There are various other optional tags that will add to the viewer experience, but are not required for basic functionality. These include: +Patient Information, Study Information, Series Information, Instance Information, and Frame Information. + + +## How do I handle large volumes for MPR and Volume Rendering + +Currently there are two ways to handle large volumes for MPR and Volume Rendering if that does not +fit in the memory of the client machine. + +### `useNorm16Texture` + +WebGL officially supports only 8-bit and 32-bit data types. For most images, 8 bits are not enough, and 32 bits are too much. However, we have to use the 32-bit data type for volume rendering and MPR, which results in suboptimal memory consumption for the application. + +Through [EXT_texture_norm16](https://registry.khronos.org/webgl/extensions/EXT_texture_norm16/) , WebGL can support 16 bit data type which is ideal +for most images. You can look into the [webgl report](https://webglreport.com/?v=2) to check if you have that extension enabled. + +![](../assets/img/webgl-report-norm16.png) + + +This is a flag that you can set in your [configuration file](../configuration/configurationFiles.md) to force usage of 16 bit data type for the volume rendering and MPR. This will reduce the memory usage by half. + + +For instance for a large pt/ct study + +![](../assets/img/large-pt-ct.jpeg) + +Before (without the flag) the app shows 399 MB of memory usage + +![](../assets/img/memory-profiling-regular.png) + + +After (with flag, running locally) the app shows 249 MB of memory usage + + +![](../assets/img/webgl-int16.png) + +:::note +Using the 16 bit texture (if supported) will not have any effect in the rendering what so ever, and pixelData +would be exactly shown as it is. For datasets that cannot be represented with 16 bit data type, the flag will be ignored +and the 32 bit data type will be used. + + +Read more about these discussions in our PRs +- https://github.com/Kitware/vtk-js/pull/2058 +::: + + +:::warning +Although the support for 16 bit data type is available in WebGL, in some settings (e.g., Intel-based Macos) there seems +to be still some issues with it. You can read and track bugs below. + +- https://bugs.chromium.org/p/chromium/issues/detail?id=1246379 +- https://bugs.chromium.org/p/chromium/issues/detail?id=1408247 +::: + +### `preferSizeOverAccuracy` + +This is another flag that you can set in your [configuration file](../configuration/configurationFiles.md) to force the usage of the `half_float` data type for volume rendering and MPR. The main reason to choose this option over `useNorm16Texture` is its broader support across hardware and browsers. However, it is less accurate than the 16-bit data type and may lead to some rendering artifacts. + +```js +Integers between 0 and 2048 can be exactly represented (and also between โˆ’2048 and 0) +Integers between 2048 and 4096 round to a multiple of 2 (even number) +Integers between 4096 and 8192 round to a multiple of 4 +Integers between 8192 and 16384 round to a multiple of 8 +Integers between 16384 and 32768 round to a multiple of 16 +Integers between 32768 and 65519 round to a multiple of 32 +``` + +As you see in the ranges above 2048 there will be inaccuracies in the rendering. + +Memory snapshot after enabling `preferSizeOverAccuracy` for the same study as above + +![](../assets/img/preferSizeOverAccuracy.png) + + +## How to dynamically load a measurement + +You can dynamically load a measurement by using a combination of `MeasurementService` and `CornerstoneTools` Annotation API. Here, we will demonstrate this with an example of loading a `Rectangle` measurement. + +![alt text](faq-measure-1.png) + +So if we look at the terminal and get the measurement service we can see there is one measurement + +![alt text](faq-measure-2.png) + +However, this is the `mapped` cornerstone measurement inside OHIF, and it has additional information such as `geReport` and `source`, which are internal details of OHIF Viewers that you don't need to worry about. + +we can call the `cornerstoneTools` api to grab the raw annotation data with the `uid` + +`cornerstoneTools.annotation.state.getAnnotation("ea45a45c-0731-47d4-9438-d2a53ffea4ff")` + +![alt text](faq-measure3.png) + + + + +:::note +Note: There is a `pointsInShape` attribute inside the data that stores the points within the annotation for some tools like `Rectangle` and `EllipticalRoi`. However, you can remove that attribute as well. +::: + +For the sake of this example, I have extracted those keys and uploaded them to our server for fetching. + +` +https://ohif-assets.s3.us-east-2.amazonaws.com/ohif-faq/rectangle-roi.json +` + +Now, let's discuss how to load this measurement dynamically and programmatically. + +There are numerous places in OHIF where you can add annotations, but we always recommend having your own extensions and modes to maintain full control over your custom API. + +For this example, I will add the logic in the `longitudinal` mode. However, as mentioned, you can create your own extension and mode, and either use `onModeEnter` or other lifecycle hooks to add annotations. Learn more about lifecycle hooks [here](../platform/extensions/lifecycle.md). + + +Of course, you need to load the appropriate measurement for each study. However, for simplicity's sake, I will hardcode the URL in this example. + +```js +import * as cs3dTools from '@cornerstonejs/tools'; + +onModeEnter: function ({ servicesManager, extensionManager, commandsManager }: withAppTypes) { + // rest of logic + + const annotationResponse = await fetch( + 'https://ohif-assets.s3.us-east-2.amazonaws.com/ohif-faq/rectangle-roi.json' + ); + + const annotationData = await annotationResponse.json(); + + cs3dTools.annotation.state.addAnnotation(annotationData); +}, +``` + +As you can see, we use the CornerstoneTools API to add the annotation. Since OHIF has mappers set up for CornerstoneTools (`extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts`), it will automatically map the annotation to the OHIF measurement service. + +If you refresh the viewer, you'll see the measurement loaded on the image. + +![alt text](faq-measure-4.png) + +But if you notice it does not appear on the right panel, the reason is that the right panel is the tracking measurement panel. You can switch to a non-tracking measurement by changing + +`rightPanels: [dicomSeg.panel, tracked.measurements],` + +to + +`rightPanels: [dicomSeg.panel, '@ohif/extension-default.panelModule.measure'],` + +which then it will look like + +![alt text](faq-measure-5.png) + + +:::info +There is also dedicated example for this in the [cornerstone3D examples](https://www.cornerstonejs.org/live-examples/dynamicallyaddannotations). +::: + + +## How do I sort the series in the study panel by a specific value + +You need to enable the experimental StudyBrowserSort component by setting the `experimentalStudyBrowserSort` to true in your config file. This will add a dropdown in the study panel to sort the series by a specific value. This component is experimental +since we are re-deigning the study panel and it might change in the future, but the functionality will remain the same. + +```js +{ + experimentalStudyBrowserSort: true, +} +``` +The component will appear in the study panel and will allow you to sort the series by a specific value. It comes with 3 default sorting functions, Series Number, Series Image Count, and Series Date. + +You can sort the series in the study panel by a specific value by adding a custom sorting function in the customizationModule, you can use the existing customizationModule in `extensions/default/src/getCustomizationModule.tsx` or create your own in your extension. + +The value to be used for the entry is `studyBrowser.sortFunctions` and should be under the `default` key. + +### Example + +```js +export default function getCustomizationModule({ servicesManager, extensionManager }) { + return [ + { + name: 'default', + value: [ + + { + id: 'studyBrowser.sortFunctions', + values: [ + { + label: 'Series Number', + sortFunction: (a, b) => { + return a?.SeriesNumber - b?.SeriesNumber; + }, + }, + // Add more sort functions as needed + ], + }, + ], + }, + ]; +} +``` + +### Explanation +This function will be retrieved by the StudyBrowserSort component and will be used to sort all displaySets, it will reflect in all parts of the app since it works at the displaySetService level, which means the thumbnails in the study panel will also be sorted by the desired value. +You can define multiple functions and pick which sort to use via the dropdown in the StudyBrowserSort component that appears in the study panel. + + +## How can i change the sorting of the thumbnail / study panel / study browser +We are currently redesigning the study panel and the study browser. During this process, you can enable our undesigned component via the `experimentalStudyBrowserSort` flag. This will look like: + +![alt text](study-sorting.png) + +You can also add your own sorting functions by utilizing the `customizationService` and adding the `studyBrowser.sortFunctions` key, as shown below: + +``` +customizationService.addModeCustomizations([ + { + id: 'studyBrowser.sortFunctions', + values: [{ + label: 'Series Images', + sortFunction: (a, b) => { + return a?.numImageFrames - b?.numImageFrames; + }, + }], + }, +]); +``` + +:::note +Notice the arrays and objects, the values are arrays +::: + + +## How do I change the cine auto mount behavior + +You can change the cine auto mount behavior by adding the `autoCineModalities` mode customization, the value is an array of modalities that should be mounted with cine. + +By default the viewer will mount with cine enabled for `OT` and `US` modalities. + +```js +customizationService.addModeCustomizations([ + { + id: 'autoCineModalities', + modalities: ['OT', 'US'], + }, +]); +``` diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/0-general.md b/platform/docs/docs/migration-guide/3p8-to-3p9/0-general.md new file mode 100644 index 0000000..3439456 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/0-general.md @@ -0,0 +1,327 @@ +--- +id: 0-general +title: General +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# No SharedArrayBuffer anymore! + +We have streamlined the process of loading volumes without sacrificing speed by eliminating the need for shared array buffers. This change resolves issues across various frameworks, where previously, specific security headers were required. Now, you can remove any previously set headers, which lowers the barrier for adopting Cornerstone 3D in frameworks that didn't support those headers. Shared array buffers are no longer necessary, and all related headers can be removed. + +You can remove `Cross-Origin-Opener-Policy` and `Cross-Origin-Embedder-Policy` from your custom headers if you don't need them in other +aspects of your app. + +# React 18 Migration Guide +As we upgrade to React 18, we're making some exciting changes to improve performance and developer experience. This guide will help you navigate the key updates and ensure your custom extensions and modes are compatible with the new version. +What's Changing? + + + + +```md +- React 17 +- Using `defaultProps` +- `babel-inline-svg` for SVG imports +``` + + + + +```md +- React 18 +- Default parameters for props +- `svgr` for SVG imports +``` + + + + + +## Update React version: +In your custom extensions and modes, change the version of react and react-dom to ^18.3.1. + +## Replace defaultProps with default parameters: + + + + +```jsx +const MyComponent = ({ prop1, prop2 }) => { + return
{prop1} {prop2}
+} + +MyComponent.defaultProps = { + prop1: 'default value', + prop2: 'default value' +} +``` + +
+ + +```jsx +const MyComponent = ({ prop1 = 'default value', prop2 = 'default value' }) => { + return
{prop1} {prop2}
+} +``` +
+
+ +## Update SVG imports: + +You might need to update your SVG imports to use the `ReactComponent` syntax, if you want to use the old Icon component. However, we have made a significant change to how we handle Icons, read the UI Migration Guide for more information. + + + + +```javascript +import arrowDown from './../../assets/icons/arrow-down.svg'; +``` + + + + +```javascript +import { ReactComponent as arrowDown } from './../../assets/icons/arrow-down.svg'; +``` + + + + +--- + +## Polyfill.io + +We have removed the Polyfill.io script from the Viewer. If you require polyfills, you can add them to your project manually. This change primarily affects Internet Explorer, which Microsoft has already [ended support for](https://learn.microsoft.com/en-us/lifecycle/faq/internet-explorer-microsoft-edge#is-internet-explorer-11-the-last-version-of-internet-explorer-). + + +--- + +## Webpack changes + +We previously were copying dicom-image-loader wasm files to the public folder via + +```js +// platform/app/.webpack/webpack.pwa.js +{ + from: '../../../node_modules/@cornerstonejs/dicom-image-loader/dist/dynamic-import', + to: DIST_DIR, +}, +``` + +but now after our upgrade to Cornerstone 3D 2.0, we don't need to do this anymore. + + +--- +## Scroll utility + + +The `jumpToSlice` utility has been relocated from `@cornerstonejs/tools` utilities to `@cornerstonejs/core/utilities`. + +migration + +```js +import { jumpToSlice } from '@cornerstonejs/core/utilities'; +``` + + +--- + +## Crosshairs + +They now have new colors in their associated viewports in the MPR view. However, you can turn this feature off. + +To disable it, remove the configuration from the `initToolGroups` in your mode. + +``` +{ + configuration: { + viewportIndicators: true, + viewportIndicatorsConfig: { + circleRadius: 5, + xOffset: 0.95, + yOffset: 0.05, + }, + } +} +``` + +--- + + +## useAuthorizationCodeFlow + +`useAuthorizationCodeFlow` config is deprecated + +now internally we detect the authorizationCodeFlow if the response_type is equal to `code` + +you can remove the config from the appConfig + +--- + +## StackScrollMouseWheel -> StackScroll Tool + Mouse bindings + +If you previously used: + +```js +{ toolName: toolNames.StackScrollMouseWheel, bindings: [] } +``` + +in your `initToolGroups`, you should now use: + +```js +{ + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], +} +``` + +This change allows for more flexible mouse bindings and keyboard combinations. + +## VolumeRotateMouseWheel -> VolumeRotate Tool + Mouse bindings + +Before: + +```js +{ + toolName: toolNames.VolumeRotateMouseWheel, + configuration: { + rotateIncrementDegrees: 5, + }, +}, +``` + +Now: + +```js +{ + toolName: toolNames.VolumeRotate, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + configuration: { + rotateIncrementDegrees: 5, + }, +}, +``` + +--- + +## CustomizationService + +The `CustomizationService` uses `contentF` instead of `content`. + +So make sure your customizations are updated accordingly. + +--- + +## SidePanel auto switch if open + +In `basic viewer` mode, when the side panel is open and the segmentation panel is active, adding a measurement will automatically switch to the measurement panel. This switch won't happen if the side panel is closed. To enable or disable this feature, adjust your mode configuration accordingly. + +To prevent this behavior, remove the following code from your mode: + +```js +panelService.addActivatePanelTriggers('your.panel.id', [ +{ + sourcePubSubService: segmentationService, + sourceEvents: [segmentationService.EVENTS.SEGMENTATION_ADDED], +}, +]) + +panelService.addActivatePanelTriggers('your.panel.id', [ + { + sourcePubSubService: measurementService, + sourceEvents: [ + measurementService.EVENTS.MEASUREMENT_ADDED, + measurementService.EVENTS.RAW_MEASUREMENT_ADDED, + ], + }, +]) +``` + +--- + +## DicomUpload + +The DICOM upload functionality in OHIF has been refactored to use the standard customization service pattern. Now you don't need to put + +`customizationService: { dicomUploadComponent: '@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent', },` + +in your config, we will automatically add that if you have `dicomUploadEnabled` + +--- + +## Viewport and Modality Support for Toolbar Buttons + +Previously, toolbar buttons had limited support for disabling themselves based on the active viewport type (e.g., `volume3d`, `video`, `sr`) or the modality of the displayed data (e.g., `US`, `SM`). This led to inconsistencies and sometimes enabled tools in contexts where they weren't applicable. + +The new implementation introduces more robust and flexible evaluators to control the enabled/disabled state of toolbar buttons based on viewport types and modalities. + +**Key Changes** + +1. **New Evaluators:** New evaluators have been added to the `getToolbarModule`: + - `evaluate.viewport.supported`: Disables a button if the active viewport's type is listed in the `unsupportedViewportTypes` property. + - `evaluate.modality.supported`: Disables a button based on the modalities of the displayed data. It checks for both `unsupportedModalities` (exclusion) and `supportedModalities` (inclusion). +2. **Removal of Legacy Evaluators:** + - Evaluators such as `evaluate.not.sm`, `evaluate.action.not.video`, `evaluate.not3D`, and `evaluate.isUS` have been removed. Migrate your toolbar button definitions to use the new evaluators mentioned above. + + +**Replace Legacy Evaluators:** + - Replace `evaluate.not.sm` with: + + ```json + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['sm'], + } + ``` + + - Replace `evaluate.action.not.video` with: + + ```json + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + } + ``` + + - Replace `evaluate.not3D` with: + + ```json + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d'], + } + ``` + + - Replace `evaluate.isUS` with: + + ```json + { + name: 'evaluate.modality.supported', + supportedModalities: ['US'], + } + ``` + +
+Example Migration + +Before: + +```json +evaluate: ['evaluate.cine', 'evaluate.not3D'], +``` + +After + +```json +evaluate: [ + 'evaluate.cine', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d'], + }, +], +``` +
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/1-Architecture.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/1-Architecture.md new file mode 100644 index 0000000..c8a92ef --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/1-Architecture.md @@ -0,0 +1,48 @@ +--- +id: seg-new-arch +title: New Architecture +--- + + +## New Architecture + +* **Viewport-Centric Architecture** + * Previous: Segmentations were tied to toolGroups + * Now: Segmentations are tied directly to viewports + * Impact: More granular control but requires significant code changes + +* **Representation Management** + * Previous: Required managing segmentation representation UIDs + * Now: Uses simpler segmentationId + type combination + * Impact: Simplified but requires API updates + + + +If you are not familiar with the difference between a segmentation and a segmentation representation, below + +
+Read More + +In Cornerstone3DTools, we have decoupled the concept of a Segmentation from a Segmentation Representation. This means that from one Segmentation we can create multiple Segmentation Representations. For instance, a Segmentation Representation of a 3D Labelmap, can be created from a Segmentation data, and a Segmentation Representation of a Contour can be created from the same Segmentation data. This way we have decouple the presentational aspect of a Segmentation from the underlying data. + + +Similar relationship structure has been adapted in popular medical imaging softwares such as 3D Slicer with the addition of polymorph segmentation. + +- https://github.com/PerkLab/PolySeg +- https://www.slicer.org/ + + + +
+ + + + +### Architecture Overview + +The new architecture in Cornerstone3D 2.0 makes a clear distinction between: + +* A segmentation (the data structure containing segments) +* A segmentation representation (how that segmentation is visualized in a specific viewport) + +Let's now review what has changed diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/2-segmentationService-basic.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/2-segmentationService-basic.md new file mode 100644 index 0000000..06601b7 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/2-segmentationService-basic.md @@ -0,0 +1,433 @@ +--- +id: seg-api +title: SegmentationService API +--- + + + +Below we will review the changes to the API of the `SegmentationService` + +# SegmentationService API + +## Events + +SEGMENTATION_UPDATED -> SEGMENTATION_MODIFIED + + +Just a rename to match the cornerstone terminology + +## VolumeId vs SegmentationId + +Previously, we used the SegmentationId as the VolumeId for volume-based segmentations, which led to confusion and issues. + +Now, we have two separate IDs: one for the segmentation and one for the volume. + +`segmentationService.getLabelmapVolume(segmentationId)` will return the volume associated with the segmentation. + +If your code uses `cache.getVolume(segmentationId)`, update it to use the new `getLabelmapVolume` method. + + +## getSegmentation(segmentationId) + +remains the same it will return the segmentation object = cornerstone segmentation object with the following properties: + +```js +/** + * Global Segmentation Data which is used for the segmentation + */ +type Segmentation = { + /** segmentation id */ + segmentationId: string; + /** segmentation label */ + label: string; + segments: { + [segmentIndex: number]: Segment; + }; + /** + * Representations of the segmentation. Each segmentation "can" be viewed + * in various representations. For instance, if a DICOM SEG is loaded, the main + * representation is the labelmap. However, for DICOM RT the main representation + * is contours, and other representations can be derived from the contour (currently + * only labelmap representation is supported) + */ + representationData: RepresentationsData; + /** + * Segmentation level stats, Note each segment can have its own stats + * This is used for caching stats for the segmentation level + */ + cachedStats: { [key: string]: unknown }; +}; + +export type Segment = { + /** segment index */ + segmentIndex: number; + /** segment label */ + label: string; + /** is segment locked for editing */ + locked: boolean; + /** cached stats for the segment, e.g., pt suv mean, max etc. */ + cachedStats: { [key: string]: unknown }; + /** is segment active for editing, at the same time only one segment can be active for editing */ + active: boolean; +}; +``` + + +
+Compared to Cornerstone3D 1.x + +Previously this function was returning this + +```js +export type Segmentation = { + segmentationId: string; + type: Enums.SegmentationRepresentations; + label: string; + activeSegmentIndex: number; + segmentsLocked: Set; + cachedStats: { [key: string]: number }; + segmentLabels: { [key: string]: string }; + representationData: SegmentationRepresentationData; +}; + +``` + +As you can see `segmentLabels`, `segmentsLocked`, `activeSegmentIndex`, are all gathered under the new `segments` object. We now have support for per segment cachedStats as well. + +
+ +--- + +## getSegmentations + +It provides all segmentations in the state. Previously, it accepted a `filterNonhydrated` flag, but since we've moved away from hydration and every loaded segmentation is now hydrated by default, it returns all segmentations. + + + + +--- + +## getActiveSegmentation + + +After migrating to viewport-specific segmentations, different viewports can have distinct active segmentations for editing. The panel will always display the active segmentation when the active viewport changes. + +Before (3.8) + +```js +// Returns full segmentation object +public getActiveSegmentation(): Segmentation { + const segmentations = this.getSegmentations(); + return segmentations.find(segmentation => segmentation.isActive); +} +``` + +After (3.9) + +```js +public getActiveSegmentation(viewportId: string): Segmentation | null { + return cstSegmentation.activeSegmentation.getActiveSegmentation(viewportId); +} +``` + +
+Key Changes + +1. **Viewport Specificity** + - Before: Global active segmentation across all tool groups + - After: Active segmentation per viewport +2. **Required Parameters** + - Before: No parameters needed + - After: Requires viewportId parameter +
+ + +
+Migration Examples + +**Before:** + +```js +// Get active segmentation +const activeSegmentation = segmentationService.getActiveSegmentation(); +if (activeSegmentation) { + console.log('Active segmentation:', activeSegmentation.segmentationId); + console.log('Active segment:', activeSegmentation.activeSegmentIndex); +} +``` + +**After:** + +```js +// Get active segmentation for specific viewport +const activeSegmentation = segmentationService.getActiveSegmentation('viewport1'); + +``` + +
+ +--- + +## getToolGroupIdsWithSegmentation + +is now -> `getViewportIdsWithSegmentation` as you guessed + + + +## setActiveSegmentationForToolGroup + +-> setActiveSegmentation + + + +**Before (OHIF 3.8)** + +```js +setActiveSegmentationForToolGroup( + segmentationId: string, + toolGroupId?: string, + suppressEvents?: boolean +): void +``` + +**After (OHIF 3.9)** + +```js +setActiveSegmentation( + viewportId: string, + segmentationId: string +): void +``` + +
+Migration Examples + +1. **Basic Usage Update** + + ```js + // Before - OHIF 3.8 + segmentationService.setActiveSegmentationForToolGroup( + segmentationId, + toolGroupId + ); + // After - OHIF 3.9 + segmentationService.setActiveSegmentation( + viewportId, + segmentationId + ); + ``` + +
+ + + +--- + + +## addSegment + +The `addSegment` method in OHIF 3.9 has been updated to handle segmentation properties in a viewport-centric way, removing tool group dependencies and simplifying the configuration structure. + + +**Before (OHIF 3.8)** + +```js +addSegment( + segmentationId: string, + config: { + segmentIndex?: number; + toolGroupId?: string; + properties?: { + label?: string; + color?: ohifTypes.RGB; + opacity?: number; + visibility?: boolean; + isLocked?: boolean; + active?: boolean; + }; + } +): void +``` + +**After (OHIF 3.9)** + +```js +addSegment( + segmentationId: string, + config: { + segmentIndex?: number; + label?: string; + isLocked?: boolean; + active?: boolean; + color?: csTypes.Color; + visibility?: boolean; + } +): void +``` + +
+Key Changes + +1. **Configuration Structure** + - Removed double nested `properties` object + - Configuration options now at top level + - Removed `toolGroupId` parameter + - Removed `opacity` parameter (now part of color) +2. **Segment Index Generation** + - Changed from length-based to max-value-based indexing + - More reliable for non-sequential segment indices +3. **Color Handling** + - Color now includes alpha channel (opacity) + - Applied to all relevant viewports automatically +
+ + + + +
+Migration Examples + +1. **Basic Segment Creation** + + ```js + // Before - OHIF 3.8 + segmentationService.addSegment(segmentationId, { + properties: { + label: 'Segment 1' + } + }); + // After - OHIF 3.9 + segmentationService.addSegment(segmentationId, { + label: 'Segment 1' + }); + ``` + +2. **Creating Segment with Color** + + ```js + // Before - OHIF 3.8 + segmentationService.addSegment(segmentationId, { + properties: { + color: [255, 0, 0], + opacity: 255 + } + }); + // After - OHIF 3.9 + segmentationService.addSegment(segmentationId, { + color: [255, 0, 0, 255] // RGB + Alpha + }); + ``` + +3. **Setting Visibility and Lock Status** + + ```js + // Before - OHIF 3.8 + segmentationService.addSegment(segmentationId, { + toolGroupId: 'myToolGroup', + properties: { + visibility: true, + isLocked: true + } + }); + // After - OHIF 3.9 + segmentationService.addSegment(segmentationId, { + visibility: true, + isLocked: true + }); + ``` + +4. **Complete Configuration Example** + + ```js + // Before - OHIF 3.8 + segmentationService.addSegment(segmentationId, { + segmentIndex: 1, + toolGroupId: 'myToolGroup', + properties: { + label: 'Tumor', + color: [255, 0, 0], + opacity: 200, + visibility: true, + isLocked: false, + active: true + } + }); + // After - OHIF 3.9 + segmentationService.addSegment(segmentationId, { + segmentIndex: 1, + label: 'Tumor', + color: [255, 0, 0, 200], // RGB + Alpha + visibility: true, + isLocked: false, + active: true + }); + ``` + +
+ + + + +
+Important Changes + +1. **Tool Group Removal** + ```js + // Before - OHIF 3.8 + segmentationService.addSegment(segmentationId, { + toolGroupId: 'myToolGroup' + // ... other properties + }); + // After - OHIF 3.9 + // No tool group needed - automatically applies to all relevant viewports + segmentationService.addSegment(segmentationId, { + // ... properties + }); + ``` + +2. **Segment Index Generation** + ```js + // Before - OHIF 3.8 + // Used array length + segmentIndex = segmentation.segments.length === 0 ? 1 : segmentation.segments.length; + // After - OHIF 3.9 + // Uses highest existing index + 1 + segmentIndex = Math.max(...Object.keys(csSegmentation.segments).map(Number)) + 1; + ``` + +3. **Color and Opacity** + ```js + // Before - OHIF 3.8 + segmentationService.addSegment(segmentationId, { + properties: { + color: [255, 0, 0], + opacity: 200 + } + }); + + // After - OHIF 3.9 + segmentationService.addSegment(segmentationId, { + color: [255, 0, 0, 200] // Combined color and opacity + }); + ``` + +
+ + +--- + +--- + +## getActiveSegment + +now requires viewportId, since we have moved away from global active segmentation to viewport specific one + +**API Changes** + +```js +// Before +getActiveSegment(): Segment + +// After +getActiveSegment(viewportId: string): Segment | null +``` diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/3-segmentationserice-representation.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/3-segmentationserice-representation.md new file mode 100644 index 0000000..5222592 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/3-segmentationserice-representation.md @@ -0,0 +1,189 @@ +--- +id: seg-representation +title: Segmentation Representations +--- + + + + +## Segmentation Representation Management API + +```js +addSegmentationRepresentationToToolGroup +removeSegmentationRepresentationFromToolGroup +getSegmentationRepresentationsForToolGroup +``` + +In Cornerstone3D 2.0, segmentation representation management has shifted from a tool group-centric approach to a viewport-centric approach. This architectural change provides better control over segmentation rendering and simplifies the mental model for managing segmentations. + + +### Adding Segmentation Representations + +**Before (3.8)**: + +```js +// Tool group-based approach +await segmentation.addSegmentationRepresentationToToolGroup( + toolGroupId, + segmentationId, + hydrateSegmentation, + csToolsEnums.SegmentationRepresentations.Labelmap +); +``` + +**After (3.9)**: + +```js +// Viewport-centric approach +await segmentation.addSegmentationRepresentation( + viewportId, + { + segmentationId: segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap, + } +); +``` + +### Removing Segmentation Representations + +**Before** : + +```js +// Remove specific representations from a tool group +segmentation.removeSegmentationRepresentationFromToolGroup( + toolGroupId, + [segmentationRepresentationUID] +); +// Remove all representations from a tool group +segmentation.removeSegmentationRepresentationFromToolGroup(toolGroupId); +``` + +**After** + +```js +// Remove specific representation from a viewport +segmentation.removeSegmentationRepresentation( + viewportId, + { + segmentationId: segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap + } +); +// Remove all representations from a viewport +segmentation.removeSegmentationRepresentations(viewportId); +``` + +### Getting Segmentation Representations + +**Before**: + +```js +// Get representations for a tool group +const representations = segmentation.getSegmentationRepresentationsForToolGroup(toolGroupId); +``` + +**After** : + +```js +// Get all representations for a viewport +const representations = segmentation.getSegmentationRepresentations(viewportId); + +// Get specific type of representations +const labelmapReps = segmentation.getSegmentationRepresentations(viewportId, { + type: csToolsEnums.SegmentationRepresentations.Labelmap +}); + +// Get representations for specific segmentation +const segmentationReps = segmentation.getSegmentationRepresentations(viewportId, { + segmentationId: segmentationId +}); + +// Get specific representation +const representation = segmentation.getSegmentationRepresentation(viewportId, { + segmentationId: segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap +}); +``` + +### Understanding the Specifier Pattern + +The Cornerstone3D 2.0 (OHIF 3.9) API introduces a "specifier" pattern that provides more flexible and precise control over segmentation representations. A specifier is an object that can include: + +```js +type Specifier = { + segmentationId?: string; // The ID of the segmentation + type?: SegmentationRepresentations; // The type of representation (Labelmap, Contour, etc.) +} +``` + +The specifier pattern allows for: + +1. **Precise Targeting**: You can target specific segmentations and representation types + - Allows direct access to individual segmentations + - Enables filtering by representation type + +2. **Flexible Querying**: You can get all representations of a certain type or for a specific segmentation + - Query by segmentation ID + - Query by representation type + - Combine queries for specific needs + +3. **Granular Control**: You can manage representations at different levels of specificity + - Viewport level control + - Segmentation level control + - Individual representation type control + +### Examples of Specifier Usage + +```js +// Get all labelmap representations in a viewport +const labelmaps = segmentation.getSegmentationRepresentations(viewportId, { + type: csToolsEnums.SegmentationRepresentations.Labelmap +}); + +// Get all representations of a specific segmentation (including contour, labelmap, surface) +const segReps = segmentation.getSegmentationRepresentations(viewportId, { + segmentationId: 'seg123' +}); + +// Get a specific representation +const specificRep = segmentation.getSegmentationRepresentation(viewportId, { + segmentationId: 'seg123', + type: csToolsEnums.SegmentationRepresentations.Labelmap +}); +``` + +
+Benefits of the New Approach + +1. **Direct Viewport Control**: + - Each viewport can have its own unique representation configuration + - No need to create separate tool groups for different viewport representations +2. **Simpler Mental Model**: + - Representations are directly tied to where they're displayed + - No intermediate tool group layer to manage +3. **More Flexible Rendering**: + - Each viewport can render the same segmentation differently + - Better support for multiple views of the same data +4. **Improved Type Safety**: + - Specifier pattern provides better TypeScript support + - More explicit API with clearer intentions +
+ + +
+Migration Tips + +1. **Replace Tool Group References**: + - Search your codebase for `toolGroupId` references in segmentation code + - Replace with appropriate `viewportId` references +2. **Update Event Handlers**: + - Update any code listening for segmentation events + - Events now include viewportId instead of toolGroupId +3. **Review Representation Management**: + - Identify where you manage segmentation representations + - Convert to using the new viewport-centric methods +4. **Consider Viewport Context**: + - Think about segmentation representation in terms of viewport display + - Use specifiers to target specific representations when needed + +
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-creation.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-creation.md new file mode 100644 index 0000000..c70021e --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-creation.md @@ -0,0 +1,215 @@ +--- +id: seg-creation +title: Segmentation Creation +--- + +## createEmptySegmentationForViewport + +is now `createLabelmapForViewport` to align with other segmentation creation methods. + +Run it using `commandsManager.runCommand('createLabelmapForViewport', {viewportId})`. + +## createSegmentationForDisplaySet + +is now -> `createLabelmapForDisplaySet` + +Since we are moving towards segmentations be contours as well, this is renamed to clearly state the purpose. +Since OHIF 3.9 introduced Stack Segmentation support, we no longer generate a volume-based labelmap or convert the viewport to a volume viewport by default. Our default creation is now stack-based. + +API Changes +- `createSegmentationForDisplaySet` has been renamed to `createLabelmapForDisplaySet`. +- Pass a `displaySet` object instead of a `displaySetInstanceUID`. This change enhances type safety and flexibility, accommodating future updates to the `displaySetService`. + +**Before (OHIF 3.8)** + +```js +async createSegmentationForDisplaySet( + displaySetInstanceUID: string, + options?: { + segmentationId: string; + FrameOfReferenceUID: string; + label: string; + } +): Promise +``` + +**After (OHIF 3.9)** + +```js +// Method 1: Display Set Based +async createLabelmapForDisplaySet( + displaySet: DisplaySet, + options?: { + segmentationId?: string; + label: string; + segments?: { + [segmentIndex: number]: Partial + }; + } +): Promise +``` + + +
+Migration Examples + + +```js +// Before - OHIF 3.8 +const segmentationId = await segmentationService.createSegmentationForDisplaySet( + displaySetInstanceUID, + { + label: 'My Segmentation' + } +); +``` + +```js +// After - OHIF 3.9 +// Option 1: If you have a display set UID +const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + +const segmentationId = await segmentationService.createLabelmapForDisplaySet( + displaySet, + { + label: 'My Segmentation' + } +); +``` + +
+ +--- + +## createSegmentationForRTDisplaySet + + +**Before (OHIF 3.8)** + +```js +async createSegmentationForRTDisplaySet( + rtDisplaySet, + segmentationId?: string, + suppressEvents = false +): Promise +``` + +**After (OHIF 3.9)** + +```js +async createSegmentationForRTDisplaySet( + rtDisplaySet, + options: { + segmentationId?: string; + type: SegmentationRepresentations; // not required, defaults to Contour + } +): Promise +``` + + +
+Migration Examples + +if you were not passing segmentationId, you don't need to change anything + + +```js +// Before - OHIF 3.8 +const segmentationId = await segmentationService.createSegmentationForRTDisplaySet( + rtDisplaySet +); + +// After - OHIF 3.9 +const segmentationId = await segmentationService.createSegmentationForRTDisplaySet( + rtDisplaySet, +); +``` + +if you were passing segmentationId, you need to update the API to pass an options object and set the segmentationId in there. + +```js +// Before - OHIF 3.8 +const segmentationId = await segmentationService.createSegmentationForRTDisplaySet( + rtDisplaySet, + 'custom-id', +); +// After - OHIF 3.9 +const segmentationId = await segmentationService.createSegmentationForRTDisplaySet( + rtDisplaySet, + { + segmentationId: 'custom-id', + type: csToolsEnums.SegmentationRepresentations.Contour + } +); +``` + +
+ +--- + + +## createSegmentationForSEGDisplaySet Changes + +**Before (OHIF 3.8)** + +```js +async createSegmentationForSEGDisplaySet( + segDisplaySet, + segmentationId?: string, + suppressEvents = false +): Promise +``` + +**After (OHIF 3.9)** + +```js +async createSegmentationForSEGDisplaySet( + segDisplaySet, + options: { + segmentationId?: string; + type: SegmentationRepresentations; // not required, defaults to Labelmap + } +): Promise +``` + +
+Migration Examples + +1. **Basic Usage Update** + + ``` + // Before - OHIF 3.8 + const segmentationId = await segmentationService.createSegmentationForSEGDisplaySet( + segDisplaySet + ); + // After - OHIF 3.9 + const segmentationId = await segmentationService.createSegmentationForSEGDisplaySet( + segDisplaySet, + { + type: csToolsEnums.SegmentationRepresentations.Labelmap + } + ); + ``` + +2. **Custom Configuration** + + ``` + // Before - OHIF 3.8 + const segmentationId = await segmentationService.createSegmentationForSEGDisplaySet( + segDisplaySet, + 'custom-id', + false + ); + // After - OHIF 3.9 + const segmentationId = await segmentationService.createSegmentationForSEGDisplaySet( + segDisplaySet, + { + segmentationId: 'custom-id', + type: csToolsEnums.SegmentationRepresentations.Labelmap + } + ); + ``` +
+ + +--- diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-modification.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-modification.md new file mode 100644 index 0000000..ebe16f7 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-modification.md @@ -0,0 +1,193 @@ +--- +id: seg-service-mod +title: SegmentationService Modifications +--- + + +--- + + +## Segmentation Representation Management API + +```js +addSegmentationRepresentationToToolGroup +removeSegmentationRepresentationFromToolGroup +getSegmentationRepresentationsForToolGroup +``` + +In Cornerstone3D 2.0, segmentation representation management has shifted from a tool group-centric approach to a viewport-centric approach. This architectural change provides better control over segmentation rendering and simplifies the mental model for managing segmentations. + + +### Adding Segmentation Representations + +**Before (3.8)**: + +```js +// Tool group-based approach +await segmentation.addSegmentationRepresentationToToolGroup( + toolGroupId, + segmentationId, + hydrateSegmentation, + csToolsEnums.SegmentationRepresentations.Labelmap +); +``` + +**After (3.9)**: + +```js +// Viewport-centric approach +await segmentation.addSegmentationRepresentation( + viewportId, + { + segmentationId: segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap, + } +); +``` + +### Removing Segmentation Representations + +**Before** : + +```js +// Remove specific representations from a tool group +segmentation.removeSegmentationRepresentationFromToolGroup( + toolGroupId, + [segmentationRepresentationUID] +); +// Remove all representations from a tool group +segmentation.removeSegmentationRepresentationFromToolGroup(toolGroupId); +``` + +**After** + +```js +// Remove specific representation from a viewport +segmentation.removeSegmentationRepresentation( + viewportId, + { + segmentationId: segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap + } +); +// Remove all representations from a viewport +segmentation.removeSegmentationRepresentations(viewportId); +``` + +### Getting Segmentation Representations + +**Before**: + +```js +// Get representations for a tool group +const representations = segmentation.getSegmentationRepresentationsForToolGroup(toolGroupId); +``` + +**After** : + +```js +// Get all representations for a viewport +const representations = segmentation.getSegmentationRepresentations(viewportId); + +// Get specific type of representations +const labelmapReps = segmentation.getSegmentationRepresentations(viewportId, { + type: csToolsEnums.SegmentationRepresentations.Labelmap +}); + +// Get representations for specific segmentation +const segmentationReps = segmentation.getSegmentationRepresentations(viewportId, { + segmentationId: segmentationId +}); + +// Get specific representation +const representation = segmentation.getSegmentationRepresentation(viewportId, { + segmentationId: segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap +}); +``` + +### Understanding the Specifier Pattern + +The Cornerstone3D 2.0 (OHIF 3.9) API introduces a "specifier" pattern that provides more flexible and precise control over segmentation representations. A specifier is an object that can include: + +```js +type Specifier = { + segmentationId?: string; // The ID of the segmentation + type?: SegmentationRepresentations; // The type of representation (Labelmap, Contour, etc.) +} +``` + +The specifier pattern allows for: + +1. **Precise Targeting**: You can target specific segmentations and representation types + - Allows direct access to individual segmentations + - Enables filtering by representation type + +2. **Flexible Querying**: You can get all representations of a certain type or for a specific segmentation + - Query by segmentation ID + - Query by representation type + - Combine queries for specific needs + +3. **Granular Control**: You can manage representations at different levels of specificity + - Viewport level control + - Segmentation level control + - Individual representation type control + +### Examples of Specifier Usage + +```js +// Get all labelmap representations in a viewport +const labelmaps = segmentation.getSegmentationRepresentations(viewportId, { + type: csToolsEnums.SegmentationRepresentations.Labelmap +}); + +// Get all representations of a specific segmentation (including contour, labelmap, surface) +const segReps = segmentation.getSegmentationRepresentations(viewportId, { + segmentationId: 'seg123' +}); + +// Get a specific representation +const specificRep = segmentation.getSegmentationRepresentation(viewportId, { + segmentationId: 'seg123', + type: csToolsEnums.SegmentationRepresentations.Labelmap +}); +``` + +
+Benefits of the New Approach + +1. **Direct Viewport Control**: + - Each viewport can have its own unique representation configuration + - No need to create separate tool groups for different viewport representations +2. **Simpler Mental Model**: + - Representations are directly tied to where they're displayed + - No intermediate tool group layer to manage +3. **More Flexible Rendering**: + - Each viewport can render the same segmentation differently + - Better support for multiple views of the same data +4. **Improved Type Safety**: + - Specifier pattern provides better TypeScript support + - More explicit API with clearer intentions +
+ + +
+Migration Tips + +1. **Replace Tool Group References**: + - Search your codebase for `toolGroupId` references in segmentation code + - Replace with appropriate `viewportId` references +2. **Update Event Handlers**: + - Update any code listening for segmentation events + - Events now include viewportId instead of toolGroupId +3. **Review Representation Management**: + - Identify where you manage segmentation representations + - Convert to using the new viewport-centric methods +4. **Consider Viewport Context**: + - Think about segmentation representation in terms of viewport display + - Use specifiers to target specific representations when needed + +
+ + +--- diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/5-segmentationserice-style.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/5-segmentationserice-style.md new file mode 100644 index 0000000..266206b --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/5-segmentationserice-style.md @@ -0,0 +1,362 @@ +--- +id: seg-style +title: SegmentationService Style +--- + + +## Style + + +### setSegmentVisibility + +since visibility is viewport concern and representation is what is being toggled -> + +**Before (OHIF 3.8)** + +```js +setSegmentVisibility( + segmentationId: string, + segmentIndex: number, + isVisible: boolean, + toolGroupId?: string +): void +``` + +**After (OHIF 3.9)** + +```js +setSegmentVisibility( + viewportId: string, + segmentationId: string, + segmentIndex: number, + isVisible: boolean, + type?: SegmentationRepresentations +): void +``` + +
+Migration Example + +```js +// Before +segmentationService.setSegmentVisibility( + 'segmentation1', + 1, + true, + 'toolGroup1' +); +// After +segmentationService.setSegmentVisibility( + 'viewport1', + 'segmentation1', + 1, + true +); +``` + +**Getting Viewport IDs** + +When you need to update visibility across multiple viewports: + +```js +// Before +const toolGroupIds = ['toolGroup1', 'toolGroup2']; +toolGroupIds.forEach(toolGroupId => { + segmentationService.setSegmentVisibility( + 'segmentation1', + 1, + true, + toolGroupId + ); +}); +// After +const viewportIds = segmentationService.getViewportIdsWithSegmentation('segmentation1'); +viewportIds.forEach(viewportId => { + segmentationService.setSegmentVisibility( + viewportId, + 'segmentation1', + 1, + true + ); +}); +``` + + +
+ + +### get/set Configuration -> get/setStyle + +The segmentation configuration system has been completely redesigned: + +- Moved from global/toolGroup configuration to viewport-specific styles +- Split rendering of inactive segmentations into separate API +- More granular control over styles at different levels (global, segmentation, viewport, segment) + + +**Before (OHIF 3.8)** + +```js +interface SegmentationConfig { + brushSize: number; + brushThresholdGate: number; + fillAlpha: number; + fillAlphaInactive: number; + outlineWidthActive: number; + renderFill: boolean; + renderInactiveSegmentations: boolean; + renderOutline: boolean; + outlineOpacity: number; + outlineOpacityInactive: number; +} +``` + +**After (OHIF 3.9)** + +```js +// Style Types +interface StyleSpecifier { + viewportId?: string; + segmentationId?: string; + type: SegmentationRepresentations; + segmentIndex?: number; +} +interface LabelmapStyle { + renderOutline: boolean; + outlineWidth: number; + renderFill: boolean; + fillAlpha: number; + outlineAlpha: number; + // .... +} +// Functions +getStyle(specifier: StyleSpecifier): LabelmapStyle | ContourStyle | SurfaceStyle; +setStyle(specifier: StyleSpecifier, style: LabelmapStyle | ContourStyle | SurfaceStyle): void; +setRenderInactiveSegmentations(viewportId: string, renderInactive: boolean): void; +getRenderInactiveSegmentations(viewportId: string): boolean; +``` + + +**Before:** + +```js +// Get global configuration +const config = segmentationService.getConfiguration(); +console.log(config.fillAlpha, config.renderOutline); +// Get tool group specific config +const toolGroupConfig = segmentationService.getConfiguration('toolGroup1'); +``` + +**After:** + +```js +// Get global style for labelmap +const labelmapStyle = segmentationService.getStyle({ + type: SegmentationRepresentations.Labelmap +}); +// Get viewport-specific style +const viewportStyle = segmentationService.getStyle({ + viewportId: 'viewport1', + type: SegmentationRepresentations.Labelmap +}); +// Get segmentation-specific style +const segmentationStyle = segmentationService.getStyle({ + segmentationId: 'seg1', + type: SegmentationRepresentations.Labelmap +}); +// Get segment-specific style +const segmentStyle = segmentationService.getStyle({ + segmentationId: 'seg1', + type: SegmentationRepresentations.Labelmap, + segmentIndex: 1 +}); +``` + + + +**Setting Configuration/Style** + +**Before:** + +```js +segmentationService.setConfiguration({ + fillAlpha: 0.5, + outlineWidthActive: 2, + renderOutline: true, + renderFill: true, + renderInactiveSegmentations: true +}); +``` + +**After:** + +```js +// Set global style +segmentationService.setStyle( + { type: SegmentationRepresentations.Labelmap }, + { + fillAlpha: 0.5, + outlineWidth: 2, + renderOutline: true, + renderFill: true + } +); +// Set viewport-specific style +segmentationService.setStyle( + { + viewportId: 'viewport1', + type: SegmentationRepresentations.Labelmap + }, + { + fillAlpha: 0.5, + outlineWidth: 2 + } +); +// Handle inactive segmentations separately +segmentationService.setRenderInactiveSegmentations('viewport1', true); +``` + + +
+Migration Examples + +**Combining Multiple Style Settings** + +**Before:** + +```js +segmentationService.setConfiguration({ + fillAlpha: 0.5, + fillAlphaInactive: 0.2, + outlineWidthActive: 2, + outlineOpacity: 1, + outlineOpacityInactive: 0.5, + renderOutline: true, + renderFill: true, + renderInactiveSegmentations: true +}); +``` + +**After:** + +```js +// Set base style +segmentationService.setStyle( + { type: SegmentationRepresentations.Labelmap }, + { + fillAlpha: 0.5, + outlineWidth: 2, + outlineAlpha: 1, + renderOutline: true, + renderFill: true + } +); +``` + +
+ + + +**Set inactive rendering per viewport** + +```js +segmentationService.setRenderInactiveSegmentations('viewport1', true); +// Set style for inactive segments if needed +segmentationService.setStyle( + { + viewportId: 'viewport1', + type: SegmentationRepresentations.Labelmap, + segmentationId: 'seg1' + }, + { + fillAlpha: 0.2, + outlineAlpha: 0.5 + } +); +``` + +--- + + + +## setSegmentRGBAColor , setSegmentOpacity, setSegmentRGBA +Previously, the SegmentationService had multiple redundant methods for setting colors and opacity (`setSegmentRGBA`, `setSegmentColor`, `setSegmentOpacity`). This led to confusion and potential state inconsistencies between the service and Cornerstone.js Tools. + +The old methods (`setSegmentRGBA`, `setSegmentRGBA`, and `setSegmentOpacity`) are now removed. + + +1. Replace `setSegmentRGBAColor`, `setSegmentRGBA`, and `setSegmentOpacity` calls: Replace all instances of the old methods with the new `setSegmentColor` method. Note that you now need to provide the `viewportId` as the first argument since segment color is managed per viewport and representation in cornerstone3D. + + +**Before** + +```js +// Old API: +segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, rgbaColor, toolGroupId); +segmentationService.setSegmentRGBA(segmentationId, segmentIndex, rgbaColor, toolGroupId); +segmentationService.setSegmentOpacity(segmentationId, segmentIndex, opacity, toolGroupId); +``` + +**After** + +```js +// New API: +segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color); // color is an array of [red, green, blue, alpha] +``` + +The new `color` argument is an array representing the RGBA color, where the alpha component determines the opacity. Since the Cornerstone Tools library handles segment color per viewport and representation, we require the `viewportId` as an argument now. + + + +2. **Retrieve Segment Color using** `getSegmentColor`: The new `getSegmentColor` provides a way to fetch the color of a segment within a specific viewport. + +```js +const color = segmentationService.getSegmentColor(viewportId, segmentationId, segmentIndex); //returns [r, g, b, a] +``` + + +--- + + + +## ToggleSegmentationVisibility + +In Cornerstone3D v2.x, `toggleSegmentationVisibility` has been replaced with `toggleSegmentationRepresentationVisibility`. This change reflects the fact that +a representation is what is being toggled, not the segmentation. + + +**Before (OHIF 3.8)** + +```js +// Toggle visibility for a segmentation globally +segmentationService.toggleSegmentationVisibility(segmentationId); +``` + +**After (OHIF 3.9)** + +```js +// Toggle visibility for a segmentation representation in a specific viewport +segmentationService.toggleSegmentationRepresentationVisibility(viewportId, { + segmentationId: segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap +}); +``` + +**Migration Steps** + +1. Update all calls to `toggleSegmentationVisibility` to use `toggleSegmentationRepresentationVisibility` +2. Add the required `viewportId` parameter +3. Add a `type` parameter specifying the representation type (e.g., Labelmap, Contour) +4. If you were toggling visibility across all viewports, you'll need to loop through the viewports: + + +
+Additional Notes + + +- Each viewport can now have independent visibility settings for the same segmentation +- The visibility state is specific to the representation type (Labelmap, Contour, etc.) +- To check current visibility, use `getSegmentationRepresentationVisibility(viewportId, { segmentationId, type })` +
+ +--- diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/6-segmentationserice-other.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/6-segmentationserice-other.md new file mode 100644 index 0000000..1236787 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/6-segmentationserice-other.md @@ -0,0 +1,374 @@ +--- +id: seg-other +title: Other Changes +--- + + + + +## addOrUpdateSegmentation + +This was a public method but there is a good chance you were not using it + + +**Before (OHIF 3.8)** + +```js +// Before +addOrUpdateSegmentation( + segmentation: Segmentation, + suppressEvents = false, + notYetUpdatedAtSource = false +): string +``` + +**After** + +```js +addOrUpdateSegmentation( + segmentationInput: SegmentationPublicInput | Partial +) +``` + +### Data Structure Changes + +The segmentation object that was used previously was a custom segmentation object that was used internally by the SegmentationService. But +we have moved to the cornerstone public segmentation input type. + +**Before:** + +```js +const segmentation = { + id: 'segmentation1', + type: SegmentationRepresentations.Labelmap, + isActive: true, + activeSegmentIndex: 1, + segments: [ + { + segmentIndex: 1, + color: [255, 0, 0], + isVisible: true, + isLocked: false, + opacity: 255 + } + ], + label: 'Segmentation 1', + cachedStats: {}, + representationData: { + LABELMAP: { + volumeId: 'volume1', + referencedVolumeId: 'reference1' + } + } +}; +``` + + +**After:** + +This matches the cornerstone public segmentation input type. + +```js +const segmentationInput = { + segmentationId: 'segmentation1', + representation: { + type: SegmentationRepresentations.Labelmap, + data: { + imageIds: segmentationImageIds, + referencedVolumeId: 'reference1' + } + }, + config: { + label: 'Segmentation 1', + segments: { + 1: { + label: 'Segment 1', + active: true, + locked: false + } + } + } +}; +``` + +
+Migration Examples + + +```js +// Before +const newSegmentation = { + id: 'seg1', + type: SegmentationRepresentations.Labelmap, + segments: [...], + representationData: { + LABELMAP: { + volumeId: 'volume1', + referencedVolumeId: 'reference1' + } + } +}; +segmentationService.addOrUpdateSegmentation(newSegmentation); + +// After +segmentationService.addOrUpdateSegmentation({ + segmentationId: 'seg1', + representation: { + type: SegmentationRepresentations.Labelmap, + data: { + imageIds: segmentationImageIds, + referencedVolumeId: 'reference1' + } + }, + config: { + segments: { + 1: { + label: 'Segment 1', + active: true + } + } + } +}); +``` + + +**Updating Existing Segmentation** + +```js +// Before +const updatedSegmentation = { + ...existingSegmentation, + segments: [...modifiedSegments], + activeSegmentIndex: 2 +}; +segmentationService.addOrUpdateSegmentation(updatedSegmentation); + +// After +segmentationService.addOrUpdateSegmentation({ + segmentationId: 'seg1', + config: { + segments: { + 2: { active: true }, + } + } +}); +``` + +
+ + +## loadSegmentationsForViewport + +same as addOrUpdateSegmentation, you should pass in the new segmentation data structure. + +For instance + +**Before** + +```js +const segmentations = [ + { + id: '1', + label: 'Segmentations', + segments: labels.map((label, index) => ({ + segmentIndex: index + 1, + label + })), + isActive: true, + activeSegmentIndex: 1, + }, +]; + +commandsManager.runCommand('loadSegmentationsForViewport', { + segmentations, +}); +``` + + + +**After** + +```js + +const labels = ['Segment 1', 'Segment 2', 'Segment 3']; + +const segmentations = [ + { + segmentationId: '1', + representation: { + type: Enums.SegmentationRepresentations.Labelmap, + }, + config: { + label: 'Segmentations', + segments: labels.reduce((acc, label, index) => { + acc[index + 1] = { + label, + active: index === 0, // First segment is active + locked: false, + }; + return acc; + }, {}), + }, + }, +]; + +commandsManager.runCommand('loadSegmentationsForViewport', { + segmentations, +}); +``` + + +--- + + + + +## highlightSegment + +**Before (OHIF 3.8)** + +```js +// Before (v1.x) +highlightSegment( + segmentationId: string, + segmentIndex: number, + toolGroupId?: string, + alpha = 0.9, + animationLength = 750, + hideOthers = true, + highlightFunctionType = 'ease-in-out' +) + +``` + +**After (OHIF 3.9)** + +```js +highlightSegment( + segmentationId: string, + segmentIndex: number, + viewportId?: string, // notice viewportId instead of toolGroupId + alpha = 0.9, + animationLength = 750, + hideOthers = true, + highlightFunctionType = 'ease-in-out' +) +``` + +
+Key Changes + +1. Removed `toolGroupId` in favor of `viewportId` +2. If no viewportId is provided, highlights in all relevant viewports + +
+ +
+Migration Examples + +**Basic Usage** + +```js +// Before +segmentationService.highlightSegment( + 'seg1', + 1, + 'toolGroup1', + 0.9, + 750, + true, +); +// After +segmentationService.highlightSegment( + 'seg1', + 1, + 'viewport1', + 0.9, + 750, + true +); +``` + +**Highlighting in Multiple Views** + +```js +// Before +const toolGroupIds = ['toolGroup1', 'toolGroup2']; +toolGroupIds.forEach(toolGroupId => { + segmentationService.highlightSegment( + 'seg1', + 1, + toolGroupId + ); +}); +// After - Method 1: Let service handle multiple viewports +segmentationService.highlightSegment('seg1', 1); +// After - Method 2: Explicitly specify viewports +const viewportIds = ['viewport1', 'viewport2']; +viewportIds.forEach(viewportId => { + segmentationService.highlightSegment( + 'seg1', + 1, + viewportId + ); +}); +``` +
+ +--- + +## jumpToSegmentCenter + +**Before (OHIF 3.8)** + +```js +jumpToSegmentCenter( + segmentationId: string, + segmentIndex: number, + toolGroupId?: string, + highlightAlpha = 0.9, + highlightSegment = true, + animationLength = 750, + highlightHideOthers = false, + highlightFunctionType = 'ease-in-out' +) +``` + +**After (OHIF 3.9)** + +```js +jumpToSegmentCenter( + segmentationId: string, + segmentIndex: number, + viewportId? string, // notice viewportId instead of toolGroupId + highlightAlpha = 0.9, + highlightSegment = true, + animationLength = 750, + highlightHideOthers = false, + highlightFunctionType = 'ease-in-out' +) +``` + +
+Key Changes + +1. Removed `toolGroupId` parameter infavor of viewportId +2. Automatically handles relevant viewports if `viewportId` not provided + + +``` +// Before +segmentationService.jumpToSegmentCenter( + 'seg1', + 1, + 'toolGroup1' +); +// After +segmentationService.jumpToSegmentCenter( + 'seg1', + 1, + 'viewportId1' +); +``` + +
diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/index.md b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/index.md new file mode 100644 index 0000000..f4a7a67 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/1-segmentation/index.md @@ -0,0 +1,17 @@ +--- +id: segmentation-index +title: Segmentation +sidebar_position: 1 +--- + +import DocCardList from '@theme/DocCardList'; +import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; + +:::info +This migration involves significant architectural changes to the segmentation system. While we typically aim for incremental updates, the shift from a tool group-centric to a viewport-centric architecture was necessary to support OHIF 3.9's advanced visualization capabilities, and more flexible segmentation handling. + +Don't worry - we'll guide you through each change step by step! +::: + + + diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/2-Renamings.md b/platform/docs/docs/migration-guide/3p8-to-3p9/2-Renamings.md new file mode 100644 index 0000000..6c0763f --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/2-Renamings.md @@ -0,0 +1,33 @@ +--- +id: 2-renamings +title: Renamings +sidebar_position: 2 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + +## Panel Measurements + +The panel in the default extension is renamed from `measure` to `panelMeasurement` to be more consistent with the rest of the extensions. + +**Action Needed** + +Update any references to the `measure` panel to `panelMeasurement` in your code. + +Find and replace + + + + @ohif/extension-default.panelModule.measure + + + @ohif/extension-cornerstone.panelModule.panelMeasurement + + + +## addIcon from ui + +The addIcon from the ui package has had a version added in the default extension as +`utils.addIcon` which adds to both `ui` and `ui-next`. diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/3-DataSources.md b/platform/docs/docs/migration-guide/3p8-to-3p9/3-DataSources.md new file mode 100644 index 0000000..2d14adf --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/3-DataSources.md @@ -0,0 +1,40 @@ +--- +id: 3-data-sources +title: Data Sources +sidebar_position: 3 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## BulkDataURI Configuration + +We've updated the configuration for BulkDataURI to provide more flexibility and control. This guide will help you migrate from the old configuration to the new one. + +### What's Changing? + + + + +```javascript +useBulkDataURI: false, +``` + + + + +```javascript +bulkDataURI: { + enabled: true, + // Additional configuration **options** +}, +``` + + + + + +**Additional Notes:** +- The new configuration allows for more granular control over BulkDataURI behavior. +- You can now add custom URL prefixing logic using the startsWith and prefixWith properties. +- This change enables easier correction of retrieval URLs, especially in scenarios where URLs pass through multiple systems. diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/4-Measurements.md b/platform/docs/docs/migration-guide/3p8-to-3p9/4-Measurements.md new file mode 100644 index 0000000..d4b89fa --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/4-Measurements.md @@ -0,0 +1,43 @@ +--- +title: Measurements +--- + + +## Display Text + + +Previously, `displayText` for measurements was often a simple string or an array of strings. This approach made it difficult to distinguish between primary measurement values (e.g., length, area) and secondary information (e.g., series number, instance number). It also limited styling options for differentiating these types of information. + +The new approach introduces a structured object for `displayText`, consisting of `primary` and `secondary` arrays. This separation allows for better organization and presentation of measurement information. The `primary` array is intended for the main measurement values (on the left), while the `secondary` array is for contextual information like series and instance numbers (on the right) + +### Migration Steps + +If you have custom measurement tools or modify existing ones, you need to update the `getDisplayText` functions within the `measurementServiceMappings` to return a structured object in the new format. + +**Update Measurement Mappings:** If your extension defines custom measurement tools or modifies existing ones, update the `getDisplayText` functions within the `measurementServiceMappings` to return a structured object in the new format. + +```js +// Old Implementation (example for Length tool) +function getDisplayText(mappedAnnotations, displaySet, customizationService) { + // ... + return `${roundedLength} ${unit} (S: ${SeriesNumber}${instanceText}${frameText})`; +} +// New Implementation +function getDisplayText(mappedAnnotations, displaySet) { + // ... + return { + primary: [`${roundedLength} ${unit}`], // Primary measurement value + secondary: [`S: ${SeriesNumber}${instanceText}${frameText}`], // Secondary information + }; +} +``` + +--- + +### selected property + +`selected` property on measurements is now renamed to `isSelected` to match the rest of `isLocked` , `isVisible` naming convention. + +Migration: you probably don't need to perform any migration + +--- diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/4-ViewportActionCorner.md b/platform/docs/docs/migration-guide/3p8-to-3p9/4-ViewportActionCorner.md new file mode 100644 index 0000000..7b85daf --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/4-ViewportActionCorner.md @@ -0,0 +1,40 @@ +--- +id: viewport-action-corner +title: ViewportActionCorner +--- + + + + +## Key Changes and Rationale + +Previously, the `ViewportActionCornersService` used the `setComponent` or `setComponents` methods to add components to viewport corners. These methods, when used with multiple components, would essentially overwrite existing components at the same location, unless great care was taken with the `indexPriority` property. This made it difficult to reliably position multiple components within the same corner. + +The new approach introduces the methods `addComponent` and `addComponents`, which insert components into the viewport corners based on an optional `indexPriority` property and provide predictable ordering based on the relative `indexPriority` of the components already at the corner. If no `indexPriority` is given, components are added to the end (for the left side) or the beginning (for the right side) by default. + +### Migration Steps + +**Update Component Addition Methods:** Replace calls to `setComponent` and `setComponents` with `addComponent` and `addComponents`, respectively. + +```js +// Old API +viewportActionCornersService.setComponent({ + viewportId, + id: 'myComponent', + component: , + location: viewportActionCornersService.LOCATIONS.topRight +}); +``` + +**New API** + +```js +viewportActionCornersService.addComponent({ + viewportId, + id: 'myComponent', + component: , + location: viewportActionCornersService.LOCATIONS.topRight, + indexPriority: 1, // indexPriority is now optional and determines placement order within the corner +}); + +``` diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/5-StateSyncService.md b/platform/docs/docs/migration-guide/3p8-to-3p9/5-StateSyncService.md new file mode 100644 index 0000000..230683c --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/5-StateSyncService.md @@ -0,0 +1,343 @@ +--- +id: state-sync-service +title: StateSyncService +--- + + +## Migrating from StateSyncService to Zustand Stores + +The `StateSyncService` has been deprecated in favor of more modern and efficient state management using Zustand stores. This migration guide outlines the reasons for the change and provides step-by-step instructions on how to migrate your extension or mode from using `StateSyncService` to Zustand. + +## Why Migrate? + +The `StateSyncService` had limitations: + +- **Limited Reactivity:** Updates weren't always reactive, requiring manual re-renders. +- **Lack of Granularity:** It stored large chunks of state, hindering performance. +- **Complexity:** Managing and syncing state across components was cumbersome. + +Zustand offers several advantages: + +- **Lightweight and Fast:** Zustand is a minimal and performant state management library. +- **Granular Control:** Create individual stores for specific data, improving reactivity and performance. +- **Simplified API:** Easy-to-use hooks for subscribing and updating state. + +## Migration Steps: + +1. **Identify State to Migrate:** Determine which parts of your extension or mode rely on the `StateSyncService`. Typical examples include: + - **Viewport Presentations:** LUT and position information for viewports. + - **Layout State:** Custom grid layouts and one-up toggling. + - **Synchronizers:** State for cross-viewport synchronization. + - **UI State:** UI-specific settings. +2. **Replace StateSyncService Usage:** In your extension or mode: + - **Import Zustand Stores:** Import the new stores you created. + - **Replace** `getState()` and `store()`: Use the Zustand hooks (`useStore`, `set`, `get`) to access and update state in your components. + - **Handle Presentation IDs:** Implement logic for generating and managing presentation IDs within your stores or relevant components. This can involve using unique keys based on viewport options, display sets, and unique indices. See the `presentationUtils.ts` file for example implementations. + - **Rehydrate State:** On mode entry, rehydrate your Zustand stores with any relevant persisted state from localStorage or other storage mechanisms. + - **Clear State on Mode Exit:** Ensure you clear your Zustand stores appropriately on mode exit to prevent memory leaks. + + + +### `LutPresentationStore` + + +**Before (StateSyncService):** + +```js +const stateSyncService = servicesManager.services.stateSyncService; +const lutPresentationStore = stateSyncService.getState().lutPresentationStore; +const lutPresentation = lutPresentationStore[presentationId]; +// ...to update +stateSyncService.store({ + lutPresentationStore: { + ...lutPresentationStore, + [presentationId]: newLutPresentation, + }, +}); +``` + +**After (Zustand):** + +```js +import { useLutPresentationStore } from '../stores/useLutPresentationStore'; +const { lutPresentationStore, setLutPresentation } = useLutPresentationStore(); +const lutPresentation = lutPresentationStore[presentationId]; +// ...to update +setLutPresentation(presentationId, newLutPresentation); +``` + +The `getPresentationId` for `lutPresentationStore` was previously registered in `platform/core`. Now, the Zustand store provides this functionality. + +```js +// Fetch getPresentationId functions from respective Zustand stores +const { getPresentationId: getLutPresentationId } = useLutPresentationStore.getState(); + +// Register presentation id providers +viewportGridService.addPresentationIdProvider('lutPresentationId', getLutPresentationId); +``` + + +--- + +### `PositionPresentationStore` + +**Before (StateSyncService):** + +```js +const stateSyncService = servicesManager.services.stateSyncService; +const positionPresentationStore = stateSyncService.getState().positionPresentationStore; +const positionPresentation = positionPresentationStore[presentationId]; +// ...to update +stateSyncService.store({ + positionPresentationStore: { + ...positionPresentationStore, + [presentationId]: newPositionPresentation, + }, +}); +``` + +**After (Zustand):** + +```js +import { usePositionPresentationStore } from '../stores/usePositionPresentationStore'; +const { positionPresentationStore, setPositionPresentation } = usePositionPresentationStore(); +const positionPresentation = positionPresentationStore[presentationId]; +// ...to update +setPositionPresentation(presentationId, newPositionPresentation); +``` + +Similar to lutPresentationId, the PositionPresentationId is also registered from outside + +```js + + const { getPresentationId: getPositionPresentationId } = usePositionPresentationStore.getState(); + + // register presentation id providers + viewportGridService.addPresentationIdProvider( + 'positionPresentationId', + getPositionPresentationId + ); +``` + +--- + +### `ViewportGridStore` + +**Before (StateSyncService):** + +```js +const stateSyncService = servicesManager.services.stateSyncService; +const viewportGridStore = stateSyncService.getState().viewportGridStore; +const gridState = viewportGridStore[storeId]; +// ...to update +stateSyncService.store({ + viewportGridStore: { + ...viewportGridStore, + [storeId]: newGridState, + }, +}); +``` + +**After (Zustand):** + +```js +import { useViewportGridStore } from '../stores/useViewportGridStore'; +const { viewportGridState, setViewportGridState } = useViewportGridStore.getState(); +const gridState = viewportGridState[storeId]; +// ...to update +setViewportGridState(storeId, newGridState); +``` + +--- + +### `DisplaySetSelectorStore` + + +**Before (StateSyncService):** + +```js +const stateSyncService = servicesManager.services.stateSyncService; +const displaySetSelectorMap = stateSyncService.getState().displaySetSelectorMap; +const displaySetUID = displaySetSelectorMap[selectorKey]; +// ...to update +stateSyncService.store({ + displaySetSelectorMap: { + ...displaySetSelectorMap, + [selectorKey]: newDisplaySetUID, + }, +}); +``` + +**After (Zustand):** + +```js +import { useDisplaySetSelectorStore } from '../stores/useDisplaySetSelectorStore'; +const { displaySetSelectorMap, setDisplaySetSelector } = useDisplaySetSelectorStore(); +const displaySetUID = displaySetSelectorMap[selectorKey]; +// ...to update +setDisplaySetSelector(selectorKey, newDisplaySetUID); +``` + +--- + +### `HangingProtocolStageIndexStore` + + +**Before (StateSyncService):** + +```js +const stateSyncService = servicesManager.services.stateSyncService; +const hangingProtocolStageIndexMap = stateSyncService.getState().hangingProtocolStageIndexMap; +const hpInfo = hangingProtocolStageIndexMap[cacheId]; +// ...to update +stateSyncService.store({ + hangingProtocolStageIndexMap: { + ...hangingProtocolStageIndexMap, + [cacheId]: newHpInfo, + }, +}); +``` + +**After (Zustand):** + +```js +import { useHangingProtocolStageIndexStore } from '../stores/useHangingProtocolStageIndexStore'; +const { hangingProtocolStageIndexMap, setHangingProtocolStageIndex } = useHangingProtocolStageIndexStore(); +const hpInfo = hangingProtocolStageIndexMap[cacheId]; +// ...to update +setHangingProtocolStageIndex(cacheId, newHpInfo); +``` + +--- + +### `ToggleHangingProtocolStore` + + +**Before (StateSyncService):** + +```js +const stateSyncService = servicesManager.services.stateSyncService; +const toggleHangingProtocol = stateSyncService.getState().toggleHangingProtocol; +const previousHpInfo = toggleHangingProtocol[storedHanging]; +// ...to update +stateSyncService.store({ + toggleHangingProtocol: { + ...toggleHangingProtocol, + [storedHanging]: newHpInfo, + }, +}); +``` + +**After (Zustand):** + +```js +import { useToggleHangingProtocolStore } from '../stores/useToggleHangingProtocolStore'; +const { toggleHangingProtocol, setToggleHangingProtocol } = useToggleHangingProtocolStore(); +const previousHpInfo = toggleHangingProtocol[storedHanging]; +// ...to update +setToggleHangingProtocol(storedHanging, newHpInfo); +``` + +--- + +### `ToggleOneUpViewportGridStore` + + +**Before (StateSyncService):** + +```js +const stateSyncService = servicesManager.services.stateSyncService; +const toggleOneUpViewportGridStore = stateSyncService.getState().toggleOneUpViewportGridStore; +const previousGridState = toggleOneUpViewportGridStore.layout; // Assuming layout was a property +// ...to update +stateSyncService.store({ + toggleOneUpViewportGridStore: newGridState, +}); +``` + +**After (Zustand):** + +```js +import { useToggleOneUpViewportGridStore } from '../stores/useToggleOneUpViewportGridStore'; +const { toggleOneUpViewportGridStore, setToggleOneUpViewportGridStore } = useToggleOneUpViewportGridStore(); +const previousGridState = toggleOneUpViewportGridStore; // No nested layout property +// ...to update +setToggleOneUpViewportGridStore(newGridState); +``` + +--- + +### `UIStateStore` + + +**Before (StateSyncService):** + +```js +const stateSyncService = servicesManager.services.stateSyncService; +const uiState = stateSyncService.getState().uiStateStore[someUIKey]; +// ...to update +stateSyncService.store({ + uiStateStore: { + ...stateSyncService.getState().uiStateStore, + [someUIKey]: newUIState, + }, +}); +``` + +**After (Zustand):** + +```js +import { useUIStateStore } from '../stores/useUIStateStore'; +const { uiState, setUIState } = useUIStateStore(); +const currentUIState = uiState[someUIKey]; +// ...to update +setUIState(someUIKey, newUIState); +``` + +--- + +### `ViewportsByPositionStore` + + +**Before (StateSyncService):** + +```js +const stateSyncService = servicesManager.services.stateSyncService; +const viewportsByPosition = stateSyncService.getState().viewportsByPosition; +const cachedViewport = viewportsByPosition[positionId]; +// ...to update +stateSyncService.store({ + viewportsByPosition: { + ...viewportsByPosition, + [positionId]: newViewport, + }, +}); +``` + +**After (Zustand):** + +```js +import { useViewportsByPositionStore } from '../stores/useViewportsByPositionStore'; +const { viewportsByPosition, setViewportsByPosition } = useViewportsByPositionStore(); +const cachedViewport = viewportsByPosition[positionId]; +// ...to update +setViewportsByPosition(positionId, newViewport); +``` + +--- + +### `SegmentationPresentationStore` + +**After (Zustand):** + +```js +import { useSegmentationPresentationStore } from '../stores/useSegmentationPresentationStore'; +const { segmentationPresentationStore, setSegmentationPresentation } = + useSegmentationPresentationStore(); +// ...to update +setSegmentationPresentation(presentationId, newSegmentationPresentation); +// You likely have functions within the store like: +// addSegmentationPresentation +// setSegmentationVisibility +// etc. +``` diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/6-RTSTRUCT.md b/platform/docs/docs/migration-guide/3p8-to-3p9/6-RTSTRUCT.md new file mode 100644 index 0000000..f13bf91 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/6-RTSTRUCT.md @@ -0,0 +1,18 @@ +--- +id: 6-rtstruct +title: RTSTRUCT +sidebar_position: 6 +--- + + + +# RTStructure Set has transitioned from VTK actors to SVG. + +We have transitioned from VTK-based rendering to SVG-based rendering for RTStructure Set contours. This change should not require any modifications to your codebase. We anticipate improved stability and speed in our contour rendering. + +As a result of this update, viewports rendering RTStructure Sets will no longer convert to volume viewports. Instead, they will remain as stack viewports. + + +Read more in Pull Requests: +- https://github.com/OHIF/Viewers/pull/4074 +- https://github.com/OHIF/Viewers/pull/4157 diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/7-UI.md b/platform/docs/docs/migration-guide/3p8-to-3p9/7-UI.md new file mode 100644 index 0000000..2be4fab --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/7-UI.md @@ -0,0 +1,156 @@ +--- +title: UI +--- + +## New Components + +You can explore our new playground at `docs.ohif.org/ui` to see the latest components and their properties. We haven't provided a migration guide yet because the old components are still available. Feel free to update your codebase, including custom extensions and UI, to use the new Button, Dropdown, Icons, and other new components from `@ohif/ui-next`. The old methods (importing from `@ohif/ui`) will continue to work for now. However, the new components have a slightly different API, and we plan to deprecate the old components in a future release, as we see the new ones as the future of OHIF. + + + + +## `UINotificationService` + + +We've switched our custom notification service to the Sonner component from https://sonner.emilkowal.ski/ + +### 1. Toast Positions (Kebab-Case) + +Toast positions are now defined using kebab-case instead of camelCase. For instance, `topRight` becomes `top-right`, `bottomRight` becomes `bottom-right`, etc. Ensure your position strings are updated accordingly. + +**Old API:** + +```js +uiNotificationService.show({ + title: 'My Title', + message: 'My Message', + duration: 3000, + position: 'topRight', + type: 'error', + autoClose: true, +}); +``` + + +**New API:** + +```js +uiNotificationService.show({ + title: 'My Title', + message: 'My Message', + duration: 3000, + position: 'top-right', // Note the change to kebab-case + type: 'error', + autoClose: true, +}); +``` + +### 2. Promise Support + +The `show()` method now supports promises, enabling you to display loading notifications and automatically update them based on the promise's resolution or rejection. This significantly simplifies asynchronous operation feedback. + +**Example:** + +```js +const myPromise = someAsyncOperation(); +const notificationId = uiNotificationService.show({ + title: 'Loading Data', + message: 'Fetching data from server...', + type: 'info', + promise: myPromise, + promiseMessages: { + loading: 'Fetching...', + success: (data) => `Data loaded: ${data.length} items`, // Access promise result + error: (error) => `Failed to load data: ${error.message}`, // Access error details + }, +}); +// Optionally hide notification manually if needed +// myPromise.finally(() => uiNotificationService.hide(notificationId)); +``` + +### 3. `hide()` API Change + +The `hide()` method no longer takes an options object. It only accepts the notification ID as a string argument. + +**Old API:** + +```js +uiNotificationService.hide({ id: notificationId }); +``` + +**New API:** + +```js +uiNotificationService.hide(notificationId); +``` + + +--- + + +## Viewport Pane Tailwindcss class + +Previously, when targeting the viewport pane to add custom CSS, you likely used `group-hover:visible` with the viewportPane having a `group` class. + +The naming was confusing as we added more groups, so we renamed it to `group/pane`. Now you can apply `group-hover/pane` for better clarity. + + +--- + +## Header Component + + +Header Component has been refactored in the @ohif/ui-next package. + + +**Before** + + +```js +function Header({ + children, + menuOptions, + isReturnEnabled, + onClickReturnButton, + isSticky, + WhiteLabeling, + showPatientInfo, + servicesManager, + Secondary, + appConfig, + ...props +}: withAppTypes): ReactNode +``` + +**After** + +```js +function Header({ + children, + menuOptions, + isReturnEnabled, + onClickReturnButton, + isSticky, + WhiteLabeling, + PatientInfo, + Secondary, + ...props +}: HeaderProps): ReactNode +``` + +The `PatientInfo` component is now preferred, and the `showPatientInfo` prop has been removed. The previous method depended on `servicesManager`, which was cumbersome because the UI shouldn't need to interact with `servicesManager`. + +All the DropDown and Icons are now in the @ohif/ui-next package. + + +--- + + +## ui, ui-next configs + +We currently have two component libraries that we plan to merge in the future, so we need to maintain both configurations. If your styles aren't applying correctly, ensure you update both `platform/ui-next/tailwind.config.js` and `platform/ui/tailwind.config.js`. + + +### addIcon from ui-next + +if you add custom icons, you may need to add them using a new `addIcon` utility which adds the icon to both `ui` and `ui-next`. diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/8-Refactorings.md b/platform/docs/docs/migration-guide/3p8-to-3p9/8-Refactorings.md new file mode 100644 index 0000000..c90717d --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/8-Refactorings.md @@ -0,0 +1,120 @@ +--- +title: Refactoring +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + + +## Panel Segmentation + +is now moved from `@ohif/extension-cornerstone-dicom-seg` to `@ohif/extension-cornerstone`. + + +The cornerstone extension now provides the panelSegmentation feature, which was previously part of the cornerstone-dicom-seg extension. This change is logical as panelSegmentation handles more than just DICOM. It can process various formats, including custom formats from the backend and potentially NIFTI format in the future. + + +Before in your modes you were using + +```js +'@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation', +``` + + +Now you should use it via + + +```js +'@ohif/extension-cornerstone.panelModule.panelSegmentation', +``` + +--- + +## `callInputDialog` and `colorPickerDialog` and `showLabelAnnotationPopup` + +Due to the excessive number of `callInputDialog` instances, we centralized them. You can now import them from `@ohif/extension-default`. + + +```js +import { showLabelAnnotationPopup, callInputDialog, colorPickerDialog } from '@ohif/extension-default'; +``` + + +--- + +## disableEditing + +The configuration has moved from appConfig to allow more precise control over component disabling. To disable editing for segmentation and measurements, add the following settings: + + +**Before: ** + +```js +customizationService.addModeCustomizations([ + { + id: 'segmentation.panel', + disableEditing: true, + }, +]); +``` + +**Now ** + +```js +customizationService.addModeCustomizations([ + // To disable editing in the SegmentationTable + { + id: 'panelSegmentation.disableEditing', + disableEditing: true, + }, + // To disable editing in the MeasurementTable + { + id: 'panelMeasurement.disableEditing', + disableEditing: true, + }, +]) +``` + + +--- + +## Customization Ids + +The primary reason for this migration is to improve modularity and maintainability in configuration management, as we plan to focus more on the customization service in the near future. + +**Before** + +```js +customizationService.addModeCustomizations([ + { + id: 'segmentation.panel', + segmentationPanelMode: 'expanded', + addSegment: false, + onSegmentationAdd: () => { + commandsManager.run('createNewLabelmapFromPT'); + }, + }, +]); +``` + + +**Now** + +```js +customizationService.addModeCustomizations([ + { + id: 'panelSegmentation.tableMode', + mode: 'expanded', + }, + { + id: 'panelSegmentation.onSegmentationAdd', + onSegmentationAdd: () => { + commandsManager.run('createNewLabelmapFromPT'); + }, + }, +]); + +``` diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/9-other.md b/platform/docs/docs/migration-guide/3p8-to-3p9/9-other.md new file mode 100644 index 0000000..5bd8b73 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/9-other.md @@ -0,0 +1,99 @@ +--- +title: Other Changes +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + +## External Libraries +Some libraries are loaded via dynamic import. You can provide a global function +`browserImport` the allows loading of dynamic imports without affecting the +webpack build. This import looks like: + +```html + +``` + +and belongs in the root html file for your application. +You then need to remove `dependencies` on the external import, and add a reference +to the external import in your `pluginConfig.json` file. + +### Example plugin config for `dicom-microscopy-viewer` +The example below imports the `dicom-microscopy-viewer` for use as an external +dependency. The example is part of the default `pluginConfig.json` file. + +```json + "public": [ + { + "directory": "./platform/public" + }, + { + "packageName": "dicom-microscopy-viewer", + "importPath": "/dicom-microscopy-viewer/dicomMicroscopyViewer.min.js", + "globalName": "dicomMicroscopyViewer", + "directory": "./node_modules/dicom-microscopy-viewer/dist/dynamic-import" + } + ] +``` + +This defines two directory modules, whose contents are copied unchanged to the +output build directory. It then defines the `dicom-microscopy-viewer` using +the `packageName` element as being a module which is imported dynamically. +Then, the import path passed into the browserImportFunction above is +specified, and then how to access the import itself, via the `window.dicomMicroscopyViewer` +global name reference. + +### Referencing External Imports +The appConfig either defines or has a default peerImport function which can be +used to load references to the modules defined in the pluginConfig file. See +the example in `init.tsx` for the cornerstone extension for how this is passed +into CS3D for loading the whole slide imaging library. + + + +--- + + + +--- + + +--- + + +## Use of ViewReference for navigation +When navigating to measurements and storing/remembering navigation positions, +the `viewport.getViewReference` is used to get a position, and `viewport.isReferenceViewable` +used to check if a reference can be applied, and finally `viewport.setViewReference` to +navigate to a view. Note that this changes the behaviour of navigation between +MPR and Stack viewports, and also enables navigation of video and microscopy +viewports in CS3D. This can cause some unexpected behaviour depending on how the +frame of reference values are configured to allow for navigation. + +The isReferenceViewable is used to determine when a view or measurement can be +shown on a given view. For stack versus volume viewports, this can cause unexpected +behaviour to be seen depending on how the view reference was fetched. + +### `getViewReference` with `forFrameOfReference` +When a view reference is fetched with the for frame of reference flag set to true, +a reference will be returned which can be displayed on any viewport containing +the same frame of reference and encompassing the given FOR and able to display the required +orientation. Without this flag, a view reference is returned which will be +displayed on a stack with the given image id, or a volume containing said image id +or the specified volume. + +### `isReferenceViewable` with navigation and/or orientation +The is reference viewable will return false unless the given reference is directly +viewable in the viewport as is. However, it can be passed various flags to determine +whether the reference could be displayed if the viewport was modified in various ways, +for example, by changing the position or orientation of the viewport. This allows +checking for degrees of closeness so that the correct viewport can be chosen. + +Note that this may result in displaying a measurement from one viewport on a completely +different viewport, for example, showing a Probe tool from the stack viewport on +an MPR view. diff --git a/platform/docs/docs/migration-guide/3p8-to-3p9/index.md b/platform/docs/docs/migration-guide/3p8-to-3p9/index.md new file mode 100644 index 0000000..c962a5d --- /dev/null +++ b/platform/docs/docs/migration-guide/3p8-to-3p9/index.md @@ -0,0 +1,13 @@ +--- +id: 3p8-to-3p9 +title: 3.8 -> 3.9 +sidebar_position: 2 +--- + + +import DocCardList from '@theme/DocCardList'; +import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; + +## Migration Guide Sections + + diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/CustomizationService/index.md b/platform/docs/docs/migration-guide/3p9-to-3p10/CustomizationService/index.md new file mode 100644 index 0000000..e0d3fdb --- /dev/null +++ b/platform/docs/docs/migration-guide/3p9-to-3p10/CustomizationService/index.md @@ -0,0 +1,239 @@ +--- +sidebar_position: 2 +title: Customization Service +--- + +# CustomizationService + + + +**Key Changes:** + + +1. **Unified Customization Getter:** + - The `getCustomization` method now uniformly retrieves customizations, prioritizing `global`, then `mode`, and finally `default` customizations. + - The `defaultValue` parameter in `getCustomization` is no longer used for setting defaults. It simply returns if no customization is found. + - The methods `getModeCustomization` and `getGlobalCustomization` are deprecated. + +2. **Simplified Customization Registration:** + - The `customizationType` property in customization definitions is renamed to `inheritsFrom`. + - The `merge` property in customization definitions is removed. Instead, a customization is merged using the helper methods. The basic update commands are listed in the table below, and you can learn more about the helper methods [here](../../../platform/services/customization-service/customizationService.md). + + | Command | Description | Example | + | :------- | :---------------------------------------- | :------------------------------------------------ | + | `$set` | Replace a value entirely | Replace a list or object | + | `$push` | Append items to an array | Add to the end of a list | + | `$unshift` | Prepend items to an array | Add to the start of a list | + | `$splice` | Insert, remove, or replace at specific index | Modify specific indices in a list | + | `$merge` | Update specific fields in an object | Change a subset of fields | + | `$apply` | Compute the new value dynamically | Apply a function to transform values | + | `$filter` | Find and update specific items in arrays | Target nested structures based on matching criteria | + + +3. **New `$transform` command:** + - If you were using the `transform` command, you should now use the `$transform` command. Just a simple rename to make it more consistent with the other commands. + + +5. **Renamed `CornerstoneOverlay` customizations:** + - The `cornerstoneOverlay` customizations (`cornerstoneOverlayTopLeft`, `cornerstoneOverlayTopRight`, `cornerstoneOverlayBottomLeft`, `cornerstoneOverlayBottomRight`) have been renamed to `viewportOverlay.topLeft`, `viewportOverlay.topRight`, `viewportOverlay.bottomLeft`, and `viewportOverlay.bottomRight`. See dedicated page for customizing viewport overlays [here](../../../platform/services/customization-service/viewportOverlay.md). + +6. **Renamed `customRoutes`:** + - The `customRoutes` customization is renamed to `routes.customRoutes`. + +7. **`contextMenu` customization:** + - The `contextMenu` customization now uses the `inheritsFrom` property to inherit from other context menus, previously it was called `customizationType` + +8. **New `immutability-helper` dependency:** + The `immutability-helper` library is now used for merging customizations. If you encounter an error related to it, you'll need to install it - though OHIF should really handle the installation for you, so this is pretty much just a heads up. + +**Migration Steps:** + +1. **Replace `getModeCustomization` and `getGlobalCustomization` with `getCustomization`:** + + - **Before:** + + ```javascript + const tools = customizationService.getModeCustomization( + 'cornerstone.overlayViewportTools' + )?.tools; + const globalValue = customizationService.getGlobalCustomization('someGlobalKey'); + ``` + + - **After:** + + ```javascript + const tools = customizationService.getCustomization('cornerstone.overlayViewportTools'); + const globalValue = customizationService.getCustomization('someGlobalKey'); + ``` + + + :::note + The returned value is the actual customization value, not an object that needs to be broken down. + ::: + +2. **Update Customization Definitions:** + - We've moved away from using random items in the customization definition, and now we use the `id` property to identify the customization as a value. Previously, it was referred to as `value`, `values`, and so on, but now an `id` is used to reference the customization. This approach really simplifies things - when you need to grab the customization, you can just use the `id` to get it, and you don't have to bother with destructuring the value from the object. + + + + +**Example: Customizing a Panel** + +**Before (v3.9):** + +```javascript +// the default value was hardcoded inside the panel itself - bad idea! +// default was given in the panel itself + +// PanelSegmentation.tsx + +// Retrieve the onSegmentationAdd customization +const { onSegmentationAdd } = customizationService.getCustomization( + 'PanelSegmentation.onSegmentationAdd', + { + id: 'segmentation.onSegmentationAdd', + onSegmentationAdd: handlers.onSegmentationAdd, + } +); + +// Retrieve the disableEditing customization +const { disableEditing } = customizationService.getCustomization( + 'PanelSegmentation.disableEditing', + { + id: 'default.disableEditing', + disableEditing: false, + } +); + + + +// mode was customizing it via +customizationService.addModeCustomizations([ + { + id: 'PanelSegmentation.tableMode', + mode: 'expanded', + }, + { + id: 'PanelSegmentation.showAddSegment', + showAddSegment: false, + }, +]); + +``` + +**After (v3.10):** + +```javascript +// cornerstone extension getCustomizationModule +// centralized customization location for all extensions - good! +function getCustomizationModule() { + return [ + { + name: 'default', + value: { + 'panelSegmentation.disableEditing': false, + 'panelSegmentation.showAddSegment': true, + }, + }, + ]; +} + + +// inside panelSegmentation.tsx +const disableEditing = customizationService.getCustomization('panelSegmentation.disableEditing'); +const showAddSegment = customizationService.getCustomization('panelSegmentation.showAddSegment'); + + +// mode can customize it via $ operators for mode customizations +customizationService.setCustomizations({ + 'panelSegmentation.disableEditing': { $set: true }, + 'panelSegmentation.showAddSegment': { $set: false }, +}); + + +//or via configuration for global customizations +window.config = { + // rest of config + customizationService: [ + { + 'panelSegmentation.disableEditing': { + $set: true, // Disables editing of segmentations in the panel + }, + }, + ], + // rest of config +}; +``` + + + +**Example: Updating a Customization** + +Let's say you have a customization in v3.9 that adds a custom overlay item to the top-left corner: + +**Before (v3.9):** + +```javascript +// In your mode's onModeEnter +customizationService.addModeCustomizations([ + { + id: 'cornerstoneOverlayTopLeft', + items: [ + { + id: 'myCustomOverlay', + customizationType: 'ohif.overlayItem', + attribute: 'PatientName', + label: 'Patient:', + }, + ], + }, +]); +``` + +**After (v3.10):** + +```javascript +// In your mode's onModeEnter or elsewhere +customizationService.setCustomizations({ + 'viewportOverlay.topLeft': { + $push: [ + { + id: 'myCustomOverlay', + inheritsFrom: 'ohif.overlayItem', + attribute: 'PatientName', + label: 'Patient:', + }, + ], + }, +}); +``` + +**Note:** + +- The `customizationType` is replaced with `inheritsFrom`. + + + + + +## Renaming + +To keep our customization system consistent, you should be aware of a few key renaming conventions. We now follow a straightforward naming convention for customizations: `scopeName.customizationItem`. + + + +| Customization Key (Old) | Customization Key (New) | Description | +| :------------------------------------------ | :------------------------------------------- | :-------------------------------------------------------------------------- | +| `PanelMeasurement.disableEditing` | `panelMeasurement.disableEditing` | Disables editing measurements in the Measurement Panel and after SR hydration. | +| `PanelSegmentation.CustomDropdownMenuContent` | `panelSegmentation.customDropdownMenuContent` | Custom content for the dropdown menu in the Segmentation Panel. | +| `PanelSegmentation.disableEditing` | `panelSegmentation.disableEditing` | Disables editing segmentations in the Segmentation Panel. | +| `PanelSegmentation.showAddSegment` | `panelSegmentation.showAddSegment` | Controls visibility of the "Add Segment" button in the Segmentation Panel. | +| `PanelSegmentation.onSegmentationAdd` | `panelSegmentation.onSegmentationAdd` | Custom function to execute when a new segmentation is added. | +| `PanelSegmentation.tableMode` | `panelSegmentation.tableMode` | Controls the table mode (collapsed/expanded) in the Segmentation Panel. | +| `PanelSegmentation.readableText` | `panelSegmentation.readableText` | Custom readable text labels for the Segmentation Panel. | +| `PanelStudyBrowser.studyMode` | `studyBrowser.studyMode` | Controls the study mode (all/primary/recent) in the Study Browser Panel. | +| `customRoutes` | `routes.customRoutes` | Defines custom routes for the application. | +| `cornerstoneOverlayTopLeft` | `viewportOverlay.topLeft` | Custom overlay items for the top-left corner of the viewport. | +| `cornerstoneOverlayTopRight` | `viewportOverlay.topRight` | Custom overlay items for the top-right corner of the viewport. | +| `cornerstoneOverlayBottomLeft` | `viewportOverlay.bottomLeft` | Custom overlay items for the bottom-left corner of the viewport. | +| `cornerstoneOverlayBottomRight` | `viewportOverlay.bottomRight` | Custom overlay items for the bottom-right corner of the viewport. | diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/General/hotkeys.md b/platform/docs/docs/migration-guide/3p9-to-3p10/General/hotkeys.md new file mode 100644 index 0000000..80fc94a --- /dev/null +++ b/platform/docs/docs/migration-guide/3p9-to-3p10/General/hotkeys.md @@ -0,0 +1,129 @@ +--- +sidebar_position: 2 +title: Hotkeys +--- + + +## Key Changes: + +* Hotkeys are no longer defined in mode factory via `hotkeys: [...hotkeys.defaults.hotkeyBindings]` +* Hotkeys are now managed through the `customizationService` under the key `ohif.hotkeyBindings` +* Default hotkeys are set automatically and can be customized using the customization service +* User-defined hotkey preferences are now stored in a new format in localStorage +* The `HotkeysManager` has undergone significant updates including better handling of defaults, key persistence, and cleanup + +## Migration Steps: + +### 1. Remove hotkeys array from mode factory definition + +**Before:** +```diff +- function modeFactory({ modeConfiguration }) { +- return { +- id: 'basic', +- // ... other configuration +- hotkeys: [...hotkeys.defaults.hotkeyBindings], +- }; +- } +``` + +**After:** +```diff ++ function modeFactory({ modeConfiguration }) { ++ return { ++ id: 'basic', ++ // ... other configuration ++ // No hotkeys array necessary ++ }; ++ } +``` + + +### 2. Set custom hotkeys using the customization service + +There are several methods to modify hotkeys using the customization service: + +#### a. Completely replace all hotkeys using `$set`: + +```diff ++ onModeEnter: function ({ servicesManager }) { ++ const { customizationService } = servicesManager.services; ++ customizationService.setCustomizations({ ++ 'ohif.hotkeyBindings': { ++ $set: [ ++ { ++ commandName: 'setToolActive', ++ commandOptions: { toolName: 'Zoom' }, ++ label: 'Zoom', ++ keys: ['z'], ++ isEditable: true, ++ }, ++ ], ++ }, ++ }); +``` + +#### b. Add new hotkeys using `$push`: + +```diff ++ onModeEnter: function ({ servicesManager }) { ++ const { customizationService } = servicesManager.services; ++ customizationService.setCustomizations({ ++ 'ohif.hotkeyBindings': { ++ $push: [ ++ { ++ commandName: 'myCustomCommand', ++ label: 'My Custom Function', ++ keys: ['ctrl+m'], ++ isEditable: true, ++ }, ++ ], ++ }, ++ }); ++} +``` + +### 4. Update configuration file if you were setting window.config.hotkeys + +If you were previously defining hotkeys in your window.config.js file, it was not really +taken into account. So you can safely remove it now. + +**Before:** +```diff +- window.config = { +- // ...other config +- hotkeys: [ +- { +- commandName: 'incrementActiveViewport', +- label: 'Next Viewport', +- keys: ['right'], +- }, +- // ...more hotkeys +- ], +- }; +``` + +**After:** +```diff ++ window.config = { ++ // ...other config ++ }; +``` + +### 5. Be aware that user preferences are now handled differently + +The new system automatically handles user-preferred hotkey mappings: + +- User hotkey preferences are stored in `localStorage` under the key `user-preferred-keys` +- The format is a hash-based mapping rather than a full array of definitions +- There's a migration utility that converts old preferences to the new format +- You don't need to manually handle this, but be aware of it if you're accessing localStorage directly + + +## Benefits of the Change + +1. **Consistent API**: Hotkeys now follow the same customization pattern as other OHIF features +2. **More flexible**: Easier to modify specific hotkeys without replacing the entire set +3. **Better user preferences**: User customizations are better preserved and migrated +4. **Runtime updates**: Hotkeys can be modified at runtime through the customization service +5. **Improved cleanup**: Better lifecycle management of hotkey bindings diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/General/index.md b/platform/docs/docs/migration-guide/3p9-to-3p10/General/index.md new file mode 100644 index 0000000..6ef59a9 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p9-to-3p10/General/index.md @@ -0,0 +1,36 @@ +--- +sidebar_position: 1 +title: General +--- + +## HTML Template Update +We have modified the `template.html` file so if you are using a custom template, you will need to update it. + +Here are the key changes needed in the migration: + +1. Added `window.PUBLIC_URL` declaration: +```javascript +window.PUBLIC_URL = '<%= PUBLIC_URL %>'; +``` + +Was added before the `` comment block. + + +## OHIF Docs + +OHIF platform/docs is no longer part of the workspace. + +- Builds are faster for 99.99% of users since only maintainers need to run the docs development. + +If you need to run the docs website locally, you must install it first, as it is not installed by default. + +Before: +```bash +yarn run dev +``` + +After: +```bash +yarn install +yarn run dev +``` diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Icons.md b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Icons.md new file mode 100644 index 0000000..7a2afed --- /dev/null +++ b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Icons.md @@ -0,0 +1,237 @@ +--- +title: Icons +--- + +## Migration Guide: Icon Component Updates + +### General Overview + +This migration involves changes to how icons are used within the OHIF platform. The core change is the move to a new icon component library, `@ohif/ui-next`, which provides more flexibility and a more consistent naming convention for icons. + +**Key Changes:** + +1. **New Icon Library:** The primary change is the shift from using `` from `@ohif/ui` to using the new `Icons` component from `@ohif/ui-next`. +2. **`AbcDef` Naming Convention:** The new library uses a `AbcDef` (PascalCase) naming convention for the icons. For instance, `status-alert` is now `StatusAlert`. +3. **Legacy Fallback:** To ease the transition, a legacy fallback has been provided using `Icons.ByName`. This allows you to continue using the old `name="status-alert"` format but is not the recommended way moving forward. +4. **Direct Icon Component Access:** The recommended approach is to use `Icons.StatusAlert` instead of `` this way will make code more clear and readable. + +### Migration Strategies + +You have two ways to approach the migration: + +1. **Recommended Approach (Gradual Adoption):** + * Start by updating your codebase to use the `Icons.Method` for the new icon naming convention. + * For example, replace `` with ``. + * This ensures your code is aligned with the new standard and provides optimal compatibility in the future. + * This method can be rolled out in phases. + +2. **Legacy Fallback Approach (Temporary):** + * If a full migration is not immediately feasible, you can use the legacy fallback temporarily: + * Replace `` with ``. + * This option allows you to complete the migration with minimal disruption to the old code + * However, it is highly recommended to move towards the `Icons.Method` approach to take advantage of all the new library offers and have a cleaner code base. + +**Recommendation:** We strongly recommend using the *Recommended Approach* for a more maintainable and consistent codebase going forward. + +### Specific Changes (Code Examples) + +Here are some specific examples based on the diff you provided, illustrating both the legacy fallback and recommended approach: + +**Example 1: Status Icons in `_getStatusComponent.tsx`** + +**Old Code (`@ohif/ui`):** + +```jsx +import { Icon, Tooltip } from '@ohif/ui'; + +// ... + case true: + StatusIcon = () => ; + break; + case false: + StatusIcon = () => ( + + ); + break; +//... + +``` + +**Legacy Fallback Approach (`Icons.ByName`):** + +```jsx +import { Tooltip } from '@ohif/ui'; +import { Icons } from '@ohif/ui-next'; + +// ... + case true: + StatusIcon = () => ; + break; + case false: + StatusIcon = () => ( + + ); + break; +//... +``` + +**Recommended Approach (`Icons.StatusAlert`, `Icons.StatusUntracked`):** + +```jsx +import { Tooltip } from '@ohif/ui'; +import { Icons } from '@ohif/ui-next'; + +// ... + case true: + StatusIcon = () => ; + break; + case false: + StatusIcon = () => ( + + ); + break; +//... +``` + + +**Example 5: Icon usage in `WorkList.tsx`** + +**Old Code (`@ohif/ui`):** + +```jsx + +``` +**Recommended Approach (`Icons.LaunchArrow`, `Icons.LaunchInfo`):** + +```jsx + isValidMode ? ( + + ) : ( + + ) +``` + + +### Detailed Renaming Table + +| Old Icon Name | New Icon Component Name | Example Usage (`Icons.`) | Notes | +| :------------------------------ | :------------------------------------------ | :--------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `status-alert` | `StatusAlert` | `Icons.StatusAlert` | | +| `status-untracked` | `StatusUntracked` | `Icons.StatusUntracked` | | +| `status-locked` | `StatusLocked` | `Icons.StatusLocked` | | +| `icon-transferring` | `IconTransferring` | `Icons.IconTransferring` | | +| `icon-alert-small` | `Alert` | `Icons.Alert` | | +| `icon-alert-outline` | `AlertOutline` | `Icons.AlertOutline` | | +| `icon-status-alert` | `Alert` | `Icons.Alert` | | +| `action-new-dialog` | `ActionNewDialog` | `Icons.ActionNewDialog` | | +| `VolumeRendering` | `VolumeRendering` | `Icons.VolumeRendering` | | +| `chevron-left` | `ChevronClosed` | `Icons.ChevronClosed` | Use when arrow direction needs to point left | +| `chevron-down` | `ChevronOpen` | `Icons.ChevronOpen` | Use when arrow direction needs to point down | +| `launch-arrow` | `LaunchArrow` | `Icons.LaunchArrow` | | +| `launch-info` | `LaunchInfo` | `Icons.LaunchInfo` | | +| `group-layers` | `GroupLayers` | `Icons.GroupLayers` | | +| `icon-upload` | `Upload` | `Icons.Upload` | | +| `icon-search` | `Search` | `Icons.Search` | | +| `icon-clear-field` | `Clear` | `Icons.Clear` | | +| `icon-add` | `Add` | `Icons.Add` | | +| `icon-close` | `Close` | `Icons.Close` | | +| `icon-pause` | `Pause` | `Icons.Pause` | | +| `icon-play` | `Play` | `Icons.Play` | | +| `icon-multiple-patients` | `MultiplePatients` | `Icons.MultiplePatients` | | +| `icon-settings` | `Settings` | `Icons.Settings` | | +| `icon-more-menu` | `More` | `Icons.More` | | +| `content-prev` | `ContentPrev` | `Icons.ContentPrev` | | +| `content-next` | `ContentNext` | `Icons.ContentNext` | | +| `checkbox-checked` | `CheckBoxChecked` | `Icons.CheckBoxChecked` | | +| `checkbox-unchecked` | `CheckBoxUnchecked` | `Icons.CheckBoxUnchecked` | | +| `checkbox-default` | `CheckBoxUnchecked` | `Icons.CheckBoxUnchecked` | | +|`checkbox-active`| `CheckBoxChecked`| `Icons.CheckBoxChecked`| | +| `sorting-active-up` | `SortingAscending` | `Icons.SortingAscending` | | +| `sorting-active-down` | `SortingDescending` | `Icons.SortingDescending` | | +| `sorting` | `Sorting` | `Icons.Sorting` | | +|`link` | `Link` | `Icons.Link` | | +|`unlink` | `Link` | `Icons.Link` | | +|`info-action` | `Info` | `Icons.Info` | | +|`database` | `Database`| `Icons.Database`| | +|`tool-3d-rotate`| `Tool3DRotate`| `Icons.Tool3DRotate`| | +|`tool-angle`| `ToolAngle`| `Icons.ToolAngle`| | +|`tool-annotate`| `ToolAnnotate`| `Icons.ToolAnnotate`| | +|`tool-bidirectional`| `ToolBidirectional`| `Icons.ToolBidirectional`| | +|`tool-calibration`| `ToolCalibrate`| `Icons.ToolCalibrate`| | +|`tool-capture`| `ToolCapture`| `Icons.ToolCapture`| | +|`tool-cine`| `ToolCine`| `Icons.ToolCine`| | +|`tool-circle`| `ToolCircle`| `Icons.ToolCircle`| | +|`tool-cobb-angle`| `ToolCobbAngle`| `Icons.ToolCobbAngle`| | +|`tool-create-threshold`| `ToolCreateThreshold` | `Icons.ToolCreateThreshold` | | +|`tool-crosshair`| `ToolCrosshair`| `Icons.ToolCrosshair`| | +|`dicom-tag-browser`| `ToolDicomTagBrowser` | `Icons.ToolDicomTagBrowser` | | +|`tool-flip-horizontal`| `ToolFlipHorizontal` | `Icons.ToolFlipHorizontal` | | +|`tool-freehand-polygon`| `ToolFreehandPolygon`| `Icons.ToolFreehandPolygon`| | +|`tool-freehand-roi`| `ToolFreehandRoi` | `Icons.ToolFreehandRoi`| | +|`tool-freehand`| `ToolFreehand`| `Icons.ToolFreehand`| | +|`tool-fusion-color`| `ToolFusionColor`| `Icons.ToolFusionColor`| | +|`tool-invert`| `ToolInvert`| `Icons.ToolInvert`| | +|`tool-layout-default`| `ToolLayoutDefault`| `Icons.ToolLayoutDefault`| | +|`tool-length`| `ToolLength`| `Icons.ToolLength`| | +|`tool-magnetic-roi`| `ToolMagneticRoi` | `Icons.ToolMagneticRoi`| | +|`tool-magnify`| `ToolMagnify`| `Icons.ToolMagnify`| | +|`tool-measure-ellipse`| `ToolMeasureEllipse`| `Icons.ToolMeasureEllipse`| | +|`tool-more-menu`| `ToolMoreMenu`| `Icons.ToolMoreMenu`| | +|`tool-move`| `ToolMove`| `Icons.ToolMove`| | +|`tool-polygon`| `ToolPolygon`| `Icons.ToolPolygon`| | +|`tool-quick-magnify`| `ToolQuickMagnify` | `Icons.ToolQuickMagnify` | | +|`tool-rectangle`| `ToolRectangle` | `Icons.ToolRectangle` | | +|`tool-referenceLines`| `ToolReferenceLines`| `Icons.ToolReferenceLines`| | +|`tool-reset`| `ToolReset`| `Icons.ToolReset`| | +|`tool-rotate-right`| `ToolRotateRight`| `Icons.ToolRotateRight`| | +|`tool-seg-brush`| `ToolSegBrush`| `Icons.ToolSegBrush`| | +|`tool-seg-eraser`| `ToolSegEraser`| `Icons.ToolSegEraser`| | +|`tool-seg-shape`| `ToolSegShape` | `Icons.ToolSegShape`| | +|`tool-seg-threshold`| `ToolSegThreshold` | `Icons.ToolSegThreshold` | | +|`tool-spline-roi`| `ToolSplineRoi`| `Icons.ToolSplineRoi`| | +|`tool-stack-image-sync`| `ToolStackImageSync`| `Icons.ToolStackImageSync`| | +|`tool-stack-scroll`| `ToolStackScroll` | `Icons.ToolStackScroll`| | +|`tool-toggle-dicom-overlay`| `ToolToggleDicomOverlay`| `Icons.ToolToggleDicomOverlay`| | +|`tool-ultrasound-bidirectional`| `ToolUltrasoundBidirectional`| `Icons.ToolUltrasoundBidirectional`| | +|`tool-window-level`| `ToolWindowLevel`| `Icons.ToolWindowLevel`| | +|`tool-window-region`| `ToolWindowRegion`| `Icons.ToolWindowRegion`| | +|`tool-zoom` | `ToolZoom` | `Icons.ToolZoom`| | +| `tool-layout` | `ToolLayout` | `Icons.ToolLayout` | | +|`icon-tool-eraser`| `ToolEraser` | `Icons.ToolEraser`| | +|`icon-tool-brush`| `ToolBrush`| `Icons.ToolBrush`| | +|`icon-tool-threshold`| `ToolThreshold` | `Icons.ToolThreshold` | | +|`icon-tool-shape`| `ToolShape`| `Icons.ToolShape` | | +|`icon-color-lut`| `IconColorLUT` | `Icons.IconColorLUT` | | +| `viewport-window-level`|`ViewportWindowLevel`|`Icons.ViewportWindowLevel`| | +|`notifications-info`| `NotificationInfo`| `Icons.NotificationInfo`| | +|`layout-advanced-3d-four-up` | `LayoutAdvanced3DFourUp` | `Icons.LayoutAdvanced3DFourUp` | | +|`layout-advanced-3d-main` | `LayoutAdvanced3DMain` | `Icons.LayoutAdvanced3DMain` | | +|`layout-advanced-3d-only` | `LayoutAdvanced3DOnly` | `Icons.LayoutAdvanced3DOnly`| | +|`layout-advanced-3d-primary` | `LayoutAdvanced3DPrimary` | `Icons.LayoutAdvanced3DPrimary` | | +|`layout-advanced-axial-primary` |`LayoutAdvancedAxialPrimary`| `Icons.LayoutAdvancedAxialPrimary` | | +|`layout-advanced-mpr`| `LayoutAdvancedMPR` | `Icons.LayoutAdvancedMPR` | | +|`layout-common-1x1` | `LayoutCommon1x1` | `Icons.LayoutCommon1x1` | | +|`layout-common-1x2` | `LayoutCommon1x2`|`Icons.LayoutCommon1x2`| | +|`layout-common-2x2` | `LayoutCommon2x2`|`Icons.LayoutCommon2x2` | | +|`layout-common-2x3` | `LayoutCommon2x3`| `Icons.LayoutCommon2x3`| | +|`illustration-investigational-use`|`InvestigationalUse`|`Icons.InvestigationalUse`| | diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Numbers.md b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Numbers.md new file mode 100644 index 0000000..768e296 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Numbers.md @@ -0,0 +1,199 @@ +--- +title: Input Number, Range, and Double Range +--- + + +# Migration Guide: Moving to `Numeric` Component + +This guide explains how to migrate from the existing `Input`, `InputRange`, and `InputDoubleRange` components to the new `Numeric` meta component. + + +## Why Migrate? + +The old components relied heavily on props, making them complex and difficult to maintain and apply custom styles. The new `Numeric` component provides a structured approach with a context-based API, reducing prop clutter and improving reusability. + + +## `Input` > `Numeric.NumberInput` + +### Basic Usage + +**Old Usage:** + +```tsx + setValue(e.target.value)} + type="number" +/> +``` + +**New Usage:** + +```tsx + + Enter a number + + +``` + + + +### `Input` with Custom Classes + +#### **Old Usage (with containerClassName, labelClassName, and className)** + +In the old implementation, we manually applied `containerClassName`, `labelClassName`, and `className` to style the `Input` component: + +```tsx + setValue(e.target.value)} + type="number" + containerClassName="flex flex-col space-y-2" + labelClassName="text-gray-500 text-sm" + className="border rounded p-2" +/> +``` + + +**New Usage (Migrating to `Numeric.NumberInput`)** + +With `Numeric`, you should wrap everything inside `Numeric.Container`, and you can directly apply class names to its subcomponents: + +```tsx + + Enter a number + + +``` + + +## `InputRange` > `Numeric.SingleRange` + +### Basic Usage + +**Old Usage:** + +```tsx + +``` + +**New Usage:** + +```tsx + + Range + + +``` + + +### Custom Classes + +**Old Usage** + +```tsx + +``` + +**New Usage** + +```tsx + + Range + + +``` + +:::note +You now have more control over the position of the label and the slider. You can use pretty much any layout you want, whether that's flex, grid, or something else. Instead of relying on `labelPosition` to position the label, you're free to use the layout that works best for you. +::: + + +### AllowNumberEdit + +**Old Usage:** + +```tsx + +``` + +**New Usage:** + +Using `Numeric.SingleRange` and `showNumberInput` + +```tsx + + Range + + +``` + + +## `InputDoubleRange` > `Numeric.DoubleRange` + + +### Basic Usage +**Old Usage:** + +```tsx + +``` + +**New Usage:** + +```tsx + + Range + + +``` + + +--- + +## Summary of Changes + +| Old Component | New Component Equivalent | +|--------------------|------------------------| +| `` | `` | +| `` | `` | +| `` | `` | diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tests.md b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tests.md new file mode 100644 index 0000000..17e8a96 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tests.md @@ -0,0 +1,68 @@ +--- +title: Tests +--- + + +## 1. ToolButton `data-active` Attribute + +- **Previous**: Checked for a `bg-primary-light` class to determine if a tool was active. +- **Now**: Check for the HTML attribute `data-active="true"`. + +### Example + +```diff +- cy.get('@wwwcButton').should('have.class', 'bg-primary-light'); ++ cy.get('@wwwcButton').should('have.attr', 'data-active', 'true'); +``` + +## 2. Additional Data Attributes + +- Each tool button now includes: + - `data-tool=""` + - `data-active=""` + +This makes it easier to identify and assert on specific tools in the DOM. + +### Example + +```diff +- ++ +``` + +## 3. MPR Button Class Change + +If you were targeting the `ohif-disabled` class, you need to update your tests to target the `cursor-not-allowed` class. + +- **Previous**: `ohif-disabled` +- **Now**: `cursor-not-allowed` + +### Example + +```diff +- cy.get('[data-cy="MPR"]').should('have.class', 'ohif-disabled'); ++ cy.get('[data-cy="MPR"]').should('have.class', 'cursor-not-allowed'); +``` + +## 4. Removal of Stack Scroll Alias + +- The `[data-cy="StackScroll"]` element is no longer reliably in the DOM at study load. +- If needed, reintroduce or conditionally assert its presence when appropriate. + +```diff +- cy.get('[data-cy="StackScroll"]').as('stackScrollBtn'); ++ // Removed due to absence in DOM at study load +``` + +--- + +## Summary + +1. **Replace** all checks for `bg-primary-light` with `data-active="true"`. +2. **Use** `data-tool` and `data-active` attributes for more robust DOM selection and assertions. +3. **Update** MPR button checks to `cursor-not-allowed`. +4. **Remove** the `[data-cy="StackScroll"]` alias (or only use it when the element is present). diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Toolbar.md b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Toolbar.md new file mode 100644 index 0000000..9a2c79b --- /dev/null +++ b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Toolbar.md @@ -0,0 +1,101 @@ +--- +title: Toolbar +--- + +# Toolbar + +## New Toolbar uiType + +We have two new toolbar button types: `ohif.toolButtonList` and `ohif.toolButton`, which are intended to replace the `ohif.radioGroup` and `ohif.splitButton` types. + +Note that these are backward compatible, so if you are not ready to pick up the new ui types (which are more flexible and powerful), you can continue using the old types. + + +```js +// Old type +{ + uiType: 'ohif.radioGroup', +} + +// New type +{ + uiType: 'ohif.toolButton', +} +``` + +and + +```js +// Old type +{ + uiType: 'ohif.splitButton', +} + +// New type +{ + uiType: 'ohif.toolButtonList', +} +``` + +The `ohif.buttonGroup` and `ohif.radioGroup` types used in the Toolbox have been replaced with `ohif.toolBoxButtonGroup` and `ohif.toolBoxButton` to reflect their usage in the Toolbox, which has distinct styling. + +```js +// Old type +{ + uiType: 'ohif.buttonGroup', +} + +// New type +{ + uiType: 'ohif.toolBoxButtonGroup', +} +``` + + +```js +// Old type +{ + uiType: 'ohif.radioGroup', +} + +// New type +{ + uiType: 'ohif.toolBoxButton', +} +``` + + + +## getToolbarModule + + +The `getToolbarModule` function previously returned `disabled`, `disabledText`, and `className` as part of its evaluation process for the button state. These properties will still be returned, but common class names are now handled internally by the new UI button components, including `ToolButton`, `ToolButtonList`, `Toolbox`, and `ToolBoxGroup`. You can override the `className` if you need to. + + +## ToolBox + +Previously, the segmentation toolbox was not using an `evaluator` property. This is now taken into account + + +```js +// old +{ + id: 'BrushTools', + uiType: 'ohif.buttonGroup', + props: { + groupId: 'BrushTools', + items: [] + } +} + +// now +{ + id: 'BrushTools', + uiType: 'ohif.buttonGroup', + props: { + groupId: 'BrushTools', + evaluate: 'evaluate.cornerstone.hasSegmentation', + items: [] + } +} +``` diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tooltip.md b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tooltip.md new file mode 100644 index 0000000..80a33ac --- /dev/null +++ b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tooltip.md @@ -0,0 +1,62 @@ +--- +title: Tooltip +--- + +## Tooltip Updates + +### Changes: +- Updated Tooltip structure to use `Tooltip`, `TooltipTrigger`, and `TooltipContent`. +- Removed deprecated `TooltipClipboard` and inline `content`/`position` properties. + +### Migration Steps: +1. Replace imports: + ```tsx + // Before + import { Tooltip } from '@ohif/ui'; + import { TooltipClipboard } from '@ohif/ui'; + + // After + import { Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next'; + ``` + +2. Update Tooltip usage: + ```tsx + // Before + Tooltip Message} position="bottom-left"> + + + + // After + + + + + + Tooltip Message + + + ``` + + +3. TooltipClipboard Replacement: +The `TooltipClipboard` component has been removed. Instead, use the `Clipboard` component inside `TooltipContent` for copying text functionality. + +#### Before: +```tsx +{text} +``` + +#### After: +```tsx + + + {text} + + +
+ {text} + {text} +
+
+
+``` diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tourss.md b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tourss.md new file mode 100644 index 0000000..16ac946 --- /dev/null +++ b/platform/docs/docs/migration-guide/3p9-to-3p10/UI/Migration-3p10-Tourss.md @@ -0,0 +1,66 @@ +--- +title: Tours and Onboarding +--- + +## Migration Guide: Tours + +* Tours are no longer defined directly in `window.config.tours` but through the customization service under the key `ohif.tours` +* The `waitForElement` utility function has been moved from the config file to a dedicated customization file +* The structure of tour definitions (steps, options, etc.) remains largely the same + +## Migration Steps: + + + +### 1. Update any direct references to window.config.tours + +If you have any code that directly references window.config.tours, update it to use the customization service: + +```diff +- const tours = window.config.tours; ++ const tours = customizationService.getCustomization('ohif.tours'); +``` + +### 2. Use config update patterns for configuring tours + +**Before:** +```diff +- window.config = { +- tours: [ +- { +- id: 'basicViewerTour', +- route: '/viewer', +- steps: [ +- // tour steps... +- ], +- tourOptions: { +- // tour options... +- }, +- }, +- ], +- }; +``` + +**After:** +```javascript +window.config = { + customizationService: { + 'ohif.tours': { + $set: [ + { + id: 'basicViewerTour', + route: '/viewer', + steps: [ + // Your tour steps + ], + }, + ], + }, + }, +}; +``` + + +## Benefits of the Change + +4. **Mode-specific Tours**: now you can have different tours for different modes diff --git a/platform/docs/docs/migration-guide/3p9-to-3p10/index.md b/platform/docs/docs/migration-guide/3p9-to-3p10/index.md new file mode 100644 index 0000000..3709d5f --- /dev/null +++ b/platform/docs/docs/migration-guide/3p9-to-3p10/index.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 1 +sidebar_label: 3.9 -> 3.10 beta +--- + +# Migration Guide + + +## General diff --git a/platform/docs/docs/migration-guide/_category_.json b/platform/docs/docs/migration-guide/_category_.json new file mode 100644 index 0000000..e4b82ff --- /dev/null +++ b/platform/docs/docs/migration-guide/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Migration Guides", + "position": 11 +} diff --git a/platform/docs/docs/migration-guide/from-3p7-to-3p8.md b/platform/docs/docs/migration-guide/from-3p7-to-3p8.md new file mode 100644 index 0000000..b07d8ff --- /dev/null +++ b/platform/docs/docs/migration-guide/from-3p7-to-3p8.md @@ -0,0 +1,212 @@ +--- +sidebar_position: 2 +sidebar_label: 3.7 -> 3.8 +--- + +# Migration Guide + +There are two main things that need to be taken care of. + + +## New Toolbar Button definitions + +### Update Active Tool Handling +The concept of `activeTool` and its associated getter and setter has been removed. The active tool should now be derived from the toolGroup and the viewport. + + +**Action Needed** + +Remove any code that sets the default tool using `toolbarService.setDefaultTool()` and activates the tool using +`toolbarService.recordInteraction()`. For example, the following code should be removed: + +```javascript +let unsubscribe; +toolbarService.setDefaultTool({ + groupId: "WindowLevel", + itemId: "WindowLevel", + interactionType: "tool", + commands: [ + { + commandName: "setToolActive", + commandOptions: { + toolName: "WindowLevel", + }, + context: "CORNERSTONE", + }, + ], +}); + +const activateTool = () => { + toolbarService.recordInteraction(toolbarService.getDefaultTool()); + + unsubscribe(); +}; + +({ unsubscribe } = toolGroupService.subscribe( + toolGroupService.EVENTS.VIEWPORT_ADDED, + activateTool +)); +``` + + + +Instead, focus on defining the buttons and their placement in the toolbar using `toolbarService.addButtons()` and `toolbarService.createButtonSection()`. For example: + +```javascript +toolbarService.addButtons([...toolbarButtons, ...moreTools]); +toolbarService.createButtonSection("primary", [ + "MeasurementTools", + "Zoom", + "WindowLevel", + "Pan", + "Capture", + "Layout", + "MPR", + "Crosshairs", + "MoreTools", +]); +``` + + +### Update Button Definitions +The concept of button types (toggle, action, tool) has been removed. Buttons are now defined using a simplified object-based definition. + +**Action Needed** + +Update your button definitions to use the new object-based format and remove the `type` property. Use the `uiType` property for the top-level UI type definition. For example: + +```javascript +// Old Implementation +{ + id: 'Capture', + type: 'ohif.action', + props: { + icon: 'tool-capture', + label: 'Capture', + type: 'action', + commands: [ + { + commandName: 'showDownloadViewportModal', + commandOptions: {}, + context: 'CORNERSTONE', + }, + ], + }, +}, +``` + +is now + +```javascript +// New Implementation +{ + id: 'Capture', + uiType: 'ohif.radioGroup', + props: { + icon: 'tool-capture', + label: 'Capture', + commands: [ + { + commandName: 'showDownloadViewportModal', + context: 'CORNERSTONE', + }, + ], + evaluate: 'evaluate.action', + }, +}, +``` + +### Add Evaluators to Button Definitions +Introduce the ๏ปฟevaluate property in your button definitions to determine the state of the button based on the app context. + +**Action Needed** + +Add the appropriate `evaluate` property to each button definition. For example: + - Use `evaluate.cornerstoneTool` if the button should be highlighted only when it is the active primary tool (left mouse). + - Use `evaluate.cornerstoneTool.toggle` if the tool is a toggle tool (like reference lines or image overlay). + +Refer to the `modes/longitudinal/src/toolbarButtons.ts` file for examples of using the `evaluate` property. + +Additional Resources + + - For more information on the new toolbar module and its usage, refer to the [Toolbar documentation](../platform/extensions/modules/toolbar.md). + - Consult the updated button definitions in `modes/longitudinal/src/toolbarButtons.ts` for examples of the new object-based button definition format and the usage of evaluators. + +### Tool listeners + +Some tools can be configured to listen to events to trigger, for example + +```ts +createButton({ + id: 'ReferenceLines', + icon: 'tool-referenceLines', + label: 'Reference Lines', + tooltip: 'Show Reference Lines', + commands: 'toggleEnabledDisabledToolbar', + listeners: { + [ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: + ReferenceLinesListeners, + [ViewportGridService.EVENTS.VIEWPORTS_READY]: + ReferenceLinesListeners, + }, + evaluate: 'evaluate.cornerstoneTool.toggle', + }), +``` + +If you have a custom viewport component, and you are overriding the ```onElementEnabled``` handler, than ensure to call ```viewportGridService.setViewportIsReady(viewportId, true)``` in your own handler so that eventually the ```VIEWPORTS_READY``` event fires as expected, if you are not modifying the handler, then an existing handler that is automatically passed down via the props will call that for you, it is passed down from ```ViewportGrid.tsx``` + +```ts + 1 ? viewportLabel : ''} + viewportId={viewportId} + dataSource={dataSource} + viewportOptions={viewportOptions} + displaySetOptions={displaySetOptions} + needsRerendering={displaySetsNeedsRerendering} + isHangingProtocolLayout={isHangingProtocolLayout} + onElementEnabled={() => { + viewportGridService.setViewportIsReady(viewportId, true); + }} +/> + +``` + +## Toolbar Service + +toolbarService.init is not a function. + +**Action Needed** +remove the call to toolbarService.init() from your codebase. + + + +## leftPanelDefaultClosed and rightPanelDefaultClosed + +Now they are renamed to `leftPanelClosed` and `rightPanelClosed` respectively. + + +## StudyInstanceUID in the URL param + +Previously there were two params that you could choose: seriesInstanceUID and seriesInstanceUIDs, they have been replaced with seriesInstanceUIDs so even if you would like to filter one series use ``seriesInstanceUIDs` + + +## UI + +### Header +Header in @ohif/ui now needs servicesManager and appConfig as input. + + +### Panels +Left and right panel lists are no longer injected into the LayoutTemplate, and have been moved to a PanelService where you have to fetch them from. + +If you're using the main layout, you're fine. However, if you have a custom layout, you'll need to update it. To get the panels, see the + +`extensions/default/src/ViewerLayout/index.tsx` + + + + +## Refactoring + +- TimingEnum (and I guess all enums exported from OHIF core have now moved from Types to Enums export). diff --git a/platform/docs/docs/migration-guide/from-v2.md b/platform/docs/docs/migration-guide/from-v2.md new file mode 100644 index 0000000..920f4d0 --- /dev/null +++ b/platform/docs/docs/migration-guide/from-v2.md @@ -0,0 +1,996 @@ +--- +sidebar_position: 3 +sidebar_label: 2.x -> 3.5 +--- + +# Migration Guide + +On this page, we will provide a guide to migrating from OHIF v2 to v3. Please note +that this document is a work in progress and will be updated as we move forward. +This document is not meant to be used as a migration recipe but as a migration overview. + + +# Introduction + +## Importance of Migration + +- Enhanced UX: the new design and UI of OHIF v3 provides a more intuitive and user-friendly experience. + OHIF v3 adds an improved side panels and toolbar, and a new layout system that lets you customize + the layout of your application. +- Improved Performance: OHIF v3 leverages the new Cornerstone3D rendering and tooling libraries, which + significantly improve performance and provide a more robust and stable foundation for your application + for rendering and interacting with medical images. Some of the new advanced features in Cornerstone3D + include: OffScreen Rendering and GPU Acceleration for all viewports, streaming of the volume data, 3D annotations and measurements, + sharing tool states between viewports and more. +- Improved Customizability: With addition of Modes and Extensions, OHIF v3 provides a more modular + and customizable framework for building medical imaging applications, this will let you + focus on your use case and not worry about the underlying infrastructure and also have less worry + to keep up to date with the latest changes. +- Community driven Modes: OHIF v3 provides a gallery of modes that you can use as a starting point + for your application. These +- Future-Proofing: By migrating to v3, you align your application with the latest advancements in the OHIF framework, ensuring ongoing support, updates, and access to new features. +- Community Support: OHIF v3 benefits from an active community of developers and contributors who provide valuable support, bug fixes, and continuous improvements. + +## Migration Timeframe + +The duration of the migration process can vary depending on factors such as the complexity of custom changes made in v2, familiarity with v3's architecture, and the size of the codebase. +If you don't have any custom changes in v2, the migration process should be relatively straightforward. If you have custom changes, you will need to update them to work with the new architecture and new +rendering and tooling engines. + + +## Complexity and Pain Points + +Certain scenarios can make the migration process more complex and potentially introduce pain points: + +- Extensive Customizations: If your v2 implementation includes extensive custom changes and overrides, adapting those customizations to the new structure and APIs of v3 may require additional effort and careful refactoring. +- UI Customizations: Since in OHIF v3 we moved our component library to Tailwind CSS + if you have any custom UI components, you will need to migrate them to Tailwind CSS too, and this might be a bit time consuming. +- Hardware requirements: Since Cornerstone3D uses WebGL for rendering volumeViewport (although it has + a CPU rendering fallback), you need to make sure that your target hardware supports WebGL. You can check + if your hardware supports WebGL [here](https://get.webgl.org/). Also regarding the GPU requirements, you can check the tier of your GPU [here](https://pmndrs.github.io/detect-gpu/), if it is tier 1 and above, you + should be good to go. + +## Summary of Changes + +OHIF v3 is a major re-architecture of the OHIF v2 to make it more modular and +easier to maintain. The main differences are: + +- platform/viewer (@ohif/viewer) has been renamed to platform/app (@ohif/app) (explanation below) +- Extensions are available to be used by modes on request, but are still injected as module components. +- To use the modules provided by the extensions, you need to write a [Mode](../platform/modes/index.md). Modes +are configuration objects that will be used by the viewer to load the modules. This lets users to be able to use common extensions with different configurations, and enhances the customizability of the viewer. +- App configuration structure is different, mainly the `servers` is renamed to `dataSources`. +- Apps can be customized significantly more than previously by providing configuration code int he customizationModule section. +- The viewer UI is completely re-written in Tailwind CSS for better maintainability, although it is a WIP but + already provides a better user experience. +- cornerstone-core and cornerstone-tools are removed and OHIF v3 is using the new Cornerstone3D rendering library and tools. Moving to Cornerstone3D has enabled us to provide a more robust and stable foundation + for 3D rendering and 3D annotations and measurements. In addition, Cornerstone3D provides APIs to load + and stream data into a volume which has huge performance benefits. +- A new CLI tool to help you create extensions and modes (more [here](../development/ohif-cli.md)) +- redux store has been removed and replaced with a simpler state management system via React Context API. + +New significant additions that might be useful for you that weren't available in OHIF v2: +- [OHIF CLI](../development/ohif-cli.md) +- [New Rendering Engine and Toolings](https://www.cornerstonejs.org/) +- [Modes](../platform/modes/index.md) +- [Mode Gallery](https://ohif.org/modes) +- [Layouts](../platform/extensions/modules/layout-template.md) +- [Data Sources](../platform/extensions/modules/data-source.md) +- [Hanging Protocols](../platform/services/data/HangingProtocolService.md) +- [URL Params](../configuration/url.md) + +## Platform/viewer (@ohif/viewer) -> platform/app (@ohif/app) + + +To ensure proper versioning of OHIF v3, we have made a decision to rename the platform/viewer to platform/app. Previously, the platform/viewer package followed software engineering versioning (currently at v4.12.51). However, going forward, we aim to align the versioning of platform/app with the product version (e.g., v3.4.0, v3.5.0, etc.). + +Since the platform/viewer (@ohif/viewer) is already at v4.12.51, we opted to rename it as platform/app to enable versioning in accordance with the product versioning approach. If you were utilizing any exports from @ohif/viewer, please update them to use @ohif/app instead. + + +## Configuration + +:::tip +There are various configurations available to customize the viewer. Each configuration is represented by a custom-tailored object that should be used with the viewer to work effectively with a specific server. Here are some examples of configuration files found in the platform/app/public/config directory. Some server-specific configurations that you should be aware are: `supportsWildcard`, `bulkDataURI`, `omitQuotationForMultipartRequest`, `staticWado` (Read more about them [here](../configuration/configurationFiles.md)). + +- default.js: This is our default configuration designed for our main server, which uses a Static WADO datasource hosted on Amazon S3. +- local_orthanc.js: Use this configuration when working with our local Orthanc server. +- local_dcm4chee.js: This configuration is intended for our local dcm4chee server. +- netlify.js: This configuration is the same as default.js and is used for deployment on Netlify. +- google.js: Use this configuration to run the viewer against the Google Health API. +::: + +OHIF v3 has a new configuration structure. The main difference is that the `servers` is renamed to `dataSources` and the configuration is now asynchronous. Datasources are more abstract and +far more capable than servers. Read more about dataSources [here](../platform/extensions/modules/data-source.md). + +- `StudyPrefetcher` is only available in OHIF v3.9 beta and will be available in the next stable 3.9 release. +- The `servers` object has been replaced with a `dataSources` array containing objects representing different data sources. +- The cornerstoneExtensionConfig property has been removed, you should use `customizationService` instead (you can read more [here](../platform/services/customization-service/customizationService.md)) +- The maxConcurrentMetadataRequests property has been removed in favor of `maxNumRequests` +- The hotkeys array has been updated with different command names and options, and some keys have been removed. +- New properties have been added, including `maxNumberOfWebWorkers`, `omitQuotationForMultipartRequest`, `showWarningMessageForCrossOrigin`, `showCPUFallbackMessage`, `showLoadingIndicator`, `strictZSpacingForVolumeViewport`. +- you should see if `supportsWildcard` is supported in your server, some servers don't support it and you need to make it false. + +## Modes + +As mentioned briefly above, modes are configuration objects that will be used by the viewer to load extensions. +This lets users to be able to use common extensions with different configurations. So as OHIF developers can focus on creating extensions while +you as the user can focus on creating modes having your own use case and configuration/initialization logic in mind. + +Separating the configuration from the extensions also makes it so that you can +have multiple modes in a single application each focusing on certain tasks. For example, you can have a mode for segmentation which uses specific panels and tools which you don't need +for a mode that will be used for reading (read more about modes [here](../platform/modes/index.md)) + +:::info +Previously, the viewer was designed around registered extensions. If you had a specific use case, you had to duplicate the viewer code and incorporate your customizations through extensions. However, with the introduction of a new layer of abstraction called Modes, you no longer need to fork the viewer. + +Modes provide a flexible approach where you can create your own mode and utilize the necessary extensions within that mode. This eliminates the need for duplicating the viewer codebase. + +Furthermore, Modes offer the advantage of having multiple applications within a single viewer. For instance, you can have a mode dedicated to segmentation tasks and another mode focused on reading. Each mode can have its own unique configuration, initialization logic, layout, tools, and hanging protocols. This ensures a cleaner user interface in the viewer and an improved user experience overall. +::: + +Upon entering a mode, the Viewer will register its declared extensions and load them. And you +can specify which modules you need from each extension in the mode configuration. For instance + +```js + +const ohif = { + layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', + sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', + measurements: '@ohif/extension-default.panelModule.measure', + thumbnailList: '@ohif/extension-default.panelModule.seriesList', +}; + +const cs3d = { + viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', +}; + +const tmtv = { + hangingProtocol: '@ohif/extension-tmtv.hangingProtocolModule.ptCT', + petSUV: '@ohif/extension-tmtv.panelModule.petSUV', + ROIThresholdPanel: '@ohif/extension-tmtv.panelModule.ROIThresholdSeg', +}; + +function modeFactory({ modeConfiguration }) { + routes: [ + { + path: 'tmtv', + layoutTemplate: ({ location, servicesManager }) => { + return { + id: ohif.layout, + props: { + // leftPanels: [ohif.thumbnailList], + rightPanels: [tmtv.ROIThresholdPanel, tmtv.petSUV], + viewports: [ + { + namespace: cs3d.viewport, + displaySetsToDisplay: [ohif.sopClassHandler], + }, + ], + }, + }; + }, + }, + ], +} +``` + +In the example above, we are using the `tmtv` mode which is a mode for reading PET/CT scans +and as you can see we are specifying the layout, the panels and the viewports that we need +for this mode. The `tmtv` mode is using the `cs3d` extension for rendering and the `ohif` extension. As you see you can reference the modules from the extensions using the `namespace` via strings. So for instance, if you need to use the `viewportModule` from the `@ohif/extension-cornerstone` you can use `@ohif/extension-cornerstone.viewportModule.cornerstone` as the namespace. + +:::tip +`ExtensionManager` will register and load the modules from the extensions and make them available to the viewer by their namespaces. +::: + +Below you can see a screen shot from the demo showcasing 3 modes for the opened study. + +![Alt text](../assets/img/migration-modes.png) + +:::tip +How do I decide certain thing should go inside a mode or extension, Here are some considerations to help you make the decision: + +- **Functionality Scope**: If the functionality is specific to a particular use case or task within your viewer, it is often best suited to be included within a mode. Modes allow you to create customized configurations, layouts, panels, tools, and other components specific to a particular task or workflow. This includes which tool to be active by default, which panels to be displayed, and which layout to be used. + +- **Reusability**: If the functionality can be used across multiple modes, it is better to implement it as an extension. Extensions provide a modular approach where you can encapsulate and share functionality across different modes. For instance, if you have a custom panel that you want to use in multiple modes, you can implement it as an extension and include it in + the mode configuration. + +- **Complexity**: If the functionality requires significant customizations, complex logic, or extensive modifications to the viewer's core behavior, it might be better suited as an extension. + +- **New Service**: If you are writing a new service, it is preferable to implement it as an extension. Services are used to provide a common interface for interacting with external systems and data sources. +There is a new way to register new services which are extendible by other extensions. + +Remember that there is no strict rule for deciding between modes and extensions. It's a matter of understanding the specific requirements of your application. + +::: + + +## Routes + +In OHIF v2 a study was loaded and mounted on `/viewer/:studyInstanceUID` route. In OHIF v3 +we have reworked the route registration to enable more sophisticated routing. Now, Modes are tied to specific routes in the viewer, and multiple modes/routes can be present within a single application, making "routes" configuration the most important part of mode configuration. + +- Routes with a dataSourceName: `{mode.id}/{dataSourceName}` +- Routes without a dataSourceName: `{mode.id}` which uses the default dataSourceName + +This makes a mode flexible enough to be able to connect to multiple datasources +without rebuild of the app for use cases such as reading from one PACS and +writing to another. + +
+ +Can I register a custom route to OHIF v3? + + +Yes, you can take advantage of the customizationService and register your own routes. +see [custom routes](../platform/services/customization-service/customRoutes.md) + + +
+ + +## DICOM Endpoints + +In OHIF v3 there is a new end point that your DICOM server should be able to respond to +`WADO-RS GET studies/{studyInstanceUid}/series` + +This is used in the viewer for fetching the series list for a study to use for the hanging protocol. + +## LifeCycle Hooks + +OHIF v2 had `preRegistration` hook for extensions for initialization. In OHIF v3 you have +even more control using `onModeEnter` and `onModeExit` hooks on the extensions and on the modes. + +- `preRegistration`: is called before the extension is registered to the viewer. So very early in the lifecycle of the viewer. +- `onModeEnter` is called when the mode is entered (component on the route is mounted, e.g., when you click on the mode to enter it) +- `onModeExit` is called when the mode is exited (component on the route is unmounted, e.g., when you navigate back to the worklist) + +## Extensions + +Since extensions in OHIF v2 were the main way of customizing the viewer, we will spend some time +below to explain how you can migrate your extensions to OHIF v3. + +### Default Extension + +Lots of common functionalities in the platform/core has been moved inside +the `@ohif/extension-default` extension. This extension is loaded by default +in the viewer and it provides the following functionalities: + +- common datasources such as DICOMWeb, DICOMLocal, and DICOMJSON datasource. +- default measurement panel and panel study browser +- common toolbar button layouts +- common hanging protocol configurations + +
+ +how can I integrate to my google health api? is there support for that? + + +You can right now, take a look into our google configuration that we use for our QA located at +`config/google.js`. Also we have some exciting UI changes coming up for the next release +that will make it easier to integrate with google health api. +
+ +
+ +Is there any recommendation for PACS integration + + +You can take a look at open source PACS such as dcm4chee or orthanc. We have support for them. Also +we have a new static wado datasource that you can use to take benefit of new deduplicated metadata +and caching features. + +
+ +### Cornerstone Extension + +In OHIF v2, the Cornerstone extension provided modules like Cornerstone ViewportModule, ToolbarModule, and CommandsModule for controlling viewport actions. +It relied on `react-cornerstone-viewport` for rendering viewports, `cornerstone-tools` for tools, and `cornerstone-core` for core functionalities. + +However, in OHIF v3, there have been significant changes. The rendering and tooling logic has been migrated to a new library called [`Cornerstone3D`](https://github.com/cornerstonejs/cornerstone3D-beta/). This means that all viewport rendering and tool functionalities are now handled by Cornerstone3D. + +Additionally, in OHIF v3, the native support for 3D functionalities previously provided by the `vtk` extension has been integrated into Cornerstone3D. As a result, any vtkjs logic is encapsulated on CS3D. Things now are much more cleaner and simpler. + +To migrate from OHIF v2 to OHIF v3: + +#### Loading + +Previously we used `cornerstone-wado-image-loader` for loading images. However, we have fully switched the a new +library called `@cornerstonejs/dicom-image-loader` which is a fork of `cornerstone-wado-image-loader` with typescript support and bug fixes. +We have deprecated `cornerstone-wado-image-loader` and you should also switch to `@cornerstonejs/dicom-image-loader` as well. +The process is very simple, you can follow this [PR](https://github.com/OHIF/Viewers/pull/3339) to see how we have migrated. + +There is also a new loader and package `@cornerstonejs/streaming-image-volume-loader`, which provides streaming of the image data +into a volume using web workers and web assembly. You can look into the cornerstone documentation and read more about the +volumeViewport and volumeLoader. + + +#### Rendering + +The significant difference between cornerstone-core and cornerstone3D is that cornerstone3D fully utilizes +[vtk.js](https://kitware.github.io/vtk-js/) for rendering, however in cornerstone-core we used a mix of webGL and vtk +for rendering. While you don't need to do a migration for this, you should be aware that the rendering is now fully performed in the +world coordinate system and the image is placed in the world coordinate system using the `imagePositionPatient` and `imageOrientationPatient` +attributes of the image. This means that you can now share the tool states between multiple viewports and you can also +use the same tool states for 2D and 3D viewports. + +:::tip + +In OHIF v3, we have removed the OHIF's vtk extension and migrated all the 3D functionalities to Cornerstone3D. + +Also you need to remove any dependencies on `react-cornerstone-viewport`, `cornerstone-tools`, and `cornerstone-core`. +::: + +#### Tools + +If you don't have any custom tools, you most likely won't need to make any changes as have tried +to migrate all the tools from `cornerstone-tools` to Cornerstone3D (except `ROIWindowLevel` which is work in progress right now). + +Cornerstone3D has moved the coordinate system of tools to the world coordinate system enabling sharing +tool states between multiple viewports, and as a result the toolData is now stored in the world coordinate system as well. +So to migrate your tools, you will need to update your toolData to be stored in the world coordinate system. You can look +into the simplest tool for instance LengthTool in both `cornerstone-tools` and `cornerstone3D` to see the difference. + + + +By following these steps, you can leverage the improved rendering and tooling capabilities of Cornerstone3D and eliminate the need for the old ohif's vtk extension in OHIF v3. + + +
+ +Is there any name standard for modes and extensions? + + +No naming standard, you can have your organization name as a prefix for your modes and extensions as we +do for ohif (`@ohif/extension-*` and `@ohif/mode-*`). + +
+ + +
+ +What happens if I have create a mode with same name as existing one + + +You shouldn't. Modes are configuration objects that you can simply. There is no real use case +for creating a mode with same name as existing one. If you do so, the last one will override the previous one + + +
+ + +
+ +How to remove an "core" extension/mode? + + +You can use the OHIF cli tool to add/remove/link and unlink extensions and modes. You can find more information +about the cli tool [here](../development/ohif-cli.md) + +
+ +
+ +If I have vtkjs implementation how can I port it? Should I create a specific extension for that? + + +Cornerstone3D has support for some vtk.js actor and mappers including imageData, polyData and volume. If you have another +implementation of vtk.js actor or mapper, you might be able to use `viewport.addActor` to include it in the rendering +pipeline, but depending on the implementation and how much it interfere with the cornerstone3D rendering pipeline, you might +not get the expected result. + +
+ + + +### DICOM Segmentation & DICOM RT + +In OHIF v3, the equivalent extensions for RT and SEG exists with similar logic, but with various improvements such as +enhanced ui/ux for segmentation panel, faster loading and interaction, and better support for multiple viewports, +animations for jump to segment, volumetric rendering, and more. Additionally, OHIF v3 introduces new functionalities with the SEG Viewport and RT Viewport. + +:::tip + +In OHIF v3, Segmentation objects +are loading using the frame of reference by default which means that if there are two viewports that are using the same frame of reference, +if you load a segmentation (labelmap or RT) which lives in the same frame of reference, it will be loaded in both viewports. +::: + +When loading a series that contains SEG (Segmentation) or RT (RT Structure Set) data, the viewport will automatically +switch to the corresponding SEG or RT viewport. The user will then be prompted to decide whether to load the segmentation +or RT structure set into the viewer. This new feature addresses a common use case in which there are multiple segmentation +series in a study, and the user only wants to load specific ones. In OHIF v3, the Segmentations are all loaded +as 3D volumes and as a result a volume viewport is used to display them. (Stack Segmentation in Cornerstone3D is still a +work in progress.) + +In OHIF v2, the user had to load all the segmentation series and then manually delete the ones they didn't want to see. +However, in OHIF v3, the user has more control. The temporary SEG or RT viewport does not immediately load (hydrate) +the segmentation or RT structure set. Instead, the user can decide which ones to load, reducing unnecessary +loading and providing a more efficient workflow. + +This enhancement in OHIF v3 allows users to selectively load specific segmentations or RT structure sets, +improving the usability and efficiency of the viewer when working with multiple SEG or RT series. + + + + +
+ +Can I load one seg in one viewport and another in another viewport? + + +If there is another viewport in the grid that is using the same frame of reference, the segmentation will be loaded in that viewport as well. + +However, since we split the concept of `load` (`hydration`) and `preview`, you can use the preview (not load), which +makes sure the SEG is contained within the viewport, but it is not hydrated so you cannot edit it. + +In future however, we will add more controls over, hiding the segmentation in other viewports via UI, however, you can +right now do it via code. + + + +
+ +
+ +Does it support nifti? + + +Nifit support for both image and segmentation is coming soon. We are working on it. + + +
+ +### DICOM SR + +In OHIF v2, DICOM SR functionality was integrated into the Cornerstone extension. However, in OHIF v3, DICOM SR is now a separate extension. The DICOM SR extension in OHIF v3 retains the same loading and hydrating logic using dcmjs adapters. Additionally, it introduces a new type of viewport called the SR Viewport, which is used to display SR data. + +Similar to the temporary SEG and RT viewports, when a SR display set is selected in OHIF v3, the user is prompted to decide whether to load the SR data into the viewer and initiate the tracking. The SR viewport allows the user to switch between different measurements within the SR instance by utilizing the arrow buttons located at the top of the viewport. + +:::tip +This separation of DICOM SR into its own extension in OHIF v3 provides a dedicated viewport type for SR data and offers enhanced functionality for interacting with SR measurements within the viewer. +::: + + +### DICOM Tag Browser + +In OHIF v2, the DICOM Tag Browser was a separate extension that provided a dedicated user interface for exploring DICOM tags. However, in OHIF v3, we have integrated the DICOM Tag Browser functionality into the `default` extension. + +The DICOM Tag Browser is a powerful tool for debugging and inspecting DICOM metadata, and we wanted to make it easily accessible to users. As a result, it is now available as a toolbar icon within the `default` extension. This allows users to conveniently access the DICOM Tag Browser directly from the toolbar, eliminating the need for a separate extension. + + +
+ +Now that dicom tag is integrated back to default extension, how can I port my code that was implemented in the old extension? Should I create an extension or change directly into default? + + +If you have a custom tag browser, you have two options, either modify the default tag browser (if you think the features +you added is useful for everyone, feel free to open a PR!), or create your own extension with your custom tag browser +which then you can add to the toolbar. + + + +
+ + +### DICOM HTML + +Since we have added graphical overlay of DICOM SR in OHIF v3, we have temporarily downgraded the priority of displaying DICOM HTML within the viewer. While DICOM HTML support is not available in the current version of OHIF v3, we acknowledge its importance and plan to reintroduce this functionality in future updates. + + + +
+ +is there any easy way for supporting my own dicom html viewer? Should I use extension? + + +Yes, you can write your own sopClassHandler and custom viewport in your custom extensions. +After, you need to associate that with the viewport that you +will use in the mode configuration, this way when that sopClassUID is requested it will use your custom viewport. + + + +
+ + + +### DICOM Microscopy + +In OHIF v2, the DICOM microscopy engine was based on an older version of the [DICOM microscopy viewer](https://github.com/ImagingDataCommons/dicom-microscopy-viewer) maintained by our friends at IDC (Imaging Data Commons). However, in OHIF v3, we have upgraded to the latest version of the DICOM microscopy viewer. This new version offers significant improvements in terms of robustness and performance, providing users with an enhanced microscopy viewing experience. + +One notable addition in the latest DICOM microscopy viewer is the support for annotations within the whole slide images (SM images). This feature allows users to annotate and mark specific regions of interest directly within the microscopy images. + +:::tip +Looking ahead, our future plans include adding DICOM SR (Structured Reporting) support for export of annotations in microscopy images. While we will enhance our support for SM images (color profiles etc.), we recommend utilizing the [SLIM Viewer](https://github.com/ImagingDataCommons/slim) developed by IDC for more sophisticated microscopy use cases. +::: + + + +## Extension Modules + + +v3 Extension is likely the same as in v2. Extensions can (like before) have +modules exported via `get{ModuleName}Module` (e.g., `getViewportModule`). + +:::info +There are new +types of modules that can be exported from extensions (such as `HangingProtocolModule`, `LayoutModule`, read more about +modules in v3 [here](../platform/extensions/index.md)). +::: + +The main difference between v3 and v2 is that exported modules were represented as a single object, whereas in OHIF v3, they are +represented as an array of objects, each having a name property. This change was implemented to +enable extensions to export multiple named submodules, providing more flexibility and modularity. + +To access these modules in OHIF v3, you can use the namespace provided by the `ExtensionManager`. For example, consider the following code snippet + + +```js +getUtilityModule({ servicesManager }) { + return [ + { + name: 'common', + exports: { + getCornerstoneLibraries: () => { + return { cornerstone, cornerstoneTools }; + }, + getEnabledElement, + dicomLoaderService, + registerColormap, + }, + }, + { + name: 'core', + exports: { + Enums: cs3DEnums, + }, + }, + { + name: 'tools', + exports: { + toolNames, + Enums: cs3DToolsEnums, + }, + }, + ]; +}, +``` + + +In this example, the extension is exporting multiple submodules named 'common', +'core', and 'tools'. To access the 'common' submodule provided by the @ohif/extension-cornerstone extension, +you can use the following code: + +```js +extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.common' +); +``` + +This allows you to access the specific submodule provided by the extension and utilize its functionalities within your application. + + +
+ +How can I have a lazy-loaded component and import it from another extension? + + +If an extension is exporting a component, you can import it from another extension. For example, if you have an extension that exports a component called `MyComponent`, you can import it from another extension like this: + +```js +import { MyComponent } from '@ohif/extension-my-extension'; +``` + + +
+ + +### ToolbarModule + +In OHIF v2, the toolbarModule was used to add buttons to the toolbar. For example, the following code snippet demonstrates adding a zoom tool button to the toolbar: + +In OHIF v2 + +```js +{ + id: 'Zoom', + label: 'Zoom', + icon: 'search-plus', + // + type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, + commandName: 'setToolActive', + commandOptions: { toolName: 'Zoom' }, +}, +``` + +However, in OHIF v3, the toolbarModule has been repurposed to define different button types. For instance, OHIF v3 introduces the ohif.radioGroup and ohif.splitButton button types, which provide more flexibility in defining toolbar buttons for each mode. + + + +```js +{ + name: 'ohif.radioGroup', + defaultComponent: ToolbarButton, + clickHandler: () => {}, +}, +{ + name: 'ohif.splitButton', + defaultComponent: ToolbarSplitButton, + clickHandler: () => {}, +}, +``` + +To use these button types within your modes, you can define the buttons in your mode's configuration. In the onModeEnter hook, you can add the defined buttons to the toolbar using the toolbarService. Here's an example of how to add buttons to the toolbar: + + + +```js +// toolbar button +{ + id: 'Zoom', + type: 'ohif.radioGroup', + props: { + type: 'tool', + icon: 'tool-zoom', + label: 'Zoom', + commands: _createSetToolActiveCommands('Zoom'), + }, +}, +``` + +and in `onModeEnter` + +```js +onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => { + const { + toolbarService, + toolGroupService, + } = servicesManager.services; + + // Init tool groups (see cornerstone3D for more details) + initToolGroups(extensionManager, toolGroupService, commandsManager); + + toolbarService.addButtons(toolbarButtons); + toolbarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'WindowLevel', + 'Pan', + 'Capture', + 'Layout', + 'Crosshairs', + 'MoreTools', + ]); +}, +``` + +By using the updated toolbarModule in OHIF v3, you can define and add toolbar buttons specific to each mode, providing greater flexibility and customization options for the toolbar configuration. + +An example of split button icon in v3 is shown below + +![Alt text](../assets/img/migration-split-button.png) + +
+ +Is the tool state shared between two different modes? + +No, the tool state is not shared between different modes in OHIF v3. Each mode operates independently and maintains its own tool state. + +
+ +
+ +I have a custom icon. How can I add it to the toolbar? + + +You need to first register it via `addIcon` in the src/components/Icon, and then you can +referenced it by name in the toolbar configuration for mode +
+ + +
+ +Can I change the toolbar's location? Can I add a secondary toolbar? + +Not in our default layout, but you can write your own layout in your custom extension +and use it instead of the default one. + +
+ +
+ +Can I have different tool sets for each viewport? + + +We don't have fully support for this yet, but we have plans for it. Basically, the plan +is to use the viewport action bar in the top of the viewport to provide viewport-specific +tool sets. +
+ +
+ +Are all tools from v2 support in v3? + + +Almost all with the exception of ROIWindow, but we have plans to add it in the future. However, there are +much more tools in v3 that are not available in v2 such as referenceLines, Stack Image Sync, and +Calibration tool. +
+ +### CommandsModule + +The structure of the commands module is the same as before. The only difference is that +we use Cornerstone3D for rendering and tools. So, if you have a custom command that you were +using in the v2, you need to migrate it to the new Cornerstone3D API. + +You can visit the migration guide for cornerstone [here](https://www.cornerstonejs.org/docs/migrationGuides). + +### PanelModule + +Previously in OHIF v2 you had + +```js +return { + menuOptions: [ + { + icon: 'list', + label: 'Segmentations', + target: 'segmentation-panel', + stateEvent: SegmentationPanelTabUpdatedEvent, + }, + ], + components: [ + { + id: 'segmentation-panel', + component: ExtendedSegmentationPanel, + }, + ], + defaultContext: ['VIEWER'], +}; +``` + +but in OHIF v3 you have + +```js +return [ + { + name: 'panelSegmentation', + iconName: 'tab-segmentation', + iconLabel: 'Segmentation', + label: 'Segmentation', + component: wrappedPanelSegmentation, + }, +]; +``` + +
+ +How can I add my own custom panel? + +To add your own custom panel in OHIF v3, you can follow these steps: + +- Create a new React component that represents your custom panel. +- Provide it in the getPanelModule of your extension. +- Inside your mode, add the panel namespace to the mode's configuration for the layout module. + +
+ +
+ +How to enhance an existing panel? + +To enhance an existing panel in OHIF v3, you can create a new React component that extends or wraps the existing panel component. In your enhanced component, you can add additional functionality, modify the appearance, or incorporate new features specific to your use case. You can also look into the customizationService to see +how you can use the registered points to customize the panel. + +
+ +
+ +How to change the order of appearance of panels? + +To change the order of appearance of panels in OHIF v3, you can modify the panel layout configuration in the mode configuration. The panel layout configuration specifies the order and arrangement of panels within the viewer interface. + +
+ +
+ +Is there a way to change the viewer layout to present right panels on the left and the toolbar on the right? + +Not with our default layout which the default extension provides. However, you can write a new layout and provide it +in the `getLayoutModule` which you can reference in the `layout` property of the mode configuration. +
+ +### SopClassHandlerModule + +The least changed module is the SopClassHandlerModule, although this now returns +an array instead of a single instance. The purpose of this module is to create +a list of displaySets based on the metadata. OHIF App uses this module to +create one or more displaySets for each series. +The displaySet is then used to then get assigned +on each viewport and the viewport renders the image. + +The `DisplaySet` created by the handler can have a member function `addInstances` +which will update the display set with new SOP instance data, allowing the +preservation of the display set UID when required. + +Multiple display sets will be returned when different parts of the series are +to be shown separately, for example, to split scout images from volume images. + + +### ViewportModule + +In OHIF v3, viewports are tied to series of SOP Class UIDs (sopClassUIDs). Each extension provides its own viewport for specific SOP Class UIDs, and you can choose which viewports and SOP Class UIDs your mode can handle in the mode configuration. + +For example, in the longitudinal mode configuration, there are multiple viewports specified along with their associated SOP Class Handler Modules: + + +```js +viewports: [ + { + namespace: '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked', + displaySetsToDisplay: [ '@ohif/extension-default.sopClassHandlerModule.stack'], + }, + { + namespace: '@ohif/extension-cornerstone-dicom-sr.viewportModule.dicom-sr', + displaySetsToDisplay: [ '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr'], + }, + // additional viewports +], +``` + +In this example, there are six viewports specified, each identified by a unique namespace. Each viewport is associated with a specific SOP Class Handler Module through the displaySetsToDisplay property. + +To add a new viewport, you would need to create a new SOP Class Handler Module and a new Viewport Module. The SOP Class Handler Module handles the logic for loading and handling specific SOP Class UIDs, while the Viewport Module defines the rendering and behavior of the viewport. + +In addition to the viewports, the mode configuration should include and register each SOP Class Handler Module that your mode can handle: + + +```js +sopClassHandlers: [ + '@ohif/extension-default.sopClassHandlerModule.stack', + '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr', + '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video', + '@ohif/extension-dicom-pdf.sopClassHandlerModule.dicom-pdf', + '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg', + '@ohif/extension-cornerstone-dicom-rt.sopClassHandlerModule.dicom-rt', +] +``` + +Here, each SOP Class Handler Module is specified with its namespace. + +By configuring the viewports and SOP Class Handler Modules in your mode, you can define how your mode interacts with different types of DICOM data and specify the appropriate rendering and behavior for each SOP Class UID. + +## Metadata Store and Provider + +In OHIF v2, we utilized the `platform/core/classes/metadata` module, which included the classes StudyeMetadata, SeriesMetadata, and InstanceMetadata for storing metadata. However, in OHIF v3, we have replaced these classes with a more versatile metadata store called `DICOMMetadataStore`. This new metadata store is used by each datasource to store the metadata associated with studies, series, and instances. The DICOMMetadataStore API allows you to add study/series/instance metadata to the store and retrieve metadata from it. + +Although we have transitioned to using DICOMMetadataStore as the primary metadata storage mechanism, you still have access to OHIF's MetadataProvider. The MetadataProvider can be found in the same `platform/core/classes` location. The MetadataProvider is internally used to retrieve instance-based metadata based on UIDs, perform queries, and includes some legacy support for older versions of the loading logic. + + +## Build + +We have recently transitioned from bundling all the extensions and the viewer into a single bundle to a more modular approach. In this new approach, the required extensions are dynamically loaded inside a mode as needed. This change brings several advantages, including: + +- Faster build time: Bundling only the necessary extensions reduces the build time, as you no longer need to bundle all extensions upfront. +- Smaller bundle size: By loading extensions on-demand, the initial bundle size is reduced, resulting in faster page load times for users. +- Faster reload for development: During development, the incremental build process allows for faster reloads, improving developer productivity. + +This new approach does not impact the deployment process of the viewer. You can continue to follow our deployment guides, such as the [Build for Production](../deployment/build-for-production.md) guide, to deploy the viewer effectively. + + +### Script tag usage of the OHIF viewer + +With the transition to more advanced visualization, loading, and rendering techniques using WebWorkers, WASM, and WebGL, the script tag usage of the OHIF viewer has been deprecated. However, if you still prefer to use the script tag usage, it is theoretically possible to bundle all the required dependencies and utilize the script tag approach. + +An alternative option for script tag usage is to employ an `iframe`. You can utilize the iframe element to load the OHIF viewer and establish communication with it using the postMessage API. This allows you to exchange messages and data between the parent window and the iframe, enabling interaction and coordination with the OHIF viewer embedded within the iframe. + +Please note that while these alternatives exist, we recommend utilizing modern development practices and incorporating OHIF viewer within your application using a more modular and integrated approach, such as leveraging bundlers, and import statements to ensure better maintainability, extensibility, and compatibility with the OHIF ecosystem. + + +
+ +I use OHIF v2 in an iframe. Is there any impediment for v3? + +No, there is no impediment for using OHIF v3 in an iframe. OHIF v3 is designed to be compatible with iframe usage, allowing you to embed the viewer within other applications or web pages seamlessly. You can still communicate with the OHIF v3 viewer using the postMessage API to exchange information and trigger actions between the parent window and the embedded iframe. + +
+ + +
+ +Does the build support dynamic imports? How can I use it? + +Yes, the build configuration in OHIF v3 supports dynamic imports. Dynamic imports allow you to asynchronously load modules or components on demand, improving performance and reducing the initial bundle size. In fact we are using this method for our viewport components. In general you can: + +``` +import('path/to/module').then((module) => { + // Use the imported module here +}).catch((error) => { + // Handle any error that occurs during dynamic import +}); +``` + +By using dynamic imports, you can selectively load modules or components at runtime when they are needed, enhancing the efficiency and responsiveness of your application. However, note +that these components must be available at BUILD time, and cannot be updated after +build. + +
+ +
+ + +How can I enhance the existing build to consume my own webpack script? + +You can't enhance the existing build to consume your own webpack script as of now. However, you can +modify the webpack.base.js and webpakc.pwa.js files to add your own webpack script/modules if needed. + +
+ +## UI Components + +Migrating to Tailwind CSS, OHIF v3 is now able to have a component-oriented styling approach, speeding up development, ensuring consistent styling, making responsive design easier, and enabling extensibility + +We have gone through extensive re-design of each part of the UI, and we have also added new components to the OHIF viewer. + +
+ +I have a huge complex styles using native CSS, how can I reuse them? + +You can leverage the power of Tailwind CSS (https://TailwindCSS.com/) in OHIF v3 to reuse your existing styles. Tailwind CSS is a utility-first approach, allowing you to create reusable CSS classes by composing utility classes together. You can migrate your existing styles to Tailwind CSS by breaking them down into utility classes and utilizing the extensive set of predefined utilities provided by Tailwind CSS. + +
+ +
+ +How can I change the page color from being purplish to blueish? + +In OHIF v3, you can easily modify the page color by customizing the Tailwind CSS configuration. You can locate the tailwind.config.js file in your project and update the theme section, specifically the colors property, to define your desired color palette. By adjusting the values for the colors, you can change the page color to any shade of blue or other colors according to your preference. + +
+ +
+ +Can I have my own React UI component working in the application? Is there a way to use the current build for it as well? + +Yes, you can integrate your own React UI components seamlessly into the OHIF v3 application. You can even have external +UI dependencies and by creating your own component inside your extensions and importing it into the application, you can +use it as if it was part of the OHIF v3 application. + +
+ +
+ +How can I replace the existing component ui/tooltip? + +You need to write your own component, and inside your mode layout you can replace the existing component with your own. +As of now, for the tooltip component, you need to use the customizationService to customize it; however, the customizationService +requires a registration of the to-be-customized property before you can customize it. Read more about customizationService. + +
+ +
+ +How can I add/consume logos/images/icons? + + +For logos you can use the whiteLabelling inside the configuration. However, if you need a more complex UI for your toolbar +you need to create you own layout. See `getLayoutModule`. + +
+ +## Redux store + +In OHIF v3, we made the decision to move away from the Redux store and adopt a new approach utilizing React context providers and services with a pub/sub pattern. This shift was driven by the need for a more flexible and scalable architecture that better aligns with the plugin and extension system of OHIF. This offers + +- Modularity and Scalability: Context providers and services enable a modular architecture for easy addition and removal of plugins and extensions. +- Reduced Boilerplate: eliminate Redux boilerplate for simpler development. +- Flexible Pub/Sub Pattern: Services provide a pub/sub pattern for inter-component communication. + +
+ + +Now that redux store is gone, how can I access the user information? + + +You can use the `authenticationService` for that purpose. + +
diff --git a/platform/docs/docs/migration-guide/index.md b/platform/docs/docs/migration-guide/index.md new file mode 100644 index 0000000..85d2af8 --- /dev/null +++ b/platform/docs/docs/migration-guide/index.md @@ -0,0 +1,13 @@ +--- +id: index +--- + + +import DocCardList from '@theme/DocCardList'; +import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; + +# Migration Guides + +Based on the version you are migrating from, you can find the migration guide for the latest version of the platform. + + diff --git a/platform/docs/docs/platform/_category_.json b/platform/docs/docs/platform/_category_.json new file mode 100644 index 0000000..842e4ab --- /dev/null +++ b/platform/docs/docs/platform/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Platform", + "position": 6 +} diff --git a/platform/docs/docs/platform/browser-support.md b/platform/docs/docs/platform/browser-support.md new file mode 100644 index 0000000..fa31439 --- /dev/null +++ b/platform/docs/docs/platform/browser-support.md @@ -0,0 +1,48 @@ +--- +sidebar_position: 2 +--- +# Browser Support + +The browsers that we support are specified in the `.browserlistrc` file located +in the `platform/app` project. While we leverage the latest language features +when writing code, we rely on `babel` to _transpile_ our code so that it can run +in the browsers that we support. + +## In Practice + +The OHIF Viewer is capable of _running_ on: + +- IE 11 +- FireFox +- Chrome +- Safari +- Edge + +However, we do not have the resources to adequately test and maintain bug free +functionality across all of these. In order to push web based medical imaging +forward, we focus our development efforts on recent version of modern evergreen +browsers. + +Our support of older browsers equates to our willingness to review PRs for bug +fixes, and target their minimum JS support whenever possible. + +### Polyfills + +> A polyfill, or polyfiller, is a piece of code (or plugin) that provides the +> technology that you, the developer, expect the browser to provide natively. + +An example of a polyfill is that you expect `Array.prototype.filter` to exist, +but for some reason, the browser that's being used has not implemented that +language feature yet. Our earlier transpilation will rectify _syntax_ +discrepancies, but unimplemented features require a "temporary" implementation. +That's where polyfills step in. + +We previously used polyfill io, but due to a security vulnerability in the library, it's necessary to switch to alternative services. + + + + +[core-js]: https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md + diff --git a/platform/docs/docs/platform/environment-variables.md b/platform/docs/docs/platform/environment-variables.md new file mode 100644 index 0000000..1b08d54 --- /dev/null +++ b/platform/docs/docs/platform/environment-variables.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 3 +sidebar_label: Environment Variables +--- +# Environment Variables + +There are a number of environment variables we use at build time to influence the output application's behavior. + +```bash +# Application +NODE_ENV=< production | development > +DEBUG=< true | false > +APP_CONFIG=< relative path to application configuration file > +PUBLIC_URL=< relative path to application root - default / > +VERSION_NUMBER= +BUILD_NUM= +# i18n +USE_LOCIZE= +LOCIZE_PROJECTID= +LOCIZE_API_KEY= +``` + +## Setting Environment Variables + +- `npx cross-env` +- `.env` files +- env variables on build machine, or for terminal session diff --git a/platform/docs/docs/platform/extensions/_category_.json b/platform/docs/docs/platform/extensions/_category_.json new file mode 100644 index 0000000..b7a30d9 --- /dev/null +++ b/platform/docs/docs/platform/extensions/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Extensions", + "position": 9 +} diff --git a/platform/docs/docs/platform/extensions/extension.md b/platform/docs/docs/platform/extensions/extension.md new file mode 100644 index 0000000..148b82a --- /dev/null +++ b/platform/docs/docs/platform/extensions/extension.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 4 +sidebar_label: Extension Manager +--- + +# Extension Manager + +## Overview + +The `ExtensionManager` is a class made available to us via the `@ohif/core` +project (platform/core). Our application instantiates a single instance of it, +and provides a `ServicesManager` and `CommandsManager` along with the +application's configuration through the appConfig key (optional). + +```js +const commandsManager = new CommandsManager(); +const servicesManager = new ServicesManager(); +const extensionManager = new ExtensionManager({ + commandsManager, + servicesManager, + appConfig, +}); +``` + +The `ExtensionManager` only has a few public members: + +- `setActiveDataSource` - Sets the active data source for the application +- `getDataSources` - Returns the registered data sources +- `getActiveDataSource` - Returns the currently active data source +- `getModuleEntry` - Returns the module entry by the give id. + +## Accessing Modules + +We use `getModuleEntry` in our `ViewerLayout` logic to find the panels based on +the provided IDs in the mode's configuration. + +For instance: +`extensionManager.getModuleEntry("@ohif/extension-measurement-tracking.panelModule.seriesList")` +accesses the `seriesList` panel from `panelModule` of the +`@ohif/extension-measurement-tracking` extension. + +```js +const getPanelData = id => { + const entry = extensionManager.getModuleEntry(id); + const content = entry.component; + + return { + iconName: entry.iconName, + iconLabel: entry.iconLabel, + label: entry.label, + name: entry.name, + content, + }; +}; +``` diff --git a/platform/docs/docs/platform/extensions/index.md b/platform/docs/docs/platform/extensions/index.md new file mode 100644 index 0000000..d9c630e --- /dev/null +++ b/platform/docs/docs/platform/extensions/index.md @@ -0,0 +1,345 @@ +--- +sidebar_position: 1 +sidebar_label: Introduction +--- + +# Introduction + +We have re-designed the architecture of the `OHIF-v3` to enable building +applications that are easily extensible to various use cases (modes) that behind +the scene would utilize desired functionalities (extensions) to reach the goal +of the use case. + +Previously, extensions were โ€œadditiveโ€ and could not easily be mixed and matched +within the same viewer for different use cases. Previous `OHIF-v2` architecture +meant that any minor extension alteration usually would require the user to hard +fork. E.g. removing some tools from the toolbar of the cornerstone +extension meant you had to hard fork it, which was frustrating if the +implementation was otherwise the same as master. + +> - Developers should make packages of _reusable_ functionality as extensions, +> and can consume publicly available extensions. +> - Any conceivable radiological workflow or viewer setup will be able to be +> built with the platform through _modes_. + +Practical examples of extensions include: + +- A set of segmentation tools that build on top of the `cornerstone` viewport +- A set of rendering functionalities to volume render the data +- [See our maintained extensions for more examples of what's possible](#maintained-extensions) + +**Diagram showing how extensions are configured and accessed.** + + + +## Extension Skeleton + +An extension is a plain JavaScript object that has `id` and `version` properties, and one or +more [modules](#modules) and/or [lifecycle hooks](#lifecycle-hooks). + +```js +// prettier-ignore +export default { + /** + * Required properties. Should be a unique value across all extensions. + */ + id, + + // Lifecycle + preRegistration() { /* */ }, + onModeEnter() { /* */ }, + onModeExit() { /* */ }, + // Modules + getLayoutTemplateModule() { /* */ }, + getDataSourcesModule() { /* */ }, + getSopClassHandlerModule() { /* */ }, + getPanelModule() { /* */ }, + getViewportModule() { /* */ }, + getCommandsModule() { /* */ }, + getContextModule() { /* */ }, + getToolbarModule() { /* */ }, + getHangingProtocolModule() { /* */ }, + getUtilityModule() { /* */ }, +} +``` + +## OHIF-Maintained Extensions + +A small number of powerful extensions for popular use cases are maintained by +OHIF. They're co-located in the [`OHIF/Viewers`][viewers-repo] repository, in +the top level [`extensions/`][ext-source] directory. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ExtensionDescriptionModules
+ + default + + + Default extension provides default viewer layout, a study/series + browser, and a datasource that maps to a DICOMWeb compliant backend + commandsModule, ContextModule, DataSourceModule, HangingProtocolModule, LayoutTemplateModule, PanelModule, SOPClassHandlerModule, ToolbarModule
+ + cornerstone + + + Provides 2d and 3d rendering functionalities + ViewportModule, CommandsModule, UtilityModule
+ dicom-pdf + + Renders PDFs for a specific SopClassUID. + Viewport, SopClassHandler
+ dicom-video + + Renders DICOM Video files. + Viewport, SopClassHandler
+ cornerstone-dicom-sr + + Maintained extensions for cornerstone and visualization of DICOM Structured Reports + ViewportModule, CommandsModule, SOPClassHandlerModule
+ measurement-tracking + + Tracking measurements in the measurement panel + ContextModule,PanelModule,ViewportModule,CommandsModule
+ +## Registering of Extensions + +`viewer` starts by registering all the extensions specified inside the +`pluginConfig.json`, by default we register all extensions in the repo. + + +```js title=platform/app/pluginConfig.json +// Simplified version of the `pluginConfig.json` file +{ + "extensions": [ + { + "packageName": "@ohif/extension-cornerstone", + "version": "3.4.0" + }, + { + "packageName": "@ohif/extension-measurement-tracking", + "version": "3.4.0" + }, + // ... + ], + "modes": [ + { + "packageName": "@ohif/mode-longitudinal", + "version": "3.4.0" + } + ] +} +``` + +:::note Important +You SHOULD NOT directly register extensions in the `pluginConfig.json` file. +Use the provided `cli` to add/remove/install/uninstall extensions. Read more [here](../../development/ohif-cli.md) +::: + +The final registration and import of the extensions happen inside a non-tracked file `pluginImport.js` (this file is also for internal use only). + +After an extension gets registered within the `viewer`, +each [module](#modules) defined by the extension becomes available to the modes +via the `ExtensionManager` by requesting it via its id. +[Read more about Extension Manager](#extension-manager) + +## Lifecycle Hooks + +Currently, there are three lifecycle hook for extensions: + +[`preRegistration`](./lifecycle/#preRegistration) This hook is called once on +initialization of the entire viewer application, used to initialize the +extensions state, and consume user defined extension configuration. If an +extension defines the [`preRegistration`](./lifecycle/#preRegistration) +lifecycle hook, it is called before any modules are registered in the +`ExtensionManager`. It's most commonly used to wire up extensions to +[services](./../services/index.md) and [commands](./modules/commands.md), and to +bootstrap 3rd party libraries. + +[`onModeEnter`](./lifecycle#onModeEnter): This hook is called whenever a new +mode is entered, or a modeโ€™s data or datasource is switched. This hook can be +used to initialize data. + +[`onModeExit`](./lifecycle#onModeExit): Similarly to onModeEnter, this hook is +called when navigating away from a mode, or before a modeโ€™s data or datasource +is changed. This can be used to cache data for reuse later, but since it +isn't known which mode will be entered next, the state after exiting should be +clean, that is, the same as the state on a clean start. This is called BEFORE +service clean up, and after mode specific onModeExit handling. + +## Modules + +Modules are the meat of extensions, the `blocks` that we have been talking about +a lot. They provide "definitions", components, and filtering/mapping logic that +are then made available to modes and services. + +Each module type has a special purpose, and is consumed by our viewer +differently. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Types + Description
+ + LayoutTemplate + + Control Layout of a route
+ + DataSource + + Control the mapping from DICOM metadata to OHIF-metadata
+ + SOPClassHandler + + Determines how retrieved study data is split into "DisplaySets"
+ + Panel + + Adds left or right hand side panels
+ + Viewport + + Adds a component responsible for rendering a "DisplaySet"
+ + Commands + + Adds named commands, scoped to a context, to the CommandsManager
+ + Toolbar + + Adds buttons or custom components to the toolbar
+ + Context + + Shared state for a workflow or set of extension module definitions
+ + HangingProtocol + + Adds hanging protocol rules
+ + Utility + + Expose utility functions to the outside of extensions
+ +Tbl. Module types +with abridged descriptions and examples. Each module links to a dedicated +documentation page. + +### Contexts + +The `@ohif/app` tracks "active contexts" that extensions can use to scope +their functionality. Some example contexts being: + +- Route: `ROUTE:VIEWER`, `ROUTE:STUDY_LIST` +- Active Viewport: `ACTIVE_VIEWPORT:CORNERSTONE`, `ACTIVE_VIEWPORT:VTK` + +An extension module can use these to say "Only show this Toolbar Button if the +active viewport is a Cornerstone viewport." This helps us use the appropriate UI +and behaviors depending on the current contexts. + +For example, if we have hotkey that "rotates the active viewport", each Viewport +module that supports this behavior can add a command with the same name, scoped +to the appropriate context. When the `command` is fired, the "active contexts" +are used to determine the appropriate implementation of the rotation behavior. + + + + +[viewers-repo]: https://github.com/OHIF/Viewers +[ext-source]: https://github.com/OHIF/Viewers/tree/master/extensions +[module-types]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/extensions/MODULE_TYPES.js + diff --git a/platform/docs/docs/platform/extensions/installation.md b/platform/docs/docs/platform/extensions/installation.md new file mode 100644 index 0000000..2e5fb81 --- /dev/null +++ b/platform/docs/docs/platform/extensions/installation.md @@ -0,0 +1,12 @@ +--- +sidebar_position: 5 +sidebar_label: Installation +--- + +# Extension: Installation + +OHIF-v3 provides the ability to utilize external extensions. + + +You can use ohif `cli` tool to install both local and publicly published +extensions on NPM. You can read more [here](../../development/ohif-cli.md) diff --git a/platform/docs/docs/platform/extensions/lifecycle.md b/platform/docs/docs/platform/extensions/lifecycle.md new file mode 100644 index 0000000..2786888 --- /dev/null +++ b/platform/docs/docs/platform/extensions/lifecycle.md @@ -0,0 +1,128 @@ +--- +sidebar_position: 3 +sidebar_label: Lifecycle Hooks +--- + +# Extensions: Lifecycle Hooks + +## Overview + +Extensions can implement specific lifecycle methods. + +- preRegistration +- onModeEnter +- onModeExit + +## preRegistration + +If an extension defines the `preRegistration` lifecycle hook, it is called +before any modules are registered in the `ExtensionManager`. This hook is an +`async` function that can be used to perform: + +- initialize 3rd party libraries +- register event listeners +- add or call services +- add or call commands + +The `preRegistration` hook receives an object containing the +`ExtensionManager`'s associated `ServicesManager`, `CommandsManager`, and any +`configuration` that was provided with the extension at time of registration. + +Example `preRegistration` implementation that register a new service and make it +available in the app. We will talk more in details for creating a new service +for `OHIF-v3`. + +```js +// new service inside new extension +import MyNewService from './MyNewService'; + +export default function MyNewServiceWithServices(servicesManager) { + return { + name: 'MyNewService', + create: ({ configuration = {} }) => { + return new MyNewService(servicesManager); + }, + }; +} +``` + +and + +```js +import MyNewService from './MyNewService' + +export default { + id, + + /** + * @param {object} params + * @param {object} params.configuration + * @param {ServicesManager} params.servicesManager + * @param {CommandsManager} params.commandsManager + * @returns void + */ + async preRegistration({ servicesManager, commandsManager, configuration }) { + console.log('Wiring up important stuff.'); + + window.importantStuff = () => { + console.log(configuration); + }; + + console.log('Important stuff has been wired.'); + window.importantStuff(); + + // Registering new services + servicesManager.registerService(MyNewService(servicesManager)); + }, + }, +}; +``` + +## onModeEnter + +If an extension defines the `onModeEnter` lifecycle hook, it is called when a +new mode is enters, or a mode's data or datasource is switched. + +For instance, in DICOM structured report extension (`dicom-sr`), we are using +`onModeEnter` to re-create the displaySets after a new mode is entered. + +_Example `onModeEnter` hook implementation_ + +```js +export default { + id: '@ohif/extension-cornerstone-dicom-sr', + + onModeEnter({ servicesManager }) { + const { DisplaySetService } = servicesManager.services; + const displaySetCache = DisplaySetService.getDisplaySetCache(); + + const srDisplaySets = displaySetCache.filter( + ds => ds.SOPClassHandlerId === SOPClassHandlerId + ); + + srDisplaySets.forEach(ds => { + // New mode route, allow SRs to be hydrated again + ds.isHydrated = false; + }); + }, +}; +``` + +## onModeExit + +If an extension defines the `onModeExit` lifecycle hook, it is called when +navigating away from a mode. This hook can be used to clean up data tasks such +as unregistering services, removing annotations that do not need to be +persisted. + +_Example `onModeExit` hook implementation_ + +```js +export default { + id: 'myExampleExtension', + + onModeExit({ servicesManager, commandsManager }) { + myCacheService.purge(); + }, +}; +``` diff --git a/platform/docs/docs/platform/extensions/modules/_category_.json b/platform/docs/docs/platform/extensions/modules/_category_.json new file mode 100644 index 0000000..c131ccd --- /dev/null +++ b/platform/docs/docs/platform/extensions/modules/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Modules", + "position": 3 +} diff --git a/platform/docs/docs/platform/extensions/modules/commands.md b/platform/docs/docs/platform/extensions/modules/commands.md new file mode 100644 index 0000000..b37202b --- /dev/null +++ b/platform/docs/docs/platform/extensions/modules/commands.md @@ -0,0 +1,121 @@ +--- +sidebar_position: 2 +sidebar_label: Commands +--- +# Module: Commands + + +## Overview +`CommandsModule` includes list of arbitrary functions. These may activate tools, communicate with a server, open a modal, etc. +The significant difference between `OHIF-v3` and `OHIF-v2` is that in `v3` a `mode` defines +its toolbar, and which commands each tool call is inside in its toolDefinition + +An extension can register a Commands Module by defining a `getCommandsModule` +method. The Commands Module allows us to register one or more commands scoped to +specific [contexts](./../index.md#contexts). Commands have several unique +characteristics that make them tremendously powerful: + +- Multiple implementations for the same command can be defined +- Only the correct command's implementation will be run, dependent on the + application's "context" +- Commands are used by hotkeys, toolbar buttons and render settings + +Here is a simple example commands module: + +```js +const getCommandsModule = () => ({ + definitions: { + exampleActionDef: { + commandFn: ({ param1 }) => { + console.log(`param1's value is: ${param1}`); + }, + // storeContexts: ['viewports'], + options: { param1: 'param1' }, + context: 'VIEWER', // optional + }, + }, + defaultContext: 'ACTIVE_VIEWPORT::DICOMSR', +}); +``` + + +Each definition returned by the Commands Module is registered to the +`ExtensionManager`'s `CommandsManager`. + +> `storeContexts` has been removed in `OHIF-v3` and now modules have access to all commands and services. This change enables support for user-registered services. + +## Command Definitions + +The command definition consists of a named command (`exampleActionDef` below) and a +`commandFn`. The command name is used to call the command, and the `commandFn` +is the "command" that is actioned. + +```js +exampleActionDef: { + commandFn: ({ param1, options }) => { }, + options: { param1: 'measurement' }, + context: 'DEFAULT', +} +``` + +| Property | Type | Description | +| --------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| `commandFn` | func | The function to call when command is run. Receives `options` and `storeContexts`. | +| `options` | object | (optional) Arguments to pass at the time of calling to the `commandFn` | +| `context` | string[] or string | (optional) Overrides the `defaultContext`. Let's us know if command is currently "available" to be run. | + +## Command Behavior + + + +**If there are multiple valid commands for the application's active contexts** + +- What happens: all commands are run +- When to use: A `clearData` command that cleans up state for multiple + extensions + +**If no commands are valid for the application's active contexts** + +- What happens: a warning is printed to the console +- When to use: a `hotkey` (like "invert") that doesn't make sense for the + current viewport (PDF or HTML) + +## `CommandsManager` Public API + +If you would like to run a command in the consuming app or an extension, you can +use `CommandsManager.runCommand(commandName, options = {}, contextName)` + + +```js +// Returns all commands for a given context +commandsManager.getContext('string'); + +// Run a command, it will run all the `speak` commands in all contexts +commandsManager.runCommand('speak', { command: 'hello' }); + +// Run command, from Default context +commandsManager.runCommand('speak', { command: 'hello' }, ['DEFAULT']); +``` + +The `ExtensionManager` handles registering commands and creating contexts, so +most consumer's won't need these methods. If you find yourself using these, ask +yourself "why can't I register these commands via an extension?" + +```js +// Used by the `ExtensionManager` to register new commands +commandsManager.registerCommand('context', 'name', commandDefinition); + +// Creates a new context; clears the context if it already exists +commandsManager.createContext('string'); +``` + +### Contexts + +It is up to the consuming application to define what contexts are possible, and +which ones are currently active. As extensions depend heavily on these, we will +likely publish guidance around creating contexts, and ways to override extension +defined contexts in the near future. If you would like to discuss potential +changes to how contexts work, please don't hesitate to create a new GitHub +issue. + +[Some additional information on Contexts can be found here.](./../index.md#contexts) diff --git a/platform/docs/docs/platform/extensions/modules/contextModule.md b/platform/docs/docs/platform/extensions/modules/contextModule.md new file mode 100644 index 0000000..35de381 --- /dev/null +++ b/platform/docs/docs/platform/extensions/modules/contextModule.md @@ -0,0 +1,30 @@ +--- +sidebar_position: 9 +sidebar_label: Context +--- +# Module: Context + +## Overview +This new module type allows you to connect components via a shared context. You can create a context that two components, e.g. a viewport and a panel can use to synchronize and communicate. An extensive example of this can be seen in the longitudinal modeโ€™s custom extensions. + + + +```jsx +const ExampleContext = React.createContext(); + +function ExampleContextProvider({ children }) { + return ( + + {children} + + ); +} + +const getContextModule = () => [ + { + name: 'ExampleContext', + context: ExampleContext, + provider: ExampleContextProvider, + }, +]; +``` diff --git a/platform/docs/docs/platform/extensions/modules/data-source.md b/platform/docs/docs/platform/extensions/modules/data-source.md new file mode 100644 index 0000000..5f682ff --- /dev/null +++ b/platform/docs/docs/platform/extensions/modules/data-source.md @@ -0,0 +1,212 @@ +--- +sidebar_position: 3 +sidebar_label: Data Source +--- + +# Module: Data Source + +## Overview + +We have built couple of methods for fetching and mapping data into OHIFโ€™s native +format, which we call DataSources, and have provided one implementation of this +standard. + +You can make another datasource implementation which communicates to your +backend and maps to OHIFโ€™s native format, then use any existing mode on your +platform. Your data doesnโ€™t even need to be DICOM if you can map some +proprietary data to the correct format. + +The DataSource is also a place to add easy helper methods that platform-specific +extensions can call in order to interact with the backend, meaning proprietary +data interactions can be wrapped in extensions. + +```js +const getDataSourcesModule = () => [ + { + name: 'exampleDataSource', + type: 'webApi', // 'webApi' | 'local' | 'other' + createDataSource: dataSourceConfig => { + return IWebApiDataSource.create(/* */); + }, + }, +]; +``` + +Default extension provides two main data sources that are commonly used: +`dicomweb` and `dicomjson` + +```js +import { createDicomWebApi } from './DicomWebDataSource/index'; +import { createDicomJSONApi } from './DicomJSONDataSource/index'; + +function getDataSourcesModule() { + return [ + { + name: 'dicomweb', + type: 'webApi', + createDataSource: createDicomWebApi, + }, + { + name: 'dicomjson', + type: 'jsonApi', + createDataSource: createDicomJSONApi, + }, + ]; +} +``` + +## Custom DataSource + +You can add your custom datasource by creating the implementation using +`IWebApiDataSource.create` from `@ohif/core`. This factory function creates a +new "Web API" data source that fetches data over HTTP. + +```js title="platform/core/src/DataSources/IWebApiDataSource.js" +function create({ + initialize, + query, + retrieve, + store, + reject, + parseRouteParams, + deleteStudyMetadataPromise, + getImageIdsForDisplaySet, + getImageIdsForInstance, +}) { + /* */ +} +``` + +You can take a look at `dicomweb` data source implementation to get an idea +`extensions/default/src/DicomWebDataSource/index.js` but here here are some +important api endpoints that you need to implement: + +- `initialize`: This method is called when the data source is first created in the mode.tsx, it is used to initialize the data source and set the configuration. For instance, `dicomwebDatasource` uses this method to grab the StudyInstanceUID from the URL and set it as the active study, as opposed to `dicomJSONDatasource` which uses url in the browser to fetch the data and store it in a cache +- `query.studies.search`: This is used in the study panel on the left to fetch the prior studies for the same MRN which is then used to display on the `All` tab. it is also used in the Worklist to show all the studies from the server. +- `query.series.search`: This is used to fetch the series information for a given study that is expanded in the Worklist. +- `retrieve.bulkDataURI`: used to render RTSTUCTURESET in the viewport. It is an object that contains `enabled` as property and other options that are specific to the data source. +- `retrieve.series.metadata`: It is a crucial end point that is used to fetch series level metadata which for hanging displaySets and displaySet creation. +- `store.dicom`: If you don't need store functionality, you can skip this method. This is used to store the data in the backend. + +## Static WADO Client + +If the configuration for the data source has the value staticWado set, then it +is assumed that queries for the studies return a super-set of the studies, as it +is assumed to be returning a static list. The StaticWadoClient performs the +search functionality manually, by interpreting the query parameters and then +applying them to the returned response. This functionality may be useful for +other types of DICOMweb back ends, where they are capable of performing queries, +but don't allow for querying certain types of fields. However, that only works +as long as the size of the studies list isn't too large that client side +selection isn't too expensive. + +## DicomMetadataStore + +In `OHIF-v3` we have a central location for the metadata of studies, and they are +located in `DicomMetadataStore`. Your custom datasource can communicate with +`DicomMetadataStore` to store, and fetch Study/Series/Instance metadata. We will +learn more about `DicomMetadataStore` in services. + +## Adding a Data Source Outside a Module + +A data source can be added outside a module via `ExtensionManager.addDataSource`. +The following snippet of code demonstrates how `addDataSource` can be used to add +a new DICOMWeb data source for the Google Cloud Healthcare API and set it as the +active data source. + +```js +extensionManager.addDataSource({ + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'google', + configuration: { + friendlyName: 'dcmjs DICOMWeb Server', + name: 'GCP', + wadoUriRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + qidoRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + wadoRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + qidoSupportsIncludeField: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: false, + dicomUploadEnabled: true, + omitQuotationForMultipartRequest: true, + }, + {activate:true} +}); +``` + +## Updating a Data Source's Configuration + +An existing data source can have its configuration updated using the +`ExtensionManager.updateDataSourceConfiguration` method. The following snippet of +code demonstrates how `updateDataSourceConfiguration` can be use to update the +configuration of an existing DICOMWeb data source (named `dicomweb`) with the +configuration for a Google Cloud Healthcare API data source. + +```js +extensionManager.updateDataSourceConfiguration( "dicomweb", + { + name: 'GCP', + wadoUriRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + qidoRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + wadoRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + qidoSupportsIncludeField: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: false, + dicomUploadEnabled: true, + omitQuotationForMultipartRequest: true, + }, +); +``` + +## Merge Data Source +The built-in merge data source is a useful tool for combining results from multiple data sources. +Currently, this data source only supports merging at the series level. This means that series from data source 'A' +and series from data source 'B' will be retrieved under the same study. If the same series exists in both data sources, +the first series arrived is the one that gets stored, and any other conflicting series will be ignored. + +The merge data source is particularly useful when dealing with derived data that is generated and stored in different servers. +For example, it can be used to retrieve annotation series from one data source and input data (images) from another data source. + +A default data source can be defined as shown below. This allows defining which of the servers should be the +fallback server in case something goes wrong. + +Configuration Example: +```js +window.config = { + ... + dataSources: [ + { + sourceName: 'merge', + namespace: '@ohif/extension-default.dataSourcesModule.merge', + configuration: { + name: 'merge', + friendlyName: 'Merge dicomweb-1 and dicomweb-2 data at the series level', + seriesMerge: { + dataSourceNames: ['dicomweb-1', 'dicomweb-2'], + defaultDataSourceName: 'dicomweb-1' + }, + }, + }, + { + sourceName: 'dicomweb-1', + ... + }, + { + sourceName: 'dicomweb-2', + ... + }, + ], +}; +``` diff --git a/platform/docs/docs/platform/extensions/modules/hpModule.md b/platform/docs/docs/platform/extensions/modules/hpModule.md new file mode 100644 index 0000000..8920ccc --- /dev/null +++ b/platform/docs/docs/platform/extensions/modules/hpModule.md @@ -0,0 +1,624 @@ +--- +sidebar_position: 8 +sidebar_label: Hanging Protocol +--- +# Module: Hanging Protocol + +## Overview + +[Hanging protocols](http://dicom.nema.org/dicom/Conf-2005/Day-2_Selected_Papers/B305_Morgan_HangProto_v1.pdf) are an essential part of any radiology viewer. +OHIF uses Hanging Protocols to handle the arrangement of the images in the viewport. In +short, the registered protocols will get matched with the DisplaySets that are +available. Each protocol gets a score, and they are ranked. The +winning protocol (highest score) gets applied and its settings run for the viewports +to be arranged. + + +In `OHIF-v3` hanging protocols you can: + +- Define what layout the viewport should starts with (e.g., 2x2 layout) +- Specify the type of the viewport and its orientation (e.g., stack, volume with Sagittal view) +- Define which displaySets gets displayed in which viewport of the layout (e.g,. displaySet that has modality of 'CT' and 'SeriesDescription' of 'Coronary Arteries' gets displayed in the first viewport of the layout) +- Apply certain initial viewport settings (e.g., inverting the contrast, jumping to a specific slice, etc.) +- Add specific synchronization rules for the viewports (e.g., synchronize the zoom of the viewports of the index 1, 2 OR synchronize the VOI of the viewports of the index 2, 3) + + +Using `hangingProtocolModule` you can provide/register the protocols for OHIF to +utilize. + +Here is an example protocol which if used will hang a 1x3 layout with the first viewport showing a CT image, the second viewport showing a PT image and the third viewport showing their fusion, all in Sagittal orientations to achieve a view of + + +![](../../../assets/img/hangingProtocolExample.png) + + +```js +const oneByThreeProtocol = { + id: 'oneByThreeProtocol', + locked: true, + name: 'Default', + createdDate: '2021-02-23T19:22:08.894Z', + modifiedDate: '2022-10-04T19:22:08.894Z', + availableTo: {}, + editableBy: {}, + imageLoadStrategy: 'interleaveTopToBottom', + protocolMatchingRules: [ + { + attribute: 'ModalitiesInStudy', + constraint: { + contains: ['CT', 'PT'], + }, + }, + ], + displaySetSelectors: { + ctDisplaySet: { + seriesMatchingRules: [ + { + weight: 1, + attribute: 'Modality', + constraint: { + equals: { + value: 'CT', + }, + }, + required: true, + }, + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + ], + }, + ptDisplaySet: { + seriesMatchingRules: [ + { + attribute: 'Modality', + constraint: { + equals: 'PT', + }, + required: true, + }, + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + { + attribute: 'SeriesDescription', + constraint: { + contains: 'Corrected', + }, + }, + ], + }, + }, + stages: [ + { + id: 'hYbmMy3b7pz7GLiaT', + name: 'default', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 3, + }, + }, + viewports: [ + { + viewportOptions: { + viewportId: 'ctAXIAL', + viewportType: 'volume', + orientation: 'sagittal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [ + { + type: 'voi', + id: 'ctWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'ptAXIAL', + viewportType: 'volume', + orientation: 'sagittal', + initialImageOptions: { + preset: 'middle', + }, + }, + displaySets: [ + { + id: 'ptDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'fusionSAGITTAL', + viewportType: 'volume', + orientation: 'sagittal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [ + { + type: 'voi', + id: 'ctWLSync', + source: false, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + { + options: { + colormap: 'hsv', + voi: { + windowWidth: 5, + windowCenter: 2.5, + }, + }, + id: 'ptDisplaySet', + }, + ], + }, + ], + createdDate: '2021-02-23T18:32:42.850Z', + }, + ], + numberOfPriorsReferenced: -1, +}; + + +function getHangingProtocolModule() { + return [ + { + id: 'oneByThreeProtocol', + protocol: oneByThreeProtocol, + }, + ]; +} +``` + + +## Skeleton of a Protocol + +The skeleton of a hanging protocol is as follows: + + +### Id +unique identifier for the protocol, this id can be used inside mode configuration +to specify which protocol should be used for a specific mode. A mode can +request a protocol by its id (which makes OHIF to apply the protocol without +matching), or provides an array of ids which will +make the ProtocolEngine to choose the best matching protocol (based on +protocolMatching rules, which is next section). + +### imageLoadStrategy +The image load strategy specifies a function (by name) containing logic to re-order +the image load requests. This allows loading images viewed earlier to be done +sooner than those loaded later. The available strategies are: + +* interleaveTopToBottom to start at the top and work towards the bottom, for all series being loaded +* interleaveCenter is like top to bottom but starts at the center +* nth is a strategy that loads every nth instance, starting with the center +and end points, and then filling in progressively all along the image. This results in partial +image view very quickly. + +### protocolMatchingRules +A list of criteria for the protocol along with the provided points for ranking. + + - `weight`: weight for the matching rule. Eventually, all the registered + protocols get sorted based on the weights, and the winning protocol gets + applied to the viewer. + - `attribute`: tag that needs to be matched against. This can be either + Study-level metadata or a custom attribute such as "StudyInstanceUID", + "StudyDescription", "ModalitiesInStudy", "NumberOfStudyRelatedSeries", "NumberOfSeriesRelatedInstances" + In addition to these tags, you can also use a custom attribute that you have registered before. + We will learn more about this later. + - `from`: Indicates the source of the attribute. This allows getting values + from other objects such as the `prior` instance object instead of from the + current one. + + + + - `constraint`: the constraint that needs to be satisfied for the attribute. It accepts a `validator` which can be + [`equals`, `doesNotEqual`, `contains`, `doesNotContain`, `startsWith`, `endsWidth`] + + | Rule | Single Value | Array Value | Example | +|------|--------------|-------------|---------| +| equals | === | All members are === in same order | value = ['abc', 'def', 'GHI']
testValue = 'abc' (Fail)
= ['abc'] (Fail)
= ['abc', 'def', 'GHI'] (Valid)
= ['abc', 'GHI', 'def'] (Fail)
= ['abc', 'def'] (Fail)

value = 'Attenuation Corrected'
testValue = 'Attenuation Corrected' (Valid)
= 'Attenuation' (Fail)

value = ['Attenuation Corrected']
testValue = ['Attenuation Corrected'] (Valid)
= 'Attenuation Corrected' (Valid)
= 'Attenuation' (Fail) | +| doesNotEqual | !== | Any member is !== for the array, either in value, order, or length | value = ['abc', 'def', 'GHI']
testValue = 'abc' (Valid)
= ['abc'] (Valid)
= ['abc', 'def', 'GHI'] (Fail)
= ['abc', 'GHI', 'def'] (Valid)
= ['abc', 'def'] (Valid)

value = 'Attenuation Corrected'
testValue = 'Attenuation Corrected' (Fail)
= 'Attenuation' (Valid)

value = ['Attenuation Corrected']
testValue = ['Attenuation Corrected'] (Fail)
= 'Attenuation Corrected' (Fail)
= 'Attenuation' (Fail) | +| includes | Not allowed | Value is equal to one of the values of the array | value = ['abc', 'def', 'GHI']
testValue = ['abc'] (Valid)
= 'abc' (Fail)
= ['abc'] (Fail)
= 'dog' (Fail)
= ['att', 'abc'] (Valid)
= ['abc', 'def', 'dog'] (Valid)
= ['cat', 'dog'] (Fail)

value = 'Attenuation Corrected'
testValue = ['Attenuation Corrected', 'Corrected'] (Valid)
= ['Attenuation', 'Corrected'] (Fail)

value = ['Attenuation Corrected']
testValue = 'Attenuation Corrected' (Fail)
= ['Attenuation Corrected', 'Corrected'] (Valid)
= ['Attenuation', 'Corrected'] (Fail) | +| doesNotInclude | Not allowed | Value is not in one of the values of the array | value = ['abc', 'def', 'GHI']
testValue = 'Corr' (Valid)
= 'abc' (Fail)
= ['att', 'cor'] (Valid)
= ['abc', 'def', 'dog'] (Fail)

value = 'Attenuation Corrected'
testValue = ['Attenuation Corrected', 'Corrected'] (Fail)
= ['Attenuation', 'Corrected'] (Valid)

value = ['Attenuation Corrected']
testValue = 'Attenuation' (Fail)
= ['Attenuation Corrected', 'Corrected'] (Fail)
= ['Attenuation', 'Corrected'] (Valid) | +| containsI | String containment (case insensitive) | String containment (case insensitive) is OK for one of the rule values | value = 'Attenuation Corrected'
testValue = 'Corr' (Valid)
= 'corr' (Valid)
= ['att', 'cor'] (Valid)
= ['Att', 'Wall'] (Valid)
= ['cat', 'dog'] (Fail)

value = ['abc', 'def', 'GHI']
testValue = 'def' (Valid)
= 'dog' (Fail)
= ['gh', 'de'] (Valid)
= ['cat', 'dog'] (Fail) | +| contains | String containment (case sensitive) | String containment (case sensitive) is OK for one of the rule values | value = 'Attenuation Corrected'
testValue = 'Corr' (Valid)
= 'corr' (Fail)
= ['att', 'cor'] (Fail)
= ['Att', 'Wall'] (Valid)
= ['cat', 'dog'] (Fail)

value = ['abc', 'def', 'GHI']
testValue = 'def' (Valid)
= 'dog' (Fail)
= ['cat', 'de'] (Valid)
= ['cat', 'dog'] (Fail) | +| doesNotContain | String containment is false | String containment is false for all values of the array | value = 'Attenuation Corrected'
testValue = 'Corr' (Fail)
= 'corr' (Valid)
= ['att', 'cor'] (Valid)
= ['Att', 'Wall'] (Fail)
= ['cat', 'dog'] (Valid)

value = ['abc', 'def', 'GHI']
testValue = 'def' (Fail)
= 'dog' (Valid)
= ['cat', 'de'] (Fail)
= ['cat', 'dog'] (Valid) | +| doesNotContainI | String containment is false (case insensitive) | String containment (case insensitive) is false for all values of the array | value = 'Attenuation Corrected'
testValue = 'Corr' (Fail)
= 'corr' (Fail)
= ['att', 'cor'] (Fail)
= ['Att', 'Wall'] (Fail)
= ['cat', 'dog'] (Valid)

value = ['abc', 'def', 'GHI']
testValue = 'DEF' (Fail)
= 'dog' (Valid)
= ['cat', 'gh'] (Fail)
= ['cat', 'dog'] (Valid) | +| startsWith | Value begins with characters | Starts with one of the values of the array | value = 'Attenuation Corrected'
testValue = 'Corr' (Fail)
= 'Att' (Fail)
= ['cat', 'dog', 'Att'] (Valid)
= ['cat', 'dog'] (Fail)

value = ['abc', 'def', 'GHI']
testValue = 'deg' (Valid)
= ['cat', 'GH'] (Valid)
= ['cat', 'gh'] (Fail)
= ['cat', 'dog'] (Fail) | +| endsWith | Value ends with characters | ends with one of the value of the array | value = 'Attenuation Corrected'
testValue = 'TED' (Fail)
= 'ted' (Valid)
= ['cat', 'dog', 'ted'] (Valid)
= ['cat', 'dog'] (Fail)

value = ['abc', 'def', 'GHI']
testValue = 'deg' (Valid)
= ['cat', 'HI'] (Valid)
= ['cat', 'hi'] (Fail)
= ['cat', 'dog'] (Fail) | +| greaterThan | value is >= to rule | Not applicable | value = 30
testValue = 20 (Valid)
= 40 (Fail) | +| lessThan | value is <= to rule | Not applicable | value = 30
testValue = 40 (Valid)
= 20 (Fail) | +| range | Not applicable | 2 value requested (min and max) | value = 50
testValue = [10,60] (Valid)
= [60, 10] (Valid)
= [0, 10] (Fail)
= [70, 80] (Fail)
= 45 (Fail)
= [45] (Fail) | +| notNull | Not Applicable | Not Applicable | No value | | + A sample of the matching rule is above which matches against the study description to be PETCT + + ```js + { + id: 'wauZK2QNEfDPwcAQo', + weight: 1, + attribute: 'StudyDescription', + constraint: { + contains: { + value: 'PETCT', + }, + }, + required: false, + }, + ``` + +### `from` attribute (optional) +The `from` attribute allows you to retrieve the attribute to test from another object, such as the previous study, the overall list of studies, or another provided value from a module. + +The values provided by OHIF which you can use are: + +- `activeStudy`: to use the metadata of the active study to match +- `studies`: to use the metadata of the list of studies (all studies) to match +- `allDisplaySets`: all available display sets +- `displaySets`: if the selector has matched a study, these are the display sets for that study +- `prior`: the metadata of the first study in the list of studies that is not the active study +- `options`: during matching, we also provide an options object with the following information that you can use as the `from` value: + - `studyInstanceUIDsIndex`: the index of the study in the list of studies + - `instance`: the metadata of the instance being matched, which is exactly the displaySet.instance metadata. + + +### displaySetSelectors (mandatory) +Defines the display sets that the protocol will use for arrangement. + +```js + displaySetSelectors: { + ctDisplaySet: { + seriesMatchingRules: [ + { + weight: 1, + attribute: 'Modality', + constraint: { + equals: { + value: 'CT', + }, + }, + required: true, + }, + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + ], + }, + ptDisplaySet: { + seriesMatchingRules: [ + { + attribute: 'Modality', + constraint: { + equals: 'PT', + }, + required: true, + }, + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + { + attribute: 'SeriesDescription', + constraint: { + contains: 'Corrected', + }, + }, + ], + }, + } +``` + +As you see above we have specified two displaysets: 1) ctDisplaySet , 2) ptDisplaySet +The ctDisplaySet will match against all the series that are CT and reconstructable +The ptDisplaySet will match against all the series that are PT and reconstructable. + +As you see each selector is composed of an `id` as the key and a set of `seriesMatchingRules` (displaySetMatchingRules) which gives score to the displaySet +based on the matching rules. The displaySet with the highest score will be used for the `id`. + +### stages +Each protocol can define one or more stages. Each stage defines a certain layout and viewport rules. Therefore, the `stages` property is an array of objects, each object being one stage. + +### viewportStructure +Defines the layout of the viewer. You can define the number of `rows` and `columns`. There should be `rows * columns` number of +viewport configuration in the `viewports` property. Note that order of viewports are rows first then columns. + +```js +viewportStructure: { + type: 'grid', + properties: { + rows: 1, + columns: 2, + viewportOptions: [], + }, +}, +``` + +In addition to the equal viewport sizes, you can define viewports to span multiple rows or columns. + +```js +viewportStructure: { + type: 'grid', + properties: { + rows: 1, + columns: 2, + viewportOptions: [ + { + x: 0, + y: 0, + width: 1 / 4, + height: 1, + }, + { + x: 1 / 4, + y: 0, + width: 3 / 4, + height: 1, + }, + ], + }, +}, + +``` + + +### viewports +This field includes the viewports that will get hung on the viewer. + +```js +viewports: [ + { + viewportOptions: { + viewportId: 'ctAXIAL', + viewportType: 'volume', + orientation: 'sagittal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [ + { + type: 'voi', + id: 'ctWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + ], + }, + // the rest +], +``` + +As you can see in the hanging protocol we defined three viewports (but only showing one of them right above). Each viewport has two properties: + +1. `viewportOptions`: defines the viewport properties such as + - `viewportId`: unique identifier for the viewport (optional) + - `viewportType`: type of the viewport (optional - options: stack, volume - default is stack + - `background`: background color of the viewport (optional) + - `orientation`: orientation of the viewport (optional - if not defined for volume -> acquisition axis) + - `toolGroupId`: tool group that will be used for the viewport (optional) + - `initialImageOptions`: initial image options (optional - can be specific imageIndex number or preset (first, middle, last)) + - `syncGroups`: sync groups for the viewport (optional) + -The `displayArea` parameter refers to the designated area within the viewport where a specific portion of the image can be displayed. This parameter is optional and allows you to choose the location of the image within the viewport. For example, in mammography images, you can display the left breast on the left side of the viewport and the right breast on the right side, with the chest wall positioned in the middle. To understand how to define the display area, you can refer to the live example provided by CornerstoneJS [here](https://www.cornerstonejs.org/live-examples/programaticpanzoom). + + +2. `displaySets`: defines the display sets that are displayed on a viewport. It is an array of objects, each object being one display set. + - `id`: id of the display set (required) + - `options` (optional): options for the display set + - voi: windowing options for the display set (optional: windowWidth, windowCenter) + - voiInverted: whether the VOI is inverted or not (optional) + - colormap: colormap for the display set (optional, it is an object with `{ name }` and optional extra `opacity` property) + - displayPreset: display preset for the display set (optional, used for 3D volume rendering. e.g., 'CT-Bone') + + +### Custom attribute +For any matching rules you can specify a custom attribute too. For instance, +if you have a timepoint attribute in for each of your studies, you can use that in the matching rules. + +```js +{ + id: 'wauZK2QNEfDPwcAQo', + weight: 1, + attribute: 'timepoint', + constraint: { + equals: { + value: 'baseline', + }, + }, + required: false, +}, +``` + +and then you need to register a callback in the HangingProtocolService to get the value for the attribute. + +```js +HangingProtocolService.addCustomAttribute( + 'timepoint', // attributeId + 'addCustomAttribute', // attributeName + study => { // callback that returns the value for the attribute + const timePoint = fetchFromMyCustomBackend(study.studyInstanceUid); + return timePoint; + } +); +``` + + +## Matching on Prior Study with UID + +Often it is desired to match a new study to a prior study (e.g., follow up on +a surgery). Since the hanging protocols run on displaySets we need to have a +way to let OHIF knows that it needs to load the prior study as well. This can +be done by specifying both StudyInstanceUIDs in the URL. The additional studies +are then accessible to the hanging protocol. Below we are +running OHIF with two studies, and a comparison hanging protocol available by +default. + +```bash +https://viewer-dev.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095258.1&hangingprotocolId=@ohif/hpCompare +``` + +The `&hangingProtocolId` option forces the specific hanging protocol to be +applied, but the mode can also add the hanging protocols to the default set, +and then the best matching hanging protocol will be applied by the run method. + +To match any other studies, it is required to enable the prior matching rules +capability using: + +```javascript + // Indicate number of priors used - 0 means any number, -1 means none. + numberOfPriorsReferenced: 1, +``` + +The matching rule that allows the hanging protocol to be runnable is: + +```javascript + protocolMatchingRules: [ + { + id: 'Two Studies', + weight: 1000, + // This will generate 1.3.6.1.4.1.25403.345050719074.3824.20170125095722.1 + // since that is study instance UID in the prior from instance. + attribute: 'StudyInstanceUID', + // The 'from' attribute says where to get the 'attribute' value from. In this case + // prior means the second study in the study list. + from: 'prior', + required: true, + constraint: { + notNull: true, + }, + }, + ], +``` + +The display set selector selecting the specific study to display is included +in the studyMatchingRules. Note that this rule will cause ONLY the second study +to be matched, so it won't attempt to match anything in other studies. +Additional series level criteria, such as modality rules must be included at the +`seriesMatchingRules`. + +```javascript + studyMatchingRules: [ + { + // The priorInstance is a study counter that indicates what position this study is in + // and the value comes from the options parameter. + attribute: 'studyInstanceUIDsIndex', + from: 'options', + required: true, + constraint: { + equals: { value: 1 }, + }, + }, + ], +``` + + +## Callbacks + + +Hanging protocols in `OHIF-v3` provide the flexibility to define various callbacks that allow you to customize the behavior of your viewer when specific events occur during protocol execution. These callbacks are defined in the `ProtocolNotifications` type and can be added to your hanging protocol configuration. + +Each callback is an array of commands or actions that are executed when the event occurs. + +```js +[ + { + commandName: 'showDownloadViewportModal', + commandOptions: {} + } +] +``` + + +Here, we'll explain the available callbacks and their purposes: + +### `onProtocolExit` + +The `onProtocolExit` callback is executed after the protocol is exited and the new one is applied. This callback is useful for performing actions or executing commands when switching between hanging protocols. + +### `onProtocolEnter` + +The `onProtocolEnter` callback is executed after the protocol is entered and applied. You can use this callback to define actions or commands that should run when entering a specific hanging protocol. + +### `onLayoutChange` + +The `onLayoutChange` callback is executed before the layout change is started. You can use it to apply a specific hanging protocol based on the current layout or other criteria. + +### `onViewportDataInitialized` + +The `onViewportDataInitialized` callback is executed after the initial viewport grid data is set and all viewport data includes a designated display set. This callback runs during the initial layout setup for each stage. You can use it to perform actions or apply settings to the viewports at the start. + +Here is an example of how you can add these callbacks to your hanging protocol configuration: + +```javascript +const protocol = { + id: 'myProtocol', + name: 'My Protocol', + // rest of the protocol configuration + callbacks: { + onProtocolExit: [ + // Array of commands or actions to execute on protocol exit + ], + onProtocolEnter: [ + // Array of commands or actions to execute on protocol enter + ], + onLayoutChange: [ + // Array of commands or actions to execute on layout change + ], + onViewportDataInitialized: [ + // Array of commands or actions to execute on viewport data initialization + ], + }, + // protocolMatchingRules + // the rest +}; diff --git a/platform/docs/docs/platform/extensions/modules/layout-template.md b/platform/docs/docs/platform/extensions/modules/layout-template.md new file mode 100644 index 0000000..8214449 --- /dev/null +++ b/platform/docs/docs/platform/extensions/modules/layout-template.md @@ -0,0 +1,139 @@ +--- +sidebar_position: 7 +sidebar_label: Layout Template +--- + +# Module: Layout Template + +## Overview + +`LayoutTemplates` are a new concept in v3 that modes use to control the layout +of a route. A layout template is a React component that is given a set of +managers that define apis to access toolbar state, commands, and hotkeys, as +well as props defined by the layout template. + +For instance the default LayoutTemplate takes in leftPanels, rightPanels and +viewports as props, which it uses to build its view. + +In addition, `layout template` has complete control over the structure of the +application. You could have tools down the left side, or a strict guided +workflow with tools set programmatically, the choice is yours for your use case. + +```jsx +const getLayoutTemplateModule = (/* ... */) => [ + { + id: 'exampleLayout', + name: 'exampleLayout', + component: ExampleLayoutComponent, + }, +]; +``` + +The `props` that are passed to `layoutTemplate` are managers and service, along +with the defined mode left/right panels, mode's defined viewports and OHIF +`ViewportGridComp`. LayoutTemplate leverages extensionManager to grab typed +extension module entries: `*.getModuleEntry(id)` + +A simplified code for `Default extension`'s layout template is: + +```jsx title="extensions/default/src/ViewerLayout/index.jsx" +import React from 'react'; +import { SidePanel } from '@ohif/ui'; + +function Toolbar({ servicesManager }) { + const { ToolBarService } = servicesManager.services; + + return ( + <> + // ToolBarService.getButtonSection('primary') to get toolbarButtons + {toolbarButtons.map((toolDef, index) => { + const { id, Component, componentProps } = toolDef; + return ( + ToolBarService.recordInteraction(args)} + /> + ); + })} + + ); +} + +function ViewerLayout({ + // From Extension Module Params + extensionManager, + servicesManager, + hotkeysManager, + commandsManager, + // From Modes + leftPanels, + rightPanels, + viewports, + ViewportGridComp, +}) { + const getPanelData = id => { + const entry = extensionManager.getModuleEntry(id); + const content = entry.component; + + return { + iconName: entry.iconName, + iconLabel: entry.iconLabel, + label: entry.label, + name: entry.name, + content, + }; + }; + + const getViewportComponentData = viewportComponent => { + const entry = extensionManager.getModuleEntry(viewportComponent.namespace); + + return { + component: entry.component, + displaySetsToDisplay: viewportComponent.displaySetsToDisplay, + }; + }; + + const leftPanelComponents = leftPanels.map(getPanelData); + const rightPanelComponents = rightPanels.map(getPanelData); + const viewportComponents = viewports.map(getViewportComponentData); + + return ( +
+ + +
+ {/* LEFT SIDEPANELS */} + + + {/* TOOLBAR + GRID */} + + + {/* Right SIDEPANELS */} + +
+
+ ); +} +``` + +## Overview Video + +
+ +
diff --git a/platform/docs/docs/platform/extensions/modules/panel.md b/platform/docs/docs/platform/extensions/modules/panel.md new file mode 100644 index 0000000..9a9235c --- /dev/null +++ b/platform/docs/docs/platform/extensions/modules/panel.md @@ -0,0 +1,163 @@ +--- +sidebar_position: 6 +sidebar_label: Panel +--- + +# Module: Panel + +## Overview + +The default LayoutTemplate has panels on the left and right sides, however one +could make a template with panels at the top or bottom and make extensions with +panels intended for such slots. + +An extension can register a Panel Module by defining a `getPanelModule` method. +The panel module provides the ability to define `menuOptions` and `components` +that can be used by the consuming application. `components` are React Components +that can be displayed in the consuming application's "Panel" Component. + +![panel-module-v3](../../../assets/img/panel-module-v3.png) + +The `menuOptions`'s `target` key, points to a registered `components`'s `id`. A +`defaultContext` is applied to all `menuOption`s; however, each `menuOption` can +optionally provide its own `context` value. + +The `getPanelModule` receives an object containing the `ExtensionManager`'s +associated `ServicesManager` and `CommandsManager`. + +An extension can also trigger to activate/open a panel via the `PanelService` - +either by explicitly calling `PanelService.activatePanel` or triggering panel +activation when some other event fires. + +```jsx +import PanelMeasurementTable from './PanelMeasurementTable.js'; + +function getPanelModule({ + commandsManager, + extensionManager, + servicesManager, +}) { + const wrappedMeasurementPanel = () => { + return ( + + ); + }; + + return [ + { + name: 'measure', + iconName: 'list-bullets', + iconLabel: 'Measure', + label: 'Measurements', + isDisabled: studies => {}, // optional + component: wrappedMeasurementPanel, + }, + ]; +} +``` + +## Consuming Panels Inside Modes + +As explained earlier, extensions make the functionalities and components +available and `modes` utilize them to build an app. So, as seen above, we are +not actually defining which side the panel should be opened. Our extension is +providing the component with its. + +New: You can easily add multiple panels to the left/right side of the viewer +using the mode configuration. As seen below, the `leftPanels` and `rightPanels` +accept an `Array` of the `IDs`. The mode configuration also allows for either (or +both) side panels to be closed by default. In the code below, the right panel +is closed by default. The mode can optionally add event triggers to +the `PanelService` that when fired will cause a side panel that was defaulted +closed to open. In the code below, the right side panel, that contains the +`trackedMeasurements` panel, is triggered to open when a measurement is added. +Note that once a default closed side panel has been opened once, +only a `PanelService.EVENTS.ACTIVATE_PANEL` event with `forceActive === true` +will cause it open (again). + +```js + +const extensionDependencies = { + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', + '@ohif/extension-measurement-tracking': '^3.0.0', + '@ohif/extension-cornerstone-dicom-sr': '^3.0.0', +}; + +const id = 'viewer' +const version = '3.0.0 + +function modeFactory({ modeConfiguration }) { + let _activatePanelTriggersSubscriptions = []; + return { + id, + routes: [ + { + path: 'longitudinal', + layoutTemplate: ({ location, servicesManager }) => { + return { + id, + props: { + leftPanels: [ + '@ohif/extension-measurement-tracking.panelModule.seriesList', + ], + rightPanels: [ + '@ohif/extension-measurement-tracking.panelModule.trackedMeasurements', + ], + rightPanelClosed: true, + viewports, + }, + }; + }, + }, + ], + onModeEnter: ({ servicesManager }) => { + const { + measurementService, + panelService, + } = servicesManager.services; + + _activatePanelTriggersSubscriptions = [ + ...panelService.addActivatePanelTriggers('@ohif/extension-measurement-tracking.panelModule.trackedMeasurements', [ + { + sourcePubSubService: measurementService, + sourceEvents: [ + measurementService.EVENTS.MEASUREMENT_ADDED, + measurementService.EVENTS.RAW_MEASUREMENT_ADDED, + ], + }, + ]), + ]; + }, + onModeExit: () => { + _activatePanelTriggersSubscriptions.forEach(sub => sub.unsubscribe()); + _activatePanelTriggersSubscriptions = []; + }, + extensions: extensionDependencies + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; +``` + +:::note +You can stack multiple panel components on top of each other by providing an array of panel components in the `rightPanels` or `leftPanels` properties. + +For instance we can use + +``` +rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.measurements]] +``` + +This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other. + +::: diff --git a/platform/docs/docs/platform/extensions/modules/sop-class-handler.md b/platform/docs/docs/platform/extensions/modules/sop-class-handler.md new file mode 100644 index 0000000..cf70fb4 --- /dev/null +++ b/platform/docs/docs/platform/extensions/modules/sop-class-handler.md @@ -0,0 +1,120 @@ +--- +sidebar_position: 4 +sidebar_label: SOP Class Handler +--- +# Module: SOP Class Handler + +## Overview +This module defines how a specific DICOM SOP class should be processed to make a list of `DisplaySet`, things which can be hung in a viewport. An extension can register a [SOP Class][sop-class-link] Handler Module by defining a `getSopClassHandlerModule` method. The [SOP Class][sop-class-link]. + +The mode chooses what SOPClassHandlers to use, so you could process a series in a different way depending on mode within the same application. + +SOPClassHandler is a bit different from the other modules, as it doesn't provide a `1:1` +schema for UI or provide its own components. It instead defines: + +- `sopClassUIDs`: an array of string SOP Class UIDs that the + `getDisplaySetFromSeries` method should be applied to. +- `getDisplaySetFromSeries`: a method that maps series and study metadata to a + display set + +A `displaySet` has the following shape: + +```js +return { + Modality: 'MR', + displaySetInstanceUIDD + SeriesDate, + SeriesTime, + SeriesInstanceUID, + StudyInstanceUID, + SeriesNumber, + FrameRate, + SeriesDescription, + isMultiFrame, + numImageFrames, + SOPClassHandlerId, + madeInClient, +} +``` + +## Example SOP Class Handler Module + +```js +import ImageSet from '@ohif/core/src/classes/ImageSet'; + + +const sopClassDictionary = { + CTImageStorage: "1.2.840.10008.5.1.4.1.1.2", + MRImageStorage: "1.2.840.10008.5.1.4.1.1.4", +}; + + +// It is important to note that the used SOPClassUIDs in the modes are in the order that is specified in the array. +const sopClassUids = [ + sopClassDictionary.CTImageStorage, + sopClassDictionary.MRImageStorage, +]; + +function addInstances(instances) { + // Add instances to this display set, and return the display set updated. +} + +const makeDisplaySet = (instances) => { + const instance = instances[0]; + const imageSet = new ImageSet(instances); + + imageSet.setAttributes({ + displaySetInstanceUID: imageSet.uid, + SeriesDate: instance.SeriesDate, + SeriesTime: instance.SeriesTime, + SeriesInstanceUID: instance.SeriesInstanceUID, + StudyInstanceUID: instance.StudyInstanceUID, + SeriesNumber: instance.SeriesNumber, + FrameRate: instance.FrameTime, + SeriesDescription: instance.SeriesDescription, + Modality: instance.Modality, + isMultiFrame: isMultiFrame(instance), + numImageFrames: instances.length, + SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`, + addInstances, + }); + + // Note returns an array now + return [imageSet]; +}; + +getSopClassHandlerModule = () => { + return [ + { + name: 'stack, + sopClassUids, + getDisplaySetsFromSeries: makeDisplaySet, + }, + ]; +}; + +``` + +### addInstances +In order to allow new SOP instances to be received and added to an existing display +set, the addInstances method can be added to a display set. It is called +on the display set to be updated, and returns it when it has added at least one +of the instances to the display set. + +### More examples : +You can find another example for this mapping between raw metadata and displaySet for +`DICOM-SR` extension. + +## `@ohif/app` usage + +We use the `sopClassHandlerModule`s in `DisplaySetService` where we +transform instances from the raw metadata format to a OHIF displaySet format. +You can read more about DisplaySetService here. + + +[sop-class-link]: http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_B.5.html +[dicom-html-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-html/src/OHIFDicomHtmlSopClassHandler.js#L4-L12 +[dicom-pdf-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-pdf/src/OHIFDicomPDFSopClassHandler.js#L4-L6 +[dicom-micro-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-microscopy/src/DicomMicroscopySopClassHandler.js#L5-L7 +[dicom-seg-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-segmentation/src/OHIFDicomSegSopClassHandler.js#L5-L7 + diff --git a/platform/docs/docs/platform/extensions/modules/toolbar.md b/platform/docs/docs/platform/extensions/modules/toolbar.md new file mode 100644 index 0000000..6d772b9 --- /dev/null +++ b/platform/docs/docs/platform/extensions/modules/toolbar.md @@ -0,0 +1,573 @@ +--- +sidebar_position: 1 +sidebar_label: Toolbar +--- + +# Module: Toolbar + +An extension can register a Toolbar Module by defining a `getToolbarModule` +method. `OHIF-v3`'s `default` extension (`"@ohif/extension-default"`) provides the +following toolbar button `uiTypes`: + +- `ohif.radioGroup`: which is a simple button that can be clicked +- `ohif.splitButton`: which is a button with a dropdown menu +- `ohif.divider`: which is a simple divider + +## Example Toolbar Module + +The Toolbar Module should return an array of `objects`. There are currently a +few different variations of definitions, each one is detailed further down. +There are two things that the toolbar module can provide, first +a component, and second evaluators. + +### Components +```js +export default function getToolbarModule({ commandsManager, servicesManager }) { + return [ + { + name: 'ohif.radioGroup', + defaultComponent: ToolbarButton, + clickHandler: () => {}, + }, + { + name: 'ohif.splitButton', + defaultComponent: ToolbarSplitButton, + clickHandler: () => {}, + }, + { + name: 'ohif.layoutSelector', + defaultComponent: ToolbarLayoutSelector, + clickHandler: (evt, clickedBtn, btnSectionName) => {}, + }, + { + name: 'ohif.toggle', + defaultComponent: ToolbarButton, + clickHandler: () => {}, + }, + ]; +} +``` + +### Custom Components + +You can also create your own extension, and add your new custom tool appearance +(e.g., split horizontally instead of vertically for split tool). Simply add +`getToolbarModule` to your extension, and pass your tool react component to its +`defaultComponent` property in the returned object. You can use `@ohif/ui` +components such as `IconButton, Icon, Tooltip, ToolbarButton` to build your own +component. + +```js +import myToolComponent from './myToolComponent'; + +export default function getToolbarModule({ commandsManager, servicesManager }) { + return [ + { + name: 'new-tool-type', + defaultComponent: myToolComponent, + clickHandler: () => {}, + }, + ]; +} +``` + +Check out how to assemble the toolbar in the [modes](../../modes/index.md) section. + + +### Evaluators +Buttons may be equipped with evaluators, which are functions invoked by the toolbarService to assess the button's status. These evaluators are expected to return an object of `{className}` and may include additional details, as elaborated in the subsequent section. + +Evaluators play a crucial role in determining the button's status based on the viewport. For example, users should be restricted from clicking on the mpr if the displaySet is not reconstructable. Additionally, certain buttons within the toolbar may be associated with specific toolGroups and should remain inactive for certain viewports. + +Let's look at one of the evaluators (for `evaluate.cornerstoneTool`) + +```js + { + name: 'evaluate.cornerstoneTool', + evaluate: ({ viewportId, button }) => { + const toolGroup = toolGroupService.getToolGroupForViewport(viewportId); + + if (!toolGroup) { + return; + } + + const toolName = toolbarService.getToolNameForButton(button); + + if (!toolGroup || !toolGroup.hasTool(toolName)) { + return { + disabled: true, + className: '!text-common-bright ohif-disabled', + disabledText: 'Tool not available', + }; + } + + const isPrimaryActive = toolGroup.getActivePrimaryMouseButtonTool() === toolName; + + return { + disabled: false, + className: isPrimaryActive + ? '!text-black bg-primary-light' + : '!text-common-bright hover:!bg-primary-dark hover:!text-primary-light', + }; + }, +}, +``` + +as you can see the job of this evaluator is to determine if the button should be disabled or not. It does so by checking the `toolGroup` and the `toolName` and then returns an object with `disabled` and `className` properties. + +The following evaluators are provided by us: + +- `evaluate.cornerstoneTool`: If assigned to a button (see next), it will make the button react to the active viewport state based on its toolGroup. +- `evaluate.cornerstoneTool.toggle`: It is designed to consider tools with toggle behavior, such as reference lines and image overlay (either on or off). +- `evaluate.cornerstone.synchronizer`: This is designed to consider the synchronizer state of the viewport, whether it is synced or not. +- `evaluate.viewportProperties.toggle`: Some properties of the viewport are toggleable, such as invert, flip, rotate, etc. By assigning this evaluator to those buttons, they will react to the active viewport state based on its properties. This allows for dynamic buttons that change their appearance based on the active viewport state. +- `evaluate.mpr`: special evaluator for MPR since it needs to check if the displaySet is reconstructable or not. + + +Sometime you want to use the same `evaluator` for different purposes, in that case you can use an object +with `name` and other properties. For example, in `'evaluate.cornerstone.segmentation'` we use +this pattern, where multiple toolbar buttons are using the same evaluator but with different options ( + in this case `toolNames` +) + +```js +{ + name: 'evaluate.cornerstone.segmentation', + toolNames: ['CircleBrush' , 'SphereBrush'] +}, +``` + + +#### Viewport and Modality Support Evaluation + +The toolbar system now uses a more robust approach for evaluating button states based on viewport types and modalities: + +**Viewport Type Support**: + +Use `evaluate.viewport.supported` to disable buttons for specific viewport types: + +```js +{ + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d', 'video', 'sm'] +} +``` + +**Modality Support**: +Use `evaluate.modality.supported` to control button state based on modalities: + +```js +{ + name: 'evaluate.modality.supported', + supportedModalities: ['CT', 'MR'], // Enable only for these modalities + // OR + unsupportedModalities: ['US'] // Disable for these modalities +} +``` + +#### Composing evaluators +Multiple evaluators can be combined to create complex conditions: + +```js +evaluate: [ + 'evaluate.cine', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d'] + }, + { + name: 'evaluate.modality.supported', + supportedModalities: ['CT'] + } +] +``` + +This evaluation system provides more precise control over when toolbar buttons are enabled or disabled based on the active viewport's characteristics. + +You can choose to set up multiple evaluators for a single button. This comes in handy when you need to assess the button according to various conditions. For example, we aim to prevent the Cine player from showing up on the 3D viewport, so we have: + +```js +evaluate: [ + 'evaluate.cine', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d'], + }, +], +``` + +You can even come up with advanced evaluators such as: + +```js +evaluate: [ + 'evaluate.cornerstone.segmentation', + // need to put the disabled text last, since each evaluator will + // merge the result text into the final result + { + name: 'evaluate.cornerstoneTool', + disabledText: 'Select the PT Axial to enable this tool', + }, +], +``` + +that we use for our RectangleROIStartEndThreshold tool in tmtv mode. + +As you see this evaluator is composed of two evaluators, one is `evaluate.cornerstone.segmentation` which makes sure (in the implementation), that +there is a segmentation created, and the second one is `evaluate.cornerstoneTool` which makes sure that the tool is available in the viewport. + +Since we are using multiple evaluators, the `disabledText` of each evaluator will be merged into the final result, so you need to +put the `disabledText` in the last evaluator. + +#### Group evaluators +Split buttons (see in [ToolbarService](../../services/data/ToolbarService.md) on how to define one) may feature a group evaluator, we provide two of them and you can write your own. + + +- `evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList`: determine the outcome of user interactions with the split buttons on what button should be promoted to the primary section. In the example above, the cornerstone tool's status is checked, and if it is not active in the list of buttons, the button is promoted to the primary section. +- `evaluate.group.promoteToPrimary`: disregarding the cornerstone tool's status and promoting the button to the primary section regardless. + +Failure to specify a group evaluator will result in no action, leaving the button in the secondary section. + +:::note +As you have learned so far, the extension modules only 'provides' the functionality +and it is the mode's job to consume it. You can next learn how to consume these components +and evaluators to build a toolbar in the +::: + + +#### Custom Evaluators +You can create your own evaluators. For instance, you have the option to design tri-state buttons, which are buttons with three states such as Show All, Show Some, or Show None of the Viewport Overlays. + + +## Toolbar buttons consumed in modes +Providing just the components is not enough. You need to add the buttons to the toolbar service and decide which ones are used for each section. + +Below we can see a simplified version of the `longitudinal` (basic viewer) mode that shows how +a mode can add buttons to the toolbar by calling +`ToolBarService.addButtons(toolbarButtons)`. `toolbarButtons` is an array of +`toolDefinitions` which we will learn next. + +```js +function modeFactory({ modeConfiguration }) { + return { + id: 'viewer', + displayName: 'Basic Viewer', + + onModeEnter: ({ servicesManager, extensionManager }) => { + const { toolBarService } = servicesManager.services; + + toolbarService.addButtons([...toolbarButtons, ...moreTools]); + toolbarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'info', + 'WindowLevel', + 'Pan', + 'Capture', + 'Layout', + 'Crosshairs', + 'MoreTools', + ]); + }, + routes: [ + { + path: 'longitudinal', + layoutTemplate: ({ location, servicesManager }) => { + return { + /* */ + }; + }, + }, + ], + }; +} +``` + + +:::note +By default OHIF's default layout (`extensions/default/src/ViewerLayout/index.tsx`) which is used in all modes use a Toolbar component that creates a +`primary` section for tools. That is why we are creating a `primary` section in the example above. + +Layouts are also customizable, and you can create your own layout in your extensions and provide it to your modes view `getLayoutTemplateModule` module. + +By default we use `@ohif/extension-default.layoutTemplateModule.viewerLayout` to use the default layout which provides a + +- Header (with logo on left, toolbar in the middle and user menu on the right) +- Left panel +- Main viewport grid area +- Right panel +::: + + +## Alternative Toolbar sections + +In your UI component, such as panels, you have the option to include a toolbar section template. +This allows you to easily add buttons to it later on. To ensure that the buttons are added properly +to the toolbar, respond to interactions correctly, and evaluate states accurately, simply utilize the `useToolbar` hook. +This hook grants you access to the `onInteraction` function and the `toolbarButtons` array, which you can customize within your UI as needed. + +```js + +function myCustomPanel({servicesManager}){ + const { onInteraction, toolbarButtons } = useToolbar({ + servicesManager, + buttonSection: 'myCustomSectionName' + }); + + // map the buttons to the UI + return ( +
+ {toolbarButtons.map((button, index) => { + return ( + + ); + })} +
+ ); +} + +``` + +We have provided a common component for toolbar buttons called `Toolbox`. +The Toolbox component serves as a versatile and configurable container for toolbar tools within your application. +It is designed to work in conjunction with the useToolbar hook to manage tool states, handle user interactions, and memorize options +using context API. + + +The `Toolbox` can be easily integrated into your application UI, requiring only the necessary services (servicesManager, commandsManager) and configuration parameters (buttonSectionId, title). Here's a simple usage scenario: + + + +```js +function MyApplication({ servicesManager, commandsManager }) { + // Configuration for the toolbox container + const config = { + servicesManager, + commandsManager, + buttonSectionId: 'customButtonSection', + title: 'My Toolbox', + }; + + return ; +} +``` + +Then in your modes you can edit the tools in that button section. + +```js + +onModeEnter: ({ servicesManager, extensionManager }) => { + const { toolBarService } = servicesManager.services; + + toolbarService.addButtons([...toolbarButtons, ...moreTools]); + toolbarService.createButtonSection('customButtonSection', [ + 'MeasurementTools', + 'Zoom', + 'info', + ]); +}, +``` + +Another example might be you want to open a modal to show some tool options when a button is clicked. +You can use this pattern + + +```js +// ToolbarButton in mode + { + id: 'Others', + uiType: 'ohif.radioGroup', + props: { + icon: 'info-action', + label: 'Others', + commands: 'showOthersModal', + }, + }, +``` + +and inside your mode factory + +```js +// adding the 'Others' button to the primary section +toolbarService.createButtonSection('primary', [ + 'Others', // --------> this one +]); + +// adding the shapes button to the 'Other' section +toolbarService.createButtonSection('other', ['Shapes']); +``` + +here as you see we are using a command `showOthersModal` which is defined in the commands module. + +```js +// inside commandsModule of your extension + showOthersModal: () => { + const { uiModalService } = servicesManager.services; + uiModalService.show({ + content: OthersModal, + title: 'Others', + customClassName: 'w-8', + movable: true, + contentProps: { + onClose: uiModalService.hide, + servicesManager, + commandsManager, + }, + containerDimensions: 'h-[125px] w-[300px]', + contentDimensions: 'h-[125px] w-[300px]', + }); + }, +``` + +as you see it is opening a modal with `OthersModal` component (below) which contains the +`Toolbox` component. + +```js +// Others modal +import { Toolbox } from '@ohif/ui'; + +function OthersModal({ servicesManager, commandsManager }) { + return ( +
+ +
+ ); +} +``` + +The result would be a modal with a toolbox inside it when the `Others` button is clicked, and the +state will get synchronized with the toolbar service automatically. + + +![alt text](../../../assets/img/toolbox-modal.png) + +## Toolbox With Options + +Your toolbox toolbar buttons can have options, this is really useful +for advanced tools that require to change some parameters. For example, the brush tool that requires the brush size to change or the mode (2D or 3D). + +:::note +Toolbox with options will run the options commands +on the mount of the toolbox component. This is useful for setting the initial state of the toolbox. +::: + +Currently we support three types of options. + +### Radio option + +We use this in segmentation shapes to let the user choose between +three different modes + +```js +{ + id: 'Shapes', + uiType: 'ohif.radioGroup', + props: { + label: 'Shapes', + evaluate: { + name: 'evaluate.cornerstone.segmentation', + toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'], + }, + icon: 'icon-tool-shape', + options: [ + { + name: 'Shape', + type: 'radio', + value: 'CircleScissor', + id: 'shape-mode', + values: [ + { value: 'CircleScissor', label: 'Circle' }, + { value: 'SphereScissor', label: 'Sphere' }, + { value: 'RectangleScissor', label: 'Rectangle' }, + ], + commands: 'setToolActiveToolbar', + }, + ], + }, +}, +``` + +### Range option + +We use this for brush radius change + +```js +{ + id: 'Brush', + icon: 'icon-tool-brush', + label: 'Brush', + evaluate: { + name: 'evaluate.cornerstone.segmentation', + toolNames: ['CircularBrush', 'SphereBrush'], + disabledText: 'Create new segmentation to enable this tool.', + }, + options: [ + { + name: 'Radius (mm)', + id: 'brush-radius', + type: 'range', + min: 0.5, + max: 99.5, + step: 0.5, + value: 25, + commands: { + commandName: 'setBrushSize', + commandOptions: { toolNames: ['CircularBrush', 'SphereBrush'] }, + }, + }, + ], +}, +``` + +### Custom option + +We use this pattern inside `tmtv` mode for `RectangleROIThreshold` + +```js +{ + id: 'RectangleROIStartEndThreshold', + uiType: 'ohif.radioGroup', + props: { + icon: 'tool-create-threshold', + label: 'Rectangle ROI Threshold', + commands: setToolActiveToolbar, + evaluate: { + name: 'evaluate.cornerstoneTool', + disabledText: 'Select the PT Axial to enable this tool', + }, + options: 'tmtv.RectangleROIThresholdOptions', + }, +}, +``` + +Note that it is your job to provide the `tmvt.RectangleROIThresholdOptions` in the getToolbarModule of your extension + + + +## Change Toolbar with hanging protocols + +If you want to change the toolbar based on the hanging protocol, you can do a pattern like this. + +```js + + const { unsubscribe } = hangingProtocolService.subscribe( + hangingProtocolService.EVENTS.PROTOCOL_CHANGED, + () => { + toolbarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'WindowLevel', + ]); + } +); +``` diff --git a/platform/docs/docs/platform/extensions/modules/utility.md b/platform/docs/docs/platform/extensions/modules/utility.md new file mode 100644 index 0000000..fbb975d --- /dev/null +++ b/platform/docs/docs/platform/extensions/modules/utility.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 9 +sidebar_label: Utility +--- + +# Module: Utility + +## Overview + +Often, an extension will need to expose some useful functionality to the other +extensions, or modes that consume the extension. For example, the `cornerstone` +extension, uses its `utility` module to expose methods via + +```js +getUtilityModule({ servicesManager }) { + return [ + { + name: 'common', + exports: { + getCornerstoneLibraries: () => { + return { cornerstone, cornerstoneTools }; + }, + getEnabledElement, + CornerstoneViewportService, + dicomLoaderService, + }, + }, + { + name: 'core', + exports: { + Enums: cs3DEnums, + }, + }, + { + name: 'tools', + exports: { + toolNames, + Enums: cs3DToolsEnums, + }, + }, + ]; + }, +}; +``` + +Then a consuming extension can use `getModuleEntry` to access the methods +Below, which is a code from `TrackedCornerstoneViewport` use the `getUtilityModule` method to get the internal `CornerstoneViewportService` which handles the `Cornerstone` viewport. + +```js title="extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx" +const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.common' +); + +const { CornerstoneViewportService } = utilityModule.exports; +``` diff --git a/platform/docs/docs/platform/extensions/modules/viewport.md b/platform/docs/docs/platform/extensions/modules/viewport.md new file mode 100644 index 0000000..b64a13e --- /dev/null +++ b/platform/docs/docs/platform/extensions/modules/viewport.md @@ -0,0 +1,134 @@ +--- +sidebar_position: 5 +sidebar_label: Viewport +--- + +# Module: Viewport + +## Overview + +Viewports consume a displaySet and display/allow the user to interact with data. +An extension can register a Viewport Module by defining a `getViewportModule` +method that returns a React component. Currently, we use viewport components to +add support for: + +- 2D Medical Image Viewing (cornerstone ext.) +- Structured Reports as SR (DICOM SR ext.) +- Encapsulated PDFs as PDFs (DICOM pdf ext.) + +The general pattern is that a mode can define which `Viewport` to use for which +specific `SOPClassHandlerUID`, so if you want to fork just a single Viewport +component for a specialized mode, this is possible. + +```jsx +// displaySet, dataSource +const getViewportModule = () => { + const wrappedViewport = props => { + return ( + { + commandsManager.runCommand('commandName', data); + }} + /> + ); + }; + + return [{ name: 'example', component: wrappedViewport }]; +}; +``` + +## Example Viewport Component + +A simplified version of the tracked `OHIFCornerstoneViewport` is shown below, which +creates a cornerstone viewport: + + +```jsx +function TrackedCornerstoneViewport({ + children, + dataSource, + displaySets, + viewportId, + servicesManager, + extensionManager, + commandsManager, +}) { + + return ( +
+ /** Resize Detector */ + + /** Div For displaying image */ +
e.preventDefault()} + onMouseDown={e => e.preventDefault()} + ref={elementRef} + >
+
+ ); +} +``` + +### Viewport re-rendering optimizations + +We make use of the React memoization pattern to prevent unnecessary re-renders +for the viewport unless certain aspects of the Viewport props change. You can take +a look into the `areEqual` function in the `OHIFCornerstoneViewport` component to +see how this is done. + +```js +function areEqual(prevProps, nextProps) { + if (prevProps.displaySets.length !== nextProps.displaySets.length) { + return false; + } + + if ( + prevProps.viewportOptions.orientation !== + nextProps.viewportOptions.orientation + ) { + return false; + } + + // rest of the code +``` + +as you see, we check if the `needsRerendering` prop is true, and if so, we will +re-render the viewport if the `displaySets` prop changes or the orientation +changes. + + +We use viewportId to identify a viewport and we use it as a key in React +rendering. This is important because it allows us to keep track of the viewport +and its state, and also let React optimize and move the viewport around in the +grid without re-rendering it. However, there are some cases where we need to +force re-render the viewport, for example, when the viewport is hydrated +with a new Segmentation. For these cases, we use the `needsRerendering` prop +to force re-render the viewport. You can add it to the `viewportOptions` + + + + + +### `@ohif/app` + +Viewport components are managed by the `ViewportGrid` Component. Which Viewport +component is used depends on: + +- Hanging Protocols +- The Layout Configuration +- Registered SopClassHandlers + +![viewportModule-layout](../../../assets/img/viewportModule-layout.png) + +
An example of three cornerstone Viewports
diff --git a/platform/docs/docs/platform/internationalization.md b/platform/docs/docs/platform/internationalization.md new file mode 100644 index 0000000..8caf9a7 --- /dev/null +++ b/platform/docs/docs/platform/internationalization.md @@ -0,0 +1,396 @@ +--- +sidebar_position: 4 +sidebar_label: Internationalization +--- + +# Viewer: Internationalization + +OHIF supports internationalization using [i18next](https://www.i18next.com/) +through the npm package [@ohif/i18n](https://www.npmjs.com/package/@ohif/i18n), +where is the main instance of i18n containing several languages and tools. + +
+

Our translation management is powered by + Locize + through their generous support of open source.

+ + Locize Translation Management Logo + +
+ +## How to change language for the viewer? + +You can take a look into user manuals to see how to change the viewer's +language. In summary, you can change the language: + +- In the preference modals +- Using the language query in the URL: `lng=Test-LNG` + +## Installing + +```bash +yarn add @ohif/i18n + +# OR + +npm install --save @ohif/i18n +``` + +## How it works + +After installing `@ohif/i18n` npm package, the translation function +[t](https://www.i18next.com/overview/api#t) can be used [with](#with-react) or +[without](#without-react) React. + +A translation will occur every time a text match happens in a +[t](https://www.i18next.com/overview/api#t) function. + +The [t](https://www.i18next.com/overview/api#t) function is responsible for +getting translations using all the power of i18next. + +E.g. + +Before: + +```html +
my translated text
+``` + +After: + +```html +
{t('my translated text')}
+``` + +If the translation.json file contains a key that matches the HTML content e.g. +`my translated text`, it will be replaced automatically by the +[t](https://www.i18next.com/overview/api#t) function. + +--- + +### With React + +This section will introduce you to [react-i18next](https://react.i18next.com/) +basics and show how to implement the [t](https://www.i18next.com/overview/api#t) +function easily. + +#### Using Hooks + +You can use `useTranslation` hooks that is provided by `react-i18next` + +You can read more about this +[here](https://react.i18next.com/latest/usetranslation-hook). + +```js +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +function MyComponent() { + const { t } = useTranslation(); + + return

{t('my translated text')}

; +} +``` + +### Using outside of OHIF viewer + +OHIF Viewer already sets a main +[I18nextProvider](https://react.i18next.com/latest/i18nextprovider) connected to +the shared i18n instance from `@ohif/i18n`, all extensions inside OHIF Viewer +will share this same provider at the end, you don't need to set new providers at +all. + +But, if you need to use it completely outside of OHIF viewer, you can set the +I18nextProvider this way: + +```jsx +import i18n from '@ohif/i18n'; +import { I18nextProvider } from 'react-i18next'; +import App from './App'; + + + +; +``` + +After setting `I18nextProvider` in your React App, all translations from +`@ohif/i18n` should be available following the basic [With React](#with-react) +usage. + +--- + +### Without React + +When needed, you can also use available translations _without React_. + +E.g. + +```js +import { T } from '@ohif/i18n'; +console.log(T('my translated text')); +console.log(T('$t(Common:Play) my translated text')); +``` + +--- + +## Main Concepts While Translating + +## Namespaces + +Namespaces are being used to organize translations in smaller portions, combined +semantically or by use. Each `.json` file inside `@ohif/i18n` npm package +becomes a new namespace automatically. + +- Buttons: All buttons translations +- CineDialog: Translations for the tool tips inside the Cine Player Dialog +- Common: all common jargons that can be reused like `t('$t(common:image)')` +- Header: translations related to OHIF's Header Top Bar +- MeasurementTable - Translations for the `@ohif/ui` Measurement Table +- UserPreferencesModal - Translations for the `@ohif/ui` Preferences Modal +- Modals - Translations available for other modals +- PatientInfo - Translations for patients info hover +- SidePanel - Translations for side panels +- ToolTip - Translations for tool tips + +### How to use another NameSpace inside the current NameSpace? + +i18next provides a parsing feature able to get translations strings from any +NameSpace, like this following example getting data from `Common` NameSpace: + +``` +$t('Common:Reset') +``` + +## Extending Languages in @ohif/i18n + +Sometimes, even using the same language, some nouns or jargons can change +according to the country, states or even from Hospital to Hospital. + +In this cases, you don't need to set an entire language again, you can extend +languages creating a new folder inside a pre existent language folder and +@ohif/i18n will do the hard work. + +This new folder must to be called with a double character name, like the `UK` in +the following file tree: + +```bash + |-- src + |-- locales + index.js + |-- en + |-- Buttons.json + index.js + | UK + |-- Buttons.js + index.js + | US + |-- Buttons.js + index.js + ... +``` + +All properties inside a Namespace will be merged in the new sub language, e.g +`en-US` and `en-UK` will merge the props with `en`, using i18next's fallback +languages tool. + +You will need to export all Json files in your `index.js` file, mounting an +object like this: + +```js + { + en: { + NameSpace: { + keyWord1: 'keyWord1Translation', + keyWord2: 'keyWord2Translation', + keyWord3: 'keyWord3Translation', + } + }, + 'en-UK': { + NameSpace: { + keyWord1: 'keyWord1DifferentTranslation', + } + } + } +``` + +Please check the `index.js` files inside locales folder for an example of this +exporting structure. + +### Extending languages dynamically + +You have access to the i18next instance, so you can use the +[addResourceBundle](https://www.i18next.com/how-to/add-or-load-translations#add-after-init) +method to add and change language resources as needed. + +E.g. + +```js +import { i18n } from '@ohif/i18n'; +i18next.addResourceBundle('pt-BR', 'Buttons', { + Angle: 'ร‚ngulo', +}); +``` + +--- + +### How to set a whole new language + +To set a brand new language you can do it in two different ways: + +- Opening a pull request for `@ohif/i18n` and sharing the translation with the + community. ๐Ÿ˜ Please see [Contributing](#contributing-with-new-languages) + section for further information. + +- Setting it only in your project or extension: + +You'll need a final object like the following, what is setting French as +language, and send it to `addLocales` method. + +```js +const newLanguage = + { + fr: { + Commons: { + "Reset": "Rรฉinitialiser", + "Previous": "Prรฉcรฉdent", + }, + Buttons: { + "Rectangle": "Rectangle", + "Circle": "Cercle", + } + } +``` + +To make it easier to translate, you can copy the .json files in the /locales +folder and theirs index.js exporters, keeping same keys and NameSpaces. +Importing the main index.js file, will provide you an Object as expected by the +method `addlocales`; + +E.g. of `addLocales` usage + +```js +import { addLocales } from '@ohif/i18n'; +import locales from './locales/index.js'; +addLocales(locales); +``` + +You can also set them manually, one by one, using this +[method](#extending-languages-dynamically). + +--- + +## Test Language + +We have created a test language that its translations can be seen in the locales +folder. You can copy paste the folder and its `.json` namespaces and add your +custom language translations. + +> If you apply the test-LNG you can see all the elements get appended with 'Test +> {}'. For instance `Study list` becomes `Test Study list`. + +## Language Detections + +@ohif/i18n uses +[i18next-browser-languageDetector](https://github.com/i18next/i18next-browser-languageDetector) +to manage detections, also exports a method called initI18n that accepts a new +detector config as parameter. + +### Changing the language + +OHIF Viewer accepts a query param called `lng` in the url to change the +language. + +E.g. + +``` +https://docs.ohif.org/demo/?lng=es-MX +``` + +### Language Persistence + +The user's language preference is kept automatically by the detector and stored +at a cookie called 'i18next', and in a localstorage key called 'i18nextLng'. +These names can be changed with a new +[Detector Config](https://github.com/i18next/i18next-browser-languageDetector). + +## Debugging translations + +There is an environment variable responsible for debugging the translations, +called `REACT_APP_I18N_DEBUG`. + +Run the project as following to get full debug information: + +```bash +REACT_APP_I18N_DEBUG=true yarn run dev +``` + +## Contributing with new languages + +We have integrated `i18next` into the OHIF Viewer and hooked it up with Locize +for translation management. Now we need your help to get the app translated into +as many languages as possible, and ensure that we haven't missed pieces of the +app that need translation. Locize has graciously offered to provide us with free +usage of their product. + +Once each crowd-sourcing project is completed, we can approve it and merge the +changes into the main project. At that point, the language will be immediately +available on https://viewer.ohif.org/ for testing, and can be used in any OHIF +project. We will support usage through both the Locize CDN and by copying the +language directly into the `@ohif/i18n` package, so that end users can serve the +content from their own domains. + +Here are a couple examples: + +Spanish: +https://viewer.ohif.org/viewer/1.2.840.113619.2.5.1762583153.215519.978957063.78?lng=es + +Chinese: +https://viewer.ohif.org/viewer/1.2.840.113619.2.5.1762583153.215519.978957063.78?lng=zh + +Portuguese: +https://viewer.ohif.org/viewer/1.2.840.113619.2.5.1762583153.215519.978957063.78?lng=pt-BR + +Here are some links you can use to sign up to help translate. All you have to do +is sign up, translate the strings, and click Save. On our side, we have a +dashboard to see how many strings are translated and by whom. + +This is a pretty random set of languages, so please post below if you'd like a +new language link to be added: + +Languages: + +[French](https://www.locize.io/register?invitation=Nj8jRPaFKYwtIfNZ6Y5GVOJOpeiXNAdVuSiOg9ceaiveP6uF6y1wVXM9lgfKoYZX) + +[German](https://www.locize.io/register?invitation=gChNiVi66YINTPpbKESVAVYPapwg3DkpvMSSomLTvVqBJTXrdmPvxi0WZYHER11q) + +[Dutch](https://www.locize.io/register?invitation=2PGe7I184aN0cazM4GXMhzeLtGTf9Zen5uyOEFhHQ8vYkfKHkgR0mJ8dwbNlIeCG) + +[Turkish](https://www.locize.io/register?invitation=NOMIXsfneqPbFDqjce5wI7Z6p2swXSjc0rHOH4KLcM6qXSNA4LGyJaLxS7nqWAe3) + +[Chinese](https://www.locize.io/register?invitation=lrcUbt7DvV4aJmQeEA4SMAj5xNWr3rltOcaZW1cFc6eod0nvzSPFU4V383tDHGGn) + +[Japanese](https://www.locize.io/register?invitation=AaRq2S22o5FsxArwgVuw1gZcQjoe2ffyxarqlAXOpN7JnR2sf2mfamc5qV6LG1Mn) + +[Arabic](https://www.locize.io/register?invitation=BiqI6fOm1sC84N3YJLbImXmaOCk8Hc3TMGpXg7NH2R0b0OKuPCp9wlCHLoqMRpfQ) + +[Hindi](https://www.locize.io/register?invitation=ph7JmOGTV95DF3EFaI1kvK5Hx98dV9w2wj9h9UhUCWnkBNAwWEdWMcyjnF94zkWb) + +[Malay](https://www.locize.io/register?invitation=HsV9F5mKZyeUZYrC3XFRzNI2l0EsIh6hK0MUIKP8IYZA3GxuzfgkvWBLCFwCpDik) + +[Russian](https://www.locize.io/register?invitation=da4V9Q8DVO3M1FIlvfT50ZiS8NDNgvC0dE5hHUEAp47FXy6pLXmf1cp2lgLBfLmb) + +[Swedish](https://www.locize.io/register?invitation=uR4kzBZC1vhJe6jyMwYXgGPj84QDMulQRlt2s6rONU6ljUh5dgwuUyhJEtZ4REA3) + +[Italian](https://www.locize.io/register?invitation=viAS1NC5q342OxtuIv3JFX9DJ3KoR4SmGoElkBlRMphsDKt4hy9bW8JfBjHlfnd7) + +[Spanish](https://www.locize.io/register?invitation=ZikXW3KI4w4eo5Cf6L1aQMWaR69XAQ0a9Va3NGorH7mAPvEPXp8w8NLkPNLs5nG8) + +[Ukrainian](https://www.locize.io/register?invitation=TY0s6onqH3Asl05Bh1qB44SNSABL2pTYoturwxAmcNKRnzBZFK7bGfn7kVi23Vpg) + +[Vietnamese](https://www.locize.io/register?invitation=eqfHDm0vaqxGfQ5TGt6SeV0dx9b2dCp1RrMRdIRavqzOCOAfD3IElzUsyIT689cK) + +[Portugese-Brazil](https://www.locize.io/register?invitation=Qc5Dq449xbblQqLTpWeMfsyFiu3gACcgpj0EIucQjjs9Ph9pzPLpq3MnZupF9t6N) + +Don't see your language in the above list? Add a request +[here](https://github.com/OHIF/Viewers/issues/618) so that we can create the +language for your translation contribution. diff --git a/platform/docs/docs/platform/managers/_category_.json b/platform/docs/docs/platform/managers/_category_.json new file mode 100644 index 0000000..f2a2e07 --- /dev/null +++ b/platform/docs/docs/platform/managers/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Managers", + "position": 10 +} diff --git a/platform/docs/docs/platform/managers/commands.md b/platform/docs/docs/platform/managers/commands.md new file mode 100644 index 0000000..8727f98 --- /dev/null +++ b/platform/docs/docs/platform/managers/commands.md @@ -0,0 +1,175 @@ +--- +sidebar_position: 4 +sidebar_label: Commands Manager +--- +# Commands Manager + +## Overview + + +The `CommandsManager` is a class defined in the `@ohif/core` project. +The Commands Manager tracks named commands (or functions) that are scoped to +a context. When we attempt to run a command with a given name, we look for it +in our active contexts, in the order specified. +If found, we run the command, passing in any application +or call specific data specified in the command's definition. + +The order specified is the REVERSE of the order the modules are registered in +for the mode dependency. That is, the last module registered has the highest priority +and will be searched first for commands. This has nothing to do with when the +command itself is registered, although registrations for the same command in different +modules in the same context will also use last registration wins. + +> Note: A single instance of `CommandsManager` should be defined in the consuming +application, and it is used when constructing the `ExtensionManager`. + +A `simplified skeleton` of the `CommandsManager` is shown below: + +```js +export class CommandsManager { + contexts = {}; + contextOrder = []; + + constructor(_ignoredConfig) {} + + getContext(contextName) { + const context = this.contexts[contextName]; + return context; + } + + /**...**/ + + createContext(contextName) { + /** ... **/ + this.contexts[contextName] = {}; + this.contextOrder.push at beginning(contextName) + } + + + registerCommand(contextName, commandName, definition) { + /**...**/ + const context = this.getContext(contextName); + /**...**/ + context[commandName] = definition; + } + + getCommand(commandName, contextName) { + const useContext = contextName || first context having commandName in contextOrder + return useContext[commandName]; + } + + runCommand(commandName, options = {}, contextName) { + const definition = this.getCommand(commandName, contextName); + /**...**/ + const { commandFn } = definition; + const commandParams = Object.assign( + {}, + definition.options, // "Command configuration" + options // "Time of call" info + ); + /**...**/ + return commandFn(commandParams); + } + /**...**/ +} +``` + + + + +### Instantiating + +No methods or configuration is used within the construction. + + +## Commands/Context Registration +The `ExtensionManager` handles registering commands and creating contexts, so you +don't need to register all your commands manually. Simply, create a `commandsModule` +in your extension, and it will get automatically registered in the `context` provided. + +A *simplified version* of this registration is shown below to give an idea about the process. + + +```js +export default class ExtensionManager { + constructor({ commandsManager }) { + this._commandsManager = commandsManager + } + /** ... **/ + registerExtension = (extension, configuration = {}, dataSources = []) => { + let extensionId = extension.id + /** ... **/ + + // Register Modules provided by the extension + moduleTypeNames.forEach((moduleType) => { + const extensionModule = this._getExtensionModule( + moduleType, + extension, + extensionId, + configuration + ) + + if (moduleType === 'commandsModule') { + this._initCommandsModule(extensionModule) + } + /** registering other modules **/ + }) + } + + _initCommandsModule = (extensionModule) => { + let { definitions, defaultContext } = extensionModule + defaultContext = defaultContext || 'VIEWER' + + if (!this._commandsManager.getContext(defaultContext)) { + this._commandsManager.createContext(defaultContext) + } + + Object.keys(definitions).forEach((commandName) => { + const commandDefinition = definitions[commandName] + const commandHasContextThatDoesNotExist = + commandDefinition.context && + !this._commandsManager.getContext(commandDefinition.context) + + if (commandHasContextThatDoesNotExist) { + this._commandsManager.createContext(commandDefinition.context) + } + + this._commandsManager.registerCommand( + commandDefinition.context || defaultContext, + commandName, + commandDefinition + ) + }) + } +} + +``` + + +If you find yourself in a situation where you want to register a command/context manually, ask +yourself "why can't I register these commands via an extension?", but if you insist, you can use the `CommandsManager` API to do so: + +```js +// Command Registration +commandsManager.registerCommand('context', 'name', commandDefinition); + +// Context Creation +commandsManager.createContext('string'); +``` + +## `CommandsManager` Public API + +If you would like to run a command in the consuming app or an extension, you can +use `runCommand(commandName, options = {}, contextName)`. + + +```js +// Run a command, it will run all the `speak` commands in all contexts +commandsManager.runCommand('speak', { command: 'hello' }); + +// Run command, from Default context +commandsManager.runCommand('speak', { command: 'hello' }, 'DEFAULT'); + +// Returns all commands for a given context +commandsManager.getContext('string'); +``` diff --git a/platform/docs/docs/platform/managers/extension.md b/platform/docs/docs/platform/managers/extension.md new file mode 100644 index 0000000..a31f224 --- /dev/null +++ b/platform/docs/docs/platform/managers/extension.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 2 +sidebar_label: Extension Manager +--- + +# Extension Manager + +## Overview + +The `ExtensionManager` is a class made available to us via the `@ohif/core` +project (platform/core). Our application instantiates a single instance of it, +and provides a `ServicesManager` and `CommandsManager` along with the +application's configuration through the appConfig key (optional). + +```js +const commandsManager = new CommandsManager(); +const servicesManager = new ServicesManager(); +const extensionManager = new ExtensionManager({ + commandsManager, + servicesManager, + appConfig, +}); +``` +## Events +The following events get published by the `ExtensionManager`: + +| Event | Description | +| ---------------------------- | ------------------------------------------------------ | +| ACTIVE_DATA_SOURCE_CHANGED | Fired when the active data source is changed - either replaced with an entirely different one or the existing active data source gets its definition changed via `updateDataSourceConfiguration`. | + +## API +The `ExtensionManager` only has the following public API: + +- `setActiveDataSource` - Sets the active data source for the application +- `getDataSources` - Returns the registered data sources +- `getActiveDataSource` - Returns the currently active data source +- `getModuleEntry` - Returns the module entry by the give id. +- `addDataSource` - Dynamically adds a data source and optionally sets it as the active data source +- `updateDataSourceConfiguration` - Updates the configuration of a specified data source (name). +- `getDataSourceDef` - Gets the data source definition for a particular data source name. + +## Accessing Modules + +We use `getModuleEntry` in our `ViewerLayout` logic to find the panels based on +the provided IDs in the mode's configuration. + +For instance: +`extensionManager.getModuleEntry("@ohif/extension-measurement-tracking.panelModule.seriesList")` +accesses the `seriesList` panel from `panelModule` of the +`@ohif/extension-measurement-tracking` extension. + +```js +const getPanelData = id => { + const entry = extensionManager.getModuleEntry(id); + const content = entry.component; + + return { + iconName: entry.iconName, + iconLabel: entry.iconLabel, + label: entry.label, + name: entry.name, + content, + }; +}; +``` diff --git a/platform/docs/docs/platform/managers/hotkeys.md b/platform/docs/docs/platform/managers/hotkeys.md new file mode 100644 index 0000000..75d4ead --- /dev/null +++ b/platform/docs/docs/platform/managers/hotkeys.md @@ -0,0 +1,80 @@ +--- +sidebar_position: 5 +sidebar_label: Hotkeys Manager +--- +# Hotkeys Managers + +## Overview +`HotkeysManager` handles all the logics for adding, setting and enabling/disabling +the hotkeys. + + + +## Instantiation +`HotkeysManager` is instantiated in the `appInit` similar to the other managers. + +```js +const commandsManager = new CommandsManager(commandsManagerConfig); +const servicesManager = new ServicesManager(commandsManager); +const hotkeysManager = new HotkeysManager(commandsManager, servicesManager); +const extensionManager = new ExtensionManager({ + commandsManager, + servicesManager, + hotkeysManager, + appConfig, +}); +``` + + + + +## Hotkeys Manager API + +- `setHotkeys`: The most important method in the `HotkeysManager` which binds the keys with commands. +- `setDefaultHotKeys`: set the defaultHotkeys **property**. Note that, this method **does not** bind the provided hotkeys; however, when `restoreDefaultBindings` +is called, the provided defaultHotkeys will get bound. +- `destroy`: reset the HotkeysManager, and remove the set hotkeys and empty out the `defaultHotkeys` + + + +## Structure of a Hotkey Definition +A hotkey definition should have the following properties: + +- `commandName`: name of the registered command +- `commandOptions`: extra arguments to the commands +- `keys`: an array defining the key to get bound to the command +- `label`: label to be shown in the hotkeys preference panel +- `isEditable`: whether the key can be edited by the user in the hotkey panel + + +### Default hotkeysBindings +The default key bindings can be find in `hotkeyBindings.js` + +```js +// platform/core/src/defaults/hotkeyBindings.js + +export default [ + /**..**/ + { + commandName: 'setToolActive', + commandOptions: { toolName: 'Zoom' }, + label: 'Zoom', + keys: ['z'], + isEditable: true, + }, + + { + commandName: 'flipViewportHorizontal', + label: 'Flip Vertically', + keys: ['v'], + isEditable: true, + }, + /**..**/ +] +``` + + +### Global vs Mode specific hotkeys + +You can can set the global hotkeys and override them using the `$set` method +in the customization service. diff --git a/platform/docs/docs/platform/managers/index.md b/platform/docs/docs/platform/managers/index.md new file mode 100644 index 0000000..ee6d6b2 --- /dev/null +++ b/platform/docs/docs/platform/managers/index.md @@ -0,0 +1,76 @@ +--- +sidebar_position: 1 +sidebar_label: Introduction +--- + +# Managers + +## Overview + +`OHIF` uses `Managers` to accomplish various purposes such as registering new +services, dependency injection, and aggregating and exposing `extension` +features. + +`OHIF-v3` provides the following managers which we will discuss in depth. + + + + + + + + + + + + + + + + + + + + + + + + + + +
ManagerDescription
+ + Extension Manager + + + Aggregating and exposing modules and features through out the app +
+ + Services Manager + + + Single point of registration for all internal and external services +
+ + Commands Manager + + + Register commands with specific context and run commands in the app +
+ + Hotkeys Manager + + + For keyboard keys assignment to commands +
+ + + + + +[core-services]: https://github.com/OHIF/Viewers/tree/master/platform/core/src/services +[services-manager]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/ServicesManager.js +[cross-cutting-concerns]: https://en.wikipedia.org/wiki/Cross-cutting_concern + diff --git a/platform/docs/docs/platform/managers/service.md b/platform/docs/docs/platform/managers/service.md new file mode 100644 index 0000000..d9269e4 --- /dev/null +++ b/platform/docs/docs/platform/managers/service.md @@ -0,0 +1,204 @@ +--- +sidebar_position: 3 +sidebar_label: Service Manager +--- + +# Services Manager + +## Overview + +Services manager is the single point of service registration. Each service needs +to implement a `create` method which gets called inside `ServicesManager` to +instantiate the service. In the app, you can get access to a registered service +via the `services` property of the `ServicesManager`. + +## Skeleton + +_Simplified_ skeleton of `ServicesManager` is shown below. There are two public +methods: + +- `registerService`: registering a new service with/without a configuration +- `registerServices`: registering batch of services + +```js +export default class ServicesManager { + constructor(commandsManager) { + this._commandsManager = commandsManager; + this.services = {}; + this.registeredServiceNames = []; + } + + registerService(service, configuration = {}) { + /** validation checks **/ + this.services[service.name] = service.create({ + configuration, + commandsManager: this._commandsManager, + }); + + /* Track service registration */ + this.registeredServiceNames.push(service.name); + } + + registerServices(services) { + /** ... **/ + } +} +``` + +## Default Registered Services + +By default, `OHIF-v3` registers the following services in the `appInit`. + +```js title="platform/app/src/appInit.js" +servicesManager.registerServices([ + CustomizationService, + UINotificationService, + UIModalService, + UIDialogService, + UIViewportDialogService, + MeasurementService, + DisplaySetService, + ToolBarService, + ViewportGridService, + HangingProtocolService, + CineService, +]); +``` + +## Service Architecture + +If you take a look at the folder of each service implementation above, you will +find out that services need to be exported as an object with `name` and `create` +method. + +For instance, `ToolBarService` is exported as: + +```js title="platform/core/src/services/ToolBarService/index.js" +import ToolBarService from './ToolBarService'; + +export default { + name: 'ToolBarService', + create: ({ configuration = {}, commandsManager }) => { + return new ToolBarService(commandsManager); + }, +}; +``` + +and the implementation of `ToolBarService` lies in the same folder at +`./ToolbarSerivce.js`. + +> Note: The create method is critical for any custom service that you write and +> want to add to the list of services + +> Note: For typescript definitions, the service type should be exported +> as part of the Types export on the module. This is recommended going forward +> and existing services will be migrated. As well, the capitalization of the +> name should be lower camel case, with the type being upper camel case. In +> the above example, the service instance should be `toolBarService` with the +> class being `ToolBarService`. + +## Accessing Services + +Throughout the app you can use `services` property of the service manager to +access the desired service. + +For instance in the `PanelMeasurementTableTracking` which is the right panel in +the `longitudinal` mode, we have the _simplified code below_ for downloading the +drawn measurements. + +```js +function PanelMeasurementTableTracking({ servicesManager }) { + const { MeasurementService } = servicesManager.services; + /** ... **/ + + async function exportReport() { + const measurements = MeasurementService.getMeasurements(); + /** ... **/ + downloadCSVReport(measurements, MeasurementService); + } + + /** ... **/ + return <> /** ... **/ ; +} +``` + +## Registering Custom Services + +You might need to write you own custom service in an extension. +`preRegistration` hook inside your extension is the place for registering your +custom service. + +```js title="extensions/customExtension/src/index.js" +import WrappedBackEndService from './services/backEndService'; + +export default { + // ID of the extension + id: 'myExtension', + preRegistration({ servicesManager }) { + servicesManager.registerService(WrappedBackEndService(servicesManager)); + }, +}; +``` + +and the logic for your service shall be + +```js title="extensions/customExtension/src/services/backEndService/index.js" +// Canonical name of upper camel case BackEndService for the class +import BackEndService from './BackEndService'; + +export default function WrappedBackEndService(servicesManager) { + return { + // Note the canonical name of lower camel case backEndService + name: 'backEndService', + create: ({ configuration = {} }) => { + return new BackEndService(servicesManager); + }, + }; +} +``` + +with implementation of + +```ts +export default class BackEndService { + constructor(servicesManager) { + this.servicesManager = servicesManager; + } + + putAnnotations() { + return post(/*...*/); + } +} +``` + +with a registration of + +```ts title="types/index.ts" +import BackEndService from "../services/BackEndService/BackEndService"; + +export { BackEndService }; +``` + +# Service Mode Lifecycle +Services may implement initialization and cleanup for mode specific data. +In order to prevent defects where there are differences between initial +and subsequent displays of a study, the contract of the service is that the +state the service is in on mode entry shall be the same whether the mode was +entered or was exited and entered again. + +To implement storage/recovery of state, the mode must store the data on +exiting the mode, and restore the data in it's onModeEnter. For example, +the mode may decide to preserve measurement data in the onModeExit, and +to restore it in the onModeEnter. This does not violate the contract since +it is the mode's decision to apply the stored state, and to cache it. + +## onModeEnter +A service may implement an onModeEnter call to initialize the service to +be ready for entering a mode. +This is called before the mode `onModeEnter` is called. + +## onModeExit +When entering a mode, the service contract states that the service needs to +be in the same state whether it is a fresh load or has previously entered the mode. +The onModeExit allows a service to clean itself up after the mode 'onModeExit' +has stored any persistent data. diff --git a/platform/docs/docs/platform/modes/_category_.json b/platform/docs/docs/platform/modes/_category_.json new file mode 100644 index 0000000..889d845 --- /dev/null +++ b/platform/docs/docs/platform/modes/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Modes", + "position": 12 +} diff --git a/platform/docs/docs/platform/modes/index.md b/platform/docs/docs/platform/modes/index.md new file mode 100644 index 0000000..aded22b --- /dev/null +++ b/platform/docs/docs/platform/modes/index.md @@ -0,0 +1,413 @@ +--- +sidebar_position: 1 +sidebar_label: Introduction +--- + +# Modes + +## Overview + +A mode can be thought of as a viewer app configured to perform a specific task, +such as tracking measurements over time, 3D segmentation, a guided radiological +workflow, etc. Addition of modes enables _application_ with many _applications_ +as each mode become a mini _app configuration_ behind the scene. + +Upon initialization the viewer will consume extensions and modes and build up +the route desired, these can then be accessed via the study list, or directly +via url parameters. + + + +OHIF-v3 architecture can be seen in the following: + +![mode-archs](../../assets/img/mode-archs.png) + +> Note: Templates are now a part of โ€œextensionsโ€ Routes are configured by modes +> and/or app + +As mentioned, modes are tied to a specific route in the viewer, and multiple +modes/routes can be present within a single application. This allows for +tremendously more flexibility than before you can now: + +- Simultaneously host multiple viewers with for different use cases from within + the same app deploy. +- Make radiological viewers for specific purposes/workflows, e.g.: + - Tracking the size of lesions over time. + - PET/CT fusion workflows. + - Guided review workflows optimized for a specific clinical trial. +- Still host one single feature-rich viewer if you desire. + +## Anatomy + +A mode configuration has a `route` name which is dynamically transformed into a +viewer route on initialization of the application. Modes that are available to a +study will appear in the study list. + +![user-study-summary](../../assets/img/user-study-summary.png) + +The mode configuration specifies which `extensions` the mode requires, which +`LayoutTemplate` to use, and what props to pass to the template. For the default +template this defines which `side panels` will be available, as well as what +`viewports` and which `displaySets` they may hang. + +Mode's config is composed of three elements: +- `id`: the mode `id` +- `modeFactory`: the function that returns the mode specific configuration +- `extensionDependencies`: the list of extensions that the mode requires + + +that return a config object with certain +properties, the high-level view of this config object is: + +```js title="modes/example/src/index.js" +function modeFactory() { + return { + id: '', + version: '', + displayName: '', + onModeEnter: () => {}, + onModeExit: () => {}, + validationTags: {}, + isValidMode: () => {}, + routes: [ + { + path: '', + init: () => {}, + layoutTemplate: () => {}, + }, + ], + extensions: extensionDependencies, + hangingProtocol: [], + sopClassHandlers: [], + hotkeys: [] + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyDescription
+ id + unique mode id used to refer to the mode
+ displayName + actual name of the mode being displayed for each study in the study summary panel
+ + onModeEnter + + hook is called when the mode is entered by the specified route
+ + onModeExit + + hook is called when the mode exited
+ + validationTags + + validationTags
+ + isValidMode + + Checks if the mode is valid for a study
+ + routes + + route config which defines the route address, and the layout for it
+ + extensionDependencies + + extensions needed by the mode
+ + hanging protocol + + list of hanging protocols that the mode should have access to
+ + sopClassHandlers + + list of SOPClass modules needed by the mode
+ + hotkeys + + hotkeys
+ +### Consuming Extensions + +As mentioned in the [Extensions](../extensions/index.md) section, in `OHIF-v3` +developers write their extensions to create reusable functionalities that later +can be used by `modes`. Now, it is time to describe how the registered +extensions will get utilized for a workflow mode via its `id`. + +Each `mode` has a list of its `extensions dependencies` which are the +the `extension` name and version number. In addition, to use a module element you can use the +`${extensionId}.${moduleType}.${element.name}` schema. For instance, if a mode +requires the left panel with name of `AIPanel` that is added by the +`myAIExtension` via the following `getPanelModule` code, it should address it as +`myAIExtension.panelModule.AIPanel` inside the mode configuration file. In the +background `OHIF` will handle grabbing the correct panel via `ExtensionManager`. + +```js title="extensions/myAIExtension/getPanelModule.js" +import PanelAI from './PanelAI.js'; + +function getPanelModule({ + commandsManager, + extensionManager, + servicesManager, +}) { + const wrappedAIPanel = () => { + return ( + + ); + }; + + return [ + { + name: 'AIPanel', + iconName: 'list-bullets', + iconLabel: '', + label: 'AI Panel', + isDisabled: studies => {}, // optional + component: wrappedAIPanel, + }, + ]; +} +``` + +Now, let's look at a simplified code of the `basic viewer` mode which consumes various functionalities +from different extensions. + +```js + +const extensionDependencies = { + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', + '@ohif/extension-measurement-tracking': '^3.0.0', +}; + +const id = 'viewer'; +const version = '3.0.0'; + +function modeFactory({ modeConfiguration }) { + return { + id, + // ... + routes: [ + { + // ... + layoutTemplate: ({ location, servicesManager }) => { + return { + id: ohif.layout, + props: { + leftPanels: ['@ohif/extension-measurement-tracking.panelModule.seriesList'], + rightPanels: ['@ohif/extension-measurement-tracking.panelModule.trackedMeasurements'], + viewports: [ + { + namespace: '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked', + displaySetsToDisplay: ['@ohif/extension-default.sopClassHandlerModule.stack'], + }, + ], + }, + }; + }, + }, + ], + extensions: extensionDependencies, + hangingProtocol: ['@ohif/extension-default.hangingProtocolModule.petCT'], + sopClassHandlers: ['@ohif/extension-default.sopClassHandlerModule.stack'], + // ... + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +} + +export default mode +``` + +### Routes + +routes config is an array of route settings, and the overall look and behavior +of the viewer at the designated route is defined by the `layoutTemplate` and +`init` functions for the route. We will learn more about each of the above +properties inside the [route documentation](./routes.md) + + +### HangingProtocols + +Currently, you can pass your defined hanging protocols inside the +`hangingProtocols` property of the mode's config. If you specify the hanging protocol +explicitly by its name (only string and not array), it will be THE hanging protocol +that the mode runs with. However, if you specify an array of hanging protocols, +they will get ranked based on the displaySetSelector requirements and the winner +will be the hanging protocol that the mode runs with. + + +### SopClassHandlers + +Mode's configuration also accepts the `sopClassHandler` modules that have been +added by the extensions. This information will get used to initialize `DisplaySetService` with the provided SOPClass modules which +handles creation of the displaySets. + + +### Hotkeys + +`hotkeys` is another property in the configuration of a mode that can be defined +to add the specific hotkeys to the viewer on the mode route. Additionally, the +name under which the hotkeys are stored can be configured as `hotkeyName`. +This allows user customization of the mode specific hotkeys. + +```js +// default hotkeys +import { utils } from '@ohif/ui'; + +const { hotkeys } = utils; + +const myHotkeys = [ + { + commandName: 'setToolActive', + commandOptions: { toolName: 'Zoom' }, + label: 'Zoom', + keys: ['z'], + isEditable: true, + }, + { + commandName: 'scaleUpViewport', + label: 'Zoom In', + keys: ['+'], + isEditable: true, + }, +] + +function modeFactory() { + return { + id: '', + id: '', + displayName: '', + /* + ... + */ + hotkeys: { + // The name in preferences to use for this set of hotkeys + // Allows defining different sets for different modes + name: 'custom-hotkey-name', + // And the actual custom values here. + hotkeys:[..hotkeys.defaults.hotkeyBindings, ...myHotkeys] + }, + } +} + +// exports +``` + + + + + +## Registration + +Similar to extension registration, `viewer` will look inside the `pluginConfig.json` to +find the `modes` to register. + + +```js title=platform/app/pluginConfig.json +// Simplified version of the `pluginConfig.json` file +{ + "extensions": [ + { + "packageName": "@ohif/extension-cornerstone", + "version": "3.4.0" + }, + // ... + ], + "modes": [ + { + "packageName": "@ohif/mode-longitudinal", + "version": "3.4.0" + } + ] +} +``` + +:::note Important +You SHOULD NOT directly register modes in the `pluginConfig.json` file. +Use the provided `cli` to add/remove/install/uninstall modes. Read more [here](../../development/ohif-cli.md) +::: + +The final registration and import of the modes happen inside a non-tracked file `pluginImport.js` (this file is also for internal use only). + + +:::note +You can stack multiple panel components on top of each other by providing an array of panel components in the `rightPanels` or `leftPanels` properties. + +For instance we can use + +``` +rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.measurements]] +``` + +This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other. + +::: diff --git a/platform/docs/docs/platform/modes/installation.md b/platform/docs/docs/platform/modes/installation.md new file mode 100644 index 0000000..08c456d --- /dev/null +++ b/platform/docs/docs/platform/modes/installation.md @@ -0,0 +1,12 @@ +--- +sidebar_position: 5 +sidebar_label: Installation +--- + +# Modes: Installation + +OHIF-v3 provides the ability to utilize external modes. + + +You can use ohif `cli` tool to install both local and publicly published +modes on NPM. You can read more [here](../../development/ohif-cli.md) diff --git a/platform/docs/docs/platform/modes/lifecycle.md b/platform/docs/docs/platform/modes/lifecycle.md new file mode 100644 index 0000000..8c0ec41 --- /dev/null +++ b/platform/docs/docs/platform/modes/lifecycle.md @@ -0,0 +1,105 @@ +--- +sidebar_position: 2 +sidebar_label: Lifecycle Hooks +--- + +# Modes: Lifecycle Hooks + +## Overview + +Currently, there are two hooks that are called for modes: + +- onModeInit +- onModeEnter +- onModeExit + +## onModeInit + +This hook gets run before the defined route has been entered by the mode. This +hook can be used for initialization before the first render. + +This is called before `onModeEnter` calls. This allows modes to add or activate their own +data sources and configuration before entering the mode (pre registrations). + +## onModeEnter + +This hook gets run after the defined route has been entered by the mode. This +hook can be used to initialize the data, services and appearance of the viewer +upon the first render, in any way that is custom to the mode. + +This is called after service `onModeEnter` calls so that the entry into a mode +is done in a predefined/fixed state. That allows any restoring of existing state +to be performed. + +For instance, in `longitudinal` mode we are using this hook to initialize the +`ToolBarService` and set the window level/width tool to be active and add +buttons to the toolbar. + +:::note Tip + +In OHIF Version 3.1, there is a new service `ToolGroupService` that is used to +define and manage tools for the group of viewports. This is a new concept +borrowed from the Cornerstone ToolGroup, and you can read more +[here](https://www.cornerstonejs.org/docs/concepts/cornerstone-tools/toolgroups/) + +::: + +```js +function modeFactory() { + return { + id: '', + version: '', + displayName: '', + onModeEnter: ({ servicesManager, extensionManager }) => { + const { ToolBarService, ToolGroupService } = servicesManager.services; + + // Init Default and SR ToolGroups + initToolGroups(extensionManager, ToolGroupService); + + ToolBarService.addButtons(toolbarButtons); + ToolBarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'WindowLevel', + 'Pan', + 'Capture', + 'Layout', + 'MoreTools', + ]); + }, + /* + ... + */ + }; +} +``` + +## onModeExit + +This hook is called when the viewer navigates away from the route in the url. +It is called BEFORE the service specific onModeExit calls are performed, and +thus still has access to stateful data which can be cached or stored before +the services clean themselves up. +This is the place for cleaning up NON-service specific data, and services +by unsubscribing to the events. The cleanup of the service itself is intended +to occur in the service `onModeEnter`. + +For instance, it can be used to reset the `ToolBarService` which reset the +toggled buttons. + +```js +function modeFactory() { + return { + id: '', + displayName: '', + onModeExit: ({ servicesManager, extensionManager }) => { + // Turn of the toggled states on exit + const { ToolBarService } = servicesManager.services; + ToolBarService.reset(); + }, + /* + ... + */ + }; +} +``` diff --git a/platform/docs/docs/platform/modes/routes.md b/platform/docs/docs/platform/modes/routes.md new file mode 100644 index 0000000..5a3bdc6 --- /dev/null +++ b/platform/docs/docs/platform/modes/routes.md @@ -0,0 +1,359 @@ +--- +sidebar_position: 3 +sidebar_label: Routes +--- + +# Mode: Routes + +## Overview + +Modes are tied to a specific route in the viewer, and multiple modes/routes can +be present within a single application. This makes `routes` config, THE most +important part of the mode configuration. + +## Route + +`@ohif/app` **compose** extensions to build applications on different routes +for the platform. + +Below, you can see a simplified version of the `longitudinal` mode and the +`routes` section which has defined one `route`. Each route has three different +configuration: + +- **route path**: defines the route path to access the built application for + that route +- **route init**: hook that runs when application enters the defined route path, + if not defined the default init function will run for the mode. +- **route layout**: defines the layout of the application for the specified + route (panels, viewports) + +```js +function modeFactory() { + return { + id: 'viewer', + version: '3.0.0', + displayName: '', + routes: [ + { + path: 'longitudinal', + /*init: ({ servicesManager, extensionManager }) => { + //defaultViewerRouteInit + },*/ + layoutTemplate: ({ location, servicesManager }) => { + return { + id: ohif.layout, + props: { + leftPanels: [ + '@ohif/extension-measurement-tracking.panelModule.seriesList', + ], + rightPanels: [ + '@ohif/extension-measurement-tracking.panelModule.trackedMeasurements', + ], + viewports: [ + { + namespace: + '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked', + displaySetsToDisplay: [ + '@ohif/extension-default.sopClassHandlerModule.stack', + ], + }, + { + namespace: '@ohif/extension-cornerstone-dicom-sr.viewportModule.dicom-sr', + displaySetsToDisplay: [ + '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr', + ], + }, + ], + }, + }; + }, + }, + ], + /* + ... + */ + }; +} +``` + +### Route: path + +Upon initialization the viewer will consume extensions and modes and build up +the route desired, these can then be accessed via the study list, or directly +via url parameters. + +> Note: Currently, only one route is built for each mode, but we will enhance +> route creation to create separate routes based on the `path` config for each +> `route` object. + +There are two types of `routes` that are created by the mode. + +- Routes with dataSourceName `/${mode.id}/${dataSourceName}` +- Routes without dataSourceName `/${mode.id}` + +Therefore, navigating to +`http://localhost:3000/viewer/?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1` +will run the app with the layout and functionalities of the `viewer` mode using +the `defaultDataSourceName` which is defined in the +[App Config](../../configuration/configurationFiles.md) + +You can use the same exact mode using a different registered data source (e.g., +`dicomjson`) by navigating to +`http://localhost:3000/viewer/dicomjson/?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1` + +### Route: init + +The mode also has an init hook, which initializes the mode. If you don't define +an `init` function the `default init` function will get run (logic is located +inside `Mode.jsx`). However, you can define you own init function following +certain steps which we will discuss next. + +#### Default init + +Default init function will: + +- `retriveSeriesMetaData` for the `studyInstanceUIDs` that are defined in the + URL. +- Subscribe to `instanceAdded` event, to make display sets after a series have + finished retrieving its instances' metadata. +- Subscribe to `seriesAdded` event, to run the `HangingProtocolService` on the + retrieves series from the study. + +A _simplified_ "pseudocode" for the `defaultRouteInit` is: + +```jsx +async function defaultRouteInit({ + servicesManager, + studyInstanceUIDs, + dataSource, +}) { + const { + DisplaySetService, + HangingProtocolService, + } = servicesManager.services; + + // subscribe to run the function after the event happens + DicomMetadataStore.subscribe( + 'instancesAdded', + ({ StudyInstanceUID, SeriesInstanceUID }) => { + const seriesMetadata = DicomMetadataStore.getSeries( + StudyInstanceUID, + SeriesInstanceUID + ); + DisplaySetService.makeDisplaySets(seriesMetadata.instances); + } + ); + + studyInstanceUIDs.forEach(StudyInstanceUID => { + dataSource.retrieve.series.metadata({ StudyInstanceUID }); + }); + + DicomMetadataStore.subscribe('seriesAdded', ({ StudyInstanceUID }) => { + const studyMetadata = // get study metadata and displaySets + HangingProtocolService.run({studies, displaySets, activeStudy}); + }); + + return unsubscriptions; +} +``` + +#### Writing a custom init + +You can add your custom init function to enhance the default initialization for: + +- Fetching annotations from a server for the current study +- Changing the initial image index of the series to be displayed at first +- Caching the next study in the work list +- Adding a custom sort for the series to be displayed on the study browser panel + +and lots of other modifications. + +You just need to make sure, the mode `dataSource.retrieve.series.metadata`, +`makeDisplaySets` and `run` the HangingProtocols at some point. There are +various `events` that you can subscribe to and add your custom logic. **point to +events** + +For instance for jumping to the slice where a measurement is located at the +initial render, you need to follow a pattern similar to the following: + +```jsx +init: async ({ + servicesManager, + extensionManager, + hotkeysManager, + dataSource, + studyInstanceUIDs, +}) => { + const { DisplaySetService } = servicesManager.services; + + /** + ... + **/ + + const onDisplaySetsAdded = ({ displaySetsAdded, options }) => { + const displaySet = displaySetsAdded[0]; + const { SeriesInstanceUID } = displaySet; + + const toolData = myServer.fetchMeasurements(SeriesInstanceUID); + + if (!toolData.length) { + return; + } + + toolData.forEach(tool => { + const instance = displaySet.images.find( + image => image.SOPInstanceUID === tool.SOPInstanceUID + ); + }); + + MeasurementService.addMeasurement(/**...**/); + }; + + // subscription to the DISPLAY_SETS_ADDED + const { unsubscribe } = DisplaySetService.subscribe( + DisplaySetService.EVENTS.DISPLAY_SETS_ADDED, + onDisplaySetsAdded + ); + + /** + ... + **/ + + return unsubscriptions; +}; +``` + +### Route: layoutTemplate + +`layoutTemplate` is the last configuration for a certain route in a `mode`. +`layoutTemplate` is a function that returns an object that configures the +overall layout of the application. The returned object has two properties: + +- `id`: the id of the `layoutTemplate` being used (it should have been + registered via an extension) +- `props`: the required properties to be passed to the `layoutTemplate`. + +For instance `default extension` provides a layoutTemplate that builds the app +using left/right panels and viewports. Therefore, the `props` include +`leftPanels`, `rightPanels` and `viewports` sections. Note that the +`layoutTemplate` defines the properties it is expecting. So, if you write a +`layoutTemplate-2` that accepts a footer section, its logic should be written in +the extension, and any mode that is interested in using `layoutTemplate-2` +**should** provide the `id` for the footer component. + +**What module should the footer be registered?** + +```js +/* +... +*/ +layoutTemplate: ({ location, servicesManager }) => { + return { + id: '@ohif/extension-default.layoutTemplateModule.viewerLayout', + props: { + leftPanels: [ + 'myExtension.panelModule.leftPanel1', + 'myExtension.panelModule.leftPanel2', + ], + rightPanels: ['myExtension.panelModule.rightPanel'], + viewports: [ + { + namespace: 'myExtension.viewportModule.viewport1', + displaySetsToDisplay: ['myExtension.sopClassHandlerModule.sop1'], + }, + { + namespace: 'myExtension.viewportModule.viewport2', + displaySetsToDisplay: ['myExtension.sopClassHandlerModule.sop2'], + }, + ], + }, + }; +}; +/* +... +*/ +``` + +:::note +You can stack multiple panel components on top of each other by providing an array of panel components in the `rightPanels` or `leftPanels` properties. + +For instance we can use + +``` +rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.measurements]] +``` + +This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other. + +::: + +## FAQ + +> What is the difference between `onModeEnter` and `route.init` + +`onModeEnter` gets run first than `route.init`; however, each route can have +their own `init`, but they share the `onModeEnter`. + +> How can I change the `workList` appearance or add a new login page? + +This is where `OHIF-v3` shines! Since the default `layoutTemplate` is written +for the viewer part, you can simply add a new `layoutTemplate` and use the +component you have written for that route. `Mode` handle showing the correct +component for the specified route. + +```js +function modeFactory() { + return { + id: 'viewer', + displayName: '', + routes: [ + { + path: 'worklist', + init, + layoutTemplate: ({ location, servicesManager }) => { + return { + id: 'worklistLayout', + props: { + component: 'myNewWorkList', + }, + }; + }, + }, + ], + /* + ... + */ + }; +} +``` + +> How can I navigate to (or show) a different study via the browser history/URL? + +There is a command that does this: `navigateHistory`. It takes an object +argument with the `NavigateHistory` type: + +``` +export type NavigateHistory = { + to: string; // the URL to navigate to + options?: { + replace?: boolean; // replace or add/push to history? + }; +}; +``` + +For instance one could bind a hot key to this command to show a specific study +like this... + +``` + { + commandName: 'navigateHistory', + commandOptions: { + to: + '/viewer?StudyInstanceUIDs=1.2.3', + }, + context: 'DEFAULT', + label: 'Nav Study', + keys: ['n'], + isEditable: true, + }, +``` diff --git a/platform/docs/docs/platform/modes/validity.md b/platform/docs/docs/platform/modes/validity.md new file mode 100644 index 0000000..e3f70af --- /dev/null +++ b/platform/docs/docs/platform/modes/validity.md @@ -0,0 +1,37 @@ +--- +sidebar_position: 4 +sidebar_label: Validity +--- +# Mode: Validity + + +## Overview +There are two mechanism for checking the validity of a mode for a study. + +- `isValidMode`: which is called on a selected study in the workList. +- `validTags` + + + +## isValidMode +This hook can be used to define a function that return a `boolean` which decided the +validity of the mode based on `StudyInstanceUID` and `modalities` that are in the study. + +For instance, for pet-ct mode, both `PT` and 'CT' modalities should be available inside the study. + +```js +function modeFactory() { + return { + id: '', + displayName: '', + isValidMode: ({ modalities, StudyInstanceUID }) => { + const modalities_list = modalities.split('\\'); + const validMode = ['CT', 'PT'].every(modality => modalities_list.includes(modality)); + return validMode; + }, + /* + ... + */ + } +} +``` diff --git a/platform/docs/docs/platform/pwa-vs-packaged.md b/platform/docs/docs/platform/pwa-vs-packaged.md new file mode 100644 index 0000000..bdc411e --- /dev/null +++ b/platform/docs/docs/platform/pwa-vs-packaged.md @@ -0,0 +1,34 @@ +--- +sidebar_position: 3 +--- + +# PWA vs Packaged + +It's important to know that the OHIF Viewer project provides two different build +processes: + +```bash +# Static Asset output: For deploying PWAs +yarn run build +``` + +## Progressive Web Application (PWA) + +> [Progressive Web Apps][pwa] are a new breed of web applications that meet the +> [following requirements][pwa-checklist]. Notably, targeting a PWA allows us +> provide a reliable, fast, and engaging experience across different devices and +> network conditions. + +The OHIF Viewer is maintained as a [monorepo][monorepo]. We use WebPack to build +the many small static assets that comprise our application. Also generated is an +`index.html` that will serve as an entry point for loading configuration and the +application, as well as a `service-worker` that can intelligently cache files so +that subsequent requests are from the local file system instead of over the +network. + +You can read more about this particular strategy in our +[Build for Production Deployment Guide](./../deployment/build-for-production.md) + +## Commonjs Bundle (Packaged Script) + +We are not supporting `Commonjs` bundling inside `OHIF-v3`. diff --git a/platform/docs/docs/platform/scope-of-project.md b/platform/docs/docs/platform/scope-of-project.md new file mode 100644 index 0000000..95460e4 --- /dev/null +++ b/platform/docs/docs/platform/scope-of-project.md @@ -0,0 +1,69 @@ +--- +sidebar_position: 1 +--- +# Scope of Project + +The OHIF Viewer is a web based medical imaging viewer. This allows it to be used +on almost any device, anywhere. The OHIF Viewer is what is commonly referred to +as a ["Dumb Client"][simplicable] + +> A dumb client is software that fully depends on a connection to a server or +> cloud service for its functionality. Without a network connection, the +> software offers nothing useful. - [simplicable.com][simplicable] + +While the Viewer persists some data, it's scope is limited to caching things +like user preferences and previous query parameters. Because of this, the Viewer +has been built to be highly configurable to work with almost any web accessible +data source. + +![scope-of-project diagram](./../assets/img/scope-of-project.png) + +To be more specific, the OHIF Viewer is a collection of HTML, JS, and CSS files. +These can be delivered to your end users however you would like: + +- From the local network +- From a remote web server +- From a CDN (content delivery network) +- From a service-worker's cache +- etc. + +These "static asset" files are referred to collectively as a "Progressive Web +Application" (PWA), and have the same capabilities and limitations that all PWAs +have. + +All studies, series, images, imageframes, metadata, and the images themselves +must come from an external source. There are many, many ways to provide this +information, the OHIF Viewer's scope **DOES NOT** encompass providing _any_ +data; only the configuration necessary to interface with one or more of these +many data sources. The OHIF Viewer's scope **DOES** include configuration and +support for services that are protected with OpenID-Connect. + +In an effort to aid our users and contributors, we attempt to provide several +[deployment and hosting recipes](../deployment/index.md) as potential starting +points. These are not meant to be rock solid, production ready, solutions; like +most recipes, they should be augmented to best fit you and your organization's +taste, preferences, etc. + +## FAQ + +_Am I able to cache studies for offline viewing?_ + +Not currently. A web page's offline cache capabilities are limited and somewhat +volatile (mostly imposed at the browser vendor level). For more robust offline +caching, you may want to consider a server on the local network, or packaging +the OHIF Viewer as a desktop application. + +_Does the OHIF Viewer work with the local filesystem?_ + +It is possible to accomplish this through extensions; however, for a user +experience that accommodates a large number of studies, you would likely need to +package the OHIF Viewer as an [Electron app][electron]. + + + + +[simplicable]: https://simplicable.com/new/dumb-client +[electron]: https://electronjs.org/ + diff --git a/platform/docs/docs/platform/services/_category_.json b/platform/docs/docs/platform/services/_category_.json new file mode 100644 index 0000000..9a6c3ae --- /dev/null +++ b/platform/docs/docs/platform/services/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Services", + "position": 11 +} diff --git a/platform/docs/docs/platform/services/customization-service/Measurements.md b/platform/docs/docs/platform/services/customization-service/Measurements.md new file mode 100644 index 0000000..7d8a698 --- /dev/null +++ b/platform/docs/docs/platform/services/customization-service/Measurements.md @@ -0,0 +1,9 @@ +--- +title: Measurements +--- + + + +import { measurementsCustomizations, TableGenerator } from './sampleCustomizations'; + +{TableGenerator(measurementsCustomizations)} diff --git a/platform/docs/docs/platform/services/customization-service/Segmentation.md b/platform/docs/docs/platform/services/customization-service/Segmentation.md new file mode 100644 index 0000000..7953b39 --- /dev/null +++ b/platform/docs/docs/platform/services/customization-service/Segmentation.md @@ -0,0 +1,9 @@ +--- +title: Segmentation +--- + + + +import { segmentationCustomizations, TableGenerator } from './sampleCustomizations'; + +{TableGenerator(segmentationCustomizations)} diff --git a/platform/docs/docs/platform/services/customization-service/StudyBrowser.md b/platform/docs/docs/platform/services/customization-service/StudyBrowser.md new file mode 100644 index 0000000..fbbf383 --- /dev/null +++ b/platform/docs/docs/platform/services/customization-service/StudyBrowser.md @@ -0,0 +1,11 @@ +--- +title: Study Browser +--- + +# Study Browser + +The Study Browser is a component that allows users to browse and manage studies. + +import { studyBrowserCustomizations, TableGenerator } from './sampleCustomizations'; + +{TableGenerator(studyBrowserCustomizations)} diff --git a/platform/docs/docs/platform/services/customization-service/_category_.json b/platform/docs/docs/platform/services/customization-service/_category_.json new file mode 100644 index 0000000..710e108 --- /dev/null +++ b/platform/docs/docs/platform/services/customization-service/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Customization Service", + "position": 4 +} diff --git a/platform/docs/docs/platform/services/customization-service/advanced.md b/platform/docs/docs/platform/services/customization-service/advanced.md new file mode 100644 index 0000000..7efe64d --- /dev/null +++ b/platform/docs/docs/platform/services/customization-service/advanced.md @@ -0,0 +1,98 @@ +--- +title: Advanced Customization +--- + + +Below is an overview of how `transform` and `inheritsFrom` work within this customization system. They allow you to build a hierarchy of customizations in which items can inherit fields from a parent and then optionally apply a transformation before returning the final result. + + +## `inheritsFrom` + +### Purpose +Indicates that the current customization should inherit and merge fields from another customization. The system fetches the parent customization, merges its properties, and returns a combined object. + +### How It Works +1. When you request or transform a customization that has `inheritsFrom: "parentCustomizationId"`, the service looks up `parentCustomizationId` via `getCustomization(...)`. +2. Properties from the parent get copied into the child, but the childโ€™s own properties overwrite any matching ones from the parent. +3. If the child has a `transform` function, it runs after the merge. + +### Example +```js +export default { + measurementsContextMenu: { + $set: { + inheritsFrom: 'ohif.contextMenu', + menus: [ + { + selector: ({ nearbyToolData }) => !!nearbyToolData, + items: [ + // ... + ], + }, + ], + }, + }, +}; +``` +Here, `measurementsContextMenu` inherits from `ohif.contextMenu`. During retrieval or transformation, the system merges `ohif.contextMenu` into `measurementsContextMenu`. + +--- + +## `transform` + +### Purpose +Specifies a function that can modify or enhance the customization object at runtime. Often used to run extra setup code or combine fields in a special way. + +### How It Works +1. You define a `transform(customizationService)` function inside your customization object. +2. When the system retrieves the customization, after merging any inherited fields, it calls `transform`. +3. The function may return an updated object, clone existing properties, or apply logic to nested items. + +### Example +```js +export default { + '@ohif/contextMenuAnnotationCode': { + $transform: function (customizationService) { + const { code: codeRef } = this; + if (!codeRef) { + throw new Error(`item ${this} has no code ref`); + } + const codingValues = customizationService.getCustomization('codingValues'); + const code = codingValues[codeRef]; + + return { + ...this, + codeRef, + code: { ref: codeRef, ...code }, + label: this.label || code.text || codeRef, + commands: [{ commandName: 'updateMeasurement' }], + }; + }, + }, +}; +``` +In this snippet, the `transform` function: +- Reads a code reference from `this`. +- Looks up more data for that code in `codingValues`. +- Merges those details back into `this` before returning the final object. + +--- + +## Common Use Cases + +1. **Base and Specialized Customizations** + Use `inheritsFrom` to define a broad, general customization (e.g., a generic context menu) and then create specialized versions that only override certain fields. + +2. **Dynamic Assembly** + Use `transform` when you need to compute or modify fields based on application state or other registered customizations. + +3. **Nested Items** + If an item within the customization also has `inheritsFrom`, it will follow the same inheritance flow and can run its own `transform` logic. + +--- + +**Key Points** +- `inheritsFrom` is a reference to another customizationโ€™s ID. +- If `transform` is defined, it always runs after inheritance is resolved. +- Merging is shallow: child properties override the parentโ€™s. +- You can nest multiple levels of inheritance, each possibly having its own `transform` step. diff --git a/platform/docs/docs/platform/services/customization-service/contextMenu.md b/platform/docs/docs/platform/services/customization-service/contextMenu.md new file mode 100644 index 0000000..3455310 --- /dev/null +++ b/platform/docs/docs/platform/services/customization-service/contextMenu.md @@ -0,0 +1,21 @@ +--- +sidebar_label: Context Menu +sidebar_position: 3 +--- + +# Context Menu + + + + + +Context menus can be created by defining the menu structure and click +interaction, as defined in the `ContextMenu/types`. There are examples +below specific to the cornerstone context, because the actual click +handler and attributes used to decide when and how to display the menu +are specific to the context used for where the menu is displayed. + +## Cornerstone Context Menu + +The default cornerstone context menu can be customized by setting the +`cornerstoneContextMenu`. For a full example, see `findingsContextMenu`. diff --git a/platform/docs/docs/platform/services/customization-service/customRoutes.md b/platform/docs/docs/platform/services/customization-service/customRoutes.md new file mode 100644 index 0000000..cc52cae --- /dev/null +++ b/platform/docs/docs/platform/services/customization-service/customRoutes.md @@ -0,0 +1,83 @@ +--- +sidebar_label: Custom Routes +sidebar_position: 2 +--- + +# customRoutes + +* Name: `routes.customRoutes` global +* Attributes: +** `routes` of type List of route objects (see `route/index.tsx`) is a set of route objects to add. +** Should any element of routes match an existing baked in element, the baked in one will be replaced. +** `notFoundRoute` is the route to display when nothing is found (this has to be at the end of the overall list, so can't be added to routes) + +### Example + +Since custom routes use React, they should be defined as modules inside the extension that is providing them. And cannot be +in the AppConfig (yet). + + +```js +export default function getCustomizationModule({ servicesManager, extensionManager }) { + return [ + { + name: 'helloPage', + value: { + 'routes.customRoutes': { + routes: { + $push: [ + { + path: '/custom', + children: () =>

Hello Custom Route

, + }, + ], + }, + }, + }, + }, + ] +``` + +Then after you define the module, you can add it to the customizationService in the AppConfig and reference it by the name you provided. + +```js +customizationService: [ + // Shows a custom route -access via http://localhost:3000/custom + '@ohif/extension-default.customizationModule.helloPage', +], +``` + +You can provide multiple custom routes in the same module, for instance another extension can also push to the routes array. + +```js +export default function getCustomizationModule({ servicesManager, extensionManager }) { + return [ + { + name: 'secondPage', + value: { + customRoutes: { + routes: { + $push: [ + { + path: '/second', + children: () =>

Hello Second Route

, + }, + ], + }, + }, + }, + }, + ] +} +``` + +Then you can add it to the customizationService in the AppConfig and reference it by the name you provided. + +```js +customizationService: [ + // Shows a custom route -access via http://localhost:3000/custom + '@ohif/extension-default.customizationModule.helloPage', + // Shows a custom route -access via http://localhost:3000/second + '@ohif/extension-default.customizationModule.secondPage', +], +``` diff --git a/platform/docs/docs/platform/services/customization-service/customizationService.md b/platform/docs/docs/platform/services/customization-service/customizationService.md new file mode 100644 index 0000000..cda4869 --- /dev/null +++ b/platform/docs/docs/platform/services/customization-service/customizationService.md @@ -0,0 +1,544 @@ +--- +sidebar_label: Introduction +sidebar_position: 1 +--- + +import { customizations, TableGenerator } from './sampleCustomizations'; +import Heading from '@theme/Heading'; +import TOCInline from '@theme/TOCInline'; + +# Customization Service + +There are a lot of places where users may want to configure certain elements +differently between different modes or for different deployments. A mode +example might be the use of a custom overlay showing mode related DICOM header +information such as radiation dose or patient age. + +The use of `customizationService` enables these to be defined in a typed fashion by +providing an easy way to set default values for this, but to allow a +non-default value to be specified by the configuration or mode. + + +:::note + +`customizationService` itself doesn't implement the actual customization, +but rather just provide mechanism to register reusable prototypes, to configure +those prototypes with actual configurations, and to use the configured objects +(components, data, whatever). + +Actual implementation of the customization is totally up to the component that +supports customization. +::: + + +## General Overview + +This framework allows you to configure many features, or "slots," through customization modules. Extensions can choose to offer their own module, which outlines which values can be changed. By looking at each extension's getCustomizationModule(), you can see which objects or components are open to customization. + +Below is a high-level example of how you might define a default customization and then consume and override it: + +1. **Defining a Customizable Default** + + In your extension, you might export a set of default configurations (for instance, a list that appears in a panel). Here, you provide an identifier and store the default list under that identifier. This makes the item discoverable by the customization service: + + ```js + // Inside your extensionโ€™s customization module + export default function getCustomizationModule() { + return [ + { + name: 'default', + value: { + defaultList: ['Item A', 'Item B'], + }, + }, + ]; + } + ``` + + By naming it `default`, it is automatically registered. + + :::info + You might want to have customizations ready to use in your application without actually applying them. In such cases, you can name them something other than `default`. For example, in your mode, you can do this: + + ```js + customizationService.setCustomizations([ + '@ohif/extension-cornerstone-dicom-seg.customizationModule.dicom-seg-sorts', + ]); + ``` + + This is really useful when you want to apply a set of customizations as a pack, kind of like a bundle. + ::: + +3. **Retrieving the Default Customization** + In the panel or component (or whatever) that needs the list, you retrieve it using `getCustomization`: + + ```js + const myList = customizationService.getCustomization('defaultList'); + // If unmodified, this returns ['Item A', 'Item B'] + ``` + + This allows your component to always fetch the most current version (original default or overridden). + +4. **Overriding from Outside** + To customize this list outside your extension, call `setCustomizations` with the identifier (`'defaultList'`). For example, a mode can modify the list to add or change items: + + ```js + // From within a mode (or globally) + customizationService.setCustomizations({ + 'defaultList': { + $set: ['New Item 1', 'New Item 2'], + }, + }); + ``` + + The next time any panel calls `getCustomization('defaultList')`, it will get the updated list. + + Don't worry we will go over the `$set` syntax in more detail later. + +--- + +## Scope of Customization + + +Customizations can be declared at three different scopes, each with its own priority and lifecycle. These scopes determine how and when customizations are applied. + + +### 1. **Default Scope** + - **Purpose**: Establish baseline or "fallback" values that extensions provide. + - **Options**: + 1. **Via Extensions**: + - Implement a `getCustomizationModule` function in your extension and name it `default`. + ```tsx + function getCustomizationModule() { + return [ + { + name: 'default', + value: { + 'studyBrowser.sortFunctions': { + $set: [ + { + label: 'Default Sort Function', + sortFunction: (a, b) => a.SeriesDate - b.SeriesDate, + }, + ], + }, + }, + }, + ]; + } + ``` + 2. **Using the `setCustomizations` Method**: + - Call `setCustomizations` in your application and specify `CustomizationScope.Default` as the second argument: + ```tsx + customizationService.setCustomizations( + { + 'studyBrowser.sortFunctions': { + $set: [ + { + label: 'Default Sort Function', + sortFunction: (a, b) => a.SeriesDate - b.SeriesDate, + }, + ], + }, + }, + CustomizationScope.Default + ); + ``` + + +### 2. **Mode Scope** + - **Purpose**: Apply customizations specific to a particular mode. + - **Lifecycle**: These customizations are cleared or reset when switching between modes. + - **Example**: Use the `setCustomizations` method to define mode-specific behavior. + ```tsx + customizationService.setCustomizations({ + 'studyBrowser.sortFunctions': { + $set: [ + { + label: 'Mode-Specific Sort Function', + sortFunction: (a, b) => b.SeriesDate - a.SeriesDate, + }, + ], + }, + }); + ``` + + + +### 3. **Global Scope** + - **Purpose**: Apply system-wide customizations that override both default and mode-scoped values. + - **How to Configure**: + 1. Add global customizations directly to the application's configuration file: + ```jsx + window.config = { + name: 'config/default.js', + routerBasename: '/', + customizationService: [ + { + 'studyBrowser.sortFunctions': { + $push: [ + { + label: 'Global Sort Function', + sortFunction: (a, b) => b.SeriesDate - a.SeriesDate, + }, + ], + }, + }, + ], + }; + ``` + + 2. Use Namespaced Extensions: + - Instead of directly specifying customizations in the configuration, you can refer to a predefined customization module within an extension: + + ```jsx + window.config = { + name: 'config/default.js', + routerBasename: '/', + customizationService: [ + '@ohif/extension-cornerstone.customizationModule.newCustomization', + ], + }; + ``` + + - In this example, the `newCustomization` module within the `@ohif/extension-cornerstone` extension contains the global customizations. The application will load and apply these settings globally. + + ```tsx + function getCustomizationModule() { + return [ + { + name: 'newCustomization', + value: { + 'studyBrowser.sortFunctions': { + $push: [ + { + label: 'Global Namespace Sort Function', + sortFunction: (a, b) => b.SeriesDate - a.SeriesDate, + }, + ], + }, + }, + }, + ]; + } + ``` + + +### Priority of Scopes +When a customization is retrieved: +1. **Global Scope**: Takes precedence if defined. +2. **Mode Scope**: Used if no global customization is defined. +3. **Default Scope**: Fallback when neither global nor mode-specific values are available. + + +As you have guessed the `.setCustomizations` accept a second argument which is the scope. By default it is set to `mode`. + + +## Customization Syntax + + +The customization syntax is designed to offer **flexibility** when modifying configurations. Instead of simply replacing values, you can perform granular updates like appending items to arrays, inserting at specific indices, updating deeply nested fields, or applying filters. This flexibility ensures that updates are efficient, targeted, and suitable for complex data structures. + +
+ +Why a Special Syntax? + + +Traditional value replacement might not be ideal in scenarios such as: +- **Appending or prepending** to an existing list instead of overwriting it. +- **Selective updates** for specific fields in an object without affecting other fields. +- **Filtering or merging** nested items in arrays or objects while preserving other parts. + +To address these needs, the customization service uses a **special syntax** inspired by [immutability-helper](https://github.com/kolodny/immutability-helper) commands. Below are examples of each operation. + +
+ +--- + +### 1. Replace a Value (`$set`) + +Use `$set` to entirely replace a value. This is the simplest operation which would replace the entire value. + +```js +// Before: someKey = 'Old Value' +customizationService.setCustomizations({ + someKey: { $set: 'New Value' }, +}); +// After: someKey = 'New Value' +``` + +Example with study browser: + +```js +// Before: studyBrowser.sortFunctions = [] + +customizationService.setCustomizations({ + 'studyBrowser.sortFunctions': { + $set: [ + { + label: 'Sort by Patient ID', + sortFunction: (a, b) => a.PatientID.localeCompare(b.PatientID), + }, + ], + }, +}); + +// After: studyBrowser.sortFunctions = [{label: 'Sort by Patient ID', sortFunction: ...}] +``` + +--- + +### 2. Add to an Array (`$push` and `$unshift`) + +- **`$push`**: Appends items to the end of an array. +- **`$unshift`**: Adds items to the beginning of an array. + +```js +// Before: NumbersList = [1, 2, 3] + +// Push items to the end +customizationService.setCustomizations({ + 'NumbersList': { $push: [5, 6] }, +}); +// After: NumbersList = [1, 2, 3, 5, 6] + +// Unshift items to the front +customizationService.setCustomizations({ + 'NumbersList': { $unshift: [0] }, +}); +// After: NumbersList = [0, 1, 2, 3, 5, 6] +``` + +--- + +### 3. Insert at Specific Index (`$splice`) + +Use `$splice` to insert, replace, or remove items at a specific index in an array. + +```js +// Before: NumbersList = [1, 2, 3] + +customizationService.setCustomizations({ + 'NumbersList': { + $splice: [ + [2, 0, 99], // Insert 99 at index 2 + ], + }, +}); +// After: NumbersList = [1, 2, 99, 3] +``` + +--- + +### 4. Merge Object Properties (`$merge`) + +Use `$merge` to update specific fields in an object without affecting other fields. + +```js +// Before: SeriesInfo = { label: 'Original Label', sortFunction: oldFunc } + +customizationService.setCustomizations({ + 'SeriesInfo': { + $merge: { + label: 'Updated Label', + extraField: true, + }, + }, +}); +// After: SeriesInfo = { label: 'Updated Label', sortFunction: oldFunc, extraField: true } +``` + +Example with nested merge: +```js +// Before: SeriesInfo = { advanced: { subKey: 'oldValue' } } + +customizationService.setCustomizations({ + 'SeriesInfo': { + advanced: { + $merge: { + subKey: 'updatedSubValue', + }, + }, + }, +}); +// After: SeriesInfo = { advanced: { subKey: 'updatedSubValue' } } +``` + +--- + +### 5. Apply a Function (`$apply`) + +Use `$apply` when you need to compute the new value dynamically. + +```js +// Before: SeriesInfo = { label: 'Old Label', data: 123 } + +customizationService.setCustomizations({ + 'SeriesInfo': { + $apply: oldValue => ({ + ...oldValue, + label: 'Computed Label', + }), + }, +}); +// After: SeriesInfo = { label: 'Computed Label', data: 123 } +``` + +--- + +### 6. Filter and Modify (`$filter`) + +Use `$filter` to find specific items in arrays (or objects) and apply changes. + +```js +// Before: advanced = { +// functions: [ +// { id: 'seriesDate', label: 'Original Label' }, +// { id: 'other', label: 'Other Label' } +// ] +// } + +customizationService.setCustomizations({ + 'advanced': { + $filter: { + match: { id: 'seriesDate' }, + $merge: { + label: 'Updated via Filter', + }, + }, + }, +}); +// After: advanced = { +// functions: [ +// { id: 'seriesDate', label: 'Updated via Filter' }, +// { id: 'other', label: 'Other Label' } +// ] +// } +``` + +:::note + +Note `$filter` will look recursively for +an object that matches the `match` criteria and then apply the `$merge` or `$set` operation to it. + +Note in the example above we are not doing anything with the `functions` array. + +::: + + +Example with deeply nested filter: +```js +// Before: advanced = { +// functions: [{ +// id: 'seriesDate', +// viewFunctions: [ +// { id: 'axial', label: 'Original Axial' } +// ] +// }] +// } + +customizationService.setCustomizations({ + 'advanced': { + $filter: { + match: { id: 'axial' }, + $merge: { + label: 'Axial (via Filter)', + }, + }, + }, +}); +// After: advanced = { +// functions: [{ +// id: 'seriesDate', +// viewFunctions: [ +// { id: 'axial', label: 'Axial (via Filter)' } +// ] +// }] +// } +``` + +--- + +### Summary of Commands + +| **Command** | **Purpose** | **Example** | +|-------------|-----------------------------------------------|---------------------------------------| +| `$set` | Replace a value entirely | Replace a list or object | +| `$push` | Append items to an array | Add to the end of a list | +| `$unshift` | Prepend items to an array | Add to the start of a list | +| `$splice` | Insert, remove, or replace at specific index | Modify specific indices in a list | +| `$merge` | Update specific fields in an object | Change a subset of fields | +| `$apply` | Compute the new value dynamically | Apply a function to transform values | +| `$filter` | Find and update specific items in arrays | Target nested structures | +| `$transform`| Apply a function to transform the customization | Apply a function to transform values | + +## Building Customizations Across Multiple Extensions + +Sometimes it is useful to build customizations across multiple extensions. For example, you may want to build a default list of tools inside a vieweport. But then each extension may want to add their own tools to the list. + +Lets say i have one default sorting function in my default extension. + +```js +function getCustomizationModule() { + return [ + { + name: 'default', + value: { + 'studyBrowser.sortFunctions': [ + { + label: 'Series Number', + sortFunction: (a, b) => { + return a?.SeriesNumber - b?.SeriesNumber; + }, + }, + ], + }, + }, + ]; +} +``` + +This will result in having only series number as the default sorting function. + +but now in another extension let's say dicom-seg extension we can add another sorting function. + +```js +function getCustomizationModule() { + return [ + { + name: "dicom-seg-sorts", + value: { + "studyBrowser.sortFunctions": { + $push: [ + { + label: "Series Date", + sortFunction: (a, b) => { + return a?.SeriesDate - b?.SeriesDate; + }, + }, + ], + }, + }, + }, + ]; +} +``` + +But since the module is not `default` it will not get applied, but in my segmentation mode i can do + + +```js +onModeEnter() { + customizationService.setCustomizations([ + '@ohif/extension-cornerstone-dicom-seg.customizationModule.dicom-seg-sorts', + ]); +} +``` + +needless to say if you opted to choose `name: default` in the `getCustomizationModule` it was applied globally. + +## Customizable Parts of OHIF + +Below we are providing the example configuration for global scenario (using the configuration file), however, you can also use the `setCustomizations` method to set the customizations. + +{TableGenerator(customizations)} diff --git a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx new file mode 100644 index 0000000..5b3bd93 --- /dev/null +++ b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx @@ -0,0 +1,1388 @@ +import React from 'react'; +import Image from '@theme/IdealImage'; + +import measurementLabelsImage from '../../../assets/img/measurement-labels-auto.png'; +import seriesSortImage from '../../../assets/img/seriesSort.png'; +import windowLevelPresetsImage from '../../../assets/img/windowLevelPresets.png'; +import colorbarImage from '../../../assets/img/colorbarImage.png'; +import segmentationTableModeImage from '../../../assets/img/segmentationTableModeImage.png'; +import segmentationTableModeImage2 from '../../../assets/img/segmentationTableModeImage2.png'; +import segmentationShowAddSegmentImage from '../../../assets/img/segmentationShowAddSegmentImage.png'; +import layoutSelectorCommonPresetsImage from '../../../assets/img/layoutSelectorCommonPresetsImage.png'; +import layoutSelectorAdvancedPresetGeneratorImage from '../../../assets/img/layoutSelectorAdvancedPresetGeneratorImage.png'; +import labellingFLow from '../../../assets/img/labelling-flow.png'; +import progressLoading from '../../../assets/img/Loading-Indicator.png'; +import loadingIndicatorProgress from '../../../assets/img/loading-indicator-icon.png'; +import loadingIndicatorPercent from '../../../assets/img/loading-indicator-percent.png'; +import viewportActionCorners from '../../../assets/img/viewport-action-corners.png'; +import contextMenu from '../../../assets/img/context-menu.jpg'; + +import segDisplayEditingTrue from '../../../assets/img/segDisplayEditingTrue.png'; +import segDisplayEditingFalse from '../../../assets/img/segDisplayEditingFalse.png'; +import thumbnailMenuItemsImage from '../../../assets/img/thumbnailMenuItemsImage.png'; +import studyMenuItemsImage from '../../../assets/img/studyMenuItemsImage.png'; +import windowLevelActionMenu from '../../../assets/img/windowLevelActionMenu.png'; +import viewPortNotificationImage from '../../../assets/img/viewport-notification.png'; + +export const viewportOverlayCustomizations = [ + { + id: 'viewportOverlay.topRight', + description: 'Defines the items displayed in the top-right overlay of the viewport.', + default: [], + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'viewportOverlay.topRight': { + $set: [ + // Add your overlay items here, e.g.: + // { id: 'CustomOverlay', inheritsFrom: 'ohif.overlayItem.custom' }, + ], + }, + }, + ], +}; + `, + }, + { + id: 'viewportOverlay.topLeft', + description: 'Defines the items displayed in the top-left overlay of the viewport.', + default: [ + { + id: 'StudyDate', + inheritsFrom: 'ohif.overlayItem', + label: '', + title: 'Study date', + condition: ({ referenceInstance }) => referenceInstance?.StudyDate, + contentF: ({ referenceInstance, formatters: { formatDate } }) => + formatDate(referenceInstance.StudyDate), + }, + { + id: 'SeriesDescription', + inheritsFrom: 'ohif.overlayItem', + label: '', + title: 'Series description', + condition: ({ referenceInstance }) => + referenceInstance && referenceInstance.SeriesDescription, + contentF: ({ referenceInstance }) => referenceInstance.SeriesDescription, + }, + ], + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'viewportOverlay.topLeft': { + $splice: [ + [0, 1], // Remove 1 item starting at index 0 (removes StudyDate) + ], + }, + }, + ], +}; + `, + }, + { + id: 'viewportOverlay.bottomLeft', + description: 'Defines the items displayed in the bottom-left overlay of the viewport.', + default: [ + { + id: 'WindowLevel', + inheritsFrom: 'ohif.overlayItem.windowLevel', + }, + { + id: 'ZoomLevel', + inheritsFrom: 'ohif.overlayItem.zoomLevel', + condition: props => { + const activeToolName = props.toolGroupService.getActiveToolForViewport(props.viewportId); + return activeToolName === 'Zoom'; + }, + }, + ], + configuration: ` + + // the following will push a yellow PatientNameOverlay to the bottomLeft overlay +window.config = { + // rest of window config + customizationService: [ + { + 'viewportOverlay.bottomLeft': { + $push: [ + { + id: 'PatientNameOverlay', + inheritsFrom: 'ohif.overlayItem', + attribute: 'PatientName', + label: 'PN:', + title: 'Patient Name', + color: 'yellow', + condition: ({ instance }) => + instance && + instance.PatientName && + instance.PatientName.Alphabetic, + contentF: ({ instance, formatters: { formatPN } }) => + formatPN(instance.PatientName.Alphabetic) + + ' ' + + (instance.PatientSex ? '(' + instance.PatientSex + ')' : ''), + }, + ], + }, + }, + ], +}; + `, + }, + { + id: 'viewportOverlay.bottomRight', + description: 'Defines the items displayed in the bottom-right overlay of the viewport.', + default: [ + { + id: 'InstanceNumber', + inheritsFrom: 'ohif.overlayItem.instanceNumber', + }, + ], + configuration: ` + // same as above + `, + }, +]; + +export const customizations = [ + { + id: 'ohif.hotkeyBindings', + description: 'Defines the hotkeys for the application.', + default: 'look at hotkeyBindingsCustomization.ts file', + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + // this will override the default hotkeys and only have one hotkey + 'ohif.hotkeyBindings': { + $set: [ + { + commandName: 'scaleDownViewport', + label: 'Zoom Out', + keys: ['-'], + isEditable: true, + }, + ], + }, + }, + ], + + // or lets say you want to change one key of the default hotkeys to default + // something else + customizationService: [ + { + // this will override the default hotkeys and only have one hotkey + 'ohif.hotkeyBindings': { + $filter: { + match: { commandName: 'scaleDownViewport' }, + $set: { + keys: ['ctrl+shift+-'], + }, + }, + }, + }, + ], + `, + }, + { + id: 'measurementLabels', + description: 'Labels for measurement tools in the viewer that are automatically asked for.', + image: measurementLabelsImage, + default: [], + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + measurementLabels: { + $set: { + labelOnMeasure: true, + exclusive: true, + items: [ + { value: 'Head', label: 'Head' }, + { value: 'Shoulder', label: 'Shoulder' }, + { value: 'Knee', label: 'Knee' }, + { value: 'Toe', label: 'Toe' }, + ], + }, + }, + }, + ], +}; + `, + }, + + { + id: 'cornerstoneViewportClickCommands', + description: 'Defines the viewport event handlers such as button1, button2, doubleClick, etc.', + default: { + doubleClick: { + commandName: 'toggleOneUp', + commandOptions: {}, + }, + button1: { + commands: [ + { + commandName: 'closeContextMenu', + }, + ], + }, + button3: { + commands: [ + { + commandName: 'showCornerstoneContextMenu', + commandOptions: { + requireNearbyToolData: true, + menuId: 'measurementsContextMenu', + }, + }, + ], + }, + }, + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + cornerstoneViewportClickCommands: { + doubleClick: { + $push: [ + () => { + console.debug('double click'); + }, + ], + }, + }, + }, + ], +}; + `, + }, + { + id: 'cinePlayer', + description: 'Customizes the cine player component.', + default: 'The CinePlayer component in the UI', + configuration: null, + }, + { + id: 'cornerstone.windowLevelActionMenu', + description: 'Window level action menu for the cornerstone viewport.', + image: windowLevelActionMenu, + default: null, + configuration: ` + window.config = { + // rest of window config + customizationService: [ + { + 'cornerstone.windowLevelActionMenu': { + $set: CustomizedComponent, + }, + }, + ], + }; + `, + }, + { + id: 'cornerstone.windowLevelPresets', + description: 'Window level presets for the cornerstone viewport.', + image: windowLevelPresetsImage, + default: { + CT: [ + { description: 'Soft tissue', window: '400', level: '40' }, + { description: 'Lung', window: '1500', level: '-600' }, + { description: 'Liver', window: '150', level: '90' }, + { description: 'Bone', window: '2500', level: '480' }, + { description: 'Brain', window: '80', level: '40' }, + ], + + PT: [ + { description: 'Default', window: '5', level: '2.5' }, + { description: 'SUV', window: '0', level: '3' }, + { description: 'SUV', window: '0', level: '5' }, + { description: 'SUV', window: '0', level: '7' }, + { description: 'SUV', window: '0', level: '8' }, + { description: 'SUV', window: '0', level: '10' }, + { description: 'SUV', window: '0', level: '15' }, + ], + }, + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'cornerstone.windowLevelPresets': { + $filter: { + match: { id: 'ct-soft-tissue' }, + $merge: { + window: '500', + level: '50', + }, + }, + }, + }, + ], +}; + `, + }, + { + id: 'cornerstone.colorbar', + description: 'Customizes the appearance and behavior of the cornerstone colorbar.', + image: colorbarImage, + default: ` + { + width: '16px', + colorbarTickPosition: 'left', + colormaps, + colorbarContainerPosition: 'right', + colorbarInitialColormap: DefaultColormap, + } + `, + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'cornerstone.colorbar': { + $merge: { + width: '20px', + colorbarContainerPosition: 'left', + }, + }, + }, + ], +}; + `, + }, + { + id: 'cornerstone.3dVolumeRendering', + description: + 'Customizes the settings for 3D volume rendering in the cornerstone viewport, including presets and rendering quality range.', + default: `{ + volumeRenderingPresets: VIEWPORT_PRESETS, + volumeRenderingQualityRange: { + min: 1, + max: 4, + step: 1, + }, + }`, + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'cornerstone.3dVolumeRendering': { + $merge: { + volumeRenderingQualityRange: { + min: 2, + max: 6, + step: 0.5, + }, + }, + }, + }, + ], +}; + `, + }, + { + id: 'autoCineModalities', + description: 'Specifies the modalities for which the cine player automatically starts.', + default: ['OT', 'US'], + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'autoCineModalities': { + $set: ['OT', 'US', 'MR'], // Adds 'MR' as an additional modality for auto cine playback + }, + }, + ], +}; + `, + }, + { + id: 'cornerstone.overlayViewportTools', + description: 'Configures the tools available in the cornerstone SEG and RT tool groups.', + default: `{ + active: [ + { + toolName: toolNames.WindowLevel, + bindings: [{ mouseButton: Enums.MouseBindings.Primary }], + }, + { + toolName: toolNames.Pan, + bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }], + }, + { + toolName: toolNames.Zoom, + bindings: [{ mouseButton: Enums.MouseBindings.Secondary }], + }, + { + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + }, + ], + enabled: [ + { + toolName: toolNames.PlanarFreehandContourSegmentation, + configuration: { + displayOnePointAsCrosshairs: true, + }, + }, + ], + }`, + configuration: ` + `, + }, + { + id: 'layoutSelector.commonPresets', + description: 'Defines the default layout presets available in the application.', + image: layoutSelectorCommonPresetsImage, + default: [ + { + icon: 'layout-common-1x1', + commandOptions: { + numRows: 1, + numCols: 1, + }, + }, + { + icon: 'layout-common-1x2', + commandOptions: { + numRows: 1, + numCols: 2, + }, + }, + { + icon: 'layout-common-2x2', + commandOptions: { + numRows: 2, + numCols: 2, + }, + }, + { + icon: 'layout-common-2x3', + commandOptions: { + numRows: 2, + numCols: 3, + }, + }, + ], + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'layoutSelector.commonPresets': { + $set: [ + { + icon: 'layout-common-1x1', + commandOptions: { + numRows: 1, + numCols: 1, + }, + }, + { + icon: 'layout-common-1x2', + commandOptions: { + numRows: 1, + numCols: 2, + }, + }, + ], + }, + }, + ], +}; + `, + }, + { + id: 'layoutSelector.advancedPresetGenerator', + description: 'Generates advanced layout presets based on hanging protocols.', + image: layoutSelectorAdvancedPresetGeneratorImage, + default: `({ servicesManager }) => { + // by default any hanging protocol that has isPreset set to true will be included + + // a function that returns an array of presets + // of form { + // icon: 'layout-common-1x1', + // title: 'Custom Protocol', + // commandOptions: { + // protocolId: 'customProtocolId', + // }, + // disabled: false, + // } + }`, + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'layoutSelector.advancedPresetGenerator': { + $apply: (defaultGenerator) => { + return ({ servicesManager }) => { + const presets = defaultGenerator({ servicesManager }); + + // Add a custom preset for a specific hanging protocol + presets.push({ + icon: 'custom-icon', + title: 'Custom Protocol', + commandOptions: { + protocolId: 'customProtocolId', + }, + disabled: false, + }); + + return presets; + }; + }, + }, + }, + ], +}; + `, + }, + { + id: 'dicomUploadComponent', + description: 'Customizes the appearance and behavior of the dicom upload component.', + default: 'The DicomUpload component in the UI', + configuration: null, + }, + { + id: 'onBeforeSRAddMeasurement', + description: 'Customizes the behavior of the SR measurement before it is added to the viewer.', + default: ({ measurement, StudyInstanceUID, SeriesInstanceUID }) => { + return measurement; + }, + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + onBeforeSRAddMeasurement: { + $set: ({ measurement, StudyInstanceUID, SeriesInstanceUID }) => { + // Note: it should return measurement + console.debug('onBeforeSRAddMeasurement'); + return measurement; + }, + }, + }, + ], +}; + `, + }, + { + id: 'onBeforeDicomStore', + description: + 'A hook that modifies the DICOM dictionary before it is stored. The customization should return the modified DICOM dictionary.', + default: ({ dicomDict, measurementData, naturalizedReport }) => { + // Default implementation returns the DICOM dictionary as is + return dicomDict; + }, + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'onBeforeDicomStore': { + $set: ({ dicomDict, measurementData, naturalizedReport }) => { + // Example customization: Add a custom tag to the DICOM dictionary + dicomDict['0010,0010'] = 'CustomizedPatientName'; // Patient's Name (example) + dicomDict['0008,103E'] = 'CustomStudyDescription'; // Study Description (example) + + // Return the modified DICOM dictionary + return dicomDict; + }, + }, + }, + ], +}; + `, + }, + { + id: 'sortingCriteria', + description: + 'Defines the series sorting criteria for hanging protocols. Note that this does not affect the order in which series are displayed in the study browser.', + default: `function seriesInfoSortingCriteria(firstSeries, secondSeries) { + const aLowPriority = isLowPriorityModality(firstSeries.Modality ?? firstSeries.modality); + const bLowPriority = isLowPriorityModality(secondSeries.Modality ?? secondSeries.modality); + + if (aLowPriority) { + // Use the reverse sort order for low priority modalities so that the + // most recent one comes up first as usually that is the one of interest. + return bLowPriority ? defaultSeriesSort(secondSeries, firstSeries) : 1; + } else if (bLowPriority) { + return -1; + } + + return defaultSeriesSort(firstSeries, secondSeries); + }`, + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'sortingCriteria': { + $set: function customSortingCriteria(firstSeries, secondSeries) { + + return someSort(firstSeries, secondSeries); + }, + }, + }, + ], +}; + `, + }, + { + id: 'customOnDropHandler ', + description: + 'CustomOnDropHandler in the viewport grid enables users to handle additional functionalities during the onDrop event in the viewport.', + default: props => { + return Promise.resolve({ handled: false }); + }, + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + customOnDropHandler: { + $set: customOnDropHandler + }, + }, + ], +}; + `, + }, + { + id: 'ui.notificationComponent', + description: 'Define the component which is used to render viewport notifications', + default: 'Default Notification component in viewport', + image: [viewPortNotificationImage], + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'ui.notificationComponent': { + $set: CustomizedComponent, + }, + }, + ], +}; + `, + }, + { + id: 'ui.loadingIndicatorTotalPercent', + description: 'Customizes the LoadingIndicatorTotalPercent component.', + image: loadingIndicatorPercent, + default: null, //use platform/ui component as default + configuration: ` + window.config = { + // rest of window config + customizationService: [ + { + 'ui.loadingIndicatorTotalPercent': { + $set: CustomizedComponent, + }, + }, + ], + }; + `, + }, + { + id: 'ui.loadingIndicatorProgress', + description: 'Customizes the LoadingIndicatorProgress component.', + image: loadingIndicatorProgress, + default: null, //use platform/ui component as default + configuration: ` + window.config = { + // rest of window config + customizationService: [ + { + 'ui.loadingIndicatorProgress': { + $set: CustomizedComponent, + }, + }, + ], + }; + `, + }, + { + id: 'ui.progressLoadingBar', + description: 'Customizes the ProgressLoadingBar component.', + image: progressLoading, + default: null, //use platform/ui component as default + configuration: ` + window.config = { + // rest of window config + customizationService: [ + { + 'ui.progressLoadingBar': { + $set: CustomizedComponent, + }, + }, + ], + }; + `, + }, + { + id: 'ui.viewportActionCorner', + description: 'Customizes the viewportActionCorner component.', + iamge: viewportActionCorners, + default: null, //use platform/ui component as default + configuration: ` + window.config = { + // rest of window config + customizationService: [ + { + 'ui.viewportActionCorner': { + $set: CustomizedComponent, + }, + }, + ], + }; + `, + }, + { + id: 'ui.contextMenu', + description: 'Customizes the Context menu component.', + image: contextMenu, + default: null, //use platform/ui component as default + configuration: ` + window.config = { + // rest of window config + customizationService: [ + { + 'ui.contextMenu': { + $set: CustomizedComponent, + }, + }, + ], + }; + `, + }, + { + id: 'ui.labellingComponent', + description: 'Customizes the labelling flow component.', + image: labellingFLow, + default: null, //use platform/ui component as default + configuration: ` + window.config = { + // rest of window config + customizationService: [ + { + 'ui.labellingComponent': { + $set: CustomizedComponent, + }, + }, + ], + }; + `, + }, +]; + +export const segmentationCustomizations = [ + { + id: 'panelSegmentation.tableMode', + description: 'Defines the mode of the segmentation table.', + image: [segmentationTableModeImage, segmentationTableModeImage2], + default: 'collapsed', + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'panelSegmentation.tableMode': { + $set: 'expanded', + }, + }, + ], +}; + `, + }, + { + id: 'panelSegmentation.showAddSegment', + description: + 'Controls whether the "Add Segment" button is displayed in the segmentation panel.', + default: true, + image: segmentationShowAddSegmentImage, + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'panelSegmentation.showAddSegment': { + $set: false, // Set to false to hide the "Add Segment" button + }, + }, + ], +}; + `, + }, + { + id: 'panelSegmentation.readableText', + description: 'Defines the readable text labels for segmentation panel statistics and metrics.', + default: { + lesionStats: 'Lesion Statistics', + minValue: 'Minimum Value', + maxValue: 'Maximum Value', + meanValue: 'Mean Value', + volume: 'Volume (ml)', + suvPeak: 'SUV Peak', + suvMax: 'Maximum SUV', + suvMaxIJK: 'SUV Max IJK', + lesionGlyoclysisStats: 'Lesion Glycolysis', + }, + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'panelSegmentation.readableText': { + $merge: { + lesionStats: 'Lesion Stats', + }, + }, + }, + ], +}; + `, + }, + { + id: 'panelSegmentation.onSegmentationAdd', + description: 'Defines the behavior when a new segmentation is added to the segmentation panel.', + default: `() => { + // default is to create a labelmap for the active viewport + const { viewportGridService } = servicesManager.services; + const viewportId = viewportGridService.getState().activeViewportId; + commandsManager.run('createLabelmapForViewport', { viewportId }); + }`, + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'panelSegmentation.onSegmentationAdd': { + $set: () => { + const { viewportGridService } = servicesManager.services; + const viewportId = viewportGridService.getState().activeViewportId; + commandsManager.run('createNewLabelmapFromPT'); + }, + }, + }, + ], +}; + `, + }, + { + id: 'panelSegmentation.disableEditing', + description: 'Determines whether editing of segmentations in the panel is disabled.', + default: false, + image: [segDisplayEditingTrue, segDisplayEditingFalse], + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'panelSegmentation.disableEditing': { + $set: true, // Disables editing of segmentations in the panel + }, + }, + ], +}; + `, + }, +]; + +export const measurementsCustomizations = [ + { + id: 'panelMeasurement.disableEditing', + description: + 'Determines whether editing measurements in the viewport is disabled after SR hydration', + default: false, + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'panelMeasurement.disableEditing': { + $set: true, // Disables editing measurements in the panel + }, + }, + ], +}; + `, + }, + { + id: 'cornerstone.measurements', + description: + 'Defines configuration for measurement tools, including display text and reporting options.', + default: { + Angle: { + displayText: [], + report: [], + }, + CobbAngle: { + displayText: [], + report: [], + }, + ArrowAnnotate: { + displayText: [], + report: [], + }, + RectangleROi: { + displayText: [], + report: [], + }, + CircleROI: { + displayText: [], + report: [], + }, + EllipticalROI: { + displayText: [], + report: [], + }, + Bidirectional: { + displayText: [], + report: [], + }, + Length: { + displayText: [], + report: [], + }, + LivewireContour: { + displayText: [], + report: [], + }, + SplineROI: { + displayText: [ + { + displayName: 'Area', + value: 'area', + type: 'value', + }, + { + value: 'areaUnits', + for: ['area'], + type: 'unit', + }, + ], + report: [ + { + displayName: 'Area', + value: 'area', + type: 'value', + }, + { + displayName: 'Unit', + value: 'areaUnits', + type: 'value', + }, + ], + }, + PlanarFreehandROI: { + displayTextOpen: [ + { + displayName: 'Length', + value: 'length', + type: 'value', + }, + ], + displayText: [ + { + displayName: 'Mean', + value: 'mean', + type: 'value', + }, + { + displayName: 'Max', + value: 'max', + type: 'value', + }, + { + displayName: 'Area', + value: 'area', + type: 'value', + }, + { + value: 'pixelValueUnits', + for: ['mean', 'max'], + type: 'unit', + }, + { + value: 'areaUnits', + for: ['area'], + type: 'unit', + }, + ], + report: [ + { + displayName: 'Mean', + value: 'mean', + type: 'value', + }, + { + displayName: 'Max', + value: 'max', + type: 'value', + }, + { + displayName: 'Area', + value: 'area', + type: 'value', + }, + { + displayName: 'Unit', + value: 'unit', + type: 'value', + }, + ], + }, + }, + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'cornerstone.measurements': { + $set: { + SplineROI: { + displayText: [ + { + displayName: 'Area', + value: 'area', + type: 'value', + }, + { + value: 'areaUnits', + for: ['area'], + type: 'unit', + }, + ], + report: [ + { + displayName: 'Area', + value: 'area', + type: 'value', + }, + { + displayName: 'Unit', + value: 'areaUnits', + type: 'value', + }, + ], + }, + PlanarFreehandROI: { + displayTextOpen: [ + { + displayName: 'Length', + value: 'length', + type: 'value', + }, + ], + displayText: [ + { + displayName: 'Mean', + value: 'mean', + type: 'value', + }, + { + displayName: 'Max', + value: 'max', + type: 'value', + }, + { + displayName: 'Area', + value: 'area', + type: 'value', + }, + { + value: 'pixelValueUnits', + for: ['mean', 'max'], + type: 'unit', + }, + { + value: 'areaUnits', + for: ['area'], + type: 'unit', + }, + ], + report: [ + { + displayName: 'Mean', + value: 'mean', + type: 'value', + }, + { + displayName: 'Max', + value: 'max', + type: 'value', + }, + { + displayName: 'Area', + value: 'area', + type: 'value', + }, + { + displayName: 'Unit', + value: 'unit', + type: 'value', + }, + ], + }, + }, + }, + }, + ], +}; + `, + }, +]; + +export const studyBrowserCustomizations = [ + { + id: 'studyBrowser.studyMode', + description: + 'Controls the study browser mode to determine whether to show all studies (including prior studies) or only the current study.', + default: `'all'`, + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'studyBrowser.studyMode': { + $set: 'primary', // or recent + }, + }, + ], +}; + `, + }, + { + id: 'studyBrowser.viewPresets', + description: 'Defines the view presets for the study browser, such as list or thumbnail views.', + default: [ + { + id: 'list', + iconName: 'ListView', + selected: false, + }, + { + id: 'thumbnails', + iconName: 'ThumbnailView', + selected: true, + }, + ], + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'studyBrowser.viewPresets': { + $set: [ + { + id: 'list', + iconName: 'ListView', + selected: true, // Makes the list view the default selected option + }, + { + id: 'thumbnails', + iconName: 'ThumbnailView', + selected: false, + }, + ], + }, + }, + ], +}; + `, + }, + { + id: 'studyBrowser.sortFunctions', + description: 'Sorting options for study browser items.', + image: seriesSortImage, + default: [ + { + label: 'Series Number', + sortFunction: (a, b) => { + return a?.SeriesNumber - b?.SeriesNumber; + }, + }, + { + label: 'Series Date', + sortFunction: (a, b) => { + const dateA = new Date(formatDate(a?.SeriesDate)); + const dateB = new Date(formatDate(b?.SeriesDate)); + return dateB.getTime() - dateA.getTime(); + }, + }, + ], + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'studyBrowser.sortFunctions': { + $push: [ + { + label: 'Series Stuff', + sortFunction: (a, b) => Stuff, + }, + ], + }, + }, + ], +}; + `, + }, + { + id: 'studyBrowser.thumbnailMenuItems', + description: + 'Defines the menu items available in the thumbnail menu items of the study browser.', + image: thumbnailMenuItemsImage, + default: [ + { + id: 'tagBrowser', + label: 'Tag Browser', + iconName: 'DicomTagBrowser', + commands: 'openDICOMTagViewer', + }, + ], + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'studyBrowser.thumbnailMenuItems': { + $set: [ + { + id: 'tagBrowser', + label: 'Tag Browser', + iconName: 'DicomTagBrowser', + commands: 'openDICOMTagViewer', + }, + { + id: 'deleteThumbnail', + label: 'Delete', + iconName: 'Delete', + commands: 'deleteThumbnail', + }, + { + id: 'markAsFavorite', + label: 'Mark as Favorite', + commands: 'markAsFavorite', + }, + ], + }, + }, + ], +}; + `, + }, + { + id: 'studyBrowser.studyMenuItems', + description: 'Defines the menu items available in the study menu items of the study browser.', + image: studyMenuItemsImage, + default: [], + configuration: ` +window.config = { + // rest of window config + customizationService: [ + { + 'studyBrowser.studyMenuItems': { + $set: [ + { + id: 'downloadStudy', + label: 'Download Study', + iconName: 'Download', + commands: () => { + console.debug('downloadStudy'); + }, + }, + ], + }, + }, + ], +}; + `, + }, +]; + +export const TableGenerator = (customizations: any[]) => { + return customizations.map(({ id, description, default: defaultValue, configuration, image }) => ( +
+

+ {id} +

+ + + + + + + + + + + + + + + + + + + +
ID + {id} +
Description +
{description}
+ {image && ( +
+ {Array.isArray(image) ? ( + image.map((img, index) => ( + {`${id}-${index + )) + ) : ( + {id} + )} +
+ )} +
Default Value +
+                {typeof defaultValue === 'string'
+                  ? defaultValue
+                  : JSON.stringify(defaultValue, null, 2)}
+              
+
Example + {configuration && ( +
+
+                    {configuration}
+                  
+
+ )} +
+
+ )); +}; diff --git a/platform/docs/docs/platform/services/customization-service/viewportOverlay.md b/platform/docs/docs/platform/services/customization-service/viewportOverlay.md new file mode 100644 index 0000000..4804617 --- /dev/null +++ b/platform/docs/docs/platform/services/customization-service/viewportOverlay.md @@ -0,0 +1,21 @@ +--- +sidebar_position: 1 +--- +import { viewportOverlayCustomizations , TableGenerator } from './sampleCustomizations'; + +# Viewport Overlay + +Viewport Overlays are the information that is displayed on the viewport. + +![](../../../assets/img/viewportOverlay-customization.png) + +There are 4 viewport overlays customization end points + +- `viewportOverlay.topRight` +- `viewportOverlay.topLeft` +- `viewportOverlay.bottomLeft` +- `viewportOverlay.bottomRight` + + + +{TableGenerator(viewportOverlayCustomizations)} diff --git a/platform/docs/docs/platform/services/data/DicomMetadataStore.md b/platform/docs/docs/platform/services/data/DicomMetadataStore.md new file mode 100644 index 0000000..927b8b8 --- /dev/null +++ b/platform/docs/docs/platform/services/data/DicomMetadataStore.md @@ -0,0 +1,112 @@ +--- +sidebar_position: 2 +sidebar_label: DICOM Metadata Store +--- +# DICOM Metadata Store + + +## Overview +`DicomMetaDataStore` is the central location that stores the metadata in `OHIF-v3`. There +are several APIs to add study/series/instance metadata and also for getting from the store. +DataSource utilize the `DicomMetaDataStore` to add the retrieved metadata to `OHIF Viewer`. + +> In `OHIF-v3` we have significantly changed the architecture of the metadata storage to +> provide a much cleaner way of handling metadata-related tasks and services. Classes such as +> `OHIFInstanceMetadata`, `OHIFSeriesMetadata` and `OHIFStudyMetadata` has been removed and +> replaced with `DicomMetaDataStore`. +> + + +## Events +There are two events that get publish in `DicomMetaDataStore`: + + +| Event | Description | +|-----------------|------------------------------------------------------------------------------------------------| +| SERIES_ADDED | Fires when all series of one study have added their summary metadata to the `DicomMetaDataStore` | +| INSTANCES_ADDED | Fires when all instances of one series have added their metadata to the `DicomMetaDataStore` | + + +## API +Let's find out about the public API for `DicomMetaDataStore` service. + +- `EVENTS`: Object including the events mentioned above. You can subscribe to these events + by calling DicomMetaDataStore.subscribe(EVENTS.SERIES_ADDED, myFunction). [Read more about pub/sub pattern here](../pubsub.md) + +- `addInstances(instances, madeInClient = false)`: adds the instances' metadata to the store. madeInClient is explained in detail below. After + adding to the store it fires the EVENTS.INSTANCES_ADDED. + +- `addSeriesMetadata(seriesSummaryMetadata, madeInClient = false)`: adds series summary metadata. After adding it fires EVENTS.SERIES_ADDED + +- `addStudy(study)`: creates and add study-level metadata to the store. + +- `getStudy(StudyInstanceUID)`: returns the study metadata from the store. It includes all the series and instance metadata in the requested study + +- `getSeries(StudyInstanceUID, SeriesInstanceUID`: returns the series-level metadata for the requested study and series UIDs. + +- `getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID)`: returns the instance metadata based on the study, series and sop instanceUIDs. + +- `geteInstanceFromImageId`: returns the instance metadata based on the requested imageId. It searches the store for the instance that has the same imageId. + + + +### madeInClient + +Since upon adding the metadata to the store, the relevant events are fired, and there are +other services that are subscribed to these events (`HangingProtocolService` or `DisplaySetService`), sometimes +we want to add instance metadata but don't want the events to get fired. For instance, when +you are caching the data for the next study in advance, you probably don't want to trigger hanging protocol +logic, so you set `madeInClient=true` to not fire events. + + +## Storage +As discussed before, there are several API that enables getting metadata from the store and adding to the store. +However, it is good to have an understanding of where they get +stored and in what format and hierarchy. `_model` is a private variable in the store +which holds all the metadata for all studies, series, and instances, and it looks like: + + +```js title="platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js" +const _model = { + studies: [ + { + /** Study Metadata **/ + seriesLists: [ + { + // Series in study from dicom web server 1 (or different backend 1) + series: [ + { + // Series 1 Metadata + instances: [ + { + // Instance 1 metadata of Series 1 + }, + { + // Instance 2 metadata of Series 1 + }, + /** Other instances metadata **/ + ], + }, + { + // Series 2 Metadata + instances: [ + { + // Instance 1 metadata of Series 2 + }, + { + // Instance 2 metadata of Series 1 + }, + /** Other instances metadata **/ + ], + }, + ], + }, + { + // Series in study from dicom web server 2 (or different backend 2) + /** ... **/ + }, + ], + }, + ], +} +``` diff --git a/platform/docs/docs/platform/services/data/DisplaySetService.md b/platform/docs/docs/platform/services/data/DisplaySetService.md new file mode 100644 index 0000000..8b8bcae --- /dev/null +++ b/platform/docs/docs/platform/services/data/DisplaySetService.md @@ -0,0 +1,73 @@ +--- +sidebar_position: 3 +sidebar_label: DisplaySet Service +--- +# DisplaySet Service + + +## Overview +`DisplaySetService` handles converting the `instanceMetadata` into `DisplaySet` that `OHIF` uses for the visualization. `DisplaySetService` gets initialized at service startup time, but is then cleared in the `Mode.jsx`. During the initialization `SOPClassHandlerIds` of the `modes` gets registered with the `DisplaySetService`. + +:::tip + +DisplaySet is a general set of entities and contains links to bunch of displayable objects (images, etc.) Some series might get split up into different displaySets e.g., MG might have mixed views in a single series, but users might want to have separate LCC, RCC, etc. for hanging protocol usage. A viewport renders a display set into a displayable object. + +imageSet is a particular implementation of image displays. +::: + + +> Based on the instanceMetadata's `SOPClassHandlerId`, the correct module from the registered extensions is found by `OHIF` and its `getDisplaySetsFromSeries` runs to create a DisplaySet for the Series. Note +that this is an ordered operation, and consumes the instances as it proceeds, with the first registered +handlers being able to consume instances first. + +DisplaySets are created synchronously when the instances metadata is retrieved and added to the [DicomMetaDataStore](../data//DicomMetadataStore.md). They are ALSO updated when +the DicommetaDataStore receives new data. This update first checks the addInstances +of existing `DisplaySet` values to see if the new instance belongs in an existing set. +Then, the same process is used as was originally done to create new display sets. + +NOTE: Any instances not matched are NOT added to any display set and will not be displayed. + +## Adding `madeInClient` display sets +It is possible to filter or combine display sets from different series by +performing the filter operation desired, and then calling the `addActiveDisplaySets` +on the new `DisplaySet` instances. This allows operations like combining +two series or sub-selecting a series. + +## Events +There are three events that get broadcasted in `DisplaySetService`: + +| Event | Description | +| -------------------- | ---------------------------------------------------- | +| DISPLAY_SETS_ADDED | Fires a displayset is added to the displaysets cache | +| DISPLAY_SETS_CHANGED | Fires when a displayset is changed | +| DISPLAY_SETS_REMOVED | Fires when a displayset is removed | +| DISPLAY_SET_SERIES_METADATA_INVALIDATED | Fires when a displayset's series metadata has been altered. An object payload for the event is sent with properties: `displaySetInstanceUID` - the UID of the display set affected; `invalidateData` - boolean indicating if data should be invalidated + + +## API +Let's find out about the public API for `DisplaySetService`. + +- `EVENTS`: Object including the events mentioned above. You can subscribe to these events + by calling DisplaySetService.subscribe(EVENTS.DISPLAY_SETS_CHANGED, myFunction). [Read more about pub/sub pattern here](../pubsub.md) + +- `makeDisplaySets(input, { batch, madeInClient, settings } = {}`): Creates displaySet for the provided + array of instances metadata. Each display set gets a random UID assigned. + + - `input`: Array of instances Metadata + - `batch = false`: If you need to pass array of arrays of instances metadata to have a batch creation + - `madeInClient = false`: Disables the events firing + - `settings = {}`: Hanging protocol viewport or rendering settings. For instance, setting the initial `voi`, or activating a tool upon + displaySet rendering. [Read more about hanging protocols settings here](./HangingProtocolService.md#Settings) + + +- `getDisplaySetByUID`: Returns the displaySet based on the DisplaySetUID. + +- `getDisplaySetForSOPInstanceUID`: Returns the displaySet that includes an image with the provided SOPInstanceUID + +- `getActiveDisplaySets`: Returns the active displaySets + +- `deleteDisplaySet`: Deletes the displaySets from the displaySets cache + +- `addActiveDisplaySets`: Adds a new display set independently of the make operation. + +- `setDisplaySetMetadataInvalidated`: Fires the `DISPLAY_SET_SERIES_METADATA_INVALIDATED` event. diff --git a/platform/docs/docs/platform/services/data/HangingProtocolService.md b/platform/docs/docs/platform/services/data/HangingProtocolService.md new file mode 100644 index 0000000..de33e9b --- /dev/null +++ b/platform/docs/docs/platform/services/data/HangingProtocolService.md @@ -0,0 +1,367 @@ +--- +sidebar_position: 4 +sidebar_label: Hanging Protocol Service +--- + +# Hanging Protocol Service + +## Overview + + +This service handles the arrangement of the images in the viewport. In +short, the registered protocols will get matched with the DisplaySets that are +available. Each protocol gets a score, and they are ranked. The +winning protocol (highest score) gets applied and its settings run for the viewports +to be arranged. + +You can read more about a HangingProtocol Structure and its properties in the +[HangingProtocol Module](../../extensions/modules/hpModule.md). + +The rest of this page is dedicated on how the Hanging Protocol Service works and +what you can do with it. + +## Protocols + +Protocols are provided by each extension's HangingProtocolModule and are +registered automatically to the HangingProtocolService. + +All protocols are stored in the `HangingProtocolService` using their `id` as the key, and the protocol itself as the value. + +## Protocol Definition +Protocols are defined in a getHangingProtocolModule inside an extension. As such, +they are defined with a module structure that starts with an id, and has field protocol +that is the actual protocol definition. This setup allows defining more than +one protocol within a module, each one needing it's own definition file. + +```javascript +import MyProtocol from './MyProtocol'; +export default function getHangingProtocolModule() { + return [ + { + id: MyProtocol.id, + protocol: MyProtocol, + }, + ]; +} +``` + +Within the protocol itself, the structure is laid out as described in the HangingProtocol.ts +type definition, starting with `Protocol`. See the type definition for more details. + +## Events + +There are two events that get publish in `HangingProtocolService`: + +| Event | Description | +| ------------ | -------------------------------------------------------------------- | +| NEW_LAYOUT | Fires when a new layout is requested by the `HangingProtocolService` | +| PROTOCOL_CHANGED | Fires when the the protocol is changed in the hanging protocols, or when the applied stage is changed. | +| RESTORE_PROTOCOL | Fires when the protocol or stage is restored, for example, after turning off MPR mode | +| STAGE_ACTIVATION | Fires when the stages are known to have stage.status set. | + +## Stage Activation and Status +Sometimes a hanging protocol can be applicable generally, but not all stages +should be shown by default, or should be shown at all. This can be handled by +using the stage activation to control whether the stage is shown by default (`enabled`), +whether it can be navigated to (`passive`) or whether it should not be shown +at all (`disabled`). + +The `stage.status` is used to control this, and the status is controlled by +the stage activate. The status values are: + +* enabled - meaning that the stage is fully applicable +* passive - meaning that the stage can be applied, but might be missing details +* disabled - meaning that the study has insufficient information for this stage + +The default values for no `stageActivation` are to assume that `enabled` has `minViewports` of 1, +and `passive` has `minViewports=0`. That is, enable the stage if at least one +viewport is filled, and make it passive if no viewports are filled. + +The setting for these are controlled by the stageActivation property, for example +the following: + +```javascript +stageActivation: { + // The enabled activation specifies requirements to enable the stage, that is, + // make it preferred. + enabled: { + // The default value here is 1, and indicates how many non-blank viewports + // are required. + minViewportsMatched: 3, + // This enables specifying cross cutting concerns, such as having a stage + // only apply to males or females, and is a list of display set selector ids + displaySetSelectorsMatched: ['dsMale'], + }, + // The passive check is performed first. If it fails, the enabled is NOT + // checked, but the status set to disabled. The default passive check + // should always be passed, so it is fine to just define enabled if desired. + passive: { + // The default is 0, which means allow the stage even if no viewports are + // filled. This allows dragging and dropping into the viewports to + // make matches manually, which can then be re-used for other stages. + minViewportsMatched: 0, + displaySetSelectorsMatched: [...], + }, +} +``` + +## API + +- `destroy`: Destroys the HP service + +- `reset` and `onModeEnter`: Resets the HP service to not have any active + hanging protocols + +- `getActiveProtocol`: Returns an object of the internal state of the HP service, + useful for storing said state, as well as for getting direct access to the + protocol and stage objects. Users of this should count on it being not completely + stable as to exactly what this returns, as internal details can change. + +- `getState`: Returns the currently applied protocol ID, stage index and active study UID. + This information is storable/usable as state information to be used elsewhere. + +- `getDefaultProtocol`: Returns the default protocol to apply. + +- `getMatchDetails`: returns an object which contains the details of the + matching for the viewports, displaySets and whether the protocol is + applied to the viewport or not yet. This is deprecated as it is expected + to be communicated by events instead. + +- `getProtocols`: Returns a list of the currently active protocols. + +- `getProtocolById`: Gets the protocol with the given id. + +- `addProtocol`: adds provided protocol to the list of registered protocols + for matching. Will replacing any protocol with the same id, allowing, for example, + to replace the default protocol. + +- `setActiveProtocols`: Choose the protocols which are active. Can take a +single protocol id or a list. When a single one is provided, that one will be +applied whether or not the required rules match. Called automatically on mode +init. + +- `setActiveStudyUID`: Sets the given study UID as active, which has significance + in terms of the matching rules being able to match against the active study. + +- `run({studies, activeStudy, displaySets }, protocolId)`: runs the HPService with the provided + studyMetaData and optional protocolId. If protocol is not given, HP Matching + engine will search all the registered protocols for the best matching one + based on the constraints. + +- `registerImageLoadStrategy`: Adds a custom image load strategy. + +- `addCustomAttribute`: adding a custom attribute for matching. (see below) + +- `setProtocol`: applies a protocol to the current studies, it can be used for instance to apply a + hanging protocol when pressing a button in the toolbar. We use this for applying 'mpr' + hanging protocol when pressing the MPR button in the toolbar. `setProtocol` will + accept a set of options that can be used to define the displaySets that will be + used for the protocol. If no options are provided, all displaySets will + be used to match the protocol. + +- `getStageIndex`: Finds the stage index for a given set of match keys. Currently + only works on the currently active protocol, but is supposed to be able to work + with other protocols as well. + +- `getMissingViewport`: Returns a viewport object to be used as the missing + viewport instance. This is used to fill out new viewports. + +Default initialization of the modes handles running the `HangingProtocolService` + +## Hanging Protocol Instance Definition +A hanging protocol has an id provided in the module which is used to identify +the protocol. Mostly these should include the module name so that they +do not overlap, with the suggested id being `${moduleId}.${simpleName}`. The +'default' name is used as the hanging protocol id when no other protocol applies, +and can be set as the last module listed containing 'default'. + +A hanging protocol can also be defined with a generator. +A generator is a function we can write this way: + +```ts +function protocolGenerator({ servicesManager, commandsManager }) { + // Some computations using services and commands ... + + return { + protocol: generatedProtocol + } +} +``` + +See the typescript definitions for more details on the structure of protocols. + +## Additional viewports for layout - `defaultViewport` +Sometimes the user manually selects a layout of a given size, say `2x3`. The +hanging protocol can define what viewport options to use for this viewport by +defining an extra viewport option in `defaultViewport`. For example: + +```javascript + defaultViewport: { + viewportOptions: { + viewportType: 'stack', + toolGroupId: 'default', + allowUnmatchedView: true, + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + matchedDisplaySetsIndex: -1, + }, + ], + }, +``` + +This allows defining the type of additional viewports, what tool group etc they +are allowed in, and which display set is used to fill them. In the above case, +the display set is the same as the other viewports, but the +`matchedDisplaySetsIndex=-1`, so that means find the next matching display set +from the display set selector which isn't already filling a view. + +## Custom Attribute +In some situations, you might want to match based on a custom +attribute and not the DICOM tags. For instance, +if you have assigned a `timepointId` to each study, and you want to match based on it. +Good news is that, in `OHIF-v3` you can define you custom attribute and use it for matching. + +There are various ways that you can let `HangingProtocolService` know of you +custom attribute. We will show how to add it inside the mode configuration. + +```js +const defaultProtocol = { + id: 'defaultProtocol', + /** ... **/ + protocolMatchingRules: [ + { + weight: 3, + attribute: 'timepoint', + constraint: { + equals: 'first', + }, + required: false, + }, + ], + displaySetSelectors: { + /** ... */ + } + stages: [ + /** ... **/ + ], + numberOfPriorsReferenced: -1, +}; + +// Custom function for custom attribute +const getTimePointUID = metaData => { + // requesting the timePoint Id + return myBackEndAPI(metaData); +}; + +function modeFactory() { + return { + id: 'myMode', + /** .. **/ + routes: [ + { + path: 'myModeRoute', + init: async ({}) => { + const { + DicomMetadataStore, + HangingProtocolService, + } = servicesManager.services; + + const onSeriesAdded = ({ + StudyInstanceUID, + madeInClient = false, + }) => { + const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID); + + // Adding custom attribute to the hangingprotocol + HangingProtocolService.addCustomAttribute( + 'timepoint', + 'timepoint', + metaData => getFirstMeasurementSeriesInstanceUID(metaData) + ); + + HangingProtocolService.run(studyMetadata); + }; + + DicomMetadataStore.subscribe( + DicomMetadataStore.EVENTS.SERIES_ADDED, + onSeriesAdded + ); + }, + }, + ], + /** ... **/ + }; +} +``` + +### Custom Attributes for Viewport Options + +The custom attributes can also be used for viewport options. This example, +from the default hanging protocol navigates the image to the image +specified in the URL: + +```javascript +viewportOptions: { + initialImageOptions: { + // custom attribute name is selected by 'custom' + custom: 'sopInstanceLocation', + // This is the value returned if the above doesn't return anything + defaultValue: { index: 5 }, + } +} +``` + +### Included Custom Attributes + +A few custom attributes are included under @ohif/extension-test, these are namely: +*sameAs +*maxNumImageFrames +*numberOfDisplaySets + +To use these included custom attributes, the extension will need to be enabled under platform/app/pluginConfig.json: + +```javascript +{ + "extensions": [ + ... + { + "packageName": "@ohif/extension-test", + "version": "3.4.0" + }, + ... + ] +} + ``` + +Furthermore, the extension will also need to be included under extensionDependencies in the desired mode (e.g. modes/tmtv/src/index.js): + +```javascript +const extensionDependencies = { + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', + '@ohif/extension-tmtv': '^3.0.0', + '@ohif/extension-test': '^0.0.1', + }; + ``` + +The below example modifies the included hanging protocol (extensions/tmtv/src/getHangingProtocolModule.js) and uses the sameAs attribute included in the @ohif/extension-test extension to check that the selected PT has the same frame of reference as the CT: + +```javascript +ptDisplaySet: { + ... + seriesMatchingRules: [ + { + attribute: 'sameAs', + sameAttribute: 'FrameOfReferenceUID', + sameDisplaySetId: 'ctDisplaySet', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + ... +``` diff --git a/platform/docs/docs/platform/services/data/MeasurementService.md b/platform/docs/docs/platform/services/data/MeasurementService.md new file mode 100644 index 0000000..f5ede2f --- /dev/null +++ b/platform/docs/docs/platform/services/data/MeasurementService.md @@ -0,0 +1,182 @@ +--- +sidebar_position: 6 +sidebar_label: Measurement Service +--- + +# Measurement Service + +## Overview + +`MeasurementService` handles the internal measurement representation inside +`OHIF` platform. Developers can add their custom `sources` with `mappers` to +enable adding measurements inside OHIF. Currently, we are maintaining +`CornerstoneTools` annotations and corresponding mappers can be found inside the +`cornerstone` extension. However, `MeasurementService` can be configured to work +with any custom tools given that its `mappers` is added to the +`MeasurementService`. We can see the overall architecture of the +`MeasurementService` below: + +![services-measurements](../../../assets/img/services-measurements.png) + +## Events + +There are seven events that get publish in `MeasurementService`: + +| Event | Description | +| --------------------- | ------------------------------------------------------ | +| MEASUREMENT_UPDATED | Fires when a measurement is updated | +| MEASUREMENT_ADDED | Fires when a new measurement is added | +| RAW_MEASUREMENT_ADDED | Fires when a raw measurement is added (e.g., dicom-sr) | +| MEASUREMENT_REMOVED | Fires when a measurement is removed | +| MEASUREMENTS_CLEARED | Fires when all measurements are deleted | +| JUMP_TO_MEASUREMENT_VIEWPORT | Fires when a measurement is requested to be jumped to, applying to individual viewports. | +| JUMP_TO_MEASUREMENT_LAYOUT | Fires when a measurement is requested to be jumped to, applying to the overall layout. | + +## API + +- `getMeasurements`: returns array of measurements + +- `getMeasurement(id)`: returns the corresponding measurement based on the + provided Id. + +- `remove(id, source)`: removes a measurement and broadcasts the + `MEASUREMENT_REMOVED` event. + +- `clearMeasurements`: removes all measurements and broadcasts + `MEASUREMENTS_CLEARED` event. + +- `createSource(name, version)`: creates a new measurement source, generates a + uid and adds it to the `sources` property of the service. + +- `addMapping(source, definition, matchingCriteria, toSourceSchema, toMeasurementSchema)`: + adds a new measurement matching criteria along with mapping functions. We will + learn more about [source/mappers below](#source--mappers) + +- `update`: updates the measurement details and fires `MEASUREMENT_UPDATED` + +- `addRawMeasurement(source,definition,data,toMeasurementSchema,dataSource = {}` + : adds a raw measurement into a source so that it may be converted to/from + annotation in the same way. E.g. import serialized data of the same form as + the measurement source. Fires `MEASUREMENT_UPDATED` or `MEASUREMENT_ADDED`. + Note that, `MeasurementService` handles finding the correct mapper upon new + measurements; however, `addRawMeasurement` provides more flexibility. You can + take a look into its usage in `dicom-sr` extension. + + - `source`: The measurement source instance. + - `definition`: The source definition you want to add the measurement to. + - `data`: The data you wish to add to the source. + - `toMeasurementSchema`: A function to get the `data` into the same shape as + the source definition. + +- `jumpToMeasurement(viewportId, id)`: calls the listeners who have + subscribed to `JUMP_TO_MEASUREMENT`. + +## Source / Mappers + +To create a custom measurement source and relevant mappers for each tool, you +can take a look at the `init.js` inside the `cornerstone` extension. In which we +are registering our `CornerstoneTools-v4` measurement source to +MeasurementService. Let's take a peek at the _simplified_ implementation +together. To achieve this, for each tool, we need to provide three mappers: + +- `matchingCriteria`: criteria used for finding the correct mapper for the drawn + tool. +- `toAnnotation`: tbd +- `toMeasurement`: a function that converts the tool data to OHIF internal + representation of measurement data. + +```js title="extensions/cornerstone/src/utils/measurementServiceMappings/Length.js" +function toMeasurement( + csToolsAnnotation, + DisplaySetService, + getValueTypeFromToolType +) { + const { element, measurementData } = csToolsAnnotation; + + /** ... **/ + + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes(element); + + const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + + /** ... **/ + return { + id: measurementData.id, + SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: measurementData.label, + description: measurementData.description, + unit: measurementData.unit, + length: measurementData.length, + type: getValueTypeFromToolType(tool), + points: getPointsFromHandles(measurementData.handles), + }; +} + +////////////////////////////////////////// + +// extensions/cornerstone/src/init.js + +const Length = { + toAnnotation, + toMeasurement, + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + ], +}; + +const _initMeasurementService = (MeasurementService, DisplaySetService) => { + /** ... **/ + + const csToolsVer4MeasurementSource = MeasurementService.createSource( + 'CornerstoneTools', + '4' + ); + + /* Mappings */ + MeasurementService.addMapping( + csToolsVer4MeasurementSource, + 'Length', + Length.matchingCriteria, + toAnnotation, + toMeasurement + ); + + /** Other tools **/ + return csToolsVer4MeasurementSource; +}; +``` + + +## Auto complete +Use a customization service to add more customizations for measurement labels. Later, when adding a measurement, the user will be prompted to choose from a list of labels. + +```js +customizationService.addModeCustomizations([ + { + id: 'measurementLabels', + labelOnMeasure: true, + exclusive: true, + items: [ + { value: 'Head', label: 'Head' }, + { value: 'Neck', label: 'Neck' }, + { value: 'Knee', label: 'Knee' }, + { value: 'Toe', label: 'Toe' }, + ], + }, +]); +``` diff --git a/platform/docs/docs/platform/services/data/MultiMonitorService.md b/platform/docs/docs/platform/services/data/MultiMonitorService.md new file mode 100644 index 0000000..eb74d28 --- /dev/null +++ b/platform/docs/docs/platform/services/data/MultiMonitorService.md @@ -0,0 +1,71 @@ +--- +sidebar_position: 5 +sidebar_label: Multi Monitor Service +--- + + +# Multi Monitor Service + +::: info + +We plan to enhance this service in the future. Currently, it offers a basic implementation of multi-monitor support, allowing you to manually open multiple windows on the same monitor. It is not yet a full multi-monitor solution! + +::: + + + + +The multi-monitor service provides detection, launch and communication support +for multiple monitors or windows/screens within a single monitor. + +:::info + +The multi-monitor service is currently applied via configuration file. + +```js +customizationService: ['@ohif/extension-default.customizationModule.multimonitor'], +``` + +::: + + + +## Configurations +The service supports two predefined configurations: + +1. **Split Screen (`multimonitor=split`)** + Splits the primary monitor into two windows. + +2. **Multi-Monitor (`multimonitor=2`)** + Opens windows across separate physical monitors. + +### Launch Methods +- Specify `&screenNumber=0` to designate the first window explicitly. +- Omit `screenNumber` to let the service handle window assignments dynamically. +- Use `launchAll` in the query parameters to launch all configured screens simultaneously. + +#### Example URLs: +- **Split Screen:** + `http://viewer.ohif.org/.....&multimonitor=split` + Splits the primary monitor into two windows when a study is viewed. + +- **Multi-Monitor with All Screens:** + `http://viewer.ohif.org/.....&multimonitor=2&screenNumber=0&launchAll` + Launches two monitors and opens all configured screens. + +--- + +## Behavior + +### Refresh, Close and Open +If you refresh the base/original window, then all the other windows will also +refresh. However, you can safely refresh any single other window, and on the next +command to the other windows, it will re-create the other window links without +losing content in the other windows. You can also close any other window and +it will be reopened the next time you try to call to it. + + +## Executing Commands +The MultiMonitorService adds the ability to run commands on other specified windows. +This allows opening up a study on another window without needing to refresh +it's contents. The command below shows an example of how this can be done: diff --git a/platform/docs/docs/platform/services/data/PanelService.md b/platform/docs/docs/platform/services/data/PanelService.md new file mode 100644 index 0000000..b0a530b --- /dev/null +++ b/platform/docs/docs/platform/services/data/PanelService.md @@ -0,0 +1,49 @@ +--- +sidebar_position: 8 +sidebar_label: Panel Service +--- + +# Panel Service + +## Overview + +The Panel Service provides for activating/showing a panel that was registered +via the `getPanelModule` extension method. Such panels can be either explicitly +activated or implicitly triggered to activate when some other event occurs. + +## Events + +The following events are published in `PanelService`. + +| Event | Description | +| --------------------- | ------------------------------------------------------ | +| ACTIVATE__PANEL | Fires a `ActivatePanelEvent` when a particular panel should be activated (i.e. shown). | + + +## API + +### Panel Activation + +- `activatePanel`: Fires the `ACTIVATE_PANEL` event for a particular panel (id). +An optional `forceActive` flag can be passed that when `true` "forces" a +panel to show. Ultimately, it is up to a panel's container whether it +is appropriate to activate/show the panel. For instance, if the user opened and then +closed a side panel that contains the panel to activate, that side panel +may decide that the user knows best and will not open the panel (again). + +- `addActivatePanelTriggers`: Creates and returns event subscriptions that when +fired will activate the specified panel with an optional `forceActive` flag +(see `activatePanel`). This allows for panel activation to be directly triggered +by some other event(s). When the triggers are no longer needed, simply +unsubscribe to the returned subscriptions. For example, a panel +for tracking measurements might get activated every time the +`MeasurementService` fires a `MEASUREMENT_ADDED` event like this: + ```js + panelService.addActivatePanelTriggers('measurement-tracking-panel-id', [ + sourcePubSubService: measurementService, + sourceEvents: [ + measurementService.EVENTS.MEASUREMENT_ADDED, + measurementService.EVENTS.RAW_MEASUREMENT_ADDED, + ], + ]); + ``` diff --git a/platform/docs/docs/platform/services/data/SegmentationService.md b/platform/docs/docs/platform/services/data/SegmentationService.md new file mode 100644 index 0000000..c2eeb60 --- /dev/null +++ b/platform/docs/docs/platform/services/data/SegmentationService.md @@ -0,0 +1,145 @@ +--- +sidebar_position: 7 +sidebar_label: Segmentation Service +--- + +# Segmentation Service + + +## Events + +```typescript +SEGMENTATION_MODIFIED // When a segmentation is updated +SEGMENTATION_DATA_MODIFIED // When segmentation data changes +SEGMENTATION_ADDED // When new segmentation is added +SEGMENTATION_REMOVED // When segmentation is removed +SEGMENT_LOADING_COMPLETE // When segment group adds pixel data to volume +SEGMENTATION_LOADING_COMPLETE // When full segmentation volume is filled +``` + +## Core APIs + +### Creation Methods + +```typescript +createLabelmapForDisplaySet( + displaySet, + { + segmentationId?: string, + label: string, + segments?: { + [segmentIndex: number]: Partial + } + } +) +``` + +### Segmentation Management + +```typescript +setActiveSegmentation(viewportId, segmentationId) +getSegmentations() +getSegmentation(segmentationId) +jumpToSegmentCenter(segmentationId, segmentIndex, viewportId) +highlightSegment(segmentationId, segmentIndex, viewportId) +``` + +### Segment Operations + +```typescript +addSegment(segmentationId, { + segmentIndex?: number, + label?: string, + color?: [number, number, number, number], // RGBA + visibility?: boolean, + isLocked?: boolean, + active?: boolean +}) + +setSegmentColor(viewportId, segmentationId, segmentIndex, color) +setSegmentVisibility(viewportId, segmentationId, segmentIndex, visibility) +``` + +## Data Structures + +### Segmentation Object + +```typescript +interface Segmentation { + segmentationId: string; + label: string; + segments: { + [segmentIndex: number]: { + segmentIndex: number; + label: string; + locked: boolean; + cachedStats: { [key: string]: unknown }; + active: boolean; + } + }; + representationData: RepresentationsData; +} +``` + +## Code Examples + +### Creating a Segmentation + +```typescript +const displaySet = displaySetService.getDisplaySetByUID(displaySetUID); +const segmentationId = await segmentationService.createLabelmapForDisplaySet( + displaySet, + { + label: 'New Segmentation', + segments: { + 1: { + label: 'First Segment', + active: true + } + } + } +); +``` + +### Managing Active Segmentations + +```typescript +segmentationService.setActiveSegmentation('viewport-1', segmentationId); +``` + +### Adding Segments + +```typescript +segmentationService.addSegment(segmentationId, { + label: 'Tumor', + color: [255, 0, 0, 255], // RGBA format + active: true +}); +``` + +### Visibility Management + +```typescript +// Set segment visibility +segmentationService.setSegmentVisibility( + 'viewport-1', + segmentationId, + 1, // segmentIndex + true // visible +); + +// Get viewport IDs with segmentation +const viewportIds = segmentationService.getViewportIdsWithSegmentation(segmentationId); +``` + +### Segment Styling + +```typescript +// Set segment color +segmentationService.setSegmentColor( + 'viewport-1', + segmentationId, + 1, // segmentIndex + [255, 0, 0, 255] // RGBA +); +``` diff --git a/platform/docs/docs/platform/services/data/SyncGroupService.md b/platform/docs/docs/platform/services/data/SyncGroupService.md new file mode 100644 index 0000000..835fd56 --- /dev/null +++ b/platform/docs/docs/platform/services/data/SyncGroupService.md @@ -0,0 +1,147 @@ +--- +sidebar_position: 8 +sidebar_label: SyncGroup Service +--- + +# Sync Group Service + +## Overview + +The `SyncGroupService` is responsible for managing synchronization groups in the OHIF Viewer. Synchronization groups allow multiple viewports to be synchronized based on various criteria, such as camera position, window level, zoom/pan, and image slice position. This service provides a centralized way to create, update, and manage synchronization groups. + +Right now, synchronization groups can be defined in the hanging protocols or manually assigning buttons. + + + + +## API + +- `getSyncCreatorForType(type)`: Returns the synchronizer creator function for the specified type. +- `addSynchronizerType(type, creator)`: Adds a new synchronizer type with a custom creator function. +- `getSynchronizer(id)`: Retrieves a synchronizer by its ID. +- `getSynchronizersOfType(type)`: Retrieves an array of synchronizers of the specified type. +- `addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups)`: Adds a viewport to one or more synchronization groups. +- `destroy()`: Destroys all synchronizers. +- `getSynchronizersForViewport(viewportId)`: Retrieves an array of synchronizers associated with the specified viewport. +- `removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroupId?)`: Removes a viewport from a specific synchronization group or all synchronization groups if no group ID is provided. + +## Usage +### Via hanging protocols +You can set up different types of synchronization groups for your viewports. For example, in the TMTV hanging protocol (`extensions/tmtv/src/getHangingProtocolModule.js`), we can see how different synchronization groups are defined for various viewports: + +```javascript +const ptAXIAL = { + viewportOptions: { + // ... + syncGroups: [ + { + type: 'cameraPosition', + id: 'axialSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ptWLSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ptFusionWLSync', + source: true, + target: false, + options: { + syncInvertState: false, + }, + }, + ], + }, + // ... +}; +``` + + + +In this example, the `ptAXIAL` viewport is part of three synchronization groups: + +1. `cameraPosition` group with the ID `'axialSync'`: This group synchronizes the camera position across viewports that are both source and target. +2. `voi` (Window Level) group with the ID `'ptWLSync'`: This group synchronizes the window level settings across viewports that are both source and target. +3. `voi` group with the ID `'ptFusionWLSync'`: This group synchronizes the window level settings, but the `ptAXIAL` viewport is only a source, not a target. + + +:::tip +You can control the state of the synchronizer via a toolbar button after you define the synchronization group in the hanging protocol. + +```js +{ + id: 'SyncToggle', + uiType: 'ohif.radioGroup', + props: { + icon: 'tool-info', + label: 'toggle', + commands: { + commandName: 'toggleSynchronizer', + commandOptions: { + syncId: 'axialSync' + } + } + }, +}, +``` + +as you can see by using the `toggleSynchronizer` command you can toggle the state of the synchronizer for the specified syncId. + +::: + +### Manually through a button +You can create a button on the toolbar that you provice the synchronization group type, +and it applys it to all viewports. + +:::note +Currently we don't have a proper way to select viewports to apply the synchronization group to. It is applied to all applicable viewports +::: + +For instance look at `imageSliceSync` button in the longitudinal mode (`modes/longitudinal/src/moreTools.ts`) and how it runs a command + +```js +ToolbarService.createButton({ + id: 'ImageSliceSync', + icon: 'link', + label: 'Image Slice Sync', + tooltip: 'Enable position synchronization on stack viewports', + commands: [ + { + commandName: 'toggleSynchronizer', + commandOptions: { + type: 'imageSlice', + }, + }, + ], +}) +``` + +You can create another button to toggle 'voi' synchronization. Currently we group +viewports by modality and apply the voi synchronization to all viewports of the same modality. + +```js +ToolbarService.createButton({ + id: 'VoiSync', + icon: 'link', + label: 'VOI Sync', + tooltip: 'Enable VOI synchronization on viewports', + commands: [ + { + commandName: 'toggleSynchronizer', + commandOptions: { + type: 'voi', + }, + }, + ], +}) +``` + +:::tip +For your custom synchronization groups, you can create a new synchronizer type and follow the +same pattern as the existing synchronizers. +::: diff --git a/platform/docs/docs/platform/services/data/ToolGroupService.md b/platform/docs/docs/platform/services/data/ToolGroupService.md new file mode 100644 index 0000000..0d7a04c --- /dev/null +++ b/platform/docs/docs/platform/services/data/ToolGroupService.md @@ -0,0 +1,89 @@ +--- +sidebar_position: 7 +sidebar_label: ToolGroup Service +--- + +# Tool Group Service + +## Overview + +The `ToolGroupService` is responsible for managing tool groups in the OHIF Viewer. + +:::tip +Read more about toolGroups [here](https://www.cornerstonejs.org/docs/concepts/cornerstone-tools/toolGroups) +::: + +It allows you to create, update, and manage tool groups and the tools associated with them. Tool groups are used to organize and control the behavior of various tools in the viewer, such as window level, pan, zoom, measurements, and annotations. + +## Events + +The `ToolGroupService` emits the following events: + +| Event | Description | +| ---------------------------------- | ----------------------------------------------- | +| `VIEWPORT_ADDED` | Fires when a viewport is added to a tool group | +| `TOOLGROUP_CREATED` | Fires when a new tool group is created | + +## API + +- `getToolGroup(toolGroupId?)`: Retrieves a tool group by its ID. If no ID is provided, it returns the tool group for the active viewport. +- `getToolGroupIds()`: Returns an array of all tool group IDs. +- `getToolGroupForViewport(viewportId)`: Returns the tool group associated with the specified viewport. +- `getActiveToolForViewport(viewportId)`: Returns the active tool for the specified viewport. +- `destroy()`: Destroys all tool groups. +- `destroyToolGroup(toolGroupId)`: Destroys the specified tool group. +- `removeViewportFromToolGroup(viewportId, renderingEngineId, deleteToolGroupIfEmpty?)`: Removes a viewport from a tool group. If `deleteToolGroupIfEmpty` is true and the tool group becomes empty after removing the viewport, it will be destroyed. +- `addViewportToToolGroup(viewportId, renderingEngineId, toolGroupId?)`: Adds a viewport to a tool group. If `toolGroupId` is not provided, the viewport will be added to all tool groups. +- `createToolGroup(toolGroupId)`: Creates a new tool group with the specified ID. +- `addToolsToToolGroup(toolGroupId, tools, configs?)`: Adds tools to the specified tool group with optional configurations. +- `createToolGroupAndAddTools(toolGroupId, tools)`: Creates a new tool group and adds the specified tools to it. +- `getToolConfiguration(toolGroupId, toolName)`: Retrieves the configuration for the specified tool in the given tool group. +- `setToolConfiguration(toolGroupId, toolName, config)`: Sets the configuration for the specified tool in the given tool group. + +## Usage + +Here's an example of how to create a new tool group and add tools to it in our basic viewer mode (modes/longitudinal/src/initToolGroups.js) + +```js +import { initToolGroups } from '@ohif/extension-cornerstone'; +import { ToolGroupService } from '@ohif/core'; + +const toolGroupService = new ToolGroupService(); + +// Create a new tool group +const defaultToolGroup = toolGroupService.createToolGroup('default'); + +// Define tools for the tool group +const tools = { + active: [ + { toolName: 'WindowLevel', bindings: [{ mouseButton: 1 }] }, + { toolName: 'Pan', bindings: [{ mouseButton: 2 }] }, + { toolName: 'Zoom', bindings: [{ mouseButton: 3 }] }, + ], + passive: [ + { toolName: 'Length' }, + { toolName: 'ArrowAnnotate' }, + { toolName: 'Bidirectional' }, + ], +}; + +// Add tools to the tool group +toolGroupService.addToolsToToolGroup('default', tools); +``` + +In this example, we create a new `ToolGroupService` instance and use it to create a new tool group with the ID `'default'`. We then define an object `tools` that contains the active and passive tools we want to add to the tool group. Finally, we call the `addToolsToToolGroup` method to add the tools to the newly created tool group. + +:::tip +You can begin the viewer with certain toggle tools already active. For example, if you have your 'referencelines' tool enabled, it will be active when the viewer starts, and the icon state will be correctly set to active as well. +::: + +```js +const tools = { + // the reset + // enabled + enabled: [{ toolName: toolNames.ImageOverlayViewer }, { toolName: toolNames.ReferenceLines }], + }; +``` + + +![alt text](../../../assets/img/reference-lines-from-start.png) diff --git a/platform/docs/docs/platform/services/data/ToolbarService.md b/platform/docs/docs/platform/services/data/ToolbarService.md new file mode 100644 index 0000000..3c8aff7 --- /dev/null +++ b/platform/docs/docs/platform/services/data/ToolbarService.md @@ -0,0 +1,266 @@ +--- +sidebar_position: 5 +sidebar_label: Toolbar Service +--- + +# Toolbar **Service** + +## Overview + +The `ToolBarService` is a straightforward service designed to handle the toolbar. Its main tasks include adding buttons, configuring them, and organizing button sections. When a button is clicked, it executes the designated commands. In the past, this service was more intricate, managing button states and logic. However, all that functionality has now been transferred to the `ToolBarModule` and evaluators. + + + +## Events + +| Event | Description | +| ----------------------- | ---------------------------------------------------------------------- | +| TOOL_BAR_MODIFIED | Fires when a button is added/removed to the toolbar | +| TOOL_BAR_STATE_MODIFIED | Fires when an interaction happens and ToolBarService state is modified | + +## API + +- `createButtonSection(key, buttons)` : creates a section of buttons in the toolbar with the given key and button Ids + +- `addButtons`: add the button definition to the service. + [See below for button definition](#button-definitions). + +- `removeButton(key)` : remove a button from the toolbar. + +- `setButtons`: sets the buttons defined in the service. It overrides all the + previous buttons + + + + +## Button Definitions + + +### Basic + +The simplest toolbarButtons definition has the following properties: + +![toolbarModule-zoom](../../../assets/img/toolbarModule-zoom.png) + + +```js +{ + id: 'Zoom', + uiType: 'ohif.radioGroup', + props: { + icon: 'tool-zoom', + label: 'Zoom', + "commands": [ + { + "commandName": "setToolActive", + "commandOptions": { + "toolName": "Zoom" + }, + "context": "CORNERSTONE" + } + ] + evaluate: 'evaluate.cornerstoneTool', + }, +}, +``` + +| property | description | values | +| ---------------- | ----------------------------------------------------------------- | ------------------------------------------- | +| `id` | Unique string identifier for the definition | \* | +| `icon` | A string name for an icon supported by the consuming application. | \* | +| `label` | User/display friendly to show in UI | \* | +| `commands` | (optional) The commands to run when the button is used. It include a commandName, commandOptions, and/or a context | Any command registered by a `CommandModule` | + + +### Nested (dropdown) + +You can use the `ohif.splitButton` type to build a button with extra tools in +the dropdown. + +- First you need to give your `primary` tool definition to the split button +- the `secondary` properties can be a simple arrow down (`chevron-down` icon) +- For adding the extra tools add them to the `items` list. + +You can see below how `longitudinal` mode is using the available toolbarModule +to create `MeasurementTools` nested button + +![toolbarModule-nested-buttons](../../../assets/img/toolbarModule-nested-buttons.png) + +```js title="modes/longitudinal/src/toolbarButtons.js" +{ + id: 'MeasurementTools', + uiType: 'ohif.splitButton', + props: { + groupId: 'MeasurementToolsGroupId', + // group evaluate to determine which item should move to the top + evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList', + primary: ToolbarService.createButton({ + id: 'Length', + icon: 'tool-length', + label: 'Length', + tooltip: 'Length Tool', + commands: _createSetToolActiveCommands('Length'), + evaluate: 'evaluate.cornerstoneTool', + }), + secondary: { + icon: 'chevron-down', + tooltip: 'More Measure Tools', + }, + items: [ + ToolbarService.createButton({ + id: 'Length', + icon: 'tool-length', + label: 'Length', + tooltip: 'Length Tool', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'Length', + }, + context: 'CORNERSTONE', + }, + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'SRLength', + toolGroupId: 'SRToolGroup', + }, + // we can use the setToolActive command for this from Cornerstone commandsModule + context: 'CORNERSTONE', + }, + ], + evaluate: 'evaluate.cornerstoneTool', + }), + ToolbarService.createButton({ + id: 'Bidirectional', + icon: 'tool-bidirectional', + label: 'Bidirectional', + tooltip: 'Bidirectional Tool', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'Bidirectional', + }, + context: 'CORNERSTONE', + }, + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'SRBidirectional', + toolGroupId: 'SRToolGroup', + }, + context: 'CORNERSTONE', + }, + ], + evaluate: 'evaluate.cornerstoneTool', + }), + ], + }, + }, +``` + +:::tip +split buttons can have a group evaluator (in the above example `evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList`) which can decide what happens +when the user interacts with the buttons. In the above example, we are promoting the button to the primary section if the cornerstone tool is not active in the list of buttons. + +There are other evaluators for instance `evaluate.group.promoteToPrimary` +which does not care about the cornerstone tool and promotes the button to the primary section anyway +::: + +:::tip +If you don't provide a group evaluator nothing would happen and the button will stay in the secondary section. +::: + +## Listeners +Sometimes you need a tool to listen to specific events in order to react properly. +You can add `listeners` for this purpose. We use this pattern for referencelineTools +which should set its source of reference upon active viewport change + +Currently you can subscribe to the following events: +- `ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED`: when the active viewport changes +- `ViewportGridService.EVENTS.VIEWPORTS_READY`: when the viewports are ready in the grid + + +```js + +const ReferenceLinesListeners: RunCommand = [ + { + commandName: 'setSourceViewportForReferenceLinesTool', + context: 'CORNERSTONE', + }, +]; + +ToolbarService.createButton({ + id: 'ReferenceLines', + icon: 'tool-referenceLines', + label: 'Reference Lines', + tooltip: 'Show Reference Lines', + commands: [ + { + commandName: 'setToolEnabled', + commandOptions: { + toolName: 'ReferenceLines', + toggle: true, + }, + context: 'CORNERSTONE', + }, + ], + listeners: { + [ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners, + [ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners, + }, + evaluate: 'evaluate.cornerstoneTool.toggle', +}), + +``` + + + +## Button Sections +In order to organize the buttons, you can create button sections in the toolbar. And +assign buttons to each section separately. + +OHIF provides a `primary` section by default. You can add more sections as needed in your UI +and use toolbarService to create and manage them. (You can look at the toolBox implementation +which take advantage of having a dedicated section for the tools with advanced options, +we use that in the segmentation mode). + + +## Example + +For instance in `longitudinal` mode we are using the `onModeEnter` hook to +add the buttons to the toolbarService and assign them to the primary section. + +```js title="modes/longitudinal/src/index.js" +toolbarService.addButtons([...toolbarButtons, ...moreTools]); +toolbarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'info', + 'WindowLevel', + 'Pan', + 'Capture', + 'Layout', + 'Crosshairs', + 'MoreTools', +]); +``` + +as you see we creating the button section and assigning buttons based on their Ids. + +:::tip +You can even duplicate the same button in different sections and the button will be +in sync in all sections (thanks to the evaluation system). +::: + +:::tip +we will add more section in the toolbar (other than primary) in the future. +::: + +:::note +Don't forget to set up your toolGroups to ensure that your buttons function correctly. Buttons serve as a visual interface. When you interact with them, they execute their commands, and evaluators determine their state post-interaction. +::: diff --git a/platform/docs/docs/platform/services/data/WorkflowStepService.md b/platform/docs/docs/platform/services/data/WorkflowStepService.md new file mode 100644 index 0000000..7070cad --- /dev/null +++ b/platform/docs/docs/platform/services/data/WorkflowStepService.md @@ -0,0 +1,174 @@ +--- +sidebar_position: 9 +sidebar_label: WorkflowStep Service +--- + +# Workflow Step Service + +This service allows you to manage your workflow in smaller steps. It provides a structured way to define and navigate through different stages +or phases of a larger process or workflow. Each step can have its own configuration, layout, toolbar buttons, and other settings tailored to the specific requirements of that stage. + +## Anatomy of a Workflow Step + +The anatomy of a workflow step refers to the different components or properties that define and configure each individual step within the workflow. Each step can be customized with various settings to tailor the user interface, available tools, and behavior of the application for that specific stage of the workflow. Here are the key components that make up a workflow step: + +- `id`: A unique identifier for the step +- `name`: A human-readable name or title for the step, which can be displayed in the user interface to help users understand the current stage of the workflow. +- `hangingProtocol`: The hanging protocol configuration specifies the protocol and stage ID to be used for displaying the images. This ensures that the appropriate data viewports and presentation are used for the current workflow step. +- `layout`: The layout configuration defines the arrangement and visibility of various panels or viewports within the application's user interface for the specific step. This can include specifying which panels should be visible on the left or right side of the screen, as well as any options for panel visibility or behavior. +- `toolbarButtons`: Each step can define a set of toolbar buttons that should be available and displayed in the application's toolbar during that step. Remember the button definitions should already be registered to toolbarService beforehand, here we are just referencing the buttons id in each section. +- `info` : An optional description or additional information about the current workflow step can be provided. which +will be displayed as tooltip in the UI. + +- Step Callbacks or Commands: Some workflow steps may require specific actions or commands to be executed when the step is entered or exited. These callbacks or commands can be defined within the step configuration and can be used to update the application's state, perform data processing, or trigger other relevant actions. For instance you have access to `onEnter` hook to run a command right after the step is entered. + +For instance, a simplified example of our pre-clinical 4D workflow steps configuration might look like this: + +```js +const dynamicVolume = { + sopClassHandler: + "@ohif/extension-cornerstone-dynamic-volume.sopClassHandlerModule.dynamic-volume", + leftPanel: + "@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-volume", + toolBox: + "@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-toolbox", + export: + "@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-export", +} + +const cs3d = { + segmentation: + "@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation", +} + + + +const steps = [ + { + id: "dataPreparation", + name: "Data Preparation", + layout: { + panels: { + left: [dynamicVolume.leftPanel], + }, + }, + toolbarButtons: { + buttonSection: "primary", + buttons: ["MeasurementTools", "Zoom", "WindowLevel", "Crosshairs", "Pan"], + }, + hangingProtocol: { + protocolId: "default4D", + stageId: "dataPreparation", + }, + info: "In the Data Preparation step...", + }, + { + id: "roiQuantification", + name: "ROI Quantification", + layout: { + panels: { + left: [dynamicVolume.leftPanel], + right: [ + [dynamicVolume.toolBox, cs3d.segmentation, dynamicVolume.export], + ], + }, + options: { + leftPanelClosed: false, + rightPanelClosed: false, + }, + }, + toolbarButtons: [ + { + buttonSection: "primary", + buttons: [ + "MeasurementTools", + "Zoom", + "WindowLevel", + "Crosshairs", + "Pan", + ], + }, + { + buttonSection: "dynamic-toolbox", + buttons: ["BrushTools", "RectangleROIStartEndThreshold"], + }, + ], + hangingProtocol: { + protocolId: "default4D", + stageId: "roiQuantification", + }, + info: "The ROI quantification step ...", + }, + { + id: "kineticAnalysis", + name: "Kinetic Analysis", + layout: { + panels: { + left: [dynamicVolume.leftPanel], + right: [], + }, + }, + toolbarButtons: { + buttonSection: "primary", + buttons: ["MeasurementTools", "Zoom", "WindowLevel", "Crosshairs", "Pan"], + }, + hangingProtocol: { + protocolId: "default4D", + stageId: "kineticAnalysis", + }, + onEnter: [ + { + commandName: "updateSegmentationsChartDisplaySet", + options: { servicesManager }, + }, + ], + info: "The Kinetic Analysis step ...", + }, +] + +``` + +## Integration + +After you have defined your workflow steps, you can integrate them into your application by using the `workflowStepsService`. + +These steps should be called on `onSetupRouteComplete` in your mode factory. + + +Note: onModeEnter is too soon to call these steps as the mode is not yet fully initialized. + + +```js +onSetupRouteComplete: ({ servicesManager }) => { + workflowStepsService.addWorkflowSteps(workflowSettings.steps); + workflowStepsService.setActiveWorkflowStep(workflowSettings.steps[0].id); +}, +``` + +check out the `modes/preclinical-4d/src/index.tsx` for a complete example. + + +## User Interface + +We have developed a simple dropdown UI element that you can use to navigate between the different steps of your workflow. This dropdown can be added to the toolbar like below: + +```js +toolbarService.addButtons([ + { + id: 'ProgressDropdown', + uiType: 'ohif.progressDropdown', + }, +]) +toolbarService.createButtonSection('secondary', ['ProgressDropdown']); +``` + +It will appear in the `secondary` location in the toolbar. + +![alt text](../../../assets/img/progressDropdown.png) + +:::note +if you like to place the progressbar in a different location, you can use the Toolbox component +to create a button section and place the progress bar there. + +Read more in the [Toolbar module](../../extensions//modules/toolbar.md) +::: diff --git a/platform/docs/docs/platform/services/data/_category_.json b/platform/docs/docs/platform/services/data/_category_.json new file mode 100644 index 0000000..984ac9a --- /dev/null +++ b/platform/docs/docs/platform/services/data/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Data Services", + "position": 2 +} diff --git a/platform/docs/docs/platform/services/data/index.md b/platform/docs/docs/platform/services/data/index.md new file mode 100644 index 0000000..e93f225 --- /dev/null +++ b/platform/docs/docs/platform/services/data/index.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 1 +sidebar_label: Overview +--- + +# Overview + +Data services are the first category of services which deal with handling non-ui +related state Each service have their own internal state which they handle. + +> We have replaced the _redux_ store. Instead, we have introduced various +> services and a pub/sub pattern to subscribe and run, which makes the `OHIF-v3` +> architecture nice and clean. + +We maintain the following non-ui Services: + +- [DicomMetadata Store](./../data/DicomMetadataStore.md) +- [DisplaySet Service](./../data/DisplaySetService.md) +- [Hanging Protocol Service](../data/HangingProtocolService.md) +- [Toolbar Service](./ToolbarService.md) +- [Measurement Service](../data/MeasurementService.md) +- [Customization Service](./../customization-service/customizationService.md) +- [State Sync Service](../../../../versioned_docs/version-3.9/migration-guide/3p8-to-3p9/5-StateSyncService.md) +- [Panel Service](../data/PanelService.md) + +## Service Architecture + +![services-data](../../../assets/img/services-data.png) + +> We have explained services and how to create a custom service in the +> [`ServicesManager`](../../managers/service.md) section of the docs + +To recap: The simplest service return a new object that has a `name` property, +and `Create` method which instantiate the service class. The "Factory Function" +that creates the service is provided with the implementation (this is slightly +different for UI Services). + +```js +// extensions/customExtension/src/services/backEndService/index.js +import backEndService from './backEndService'; + +export default function WrappedBackEndService(servicesManager) { + return { + name: 'myService', + create: ({ configuration = {} }) => { + return new backEndService(servicesManager); + }, + }; +} +``` + +A service, once created, can be registered with the `ServicesManager` to make it +accessible to extensions. Similarly, the application code can access named +services from the `ServicesManager`. + +[Read more of how to design a new custom service and register it](../../managers/service.md) diff --git a/platform/docs/docs/platform/services/index.md b/platform/docs/docs/platform/services/index.md new file mode 100644 index 0000000..1096ba6 --- /dev/null +++ b/platform/docs/docs/platform/services/index.md @@ -0,0 +1,195 @@ +--- +sidebar_position: 1 +sidebar_label: Introduction +--- + +# Services + +## Overview + +Services are "concern-specific" code modules that can be consumed across layers. +Services provide a set of operations, often tied to some shared state, and are +made available to through out the app via the `ServicesManager`. Services are +particularly well suited to address [cross-cutting +concerns][cross-cutting-concerns]. + +Each service should be: + +- self-contained +- able to fail and/or be removed without breaking the application +- completely interchangeable with another module implementing the same interface + +> In `OHIF-v3` we have added multiple non-UI services and have introduced +> **pub/sub** pattern to reduce coupling between layers. +> +> [Read more about Pub/Sub](./pubsub.md) + +## Services + +The following services is available in the `OHIF-v3`. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ServiceTypeDescription
+ + DicomMetadataStore + + Data Service + DicomMetadataStore +
+ + DisplaySetService + + Data Service + DisplaySetService +
+ + segmentationService + + Segmentation Service + segmentationService +
+ + HangingProtocolService + + Data Service + HangingProtocolService +
+ + MeasurementService (MODIFIED) + + Data Service + MeasurementService +
+ + ToolBarService + + Data Service + ToolBarService +
+ + ViewportGridService + + UI Service + ViewportGridService +
+ + Cine Service + + UI Service + cine +
+ + CustomizationService + + UI Service + customizationService +
+ + UIDialogService + + UI Service + UIDialogService +
+ + UIModalService + + UI Service + UIModalService +
+ + UINotificationService + + UI Service + UINotificationService +
+ + UIViewportDialogService + + UI Service + UIViewportDialogService +
+ + + + + +[core-services]: https://github.com/OHIF/Viewers/tree/master/platform/core/src/services +[services-manager]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/ServicesManager.js +[cross-cutting-concerns]: https://en.wikipedia.org/wiki/Cross-cutting_concern + diff --git a/platform/docs/docs/platform/services/pubsub.md b/platform/docs/docs/platform/services/pubsub.md new file mode 100644 index 0000000..036e592 --- /dev/null +++ b/platform/docs/docs/platform/services/pubsub.md @@ -0,0 +1,114 @@ +--- +sidebar_position: 4 +sidebar_label: Pub Sub +--- + +# Pub sub + +## Overview + +Publishโ€“subscribe pattern is a messaging pattern that is one of the fundamentals +patterns used in reusable software components. + +In short, services that implement this pattern, can have listeners subscribed +to their broadcasted events. After the event is fired, the corresponding +listener will execute the function that is registered. + +You can read more about this design pattern +[here](https://cloud.google.com/pubsub/docs/overview). + +## Example: Default Initialization + +In `Mode.jsx` we have a default initialization that demonstrates a series of +subscriptions to various events. + +```js +async function defaultRouteInit({ + servicesManager, + studyInstanceUIDs, + dataSource, +}) { + const { + DisplaySetService, + HangingProtocolService, + } = servicesManager.services; + + const unsubscriptions = []; + + const { + unsubscribe: instanceAddedUnsubscribe, + } = DicomMetadataStore.subscribe( + DicomMetadataStore.EVENTS.INSTANCES_ADDED, + ({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) => { + const seriesMetadata = DicomMetadataStore.getSeries( + StudyInstanceUID, + SeriesInstanceUID + ); + + DisplaySetService.makeDisplaySets(seriesMetadata.instances, madeInClient); + } + ); + + unsubscriptions.push(instanceAddedUnsubscribe); + + studyInstanceUIDs.forEach(StudyInstanceUID => { + dataSource.retrieve.series.metadata({ StudyInstanceUID }); + }); + + const { unsubscribe: seriesAddedUnsubscribe } = DicomMetadataStore.subscribe( + DicomMetadataStore.EVENTS.SERIES_ADDED, + ({ StudyInstanceUID }) => { + HangingProtocolService.run({studies, displaySets, activeStudy}); + } + ); + unsubscriptions.push(seriesAddedUnsubscribe); + + return unsubscriptions; +} +``` + +## Unsubscription + +You need to be careful if you are adding custom subscriptions to the app. Each +subscription will return an unsubscription function that needs to be executed on +component destruction to avoid adding multiple subscriptions to the same +observer. + +Below, we can see `simplified` `Mode.jsx` and the corresponding `useEffect` +where the unsubscription functions are executed upon destruction. + +```js title="platform/app/src/routes/Mode/Mode.jsx" +export default function ModeRoute(/**..**/) { + /**...**/ + useEffect(() => { + /**...**/ + + DisplaySetService.init(extensionManager, sopClassHandlers); + + extensionManager.onModeEnter(); + mode?.onModeEnter({ servicesManager, extensionManager }); + + const setupRouteInit = async () => { + if (route.init) { + return await route.init(/**...**/); + } + + return await defaultRouteInit(/**...**/); + }; + + let unsubscriptions; + setupRouteInit().then(unsubs => { + unsubscriptions = unsubs; + }); + + return () => { + extensionManager.onModeExit(); + mode?.onModeExit({ servicesManager, extensionManager }); + unsubscriptions.forEach(unsub => { + unsub(); + }); + }; + }); + return <> /**...**/ ; +} +``` diff --git a/platform/docs/docs/platform/services/ui/_category_.json b/platform/docs/docs/platform/services/ui/_category_.json new file mode 100644 index 0000000..9c01213 --- /dev/null +++ b/platform/docs/docs/platform/services/ui/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "UI Services", + "position": 3 +} diff --git a/platform/docs/docs/platform/services/ui/cine-service.md b/platform/docs/docs/platform/services/ui/cine-service.md new file mode 100644 index 0000000..707b5d9 --- /dev/null +++ b/platform/docs/docs/platform/services/ui/cine-service.md @@ -0,0 +1,8 @@ +--- +sidebar_position: 7 +sidebar_label: CINE Service +--- + +# CINE Service + +TODO... diff --git a/platform/docs/docs/platform/services/ui/index.md b/platform/docs/docs/platform/services/ui/index.md new file mode 100644 index 0000000..2a0203a --- /dev/null +++ b/platform/docs/docs/platform/services/ui/index.md @@ -0,0 +1,304 @@ +--- +sidebar_position: 1 +sidebar_label: Overview +--- + +# Overview + + + +A typical web application will have components and state for common UI like +modals, notifications, dialogs, etc. A UI service makes it possible to leverage +these components from an extension. + +We maintain the following UI Services: + +- [UI Notification Service](ui-notification-service.md) +- [UI Modal Service](ui-modal-service.md) +- [UI Dialog Service](ui-dialog-service.md) +- [UI Viewport Dialog Service](ui-viewport-dialog-service.md) +- [CINE Service](cine-service.md) +- [Viewport Grid Service](viewport-grid-service.md) + + + +![UIService](../../../assets/img/ui-services.png) + + + +## Providers for UI services + +**There are several context providers that wraps the application routes. This +makes the context values exposed in the app, and service's `setImplementation` +can get run to override the implementation of the service.** + +```js title="platform/app/src/App.jsx" +function App({ config, defaultExtensions }) { + /**...**/ + /**...**/ + return ( + /**...**/ + + + + + + + {appRoutes} + + + + + + + /**...**/ + ); +} +``` + +## Example + +For instance `UIModalService` has the following Public API: + +```js title="platform/core/src/services/UIModalService/index.js" +const publicAPI = { + name, + hide: _hide, + show: _show, + setServiceImplementation, +}; + +function setServiceImplementation({ + hide: hideImplementation, + show: showImplementation, +}) { + /** ... **/ + serviceImplementation._hide = hideImplementation; + serviceImplementation._show = showImplementation; + /** ... **/ +} + +export default { + name: 'UIModalService', + create: ({ configuration = {} }) => { + return publicAPI; + }, +}; +``` + +`UIModalService` implementation can be set (override) in its context provider. +For instance in `ModalProvider` we have: + +```js title="platform/ui/src/contextProviders/ModalProvider.jsx" +import { Modal } from '@ohif/ui'; + +const ModalContext = createContext(null); +const { Provider } = ModalContext; + +export const useModal = () => useContext(ModalContext); + +const ModalProvider = ({ children, modal: Modal, service }) => { + const DEFAULT_OPTIONS = { + content: null, + contentProps: null, + shouldCloseOnEsc: true, + isOpen: true, + closeButton: true, + title: null, + customClassName: '', + }; + + const show = useCallback(props => setOptions({ ...options, ...props }), [ + options, + ]); + + const hide = useCallback(() => setOptions(DEFAULT_OPTIONS), [ + DEFAULT_OPTIONS, + ]); + + useEffect(() => { + if (service) { + service.setServiceImplementation({ hide, show }); + } + }, [hide, service, show]); + + const { + content: ModalContent, + contentProps, + isOpen, + title, + customClassName, + shouldCloseOnEsc, + closeButton, + } = options; + + return ( + + {ModalContent && ( + + + + )} + {children} + + ); +}; + +export default ModalProvider; + +export const ModalConsumer = ModalContext.Consumer; +``` + +Therefore, anywhere in the app that we have access to react context we can use +it by calling the `useModal` from `@ohif/ui`. As a matter of fact, we are +utilizing the modal for the preference window which shows the hotkeys after +clicking on the gear button on the right side of the header. + +A `simplified` code for our worklist is: + +```js title="platform/app/src/routes/WorkList/WorkList.jsx" +import { useModal, Header } from '@ohif/ui'; + +function WorkList({ + history, + data: studies, + dataTotal: studiesTotal, + isLoadingData, + dataSource, + hotkeysManager, +}) { + const { show, hide } = useModal(); + + /** ... **/ + + const menuOptions = [ + { + title: t('Header:About'), + icon: 'info', + onClick: () => show({ content: AboutModal, title: 'About OHIF Viewer' }), + }, + { + title: t('Header:Preferences'), + icon: 'settings', + onClick: () => + show({ + title: t('UserPreferencesModal:User Preferences'), + content: UserPreferences, + contentProps: { + hotkeyDefaults: hotkeysManager.getValidHotkeyDefinitions( + hotkeyDefaults + ), + hotkeyDefinitions, + onCancel: hide, + currentLanguage: currentLanguage(), + availableLanguages, + defaultLanguage, + onSubmit: state => { + i18n.changeLanguage(state.language.value); + hotkeysManager.setHotkeys(state.hotkeyDefinitions); + hide(); + }, + onReset: () => hotkeysManager.restoreDefaultBindings(), + }, + }), + }, + ]; + /** ... **/ + return ( +
+ /** ... **/ +
+ /** ... **/ +
+ ); +} +``` + + + + + + + +## Tips & Tricks + +It's important to remember that all we're doing is making it possible to control +bits of the application's UI from an extension. Here are a few non-obvious +takeaways worth mentioning: + +- Your application code should continue to use React context + (consumers/providers) as it normally would +- You can substitute our "out of the box" UI implementations with your own +- You can create and register your own UI services +- You can choose not to register a service or provide a service implementation +- In extensions, you can provide fallback/alternative behavior if an expected + service is not registered + - No `UIModalService`? Use the `UINotificationService` to notify users. +- You can technically register a service in an extension and expose it to the + core application + +> Note: These are recommended patterns, not hard and fast rules. Following them +> will help reduce confusion and interoperability with the larger OHIF +> community, but they're not silver bullets. Please speak up, create an issue, +> if you would like to discuss new services or improvements to this pattern. diff --git a/platform/docs/docs/platform/services/ui/ui-dialog-service.md b/platform/docs/docs/platform/services/ui/ui-dialog-service.md new file mode 100644 index 0000000..152841a --- /dev/null +++ b/platform/docs/docs/platform/services/ui/ui-dialog-service.md @@ -0,0 +1,48 @@ +--- +sidebar_position: 4 +sidebar_label: UI Dialog Service +--- +# UI Dialog Service + +Dialogs have similar characteristics to that of Modals, but often with a +streamlined focus. They can be helpful when: + +- We need to grab the user's attention +- We need user input +- We need to show additional information + +If you're curious about the DOs and DON'Ts of dialogs and modals, check out this +article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article] + + + +## Interface + +For a more detailed look on the options and return values each of these methods +is expected to support, [check out it's interface in `@ohif/core`][interface] + +| API Member | Description | +| -------------- | ------------------------------------------------------ | +| `create()` | Creates a new Dialog that is displayed until dismissed | +| `dismiss()` | Dismisses the specified dialog | +| `dismissAll()` | Dismisses all dialogs | + +## Implementations + +| Implementation | Consumer | +| ------------------------------------ | -------------------------- | +| [Dialog Provider][dialog-provider]\* | Baked into Dialog Provider | + +`*` - Denotes maintained by OHIF + +> 3rd Party implementers may be added to this table via pull requests. + + + + +[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIDialogService/index.js +[dialog-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/DialogProvider.js +[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c + diff --git a/platform/docs/docs/platform/services/ui/ui-modal-service.md b/platform/docs/docs/platform/services/ui/ui-modal-service.md new file mode 100644 index 0000000..5fe5a64 --- /dev/null +++ b/platform/docs/docs/platform/services/ui/ui-modal-service.md @@ -0,0 +1,64 @@ +--- +sidebar_position: 3 +sidebar_label: UI Modal Service +--- +# UI Modal Service + +Modals have similar characteristics to that of Dialogs, but are often larger, +and only allow for a single instance to be viewable at once. They also tend to +be centered, and not draggable. They're commonly used when: + +- We need to grab the user's attention +- We need user input +- We need to show additional information + +If you're curious about the DOs and DON'Ts of dialogs and modals, check out this +article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article] + + + +
+ +
+ +## Interface + +For a more detailed look on the options and return values each of these methods +is expected to support, [check out it's interface in `@ohif/core`][interface] + +| API Member | Description | +| ---------- | ------------------------------------- | +| `hide()` | Hides the open modal | +| `show()` | Shows the provided content in a modal | +| `customComponent()` | Overrides the default Modal component | + +## Implementations + +| Implementation | Consumer | +| ---------------------------------- | --------- | +| [Modal Provider][modal-provider]\* | Modal.jsx | +| customComponent | user extensions via `setServiceImplementation({customComponent: Modal})` | + + + +### Custom Component +If you would like to customize the modal component that OHIF uses, you can register your own +component with the `customComponent` property. + +```js +setServiceImplementation({customComponent: Modal}) +``` + + +> 3rd Party implementers may be added to this table via pull requests. + + + + +[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIModalService/index.js +[modal-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/ModalProvider.js +[modal-consumer]: https://github.com/OHIF/Viewers/tree/master/platform/ui/src/components/ohifModal +[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c + diff --git a/platform/docs/docs/platform/services/ui/ui-notification-service.md b/platform/docs/docs/platform/services/ui/ui-notification-service.md new file mode 100644 index 0000000..815ac6d --- /dev/null +++ b/platform/docs/docs/platform/services/ui/ui-notification-service.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 2 +sidebar_label: UI Notification Service +--- +# UI Notification Service + +Notifications can be annoying and disruptive. They can also deliver timely +helpful information, or expedite the user's workflow. Here is some high level +guidance on when and how to use them: + +- Notifications should be non-interfering (timely, relevant, important) +- We should only show small/brief notifications +- Notifications should be contextual to current behavior/actions +- Notifications can serve warnings (acting as a confirmation) + +If you're curious about the DOs and DON'Ts of notifications, check out this +article: ["How To Design Notifications For Better UX"][ux-article] + + + +
+ +
+ + +## Interface + +For a more detailed look on the options and return values each of these methods +is expected to support, [check out it's interface in `@ohif/core`][interface] + +| API Member | Description | +| ---------- | --------------------------------------- | +| `hide()` | Hides the specified notification | +| `show()` | Creates and displays a new notification | + +## Implementations + +| Implementation | Consumer | +| ---------------------------------------- | ----------------------------------------- | +| [Snackbar Provider][snackbar-provider]\* | [SnackbarContainer][snackbar-container]\* | + +`*` - Denotes maintained by OHIF + +> 3rd Party implementers may be added to this table via pull requests. + + + + +[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UINotificationService/index.js +[snackbar-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/SnackbarProvider.js +[snackbar-container]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/components/snackbar/SnackbarContainer.js +[ux-article]: https://uxplanet.org/how-to-design-notifications-for-better-ux-6fb0711be54d + diff --git a/platform/docs/docs/platform/services/ui/ui-viewport-dialog-service.md b/platform/docs/docs/platform/services/ui/ui-viewport-dialog-service.md new file mode 100644 index 0000000..b50e3e7 --- /dev/null +++ b/platform/docs/docs/platform/services/ui/ui-viewport-dialog-service.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 5 +sidebar_label: UI Viewport Dialog Service +--- + +# UI Viewport Dialog Service + +## Overview +This is a new UI service, that creates a modal inside the viewport. + +Dialogs have similar characteristics to that of Modals, but often with a +streamlined focus. They can be helpful when: + +- We need to grab the user's attention +- We need user input +- We need to show additional information + +If you're curious about the DOs and DON'Ts of dialogs and modals, check out this +article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article] + + + +
+ +
+ +## Interface + +For a more detailed look on the options and return values each of these methods +is expected to support, [check out it's interface in `@ohif/core`][interface] + +| API Member | Description | +| -------------- | ------------------------------------------------------ | +| `create()` | Creates a new Dialog that is displayed until dismissed | +| `dismiss()` | Dismisses the specified dialog | +| `dismissAll()` | Dismisses all dialogs | + +## Implementations + +| Implementation | Consumer | +| ------------------------ | -------------------------- | +| [ViewportDialogProvider] | Baked into Dialog Provider | + +`*` - Denotes maintained by OHIF + + +## State + +```js +const DEFAULT_STATE = { + viewportId: null, + message: undefined, + type: 'info', // "error" | "warning" | "info" | "success" + actions: undefined, // array of { type, text, value } + onSubmit: () => { + console.log('btn value?'); + }, + onOutsideClick: () => { + console.warn('default: onOutsideClick') + }, + onDismiss: () => { + console.log('dismiss? -1'); + }, +}; +``` diff --git a/platform/docs/docs/platform/services/ui/viewport-action-menu.md b/platform/docs/docs/platform/services/ui/viewport-action-menu.md new file mode 100644 index 0000000..5a05925 --- /dev/null +++ b/platform/docs/docs/platform/services/ui/viewport-action-menu.md @@ -0,0 +1,30 @@ +--- +sidebar_position: 8 +sidebar_label: Viewport Action Corners +--- + +# Viewport Action Corners Service + +The Viewport Action Corners Service is a powerful tool for managing interactive components in the corners of viewports within the OHIF viewer. This service allows developers to dynamically add, remove, and organize various UI elements such as menus, buttons, or custom components in specific locations around the viewport. + +## Overview + +The Viewport Action Corners Service extends the PubSubService and provides methods to: + +- Add single or multiple components to viewport corners +- Clear components from a specific viewport +- Manage the state of viewport corner components + +## Key Features + +- **Flexible Positioning**: Components can be placed in top-left, top-right, bottom-left, or bottom-right corners of the viewport. +- **Priority Ordering**: Components can be assigned priority indices for ordering within a corner. +- **Viewport-Specific**: Actions are associated with specific viewports, allowing for individualized control. +- **Dynamic Updates**: Components can be added or removed at runtime, enabling context-sensitive UI elements. + +## Usage + +To use the Viewport Action Corners Service, you typically interact with it through the `servicesManager`. Here's a basic example of how to add a component: + + +Take a look at how we add window level menu to the top right corner of the viewport in the `OHIFCornerstoneViewport` component. diff --git a/platform/docs/docs/platform/services/ui/viewport-grid-service.md b/platform/docs/docs/platform/services/ui/viewport-grid-service.md new file mode 100644 index 0000000..186afc4 --- /dev/null +++ b/platform/docs/docs/platform/services/ui/viewport-grid-service.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 6 +sidebar_label: Viewport Grid Service +--- + +# Viewport Grid Service + +## Overview + +This is a new UI service, that handles the grid layout of the viewer. + +## Events + +There are seven events that get publish in `ViewportGridService `: + +| Event | Description | +| ----------------------------- | --------------------------------------------------| +| ACTIVE_VIEWPORT_ID_CHANGED | Fires the Id of the active viewport is changed | +| LAYOUT_CHANGED | Fires the layout is changed | +| GRID_STATE_CHANGED | Fires when the entire grid state is changed | +| VIEWPORTS_READY | Fires when the viewports are ready in the grid | + +## Interface + +For a more detailed look on the options and return values each of these methods +is expected to support, [check out it's interface in `@ohif/core`][interface] + +| API Member | Description | +| --------------------------------------------------------------------- | --------------------------------------------------- | +| `setActiveViewportId(viewportId)` | Sets the active viewport Id in the app | +| `getState()` | Gets the states of the viewport (see below) | +| `setDisplaySetsForViewport({ viewportId, displaySetInstanceUID })` | Sets displaySet for viewport based on displaySet Id | +| `setLayout({numCols, numRows, keepExtraViewports})` | Sets rows and columns. When the total number of viewports decreases, optionally keep the extra/offscreen viewports. | +| `reset()` | Resets the default states | +| `getNumViewportPanes()` | Gets the number of visible viewport panes | +| `getLayoutOptionsFromState(gridState)` | Utility method that produces a `ViewportLayoutOptions` based on the passed in state| +| `getActiveViewportId()` | Returns the viewport Id of the active viewport in the grid| +| `getActiveViewportOptionByKey(key)` | Gets the specified viewport option field (key) for the active viewport | + +## Implementations + +| Implementation | Consumer | +| ---------------------- | -------------------------- | +| [ViewportGridProvider] | Baked into Dialog Provider | + +`*` - Denotes maintained by OHIF + +## State + +```js +const DEFAULT_STATE = { + // starting from null, hanging + // protocol will defined number of rows and cols + numRows: null, + numCols: null, + viewports: [ + /* + * { + * displaySetInstanceUID: string, + * } + */ + ], + activeViewportId: null, +}; +``` diff --git a/platform/docs/docs/platform/themeing.md b/platform/docs/docs/platform/themeing.md new file mode 100644 index 0000000..48a881c --- /dev/null +++ b/platform/docs/docs/platform/themeing.md @@ -0,0 +1,166 @@ +--- +sidebar_position: 2 +sidebar_label: Theming +--- + +# Viewer: Theming + +`OHIF-v3` has introduced the +[`LayoutTemplateModule`](./extensions/modules/layout-template.md) which enables +addition of custom layouts. You can easily design your custom components inside +an extension and consume it via the layoutTemplate module you write. + +## Tailwind CSS + +[Tailwind CSS](https://tailwindcss.com/) is a utility-first CSS framework for +creating custom user interfaces. + +Below you can see a compiled version of the tailwind configs. Each section can +be edited accordingly. For instance screen size break points, primary and +secondary colors, etc. + +```js +module.exports = { + prefix: '', + important: false, + separator: ':', + theme: { + screens: { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + }, + colors: { + overlay: 'rgba(0, 0, 0, 0.8)', + transparent: 'transparent', + black: '#000', + white: '#fff', + initial: 'initial', + inherit: 'inherit', + + indigo: { + dark: '#0b1a42', + }, + aqua: { + pale: '#7bb2ce', + }, + + primary: { + light: '#5acce6', + main: '#0944b3', + dark: '#090c29', + active: '#348cfd', + }, + + secondary: { + light: '#3a3f99', + main: '#2b166b', + dark: '#041c4a', + active: '#1f1f27', + }, + + common: { + bright: '#e1e1e1', + light: '#a19fad', + main: '#fff', + dark: '#726f7e', + active: '#2c3074', + }, + + customgreen: { + 100: '#05D97C', + }, + + customblue: { + 100: '#c4fdff', + 200: '#38daff', + }, + }, + }, +}; +``` + +You can also use the color variable like before. For instance: + +```js +primary: { + default: โ€˜var(--default-color)โ€˜, + light: โ€˜#5ACCE6โ€™, + main: โ€˜#0944B3โ€™, + dark: โ€˜#090C29โ€™, + active: โ€˜#348CFDโ€™, +} +``` + +## White Labeling + +A white-label product is a product or service produced by one company (the +producer) that other companies (the marketers) rebrand to make it appear as if +they had made it - +[Wikipedia: White-Label Product](https://en.wikipedia.org/wiki/White-label_product) + +Current white-labeling options are limited. We expose the ability to replace the +"Logo" section of the application with a custom "Logo" component. You can do +this by adding a whiteLabeling key to your configuration file. + +```js +window.config = { + /** .. **/ + whiteLabeling: { + createLogoComponentFn: function(React) { + return React.createElement( + 'a', + { + target: '_blank', + rel: 'noopener noreferrer', + className: 'text-white underline', + href: 'http://radicalimaging.com', + }, + React.createElement('h5', {}, 'RADICAL IMAGING') + ); + }, + }, + /** .. **/ +}; +``` + +> You can simply use the stylings from tailwind CSS in the whiteLabeling + +In addition to text, you can also add your custom logo + +```js +window.config = { + /** .. **/ + whiteLabeling: { + createLogoComponentFn: function(React) { + return React.createElement( + 'a', + { + target: '_self', + rel: 'noopener noreferrer', + className: 'text-purple-600 line-through', + href: '/', + }, + React.createElement('img', { + src: './customLogo.svg', + // className: 'w-8 h-8', + }) + ); + }, + }, + /** .. **/ +}; +``` + +The output will look like + +![custom-logo](../assets/img/custom-logo.png) + + + + +[wikipedia]: https://en.wikipedia.org/wiki/White-label_product + diff --git a/platform/docs/docs/release-notes.md b/platform/docs/docs/release-notes.md new file mode 100644 index 0000000..48491be --- /dev/null +++ b/platform/docs/docs/release-notes.md @@ -0,0 +1,10 @@ +--- +sidebar_position: 2 +sidebar_label: Release Notes +--- + +# Release Notes + + + +You can find the detailed release notes on the OHIF website. Please visit [https://ohif.org/release-notes](https://ohif.org/release-notes) diff --git a/platform/docs/docs/resources.md b/platform/docs/docs/resources.md new file mode 100644 index 0000000..bbdd4a1 --- /dev/null +++ b/platform/docs/docs/resources.md @@ -0,0 +1,177 @@ +--- +sidebar_position: 13 +sidebar_label: Resources +--- + +# Resources + +Throughout the development of the OHIF Viewer, we have participated in various +conferences and "hackathons". In this page, we will provide the presentations +and other resources that we have provided to the community in the past: + +## 2025 + +### Machine Learning in Medical Imaging Consortium (MaLMIC) | January 2025 + +We presented two talks at the Machine Learning in Medical Imaging Consortium (MaLMIC) 2025 conference. + +- Advanced Medical Imaging Visualization [Slides](https://docs.google.com/presentation/d/1HZDL-72nNe4BPawDxR3XnSFLB3oLo72RjExc-KHDZfo/edit?usp=sharing) +- Introducing Advanced Segmentation Tools in the OHIF Viewer and Cornerstone3D [Slides](https://docs.google.com/presentation/d/146oJ24PPsFZaDPHeFudRF1dmbL42K9yHzQdXXAXdWxk/edit?usp=sharing) + + +## 2024 + +### ITCR Sustainment Session 2024 + +Dr. Gordon Harris presented at ITCR sustainment session about the future of OHIF. + +- OHIF Sustainability [Slides](https://docs.google.com/presentation/d/15380mjCzBKBj9PuysCW1Q9ODnyoypJrCDpj3atTtK6I/edit?usp=sharing) + +### ITCR Sustainment Panel 2024 + +- Advanced Medical Imaging Visualization [Slides](https://docs.google.com/presentation/d/1alUp9uJpoJs3aAUE0KqrufGo6e6HHvXYmOAdJp-Rlkc/edit?usp=sharing) + + +### IMNO 2024 - March 19-20, 2024 + +We participated in the Imaging Network Ontario (ImNO) 2024 symposium, presenting three posters. One of our presentations received the best talk award during the session. + + +- Advancing Medical Imaging on the Web: Implementation of Hanging Protocols for Automated Image Display Configuration in OHIF V3 [Poster](https://www.dropbox.com/scl/fi/z4h86bmsxi0c62e1n6h9l/P7-9-Alireza-Sedghi-Final.pdf?rlkey=v5pm0p5ygkbq41x9bz3hr5yi8&dl=0) +- Advancing Medical Imaging on the Web: Optimizing the Dicomweb Server Architecture with Static Dicomweb [Poster](https://www.dropbox.com/scl/fi/ep0lxjp90kbxhjoffe4kh/P7-10-Bill-Wallace-Final.pdf?rlkey=xl2u6tdnh9j9hgvkajxv3b02o&dl=0) +- (**๐Ÿ†๐Ÿ† BEST PRESENTATION AWARD in the Session 7 Pitches: Devices, HW, SW Development ๐Ÿ†๐Ÿ†**) Advancing Medical Imaging on the Web: Integrating High Throughput JPEG 2000 (HTJ2K) in Cornerstone3D for Streamlined Progressive Loading and Visualization [Poster](https://www.dropbox.com/scl/fi/srs2rxgtv2r69ver9ub1j/P7-8-Bill-Wallace-Final.pdf?rlkey=k9mmraw76r9q2s3b9w9s0793w&dl=0) + +## 2023 + +### ITCR 2023 Conference | September 11-13, 2023 + +Dr. Gordon Harris presented an update on OHIF in [NCI Informatics Technology for Cancer Research Annual Meeting](https://www.itcr2023.org/). You can find the slides and poster here: +[[Slides]](https://docs.google.com/presentation/d/1R38s95db_yZj0WoYdlUbaWGZsWVb3H-3u_hXBZXiTaE/edit?usp=sharing)[[Poster]](https://ohif-assets.s3.us-east-2.amazonaws.com/presentations/OHIF-ITCR-2023-FINAL-PRINT.pdf) + + + + +### SIIM 2023 Tech Tools Webinar | April 12th, 2023 + +Free, Open Source Tools for Research: MONAI and OHIF Viewer +[[Slides](https://docs.google.com/presentation/d/1afJ5Y9Tzukgn7eAbaO1oiCtN7XvIimFdmZP-HcOUofA/edit?usp=sharing)][[Video](https://www.youtube.com/watch?v=lo8J5w5jUJI)] + + +### NA-MIC Project Week 38th 2023 - Remote + +We participated in the 38th Project Week with three projects around OHIF. [[Website](https://projectweek.na-mic.org/PW38_2023_GranCanaria/)] + +- PolySeg representations for OHIF Viewer ([link](https://projectweek.na-mic.org/PW38_2023_GranCanaria/Projects/OHIF_PolySeg/)) +- Cross study synchronizer for OHIF Crosshair ([link](https://projectweek.na-mic.org/PW38_2023_GranCanaria/Projects/OHIF_SyncCrosshair/)) +- DATSCAN Viewer implementation in OHIF ([link](https://projectweek.na-mic.org/PW38_2023_GranCanaria/Projects/OHIF_DATSCAN/)) + + + +## 2022 + +### OHIF Demo to Interns +[[Slides]](https://docs.google.com/presentation/d/1a2PkUnqkVMaXaBsuFn7-PPlBJULU3dBwzI_44gKFeYI/edit?usp=sharing) + +### SIIM 2022 - Updates from the Imaging Informatics Community +We participated in the SIIM 2022 conference to give update for the imaging +informatics community. +[[Slides]](https://docs.google.com/presentation/d/1EUGaUzQtGhZbZWpGLe6ONqChpVMw9Qr9l3KHODevMow/edit?usp=sharing) +[[Video]](https://vimeo.com/734463662/dbd5a88371) + +### The Imaging Network Ontario - Remote + +The Imaging Network Ontario (ImNO) is an annual symposium that brings together +medical imaging researchers and scientists from across Canada to share +knowledge, ideas, and experiences. +[[Slides]](https://docs.google.com/presentation/d/18XZDon4-Sitc2a70V5sFyhyUVZI_mIgfXHGtdxhZMjE/edit?usp=sharing) +[[Video]](https://vimeo.com/843234581/ad7d308a44) + + +### [NA-MIC Project Week 36th 2022 - Remote](https://github.com/NA-MIC/ProjectWeek/blob/master/PW36_2022_Virtual/README.md) + +The Project Week is a week-long hackathon of hands-on activity in which medical +image computing researchers. OHIF team participated and gave a talk on OHIF and +Cornerstone in the 36th Project Week: +[[Slides]](https://docs.google.com/presentation/d/1-GtOKmr2cQi-r3OFyseSmgLeurtB3KXUkGMx2pVLh1I/edit?usp=sharing) +[[Video]](https://vimeo.com/668339696/63a2c48de8) + +## 2021 + +### [NA-MIC Project Week 35th 2021 - Remote](https://github.com/NA-MIC/ProjectWeek/tree/master/PW35_2021_Virtual) + +The Project Week is a week-long hackathon of hands-on activity in which medical +image computing researchers. OHIF team participated in the 35th Project Week +in 2021. +[[Slides]](https://docs.google.com/presentation/d/1KYNjuiI8lT1foQ4P9TGNV0lBhM6H-5KBs0wkYj4JJbk/edit?usp=sharing) + +### Chan Zuckerberg Initiative (CZI) + +Project presentations and demonstrations of Essential Open Source Software for +Science (EOSS) grantees +[[Slides]](https://docs.google.com/presentation/d/1_CLtG2hsL3ZxOtV2olVnzBOzq-TMLrHLomOy3FiU4NE/edit?usp=sharing) +[[Video]](https://youtu.be/0FjKkTJO0Rc?t=3737) + +### Google Cloud Tech + +Healthcare Imaging with Cloud Healthcare API +[[Video]](https://www.youtube.com/watch?v=2MiX9ScHFhY) + +## 2020 + +### OHIF ITCR Pitch + +OHIF pitch for Informatics Technology for Cancer Research (ITCR) +[[Slides]](https://docs.google.com/presentation/d/1MZXnZrVAnjmhVIWqC-aRSvJOoMMRLhLddACdCa1TybM/edit?usp=sharing) +[[Video]](https://vimeo.com/843234613/625bdb8793) + +## 2019 + +### OHIF and VTK.js Training Course + +OHIF and Kitware collaboration to create a training course for OHIF and VTK.js +developers. Funding for this work was provided by Kitware (NIH NINDS +R44NS081792, NIH NINDS R42NS086295, NIH NIBIB and NIGMS R01EB021396, NIH NIBIB +R01EB014955), Isomics (NIH P41 EB015902), and Massachusetts General Hospital +(NIH U24 CA199460). + +1. Introduction to VTK.js and OHIF + [[Slides]](https://docs.google.com/presentation/d/1NCJxpfx_qUGJI_2DhbECzaOg0k-Z6b65QlUptCofN-A/edit#slide=id.p) + [[Video]](https://vimeo.com/375520781) +2. Developing with VTK.js + [[Slides]](https://docs.google.com/presentation/d/17TCS6EhFi6SWFIrcAJ-DFdFzFFL-WD9BBTv-owmMdDU/edit#slide=id.p) + [[Video]](https://vimeo.com/375521036) +3. VTK.js Architecture and Tooling + [[Slides]](https://docs.google.com/presentation/d/1Sr1OGxMSw0oCt46koKQbmwSIE11Kqq8MGtyW3W0ASpk/edit?usp=gmail_thread) + [[Video]](https://vimeo.com/375521810) +4. OHIF + VTK.js Integration + [[Slides]](https://docs.google.com/presentation/d/1Iwg-u01HGVf1CgC6NbcBD3gm3uHN9WhjU59FSz55TN8/edit?ts=5d9c9ce4#slide=id.g59aa99cda4_0_131) + [[Video]](https://vimeo.com/375521206) + +## 2017 + +### Lesion Tracker + +LesionTracker: Extensible Open-Source Zero-Footprint Web Viewer for Cancer +Imaging Research and Clinical Trials. This project was supported in part by +grant U24 CA199460 from the National Cancer Institute (NCI) Informatics +Technology for Cancer Research (ITCR) Program. +[[Video]](https://www.youtube.com/watch?v=gUIPtoSBL-Q) + +### OHIF Community Meeting - June + +[[Slides]](https://docs.google.com/presentation/d/1K9Y6eP5DYTXoDlfwCZE6GkCUp83AK4_40YQS0dlzVBo/edit?usp=sharing) + +## 2016 + +### Imaging Community Call + +Open Source Oncology Web Viewer; Presentation by Gordon J. Harris +[[Slides]](https://www.slideshare.net/imgcommcall/lesiontracker) + +### OHIF Community Meeting - June + +[[Slides]](https://docs.google.com/presentation/d/1Ai25mBG0ZWUPhaadp3VnbCVmkYs9K51sQ8osMixrvJ0/edit?usp=sharing) + +### OHIF Community Meeting - September + +[[Slides]](https://docs.google.com/presentation/d/1iYZoU7v7KHSLHiKwH1_9_wweAkG7RGnyxrWeeHva4zQ/edit?usp=sharing) diff --git a/platform/docs/docs/user-guide/_category_.json b/platform/docs/docs/user-guide/_category_.json new file mode 100644 index 0000000..68ea78e --- /dev/null +++ b/platform/docs/docs/user-guide/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "User Guide", + "position": 2 +} diff --git a/platform/docs/docs/user-guide/index.md b/platform/docs/docs/user-guide/index.md new file mode 100644 index 0000000..f40ef30 --- /dev/null +++ b/platform/docs/docs/user-guide/index.md @@ -0,0 +1,85 @@ +--- +sidebar_position: 2 +sidebar_label: Study List +--- + +# Study List + +## Overview + +The first page you will see when the viewer is loaded is the `Study List`. In +this page you can explore all the studies that are stored on the configured +server for the `OHIF Viewer`. + +![user-study-list](../assets/img/user-study-list.png) + +## Sorting + +When the Study List is opened, the application queries the PACS for 101 studies +by default. If there are greater than 100 studies returned, the default sort for +the study list is dictated by the image archive that hosts these studies for the +viewer and study list sorting will be disabled. If there are less than or equal +to 100 studies returned, they will be sorted by study date (most recent to +oldest) and study list sorting will be enabled. Whenever a query returns greater +than 100 studies, use filters to narrow results below 100 studies to enable +Study List sorting. + +## Filters + +There are certain filters that can be used to limit the study list to the +desired criteria. + +- Patient Name: Searches between patients names +- MRN: Searches between patients Medical Record Number +- Study Date: Filters the date of the acquisition +- Description: Searches between study descriptions +- Modality: Filters the modalities +- Accession: Searches between patients accession number + +An example of using study list filter is shown below: + +![user-study-filter](../assets/img/user-study-filter.png) + +Below the study list are pagination options for 25, 50, or 100 studies per page. + +![user-study-next](../assets/img/user-study-next.png) + +For static wado servers, you can enable fuzzy matching by setting the `supportsFuzzyMatching` property to true, after enabling it, fuzzy matching will be peformed on the patient name field, for example, if PatientName is "John^Doe", then "jo", "Do" and "John Doe" will all match. However "ohn" will not match. + +## Study Summary + +Click on a study to expand the study summary panel. + +![user-study-summary](../assets/img/user-study-summary.png) + +A summary of series available in the study is shown, which contains the series +description, series number, modality of the series, instances in the series, and +buttons to launch viewer modes to display the study. + +## Study Specific Modes + +All available modes are seen in the study expanded view. Modes can be enabled or +disabled for a study based on the modalities contained within the study. + +In the screenshot below, there are two modes shown for the selected study + +- Basic Viewer: Default mode that enables rendering and measurement tracking + +- PET/CT Fusion: Mode for visualizing the PET CT study in a 3x3 format. + +Based on the mode configurations (e.g., available modalities), PET/CT mode is +disabled for studies that do not contain PET AND CT images. + + + +![user-studyist-modespecific](../assets/img/user-studyist-modespecific.png) + +The previous screenshot shows a study containing PET and CT images and both +Basic Viewer and PET/CT Mode are available. + +## View Study + +The `Basic Viewer` mode is available for all studies by default. Click on the +mode button to launch the viewer. + +![user-open-viewer](../assets/img/user-open-viewer.png) diff --git a/platform/docs/docs/user-guide/viewer/Language.md b/platform/docs/docs/user-guide/viewer/Language.md new file mode 100644 index 0000000..de06487 --- /dev/null +++ b/platform/docs/docs/user-guide/viewer/Language.md @@ -0,0 +1,22 @@ +--- +sidebar_position: 8 +--- + +# Language + +OHIF supports internationalization capabilities and setting the general language +of the Viewer. + +It should be noted that we don't have complete translations for all the components +and all the languages; however, you can easily add the key value translation pairs +following developer guides. + +Summary of language changing usage can be seen below: + + + +## Overview Video + +
+ +
diff --git a/platform/docs/docs/user-guide/viewer/_category_.json b/platform/docs/docs/user-guide/viewer/_category_.json new file mode 100644 index 0000000..417861d --- /dev/null +++ b/platform/docs/docs/user-guide/viewer/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Basic Viewer", + "position": 2 +} diff --git a/platform/docs/docs/user-guide/viewer/hotkeys.md b/platform/docs/docs/user-guide/viewer/hotkeys.md new file mode 100644 index 0000000..5d8aac1 --- /dev/null +++ b/platform/docs/docs/user-guide/viewer/hotkeys.md @@ -0,0 +1,15 @@ +--- +sidebar_position: 7 +--- + +# Hotkeys + +To open the hotkey assignment panel, you can click on the Preferences gear on the +top right side of the viewer. + + +Below, you can see the default hotkeys key bindings: + +![user-hotkeys-default](../../assets/img/user-hotkeys-default.png) + +Hotkeys can be assigned to custom bindings that persist for the duration of the browser session. diff --git a/platform/docs/docs/user-guide/viewer/index.md b/platform/docs/docs/user-guide/viewer/index.md new file mode 100644 index 0000000..6918ce3 --- /dev/null +++ b/platform/docs/docs/user-guide/viewer/index.md @@ -0,0 +1,29 @@ +--- +sidebar_position: 1 +sidebar_label: Overview +--- + + +# Overview +When you open a mode, viewport, toolbar and panels of the mode get shown. +It is important to note that each mode has a different UI, which serves its purpose. +Here we explain various components of `Basic Viewer` mode which includes measurement +tracking functionalities. + +Basic viewer mode (longitudinal): + +![user-viewer](../../assets/img/user-viewer.png) + +Let's break different aspects of the viewer to the main components: + +- Left Panel (study panel): displays series thumbnails with series details +- Viewport: renders the image and displays annotations +- Right Panel (measurements): displays annotations details +- Toolbar: displays tools and logo + +![user-viewer-components](../../assets/img/overview.png) + + + + +Now, we explain each component and its sub-elements in detail. diff --git a/platform/docs/docs/user-guide/viewer/measurement-panel.md b/platform/docs/docs/user-guide/viewer/measurement-panel.md new file mode 100644 index 0000000..b4a728d --- /dev/null +++ b/platform/docs/docs/user-guide/viewer/measurement-panel.md @@ -0,0 +1,74 @@ +--- +sidebar_position: 3 +--- + +# Measurement Panel + +## Introduction +In `Basic Viewer` mode, the right panel is the `Measurement Panel`. The Measurement Panel can be expanded or hidden by clicking on the arrow to the left of `Measurements`. + +Select a measurement tool and mark an image to initiate measurement tracking. A pop-up will ask if you want to track measurements for the series on which the annotation was drawn. + +![user-measurement-panel-modal](../../assets/img/measurement-panel-prompt.png) + + + + + +If you select `Yes`, the series becomes a `tracked series`, and the current drawn measurement and next measurements are shown on the measurement panel on the right. + +![user-measurement-panel-tracked](../../assets/img/measurement-panel-tracked.png) + +If you select `No`, the measurement becomes temporary. The next annotation made will repeat the measurement tracking prompt. + +If you select `No, do not ask again`, all annotations made on the study will be temporary. + +![measurement-temporary](../../assets/img/measurement-temporary.png) + + +## Labeling Measurements +You can edit the measurement name by hovering over the measurement and selecting the edit icon. You can also label or relabel a measurement by right-clicking on it in the viewport. + +![user-measurement-edit](../../assets/img/measurement-panel-1.png) + + + +## Deleting a Measurement +A measurement can be deleting by dragging it outside the image in the viewport or by right-clicking on the measurement in the viewport and selecting 'Delete'. + + +## Jumping to a Measurement +Measurement navigation inside the top viewport can be used to move to previous and next measurement. + + +![measurements-prevNext](../../assets/img/measurements-prevNext.png) + +If a series containing a measurement is currently being displayed in a viewport, you can jump to display the measurement in the viewport by clicking on it in the Measurement Panel. + +## Export Measurements + +You can export the measurements by clicking on the `Export`. A CSV file will get downloaded to your local computer containing the drawn measurements. + + +![user-measurement-export](../../assets/img/user-measurement-export.png) + + +If you have set up your DICOM server to be able to store instances from the viewer, then you are able to create a report by clicking on the `Create Report`. +This will create a DICOM Structured Report (SR) from the measurements and push it +to the server. + +For instance, running the Viewer on a local DCM4CHEE: + + + +
+ +
+ +## Overview Video +An overview of measurement drawing and exporting can be seen below: + + +
+ +
diff --git a/platform/docs/docs/user-guide/viewer/measurement-tracking.md b/platform/docs/docs/user-guide/viewer/measurement-tracking.md new file mode 100644 index 0000000..528ec78 --- /dev/null +++ b/platform/docs/docs/user-guide/viewer/measurement-tracking.md @@ -0,0 +1,124 @@ +--- +sidebar_position: 4 +--- + +# Measurement Tracking + +## Introduction +OHIF-V3's `Basic Viewer` implements a `Measurement Tracking` workflow. Measurement +tracking allows you to: + +- Draw annotations and have them shown in the measurement panel +- Create a report from the tracked measurement and export them as DICOM SR +- Use already exported DICOM SR to re-hydrate the measurements in the viewer + + +## Status Icon +Each viewport has a left icon indicating whether the series within the viewport +contains: + +- tracked measurement OR +- untracked measurement OR +- Structured Report OR +- Locked (uneditable) Structured Report + +In the following, we will discuss each category. + +### Tracked vs Untracked Measurements + +`OHIF-v3` implements a workflow for measurement tracking that can be seen below. + +![user-measurement-panel-modal](../../assets/img/tracking-workflow1.png) + +In summary, when you create an annotation, a prompt will be shown whether to start tracking or not. If you start the tracking, the annotation style will change to a solid line, and annotation details get displayed on the measurement panel. +On the other hand, if you decline the tracking prompt, the measurement will be considered "temporary," and annotation style remains as a dashed line and not shown on the right panel, and cannot be exported. + + +Below, you can see different icons that appear for a tracked vs. untracked series in +`OHIF-v3`. + +![tracked-not-tracked](../../assets/img/tracked-not-tracked.png) + + + +#### Overview video for starting the tracking for measurements: + + +
+ +
+ + +

+ +#### Overview video for not starting tracking for measurements: + + +
+ +
+ + +### Reading and Writing DICOM SR + +`OHIF-v3` provides full support for reading, writing and mapping the DICOM Structured +Report (SR) to interactable `Cornerstone Tools`. When you load an already exported +DICOM SR into the viewer, you will be prompted whether to track the measurements +for the series or not. + +![SR-exported](../../assets/img/SR-exported.png) + +If you click Yes, DICOM SR measurements gets re-hydrated into the viewer and +the series become a tracked series. However, If you say no and later decide to say track the measurements, you can always click on the SR button that will prompt you +with the same message again. + +![restore-exported-sr](../../assets/img/restore-exported-sr.png) + +The full workflow for saving measurements to SR and loading SR into the viewer is shown below. + +![user-measurement-panel-modal](../../assets/img/tracking-workflow2.png) +![user-measurement-panel-modal](../../assets/img/tracking-workflow3.png) + + +#### Overview video for loading DICOM SR and making a tracked series: + + +
+ +
+ +

+ +#### Overview video for loading DICOM SR and not making a tracked series: + + +
+ +
+ +

+ +
+ +
+ +### Loading DICOM SR into an Already Tracked Series + +If you have an already tracked series and try to load a DICOM SR measurements, +you will be shown the following lock icon. This means that, you can review the +DICOM SR measurement, manipulate image and draw "temporary" measurements; however, +you cannot edit the DICOM SR measurement. + + +![locked-sr](../../assets/img/locked-sr.png) + +

+ + +#### Overview video for loading DICOM SR inside an already tracked series: + + + +
+ +
diff --git a/platform/docs/docs/user-guide/viewer/study-panel.md b/platform/docs/docs/user-guide/viewer/study-panel.md new file mode 100644 index 0000000..1b5190f --- /dev/null +++ b/platform/docs/docs/user-guide/viewer/study-panel.md @@ -0,0 +1,32 @@ +--- +sidebar_position: 2 +--- + +# Study Panel + +In `Basic Viewer` mode, the left panel includes Studies related to the current +patient. You can see three main type of studies below + +- Primary: The opened study from the study list. This study is always expanded + by default. +- Recent: All studies for the patient that contain study dates within 1 year of + the primary study +- All: All studies available for the patient contained within the source + repository + +The `Study Panel` displays the measurement tracking status of each series within +a study. As you can see in the first picture, the dashed circle on the left side +of each series demonstrates whether the series is being tracked for measurement +or not. + + + +![user-study-panel](../../assets/img/user-study-panel.png) + +Studies can be expanded or collapsed by clicking on the study information in the +Study Panel. If a series is being tracking within a study, the Measurement Panel +will display this information while the study is collapsed. + + + + diff --git a/platform/docs/docs/user-guide/viewer/toolbar.md b/platform/docs/docs/user-guide/viewer/toolbar.md new file mode 100644 index 0000000..1fb5527 --- /dev/null +++ b/platform/docs/docs/user-guide/viewer/toolbar.md @@ -0,0 +1,85 @@ +--- +sidebar_position: 6 +--- + + +# Toolbar + +The four main components of the toolbar are: + +- Navigation back to the [Study List](../index.md) +- Logo and white labelling +- [Tools](#tools) +- [Preferences](#preferences) + +![user-viewer-toolbar](../../assets/img/user-viewer-toolbar.png) + + +## Tools +This section displays all the available tools inside the mode. +## Measurement tools +The basic viewer comes with the following default measurement tools: + +- Length Tool: Calculates the linear distance between two points in *mm* +- Bidirectional Tool: Creates a measurement of the longest diameter (LD) and longest perpendicular diameter (LPD) in *mm* +- Annotation: Used to create a qualitative marker with a freetext label +- Ellipse: Measures an elliptical area in *mm2* and Hounsfield Units (HU) +- Calibration Tool: Calibrate (or override) the Pixel Spacing Attribute (Physical distance in the patient between the center of each pixel, specified by a numeric pair - adjacent row spacing (delimiter) adjacent column spacing in mm) + +When a measurement tool is selected from the toolbar, it becomes the `active` tool. Use the caret to expand the measurement tools and select another tool. + + +![user-viewer-toolbar-measurements](../../assets/img/user-viewer-toolbar-measurements.png) + + +## Window/Level +The `Window/Level` tool enables manipulating the window level and window width of the rendered image. Click on the tool to enable freeform adjustment, then click and drag on the viewport to freely adjust the window/level. + +Click on the caret to expand the tool and choose from predefined W/L settings for common imaging scenarios. + + +![user-toolbar-preset](../../assets/img/user-toolbar-preset.png) + + +## Pan and Zoom +With the Zoom tool selected, click and drag the cursor on an image to adjust the zoom. The magnification level is displayed in the viewport. + +With the Pan tool selected, click and drag the cursor on an image to adjust the image position. + +## Image Capture +Click on the Camera icon to download a high quality image capture using common image formats (png, jpg) + +![user-toolbar-download-icon](../../assets/img/user-toolbar-download-icon.png) + +In the opened modal, the filename, image's width and height, and filetype and can be configured before downloading the image to your local computer. + +![user-toolbarDownload](../../assets/img/user-toolbarDownload.png) + + + +## Layout Selector +Please see the `Viewport` section for details. + + +## More Tools Menu +- Reset View: Resets all image manipulation such as position, zoom, and W/L +- Rotate Right: Flips the image 90 degrees clockwise +- Flip Horizontally: Flips the image 180 degrees horizontally +- Stack Scroll: Links all viewports containing images to scroll together +- Magnify: Click on an image to magnify a particular area of interest +- Invert: Inverts the color scale +- Cine: Toggles the Cine player control in the currently selected viewport. Click the `x` on the Cine player or click the tool again to toggle off. +- Angle: Measures an adjustable angle on an image +- Probe: Drag the probe to see pixel values +- Rectangle: Measures a rectangular area in mm^2 and HU + +When a tool is selected from the `More Tools` menu, it becomes the active tool until it is replaced by clicking on a different tool in the More Tools menu or main toolbar. + + +## Overview Video +An overview of tool usage can been seen below: + + +
+ +
diff --git a/platform/docs/docs/user-guide/viewer/viewport.md b/platform/docs/docs/user-guide/viewer/viewport.md new file mode 100644 index 0000000..1bfc693 --- /dev/null +++ b/platform/docs/docs/user-guide/viewer/viewport.md @@ -0,0 +1,39 @@ +--- +sidebar_position: 5 +--- + +# Viewport + +Image visualization happens at the viewport which contains canvas or canvases that +renders series. + +![user-viewer-main](../../assets/img/user-viewer-main.png) + + +By default, you can modify: + +- Zoom: right click dragging up or down +- Contrast/brightness: left click dragging up/down to change contrast, and left/right for changing brightness +- Pan: middle click dragging + + +## Changing Series for display +To change the displayed series, you can drag and drop the desired series from the left panel. Start, by dragging the thumbnail of the series, and drop it on the viewport. + +## Changing Layout +If you click on the layout icon on the toolbar, you can use the layout selector UI. After changing the layout, you can select studies for each new viewport by dragging and dropping in to the viewport. + +After changing the layout from 1x1, you will see each viewport gets tagged by a letter, +which matches its series section in the study list. + + +![user-viewer-layout](../../assets/img/user-viewer-layout.png) + + +## Overview Video +An overview of viewport layout change, and manipulation can be seen below: + + +
+ +
diff --git a/platform/docs/docusaurus.config.js b/platform/docs/docusaurus.config.js new file mode 100644 index 0000000..a10ffa0 --- /dev/null +++ b/platform/docs/docusaurus.config.js @@ -0,0 +1,305 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const path = require('path'); + +// read this text file +const fs = require('fs'); +const versions = fs.readFileSync('../../version.txt', 'utf8').split('\n'); + +const ArchivedVersionsDropdownItems = [ + { + version: '3.8.5', + href: 'https://v3p8.docs.ohif.org', + isExternal: true, + }, + { + version: '2.0', + href: 'https://v2.docs.ohif.org', + isExternal: true, + }, + { + version: '1.0', + href: 'https://v1.docs.ohif.org', + isExternal: true, + }, +]; + +const baseUrl = process.env.BASE_URL || '/'; + +/** @type {import('@docusaurus/types').DocusaurusConfig} */ +module.exports = { + future: { + experimental_faster: true, + }, + title: 'OHIF', + tagline: 'Open-source web-based medical imaging platform', + organizationName: 'Open Health Imaging Foundation', + projectName: 'OHIF', + baseUrl, + baseUrlIssueBanner: true, + url: 'https://docs.ohif.org', + i18n: { + defaultLocale: 'en', + locales: ['en'], + }, + onBrokenLinks: 'throw', + onBrokenMarkdownLinks: 'throw', + favicon: 'img/favicon.ico', + themes: ['@docusaurus/theme-live-codeblock'], + plugins: [ + // path.resolve(__dirname, './pluginOHIFWebpackConfig.js'), + // /path.resolve(__dirname, './postcss.js'), + 'docusaurus-plugin-image-zoom', // 3rd party plugin for image click to pop + [ + '@docusaurus/plugin-ideal-image', + { + quality: 70, + max: 1030, // max resized image's size. + min: 640, // min resized image's size. if original is lower, use that size. + steps: 2, // the max number of images generated between min and max (inclusive) + }, + ], + ], + presets: [ + [ + 'classic', + { + debug: true, // force debug plugin usage + docs: { + routeBasePath: '/', + path: 'docs', + sidebarPath: require.resolve('./sidebars.js'), + editUrl: ({ locale, docPath }) => { + /*if (locale !== 'en') { + return `https://crowdin.com/project/docusaurus-v2/${locale}`; + }*/ + + // We want users to submit doc updates to the upstream/next version! + // Otherwise we risk losing the update on the next release. + return `https://github.com/OHIF/Viewers/edit/master/platform/docs/docs/${docPath}`; + }, + showLastUpdateAuthor: true, + showLastUpdateTime: true, + // remarkPlugins: [ + // [require('@docusaurus/remark-plugin-npm2yarn'), { sync: true }], + // ], + // disableVersioning: isVersioningDisabled, + lastVersion: 'current', + // onlyIncludeVersions: + // !isVersioningDisabled && (isDev || isDeployPreview) + // ? ['current', ...versions.slice(0, 2)] + // : undefined, + versions: { + current: { + label: `${versions} (Latest)`, + }, + }, + }, + theme: { + customCss: [require.resolve('./src/css/custom.css')], + }, + gtag: { + trackingID: 'G-DDBJFE34EG', + anonymizeIP: true, + }, + }, + ], + ], + themeConfig: + /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ + ({ + liveCodeBlock: { + playgroundPosition: 'bottom', + }, + docs: { + sidebar: { + hideable: true, + autoCollapseCategories: true, + }, + }, + colorMode: { + defaultMode: 'dark', + disableSwitch: false, + // respectPrefersColorScheme: true, + }, + announcementBar: { + id: 'cornerstone20_ohif_anniversary', + content: + '๐ŸŽ‰ Celebrating OHIFโ€™s 10-Year Anniversary with Cornerstone 2.0! Explore enhanced segmentation, new video & microscopy viewports, UI/UX upgrades, and blazing fast prefetching. Dive into the release notes here! ๐Ÿš€', + }, + + prism: { + theme: require('prism-react-renderer').themes.github, + darkTheme: require('prism-react-renderer').themes.dracula, + }, + algolia: { + appId: 'EFLT6YIHHZ', + apiKey: 'c220dd24fe4f86248eea3b1238a1fb60', + indexName: 'ohif', + }, + navbar: { + hideOnScroll: false, + logo: { + alt: 'OHIF Logo', + src: 'img/ohif-logo-light.svg', + srcDark: 'img/ohif-logo.svg', + }, + items: [ + { + position: 'left', + to: '/', + activeBaseRegex: '^(/next/|/)$', + docId: 'Introduction', + label: 'Docs', + }, + { + to: '/components', + label: 'Components', + position: 'left', + }, + { + href: 'https://ohif.org/showcase', + label: 'Showcase', + target: '_blank', + position: 'left', + }, + { + href: 'https://ohif.org/collaborate', + label: 'Collaborate', + target: '_blank', + position: 'left', + }, + { + to: '/help', + //activeBaseRegex: '(^/help$)|(/help)', + label: 'Help', + position: 'left', + }, + { + to: '/migration-guide/3p8-to-3p9/', + //activeBaseRegex: '(^/help$)|(/help)', + label: '3.9 Migration Guides', + position: 'left', + }, + { + type: 'docsVersionDropdown', + position: 'right', + dropdownActiveClassDisabled: true, + dropdownItemsAfter: [ + { + type: 'html', + value: '', + }, + { + type: 'html', + className: 'dropdown-archived-versions', + value: 'Archived versions', + }, + ...ArchivedVersionsDropdownItems.map(item => ({ + label: `${item.version} `, + href: item.href, + target: item.isExternal ? '_blank' : undefined, + rel: item.isExternal ? 'noopener noreferrer' : undefined, + })), + ], + }, + { + type: 'localeDropdown', + position: 'right', + dropdownItemsAfter: [ + { + to: '/platform/internationalization', + label: 'Help Us Translate', + }, + ], + }, + { + to: 'https://github.com/OHIF/Viewers', + position: 'right', + className: 'header-github-link', + 'aria-label': 'GitHub Repository', + }, + ], + }, + footer: { + style: 'dark', + links: [ + { + title: ' ', + items: [ + { + // This doesn't show up on dev for some reason, but displays in build + html: ` + + + + `, + }, + ], + }, + { + title: 'Learn', + items: [ + { + label: 'Introduction', + to: '/', + }, + { + label: 'Getting Started', + to: 'development/getting-started', + }, + { + label: 'FAQ', + to: '/faq', + }, + { + label: 'Resources', + to: '/resources', + }, + ], + }, + { + title: 'Community', + items: [ + { + label: 'Discussion board', + href: 'https://community.ohif.org/', + }, + { + label: 'Help', + to: '/help', + }, + ], + }, + { + title: 'More', + items: [ + { + label: 'Donate', + href: 'https://giving.massgeneral.org/ohif', + }, + { + label: 'GitHub', + href: 'https://github.com/OHIF/Viewers', + }, + { + label: 'Twitter', + href: 'https://twitter.com/OHIFviewer', + }, + ], + }, + ], + logo: { + alt: 'OHIF ', + src: 'img/netlify-color-accent.svg', + href: 'https://viewer.ohif.org/', + }, + copyright: `OHIF is open source software released under the MIT license.`, + }, + }), +}; diff --git a/platform/docs/netlify.toml b/platform/docs/netlify.toml new file mode 100644 index 0000000..7354856 --- /dev/null +++ b/platform/docs/netlify.toml @@ -0,0 +1,38 @@ +# Netlify Config +# +# TOML Reference: +# https://www.netlify.com/docs/netlify-toml-reference/ +# +# We use Netlify for deploy previews and for publishing docs (gh-pages branch). +# https://viewer.ohif.org is created using a different process that is +# managed by CircleCI and deployed to our Google Hosting +# + +[build] + ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF . ../ui/ ../core/ ../i18n/" + +# NODE_VERSION in root `.nvmrc` takes priority +# YARN_FLAGS: https://www.netlify.com/docs/build-gotchas/#yarn +[build.environment] + # If 'production', `yarn install` does not install devDependencies + NODE_ENV = "development" + NODE_VERSION = "20.18.1" + YARN_VERSION = "1.22.5" + RUBY_VERSION = "2.6.2" + YARN_FLAGS = "--no-ignore-optional --pure-lockfile" + NETLIFY_USE_YARN = "true" + +[[headers]] + # Define which paths this specific [[headers]] block will cover. + for = "/*" + + [headers.values] + X-Frame-Options = "DENY" + X-XSS-Protection = "1; mode=block" + + # Multi-key header rules are expressed with multi-line strings. + cache-control = ''' + max-age=0, + no-cache, + no-store, + must-revalidate''' diff --git a/platform/docs/package.json b/platform/docs/package.json new file mode 100644 index 0000000..94f0641 --- /dev/null +++ b/platform/docs/package.json @@ -0,0 +1,92 @@ +{ + "name": "ohif-docs", + "version": "3.10.0-beta.111", + "private": true, + "scripts": { + "docusaurus": "docusaurus", + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "start": "docusaurus start --port 8001", + "dev": "docusaurus clear && docusaurus start --port 8001", + "docs": "docusaurus start --port 8001", + "build": "docusaurus build", + "swizzle": "docusaurus swizzle", + "deploy": "docusaurus deploy", + "clear": "docusaurus clear", + "serve": "docusaurus serve", + "write-translations": "docusaurus write-translations", + "write-heading-ids": "docusaurus write-heading-ids" + }, + "browserslist": { + "production": [ + ">0.5%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "dependencies": { + "@docusaurus/core": "3.7.0", + "@docusaurus/faster": "3.7.0", + "@docusaurus/module-type-aliases": "3.7.0", + "@docusaurus/plugin-client-redirects": "3.7.0", + "@docusaurus/plugin-google-gtag": "3.7.0", + "@docusaurus/plugin-ideal-image": "3.7.0", + "@docusaurus/plugin-pwa": "3.7.0", + "@docusaurus/preset-classic": "3.7.0", + "@docusaurus/remark-plugin-npm2yarn": "3.7.0", + "@docusaurus/theme-classic": "3.7.0", + "@docusaurus/theme-live-codeblock": "3.7.0", + "@docusaurus/tsconfig": "3.0.0", + "@docusaurus/types": "3.0.0", + "@mdx-js/react": "3.0.1", + "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-scroll-area": "^1.1.0", + "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slider": "^1.2.0", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.0", + "@radix-ui/react-toggle": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.2", + "@svgr/webpack": "^5.5.0", + "@types/react": "^18.2.29", + "autoprefixer": "^10.4.20", + "class-variance-authority": "^0.7.0", + "classnames": "^2.3.2", + "clsx": "^1.1.1", + "cmdk": "^1.0.0", + "date-fns": "^3.6.0", + "docusaurus-plugin-image-zoom": "^1.0.1", + "file-loader": "^6.2.0", + "framer-motion": "6.2.4", + "lucide-react": "^0.379.0", + "next-themes": "^0.3.0", + "postcss": "^8.4.47", + "postcss-import": "^14.0.2", + "postcss-preset-env": "^7.4.3", + "prism-react-renderer": "^2.1.0", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-shepherd": "6.1.1", + "shepherd.js": "13.0.3", + "sonner": "^1.4.41", + "tailwind-merge": "^2.3.0", + "tailwindcss": "^3.4.13", + "tailwindcss-animate": "^1.0.7", + "typescript": "~5.2.2", + "url-loader": "^4.1.1" + } +} diff --git a/platform/docs/pluginOHIFWebpackConfig.js b/platform/docs/pluginOHIFWebpackConfig.js new file mode 100644 index 0000000..8c99bfa --- /dev/null +++ b/platform/docs/pluginOHIFWebpackConfig.js @@ -0,0 +1,25 @@ +module.exports = function (context, options) { + return { + name: 'plugin-ohif-webpack-config', + configureWebpack(config, isServer, utils) { + return { + resolve: { + fallback: { + fs: false, + path: false, + }, + }, + module: { + rules: [ + { + test: /\.m?jsx?$/, + resolve: { + fullySpecified: false, + }, + }, + ], + }, + }; + }, + }; +}; diff --git a/platform/docs/postcss.config.js b/platform/docs/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/platform/docs/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/platform/docs/sidebars.js b/platform/docs/sidebars.js new file mode 100644 index 0000000..de26dde --- /dev/null +++ b/platform/docs/sidebars.js @@ -0,0 +1,26 @@ +/** + * Creating a sidebar enables you to: + - create an ordered group of docs + - render a sidebar for each doc of that group + - provide next/previous navigation + + The sidebars can be generated from the filesystem, or explicitly defined here. + + Create as many sidebars as you want. + */ + +module.exports = { + // By default, Docusaurus generates a sidebar from the docs folder structure + tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }], + + // But you can create a sidebar manually + /* + tutorialSidebar: [ + { + type: 'category', + label: 'Tutorial', + items: ['hello'], + }, + ], + */ +}; diff --git a/platform/docs/src/css/custom.css b/platform/docs/src/css/custom.css new file mode 100644 index 0000000..48c072b --- /dev/null +++ b/platform/docs/src/css/custom.css @@ -0,0 +1,727 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap'); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* OHIF Theme */ + +@layer base { + :root { + --highlight: 191 74% 63%; + --background: 236 62% 5%; + --foreground: 0 0% 98%; + --card: 236 62% 5%; + --card-foreground: 0 0% 98%; + --popover: 219 90% 15%; + --popover-foreground: 0 0% 98%; + --primary: 214 98% 60%; + --primary-foreground: 0 0% 98%; + --secondary: 214 66% 48%; + --secondary-foreground: 200 50% 84%; + --muted: 234 64% 10%; + --muted-foreground: 200 46% 65%; + --accent: 217 79% 24%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 236 45% 21%; + --ring: 214 98% 60%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --radius: 0.5rem; + --badge-new-color: hsl(var(--primary-foreground)); + --badge-new-background: linear-gradient(135deg, hsl(var(--highlight)), hsl(var(--primary))); + --badge-latest-stable-color: hsl(var(--primary)); + } + + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 214 98% 60%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 236 45% 21%; + --ring: 214 98% 60%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --badge-new-color: hsl(var(--primary-foreground)); + --badge-new-background: linear-gradient(135deg, hsl(var(--highlight)), hsl(var(--primary))); + --badge-latest-stable-color: hsl(var(--primary)); + } +} + +/* ORIGINAL THEME for comparison and testing + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 72.22% 50.59%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 5% 64.9%; + --radius: 0.5rem; + + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + } + + + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 85.7% 97.3%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +*/ + +/* Theme Copy Example + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 224 71.4% 4.1%; + --card: 0 0% 100%; + --card-foreground: 224 71.4% 4.1%; + --popover: 0 0% 100%; + --popover-foreground: 224 71.4% 4.1%; + --primary: 262.1 83.3% 57.8%; + --primary-foreground: 210 20% 98%; + --secondary: 220 14.3% 95.9%; + --secondary-foreground: 220.9 39.3% 11%; + --muted: 220 14.3% 95.9%; + --muted-foreground: 220 8.9% 46.1%; + --accent: 220 14.3% 95.9%; + --accent-foreground: 220.9 39.3% 11%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 20% 98%; + --border: 220 13% 91%; + --input: 220 13% 91%; + --ring: 262.1 83.3% 57.8%; + --radius: 0.5rem; + --chart-1: ; + --chart-2: ; + --chart-3: ; + --chart-4: ; + --chart-5: ; + } + + .dark { + --background: 224 71.4% 4.1%; + --foreground: 210 20% 98%; + --card: 224 71.4% 4.1%; + --card-foreground: 210 20% 98%; + --popover: 224 71.4% 4.1%; + --popover-foreground: 210 20% 98%; + --primary: 263.4 70% 50.4%; + --primary-foreground: 210 20% 98%; + --secondary: 215 27.9% 16.9%; + --secondary-foreground: 210 20% 98%; + --muted: 215 27.9% 16.9%; + --muted-foreground: 217.9 10.6% 64.9%; + --accent: 215 27.9% 16.9%; + --accent-foreground: 210 20% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 20% 98%; + --border: 215 27.9% 16.9%; + --input: 215 27.9% 16.9%; + --ring: 263.4 70% 50.4%; + --chart-1: ; + --chart-2: ; + --chart-3: ; + --chart-4: ; + --chart-5: ; + } +} + +*/ + +h2.section-header { + @apply py-4 text-2xl font-normal text-white; +} + +h3.section-header { + @apply py-3 text-xl text-white; +} + +.playground-row { + @apply bg-card mb-6 flex flex-row flex-wrap rounded-md border py-10; +} + +.example { + @apply flex-initial px-6; +} + +.example2 { + @apply flex-initial px-4; +} + +/* Additional CSS edits to components */ + +/* Tooltip */ + +.TooltipContent[data-side='bottom'] { + animation-name: slideDown; +} + +/* Custom CSS to hide default number input arrows */ +input[type='number']::-webkit-inner-spin-button, +input[type='number']::-webkit-outer-spin-button { + @apply appearance-none; +} + +input[type='number'] { + -moz-appearance: textfield; /* For Firefox */ +} + +.navbar__item { + display: flex; + align-items: center; +} + +.navbar__item svg { + margin-right: 5px; + display: inline-block; + vertical-align: middle; +} + +/* stylelint-disable docusaurus/copyright-header */ +/** + * Any CSS included here will be global. The classic template + * bundles Infima by default. Infima is a CSS framework designed to + * work well for content-centric websites. + */ +/* You can override the default Infima variables here. */ +/* https://infima.dev/docs/utilities/colors */ +/* https://docs.theochu.com/docusaurus/styling/ */ +:root { + --ifm-color-primary: #25c2a0; + --ifm-color-primary-dark: rgb(33, 175, 144); + --ifm-color-primary-darker: rgb(31, 165, 136); + --ifm-color-primary-darkest: rgb(26, 136, 112); + --ifm-color-primary-light: rgb(70, 203, 174); + --ifm-color-primary-lighter: rgb(102, 212, 189); + --ifm-color-primary-lightest: rgb(146, 224, 208); + --ifm-font-color-base: #474747; + --ifm-color-primary: #0151d9; + --ifm-color-primary-dark: #0149c3; + --ifm-color-primary-darker: #0145b8; + --ifm-color-primary-darkest: #013998; + --ifm-color-primary-light: #0159ef; + --ifm-color-primary-lighter: #015dfa; + --ifm-color-primary-lightest: #1d71fe; + --ifm-color-secondary: #e8f7f7; + --ifm-code-font-size: 95%; + --ifm-background-color: #ffffff; + --ifm-zoom-image-background-color: #ffffffe5; + --ifm-background-surface-color: #ffffff; + --ifm-menu-color: #1e427e; + --ifm-code-background: #e8f7f7; + --ifm-toc-border-color: #ffffff; + --ifm-footer-background-color: #000000; + --ifm-table-stripe-background: #f4fbfb; + --ifm-color-warning: #e9e489; + --ifm-alert-color: #333333; + --ohif-color-border: #7bb2ce; + --site-primary-hue-saturation: 167 68%; + --site-primary-hue-saturation-light: 167 56%; /* do we really need this extra one? */ +} + +html[data-theme='dark'] .header-github-link:before { + background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") + no-repeat; +} + +html[data-theme='dark'] { + --ifm-menu-link-sublist-icon: url('data:image/svg+xml;utf8,'); + --ifm-color-primary: #5acce6; + --ifm-color-primary-dark: #3ec3e2; + --ifm-color-primary-darker: #30bfe0; + --ifm-color-primary-darkest: #1da4c3; + --ifm-color-primary-light: #76d5ea; + --ifm-color-primary-lighter: #84d9ec; + --ifm-color-primary-lightest: #ade6f3; + --ifm-font-color-base: #ffffff; + --ifm-color-secondary: #050719; + --ifm-blockquote-color: #7bb2ce; + --ifm-background-color: #080b2b; + --ifm-zoom-image-background-color: #080b2be5; + --ifm-background-surface-color: #080b2b; + --ifm-menu-color: #7bb2ce; + --ifm-toc-link-color: #7bb2ce; + --ifm-code-background: #1c296d; + --ifm-toc-border-color: #080b2b; + --ifm-menu-color-active: #ffffff; + --ifm-footer-background-color: #000000; + --ifm-table-stripe-background: #060920; + --ifm-color-warning: #f1c55a; + --ifm-alert-color: #000000; + --ohif-color-border: #3a3f99; +} + +.medium-zoom-overlay { + background: var(--ifm-zoom-image-background-color) !important; +} + +.header-github-link:hover { + opacity: 0.6; +} +.header-github-link:before { + content: ''; + width: 24px; + height: 24px; + display: flex; + background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") + no-repeat; +} +/* +.docusaurus-highlight-code-line { + background-color: rgb(72, 77, 91); + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); +} +*/ + +/* Typography updates */ + +html { + font-size: 1em; +} + +body { + font-family: 'Inter', sans-serif; +} + +h1, +h2, +h3, +h4, +h5 { + color: var(--ifm-color-primary); + font-weight: 400; + font-family: 'Inter', sans-serif; +} + +blockquote { + border-left: 3px solid #4042af; +} + +/* Temporary Type Size Changes */ + +article header h1 { + font-size: 2.6rem !important; +} + +article h2 { + font-size: 1.85rem !important; +} + +article header h3 { + font-size: 1.5rem !important; +} + +/* Remove navigation shadow */ + +.navbar { + box-shadow: none; +} + +/* Navigation color and type updates */ + +.footer { + background-color: #000000; + color: #ffffff; +} + +.footer a { + color: #7bb2ce; +} + +.footer a:hover { + color: #ffffff; + text-decoration: none; +} + +.menu { + font-weight: 400; + font-size: 0.9rem; +} + +.table-of-contents { + font-size: 0.8rem; + font-weight: 600; +} + +.menu { + padding-top: 2rem !important; +} + +.menu__link--active { + color: var(--ifm-menu-color-active); + font-weight: 600; +} + +.table-of-contents__link:hover, +.table-of-contents__link:hover code, +.table-of-contents__link--active, +.table-of-contents__link--active code { + color: var(--ifm-menu-color-active); + text-decoration: none; + font-weight: 600; +} + +.badge--secondary { + --ifm-badge-background-color: var(--ifm-color-secondary); + --ifm-badge-border-color: var(--ifm-badge-background-color); + color: var(--ifm-color-primary); + border: 1px solid var(--ohif-color-border); +} + +/* Alerts */ + +.alert { + font-size: 0.9rem; + padding: 10px; +} + +.alert--secondary { + --ifm-alert-background-color: var(--ifm-color-secondary); + --ifm-alert-color: var(--ifm-font-color-base); +} + +.button--secondary:not(.button--outline) { + --ifm-button-background-color: #e8f7f7; +} + +.admonition-icon svg { + fill: #0151d9; +} + +.table-of-contents__left-border { + border-left: #013998; +} + +.footer__col:first-of-type { + flex-grow: 2; + margin-right: 20%; +} + +.footer_logo { + margin-top: 0; +} + +.docusaurus-highlight-code-line { + background-color: rgb(206, 208, 211); + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); +} + +/* If you have a different syntax highlighting theme for dark mode. */ +html[data-theme='dark'] .docusaurus-highlight-code-line { + /* Color which works with dark mode syntax highlighting theme */ + background-color: rgb(100, 100, 100); +} + +/* .DocSearch { + display: none; +} */ + +/* Footer logo MGH */ + +@media (max-width: 1200px) { + #mgh-logo { + margin-right: 100px; + width: 300px; + } +} + +@media (max-width: 480px) { + #mgh-logo { + margin-right: 10px; + width: 300px; + } +} + +.dropdown-separator { + margin: 0.3rem 0; +} + +.dropdown-archived-versions { + font-size: 0.875rem; + padding: 0.2rem 0.5rem; +} + +.code-block-error-line { + background-color: #ff000020; + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); + border-left: 3px solid #ff000080; +} + +.new-badge::after, +.deprecated-badge::after { + font-size: 11px; + @apply inline-flex items-center justify-center rounded-sm; + @apply ml-1.5 px-1 py-0; +} + +.new-badge::after { + content: ''; + @apply bg-red-300 text-red-500; + @apply dark:bg-blue-900 dark:text-blue-100; +} + +div[class^='announcementBar_'] { + --site-announcement-bar-stripe-color1: hsl(var(--site-primary-hue-saturation) 85%); + --site-announcement-bar-stripe-color2: hsl(var(--site-primary-hue-saturation) 95%); + background: repeating-linear-gradient( + 35deg, + var(--site-announcement-bar-stripe-color1), + var(--site-announcement-bar-stripe-color1) 20px, + var(--site-announcement-bar-stripe-color2) 10px, + var(--site-announcement-bar-stripe-color2) 40px + ); + font-weight: 700; +} + +/* #__docusaurus { + height: 100%; +} */ + +.dropdown-separator { + border-top: 1px solid #808080; +} + +/* flex items , center */ +.dropdown__link { + display: flex; + align-items: center; +} + +.footer__link-item { + display: flex; + align-items: center; +} + +/* add proper ui link styling */ + +/* Bullet point styling */ +ul { + list-style-type: disc; + padding-left: 1.5rem; + margin: 1rem 0; +} + +ul li { + margin-bottom: 0.5rem; +} + +ol { + list-style-type: decimal; + padding-left: 1.5rem; + margin: 1rem 0; +} + +ol li { + margin-bottom: 0.5rem; +} + +/* Nested bullet points */ +ul ul { + list-style-type: circle; + margin: 0.5rem 0; +} + +/* For documentation bullet points specifically */ +.markdown ul { + list-style-type: disc; + padding-left: 1.5rem; +} + +.markdown ul li { + margin-bottom: 0.5rem; +} + +/* Markdown link styling */ +.markdown a { + color: #0066cc; + text-decoration: none; + transition: color 0.2s ease; +} + +.markdown a:hover { + color: #0051a3; + text-decoration: underline; +} + +/* Dark mode link styling */ +html[data-theme='dark'] .markdown a { + color: #66b3ff; +} + +html[data-theme='dark'] .markdown a:hover { + color: #99ccff; +} + +/* Horizontal rule styling */ +.markdown hr { + height: 1px; + border: none; + background: linear-gradient(to right, #0066cc, #66b3ff); + margin: 2rem 0; + opacity: 0.6; +} + +/* Dark mode horizontal rule */ +html[data-theme='dark'] .markdown hr { + background: linear-gradient(to right, #66b3ff, #99ccff); +} + +/* Markdown code block styling */ +.markdown pre { + font-size: 0.9rem; +} + +.theme-code-block { + font-size: 0.9rem; +} + +/* Target both light and dark themes */ +[data-theme='light'] .theme-code-block, +[data-theme='dark'] .theme-code-block { + font-size: 0.9rem; +} + +/* Dropdown menu positioning and interaction fixes */ +.dropdown { + position: relative; +} + +.dropdown__menu { + top: 100%; + margin-top: 0; + padding-top: 0.5rem; +} + +/* Add a hover area to prevent menu from disappearing */ +.dropdown__menu::before { + content: ''; + position: absolute; + top: -10px; + left: 0; + right: 0; + height: 10px; +} + +/* Ensure menu stays visible while hovering */ +.dropdown:hover .dropdown__menu, +.dropdown__menu:hover { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.theme-doc-version-banner { + display: none; +} + +/* New badge styling */ +/* New Badge Styling */ +a.navbar__item.navbar__link.navbar__link--active[href='/migration-guide/3p8-to-3p9/'][aria-current='page']::after { + content: ' NEW'; + display: inline-block; + margin-left: 8px; + padding: 2px 6px; + font-size: 0.75em; + font-weight: bold; + color: var(--badge-new-color); + background: var(--badge-new-background); + border-radius: 12px; + position: relative; + overflow: hidden; + white-space: nowrap; + /* Shiny Effect */ + background-size: 200% 200%; + animation: shine 2s linear infinite; + box-shadow: 0 0 5px rgba(255, 255, 255, 0.5); +} + +/* Shiny Animation */ +@keyframes shine { + 0% { + background-position: 0% 50%; + } + 100% { + background-position: 100% 50%; + } +} + +/* "(latest stable)" Text Styling */ +a.dropdown__link[href='/3.9/migration-guide/3p8-to-3p9/']::after { + content: ' (latest stable)'; + color: var(--badge-latest-stable-color); + font-size: 0.9em; + margin-left: 4px; +} diff --git a/platform/docs/src/mocks/studyList.json b/platform/docs/src/mocks/studyList.json new file mode 100644 index 0000000..a727790 --- /dev/null +++ b/platform/docs/src/mocks/studyList.json @@ -0,0 +1,829 @@ +{ + "studies": [ + { + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78", + "StudyDescription": "BRAIN SELLA", + "AccessionNumber": "11788761116031", + "StudyDate": "2020-03-26T23:33:59.073Z", + "StudyTime": "120022", + "PatientName": "MISTER^MR", + "PatientId": "832040", + "Instances": 33, + "Modalities": "MR", + "series": [ + { + "SeriesDescription": "SAG T-1", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121", + "SeriesNumber": 2, + "SeriesDate": "20010108", + "SeriesTime": "120318", + "Modality": "MR", + "instances": [ + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 3, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [0, 1, 0, 0, 0, -1], + "ImagePositionPatient": [11.6, -92.5, 98.099998], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.124", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.124.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 2, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [0, 1, 0, 0, 0, -1], + "ImagePositionPatient": [14.6, -92.5, 98.099998], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.123", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.123.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 1, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [0, 1, 0, 0, 0, -1], + "ImagePositionPatient": [17.6, -92.5, 98.099998], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.122", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.122.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 4, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [0, 1, 0, 0, 0, -1], + "ImagePositionPatient": [8.6, -92.5, 98.099998], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.125", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.125.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 5, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [0, 1, 0, 0, 0, -1], + "ImagePositionPatient": [5.6, -92.5, 98.099998], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.126", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.126.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 9, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [0, 1, 0, 0, 0, -1], + "ImagePositionPatient": [-6.4, -92.5, 98.099998], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.130", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.130.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 10, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [0, 1, 0, 0, 0, -1], + "ImagePositionPatient": [-9.4, -92.5, 98.099998], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.131", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.131.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 6, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [0, 1, 0, 0, 0, -1], + "ImagePositionPatient": [2.6, -92.5, 98.099998], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.127", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.127.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 7, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [0, 1, 0, 0, 0, -1], + "ImagePositionPatient": [-0.4, -92.5, 98.099998], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.128", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.128.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 8, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [0, 1, 0, 0, 0, -1], + "ImagePositionPatient": [-3.4, -92.5, 98.099998], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.129", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.129.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 11, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [0, 1, 0, 0, 0, -1], + "ImagePositionPatient": [-12.4, -92.5, 98.099998], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.132", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.132.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 12, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [0, 1, 0, 0, 0, -1], + "ImagePositionPatient": [-15.4, -92.5, 98.099998], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.133", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.133.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 13, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [0, 1, 0, 0, 0, -1], + "ImagePositionPatient": [-18.4, -92.5, 98.099998], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.134", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.134.dcm" + } + ] + }, + { + "SeriesDescription": "COR T-1", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "SeriesNumber": 3, + "SeriesDate": "20010108", + "SeriesTime": "121105", + "Modality": "MR", + "instances": [ + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 1, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, 26.299999, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.136", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.136.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 2, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, 23.0, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.137", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.137.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 4, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, 16.4, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.139", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.139.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 3, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, 19.700001, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.138", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.138.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 6, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, 9.8, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.141", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.141.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 7, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, 6.5, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.142", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.142.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 5, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, 13.1, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.140", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.140.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 8, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, 3.2, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.143", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.143.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 9, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, -0.1, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.144", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.144.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 10, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, -3.4, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.145", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.145.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 12, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, -10.0, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.147", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.147.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 13, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, -13.3, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.148", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.148.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 15, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, -19.9, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.150", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.150.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 17, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, -26.5, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.152", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.152.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 14, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, -16.6, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.149", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.149.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 19, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, -33.099998, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.154", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.154.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 20, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, -36.400002, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.155", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.155.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 18, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, -29.799999, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.153", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.153.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 11, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, -6.7, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.146", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.146.dcm" + }, + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 16, + "AcquisitionNumber": 0, + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.390625, 0.390625], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 0, -1], + "ImagePositionPatient": [-100.400002, -23.200001, 102.400002], + "FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470", + "ImageType": ["ORIGINAL", "PRIMARY", "OTHER"], + "Modality": "MR", + "SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.151", + "SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.135", + "StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78" + }, + "url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.151.dcm" + } + ] + } + ] + } + ] +} diff --git a/platform/docs/src/pages/colors-and-type.tsx b/platform/docs/src/pages/colors-and-type.tsx new file mode 100644 index 0000000..8aad79e --- /dev/null +++ b/platform/docs/src/pages/colors-and-type.tsx @@ -0,0 +1,418 @@ +import React, { useState } from 'react'; +import '../css/custom.css'; + +import Layout from '@theme/Layout'; +import { Label } from '../../../ui-next/src/components/Label'; +import { Input } from '../../../ui-next/src/components/Input'; +import { Separator } from '../../../ui-next/src/components/Separator'; +import { Tabs, TabsList, TabsTrigger } from '../../../ui-next/src/components/Tabs'; +import { + Select, + SelectTrigger, + SelectContent, + SelectItem, + SelectValue, +} from '../../../ui-next/src/components/Select'; +import { Button } from '../../../ui-next/src/components/Button'; +import { Switch } from '../../../ui-next/src/components/Switch'; +import { Checkbox } from '../../../ui-next/src/components/Checkbox'; +import { Toggle } from '../../../ui-next/src/components/Toggle'; +import { Slider } from '../../../ui-next/src/components/Slider'; +import { ScrollArea } from '../../../ui-next/src/components/ScrollArea'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from '../../../ui-next/src/components/DropdownMenu'; +import { Icons } from '../../../ui-next/src/components/Icons'; +import { Toaster, toast } from '../../../ui-next/src/components/Sonner'; +import { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +} from '../../../ui-next/src/components/Card'; + +interface ShowcaseRowProps { + title: string; + description?: string; + children: React.ReactNode; + code: string; +} + +export default function ComponentShowcase() { + // Handlers to trigger different types of toasts + const triggerSuccess = () => { + toast.success('This is a success toast!'); + }; + + const triggerError = () => { + toast.error('This is an error toast!'); + }; + + const triggerInfo = () => { + toast.info('This is an info toast!'); + }; + + const triggerWarning = () => { + toast.warning('This is a warning toast!'); + }; + + // Handler to trigger a toast.promise example + const triggerPromiseToast = () => { + const promise = () => + new Promise<{ name: string }>(resolve => + setTimeout(() => resolve({ name: 'Segmentation 1' }), 3000) + ); + + toast.promise(promise(), { + loading: 'Loading Segmentation...', + success: data => `${data.name} has been added`, + error: 'Error', + }); + }; + + // Handler to trigger a toast with description + const triggerDescriptionToast = () => { + toast.success('Success heading', { + description: 'This is a detailed description of the success message.', + }); + }; + + // Handler to trigger a toast with an action button + const triggerActionButtonToast = () => { + toast.info('No active segmentation detected', { + description: 'Create a segmentation before using the Brush', + }); + }; + + // Handler to trigger a toast with a cancel button + const triggerCancelButtonToast = () => { + toast.error('No active segmentation detected', { + description: 'Create a segmentation before using the Brush', + }); + }; + + // Handler to trigger a toast with both action and cancel buttons + const triggerCombinedToast = () => { + toast.warning('Warning!', { + description: 'This is a warning with both action and cancel buttons.', + action: ( + + ), + cancel: ( + + ), + }); + }; + + // Handler to trigger a loading toast using Toaster's default loading icon + const showLoadingToast = () => { + toast.loading('Loading your data...'); + }; + + return ( + +
+ + +
+

Colors & Typography

+ + +
+
+
+
+ highlight +
+
+
+
+ Used for active or selected elements in the Viewer. +
+
+ +
+
+
+
+ primary +
+
+
+
+ Used for Actions. Icons use 'primary' at 100% opacity while various components will + use a reduced opacity. Hover and other states increase the opacity. +
+
+ +
+
+
+
+ popover +
+
+
+ muted +
+
+
+ background +
+
+
+ These three colors are used as background colors. For the lowest level above black + use 'background'. For normal panel backgrounds and other interactive components, use + 'muted'. For elements such as menus and popovers, use 'popover'. +
+
+ +
+
+
+
+ foreground +
+
+
+ muted-foreground +
+
+
+ For primary and important text, use 'foreground'. When secondary text is available, + use 'muted-foreground' to create separation and readability. +
+
+
+ + +
+
+
+ text-base + 13px +
+
+
+
+ text-base is used as the base font size of the Viewer interface. Use when putting + text in panels or other interface elements next to medical images. +
+
+ +
+
+
+ text-lg + 14px +
+
+
+
+ text-lg can be used for dialog text or important messaging text within the Viewer. + Use this font size for easier reading on other standard text pages. +
+
+ +
+
+
+ text-xl + 16px +
+
+
+
+ text-xl can be used as headings within dialogs or messaging. +
+
+ +
+
+
+ text-2xl + 18px +
+
+
+
+ text-2xl can be used for page headers in the Viewer application or as dialog titles. +
+
+ +
+
+
+ text-3xl + 20px +
+
+
+
+ text-3xl can be used for extra large text size in the application. +
+
+ +
+
+
+ text-sm + 12px +
+
+
+
+ text-sm can be used for details that do not need to be standard sizes in the Viewer. +
+
+
+
+
+
+ ); +} + +function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) { + const [showCode, setShowCode] = useState(false); + + return ( +
+
+
+

{title}

+
+ +
+
+
+ {description &&

{description}

} +
+
+
{children}
+
+
+ {showCode && ( +
+          {code}
+        
+ )} +
+ ); +} + +// function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) { +// const [showCode, setShowCode] = useState(false); + +// return ( +//
+//
+//
+//

{title}

+// {description &&

{description}

} +//
+// +//
+//
{children}
+// {showCode && ( +//
+//           {code}
+//         
+// )} +//
+// ); +// } diff --git a/platform/docs/src/pages/components-list.tsx b/platform/docs/src/pages/components-list.tsx new file mode 100644 index 0000000..cd9a57a --- /dev/null +++ b/platform/docs/src/pages/components-list.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import '../css/custom.css'; +import Layout from '@theme/Layout'; +import { TooltipProvider } from '../../../ui-next/src/components/Tooltip'; + +// Import all showcase components +import ButtonShowcase from './components/ButtonShowcase'; +import CheckboxShowcase from './components/CheckboxShowcase'; +import DataRowShowcase from './components/DataRowShowcase'; +import DropdownMenuShowcase from './components/DropdownMenuShowcase'; +import InputShowcase from './components/InputShowcase'; +import ScrollAreaShowcase from './components/ScrollAreaShowcase'; +import SelectShowcase from './components/SelectShowcase'; +import SliderShowcase from './components/SliderShowcase'; +import SwitchShowcase from './components/SwitchShowcase'; +import TabsShowcase from './components/TabsShowcase'; +import ToastShowcase from './components/ToastShowcase'; +import ToolButtonShowcase from './components/ToolButtonShowcase'; +import ToolButtonListShowcase from './components/ToolButtonListShowcase'; +import NumericMetaShowcase from './components/NumericMetaShowcase'; +/** + * Components List page that displays all available UI components + */ +export default function ComponentsList() { + return ( + + +
+
+
+

Components

+
+ + + + + + + + + + + + + + + + +
+
+
+
+ ); +} diff --git a/platform/docs/src/pages/components.tsx b/platform/docs/src/pages/components.tsx new file mode 100644 index 0000000..ae22670 --- /dev/null +++ b/platform/docs/src/pages/components.tsx @@ -0,0 +1,265 @@ +import React, { useState } from 'react'; +import '../css/custom.css'; + +import Layout from '@theme/Layout'; +import { Label } from '../../../ui-next/src/components/Label'; +import { Input } from '../../../ui-next/src/components/Input'; +import { Separator } from '../../../ui-next/src/components/Separator'; +import { Tabs, TabsList, TabsTrigger } from '../../../ui-next/src/components/Tabs'; +import { + Select, + SelectTrigger, + SelectContent, + SelectItem, + SelectValue, +} from '../../../ui-next/src/components/Select'; +import { Button } from '../../../ui-next/src/components/Button'; +import { Switch } from '../../../ui-next/src/components/Switch'; +import { Checkbox } from '../../../ui-next/src/components/Checkbox'; +import { Toggle } from '../../../ui-next/src/components/Toggle'; +import { Slider } from '../../../ui-next/src/components/Slider'; +import { ScrollArea } from '../../../ui-next/src/components/ScrollArea'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from '../../../ui-next/src/components/DropdownMenu'; +import { Icons } from '../../../ui-next/src/components/Icons'; +import { Toaster, toast } from '../../../ui-next/src/components/Sonner'; +import { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +} from '../../../ui-next/src/components/Card'; + +interface ShowcaseRowProps { + title: string; + description?: string; + children: React.ReactNode; + code: string; +} + +export default function ComponentShowcase() { + // Handlers to trigger different types of toasts + const triggerSuccess = () => { + toast.success('This is a success toast!'); + }; + + const triggerError = () => { + toast.error('This is an error toast!'); + }; + + const triggerInfo = () => { + toast.info('This is an info toast!'); + }; + + const triggerWarning = () => { + toast.warning('This is a warning toast!'); + }; + + // Handler to trigger a toast.promise example + const triggerPromiseToast = () => { + const promise = () => + new Promise<{ name: string }>(resolve => + setTimeout(() => resolve({ name: 'Segmentation 1' }), 3000) + ); + + toast.promise(promise(), { + loading: 'Loading Segmentation...', + success: data => `${data.name} has been added`, + error: 'Error', + }); + }; + + // Handler to trigger a toast with description + const triggerDescriptionToast = () => { + toast.success('Success heading', { + description: 'This is a detailed description of the success message.', + }); + }; + + // Handler to trigger a toast with an action button + const triggerActionButtonToast = () => { + toast.info('No active segmentation detected', { + description: 'Create a segmentation before using the Brush', + }); + }; + + // Handler to trigger a toast with a cancel button + const triggerCancelButtonToast = () => { + toast.error('No active segmentation detected', { + description: 'Create a segmentation before using the Brush', + }); + }; + + // Handler to trigger a toast with both action and cancel buttons + const triggerCombinedToast = () => { + toast.warning('Warning!', { + description: 'This is a warning with both action and cancel buttons.', + action: ( + + ), + cancel: ( + + ), + }); + }; + + // Handler to trigger a loading toast using Toaster's default loading icon + const showLoadingToast = () => { + toast.loading('Loading your data...'); + }; + + return ( + + + + ); +} + +function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) { + const [showCode, setShowCode] = useState(false); + + return ( +
+ {/* Header Section */} +
+
+

{title}

+
+ +
+ + {/* Content Section: 1/3 Left, 2/3 Right */} +
+ {/* Left Side: Title and Description */} +
+ {description &&

{description}

} +
+ + {/* Right Side: Example */} +
+
{children}
+
+
+ + {/* Code Section */} + {showCode && ( +
+          {code}
+        
+ )} +
+ ); +} + +// function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) { +// const [showCode, setShowCode] = useState(false); + +// return ( +//
+//
+//
+//

{title}

+// {description &&

{description}

} +//
+// +//
+//
{children}
+// {showCode && ( +//
+//           {code}
+//         
+// )} +//
+// ); +// } diff --git a/platform/docs/src/pages/components/ButtonShowcase.tsx b/platform/docs/src/pages/components/ButtonShowcase.tsx new file mode 100644 index 0000000..2c3debd --- /dev/null +++ b/platform/docs/src/pages/components/ButtonShowcase.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Button } from '../../../../ui-next/src/components/Button'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * ButtonShowcase component displays button variants and examples + */ +export default function ButtonShowcase() { + return ( + Primary Button + + + + + + + + + `} + > +
+ + + + + +
+
+ + +
+
+ ); +} diff --git a/platform/docs/src/pages/components/CheckboxShowcase.tsx b/platform/docs/src/pages/components/CheckboxShowcase.tsx new file mode 100644 index 0000000..4279114 --- /dev/null +++ b/platform/docs/src/pages/components/CheckboxShowcase.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Checkbox } from '../../../../ui-next/src/components/Checkbox'; +import { Label } from '../../../../ui-next/src/components/Label'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * CheckboxShowcase component displays checkbox variants and examples + */ +export default function CheckboxShowcase() { + return ( + + +
+ +
+ + `} + > +
+ +
+ +
+
+
+ ); +} diff --git a/platform/docs/src/pages/components/DataRowShowcase.tsx b/platform/docs/src/pages/components/DataRowShowcase.tsx new file mode 100644 index 0000000..200b165 --- /dev/null +++ b/platform/docs/src/pages/components/DataRowShowcase.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import DataRowExample from '../patterns/DataRowExample'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * DataRowShowcase component displays DataRow variants and examples + */ +export default function DataRowShowcase() { + return ( + + + + ); +} diff --git a/platform/docs/src/pages/components/DropdownMenuShowcase.tsx b/platform/docs/src/pages/components/DropdownMenuShowcase.tsx new file mode 100644 index 0000000..5309847 --- /dev/null +++ b/platform/docs/src/pages/components/DropdownMenuShowcase.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from '../../../../ui-next/src/components/DropdownMenu'; +import { Button } from '../../../../ui-next/src/components/Button'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * DropdownMenuShowcase component displays DropdownMenu variants and examples + */ +export default function DropdownMenuShowcase() { + return ( + + + + + + Item 1 + Item 2 + Long name Item 3 + + + `} + > +
+ + + + + + Item 1 + Item 2 + Long name Item 3 + + + + + + + + Item 1 + Item 2 + Long name Item 3 + + + + + + + + Item 1 + Item 2 + Long name Item 3 + + + + + + + + console.debug('Item 1')}>Item 1 + console.debug('Item 2')}>Item 2 + console.debug('Item 3')}> + Long name Item 3 + + + +
+
+ ); +} diff --git a/platform/docs/src/pages/components/InputShowcase.tsx b/platform/docs/src/pages/components/InputShowcase.tsx new file mode 100644 index 0000000..77c0484 --- /dev/null +++ b/platform/docs/src/pages/components/InputShowcase.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Input } from '../../../../ui-next/src/components/Input'; +import { Label } from '../../../../ui-next/src/components/Label'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * InputShowcase component displays Input variants and examples + */ +export default function InputShowcase() { + return ( + +
+ +
+
+ +
+ + `} + > +
+
+ +
+
+ +
+
+
+ ); +} diff --git a/platform/docs/src/pages/components/NumericMetaShowcase.tsx b/platform/docs/src/pages/components/NumericMetaShowcase.tsx new file mode 100644 index 0000000..0107708 --- /dev/null +++ b/platform/docs/src/pages/components/NumericMetaShowcase.tsx @@ -0,0 +1,330 @@ +import React, { useState } from 'react'; +import Numeric from '../../../../ui-next/src/components/Numeric'; +import Icons from '../../../../ui-next/src/components/Icons'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * NumericShowcase component displays Numeric variants and examples + */ +export default function NumericShowcase() { + const [controlledValue, setControlledValue] = useState(0); + const [controlledValues, setControlledValues] = useState([0, 100] as [number, number]); + + return ( +
+ {/* Basic Number Input */} + console.debug('Value changed:', val)}> +
+ Width + +
+ + + console.debug('Value changed:', val)}> + Bolder + + + + console.debug('Value changed:', val)} + min={0} + value={123465789} + max={10000000000000} +> + + + With Icon + + +`} + > +
+ console.debug('Value changed:', val)} + > +
+ Width + +
+
+ + console.debug('Value changed:', val)} + > + + Bolder + + + + + console.debug('Value changed:', val)} + min={0} + value={123465789} + max={10000000000000} + > + + + With Icon + + + +
+
+ + {/* Single Range Slider */} + console.debug('Value changed:', val)}> + Brightness + + + + console.debug('Value changed:', val)} +> + Contrast + + + + setControlledValue(val as number)} +> + Controlled State (Parent) + +`} + > +
+ console.debug('Value changed:', val)} + > + Brightness + + + + console.debug('Value changed:', val)} + > + Contrast + + + + setControlledValue(val as number)} + > + Controlled State (Parent) + + +
+
+ + {/* Double Range Slider */} + ([0, 100]); + + console.debug('Values changed:', vals)} +> + Window Width/Level + + + + console.debug('Values changed:', vals)} +> + Window Width/Level + + + + setControlledValues(vals as [number, number])} +> + Controlled State (Parent) + +`} + > +
+ console.debug('Values changed:', vals)} + > + Window Width/Level + + + + console.debug('Values changed:', vals)} + > + Window Width/Level + + + + setControlledValues(vals as [number, number])} + > + Controlled State (Parent) + + +
+
+ + {/* Combined Examples */} + + Zoom Factor + + + + + Rotation + + + + + CT Window + +`} + > +
+ + Zoom Factor + + + + + Rotation + + + + + CT Window + + +
+
+
+ ); +} diff --git a/platform/docs/src/pages/components/ScrollAreaShowcase.tsx b/platform/docs/src/pages/components/ScrollAreaShowcase.tsx new file mode 100644 index 0000000..d9eaffd --- /dev/null +++ b/platform/docs/src/pages/components/ScrollAreaShowcase.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { ScrollArea } from '../../../../ui-next/src/components/ScrollArea'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * ScrollAreaShowcase component displays ScrollArea variants and examples + */ +export default function ScrollAreaShowcase() { + return ( + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco + laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat + non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. + + `} + > + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut + labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco + laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in + voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat + cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem + ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut + labore et dolore magna aliqua. + + + ); +} diff --git a/platform/docs/src/pages/components/SelectShowcase.tsx b/platform/docs/src/pages/components/SelectShowcase.tsx new file mode 100644 index 0000000..8ee244b --- /dev/null +++ b/platform/docs/src/pages/components/SelectShowcase.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { + Select, + SelectTrigger, + SelectContent, + SelectItem, + SelectValue, +} from '../../../../ui-next/src/components/Select'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * SelectShowcase component displays Select variants and examples + */ +export default function SelectShowcase() { + return ( + + + + + + Light + Dark + System + + + `} + > + + + ); +} diff --git a/platform/docs/src/pages/components/ShowcaseRow.tsx b/platform/docs/src/pages/components/ShowcaseRow.tsx new file mode 100644 index 0000000..a1c9240 --- /dev/null +++ b/platform/docs/src/pages/components/ShowcaseRow.tsx @@ -0,0 +1,49 @@ +import React, { useState } from 'react'; +import { Button } from '../../../../ui-next/src/components/Button'; +import { Icons } from '../../../../ui-next/src/components/Icons'; + +interface ShowcaseRowProps { + title: string; + description?: string; + children: React.ReactNode; + code: string; +} + +/** + * ShowcaseRow component displays a UI component example with title, description, + * and optional code snippet that can be toggled. + */ +export default function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) { + const [showCode, setShowCode] = useState(false); + + return ( +
+
+
+

{title}

+
+ +
+
+
+ {description &&

{description}

} +
+
+
{children}
+
+
+ {showCode && ( +
+          {code}
+        
+ )} +
+ ); +} diff --git a/platform/docs/src/pages/components/SliderShowcase.tsx b/platform/docs/src/pages/components/SliderShowcase.tsx new file mode 100644 index 0000000..0788ceb --- /dev/null +++ b/platform/docs/src/pages/components/SliderShowcase.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Slider } from '../../../../ui-next/src/components/Slider'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * SliderShowcase component displays Slider variants and examples + */ +export default function SliderShowcase() { + return ( + + + + `} + > +
+ +
+
+ ); +} diff --git a/platform/docs/src/pages/components/SwitchShowcase.tsx b/platform/docs/src/pages/components/SwitchShowcase.tsx new file mode 100644 index 0000000..7d814a2 --- /dev/null +++ b/platform/docs/src/pages/components/SwitchShowcase.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { Switch } from '../../../../ui-next/src/components/Switch'; +import { Label } from '../../../../ui-next/src/components/Label'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * SwitchShowcase component displays Switch variants and examples + */ +export default function SwitchShowcase() { + return ( + + `} + > + + + + ); +} diff --git a/platform/docs/src/pages/components/TabsShowcase.tsx b/platform/docs/src/pages/components/TabsShowcase.tsx new file mode 100644 index 0000000..e887503 --- /dev/null +++ b/platform/docs/src/pages/components/TabsShowcase.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs'; +import { Separator } from '../../../../ui-next/src/components/Separator'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * TabsShowcase component displays Tabs variants and examples + */ +export default function TabsShowcase() { + return ( + console.log(newValue)}> + + Circle + + Sphere + + Square + + + `} + > + console.log(newValue)} + > + + Circle + + Sphere + + Square + + + + ); +} diff --git a/platform/docs/src/pages/components/ToastShowcase.tsx b/platform/docs/src/pages/components/ToastShowcase.tsx new file mode 100644 index 0000000..b79ae5c --- /dev/null +++ b/platform/docs/src/pages/components/ToastShowcase.tsx @@ -0,0 +1,158 @@ +import React from 'react'; +import { Button } from '../../../../ui-next/src/components/Button'; +import { Toaster, toast } from '../../../../ui-next/src/components/Sonner'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * ToastShowcase component displays Toast variants and examples + */ +export default function ToastShowcase() { + // Handlers to trigger different types of toasts + const triggerSuccess = () => { + toast.success('This is a success toast!'); + }; + + const triggerError = () => { + toast.error('This is an error toast!'); + }; + + const triggerInfo = () => { + toast.info('This is an info toast!'); + }; + + const triggerWarning = () => { + toast.warning('This is a warning toast!'); + }; + + // Handler to trigger a toast.promise example + const triggerPromiseToast = () => { + const promise = () => + new Promise<{ name: string }>(resolve => + setTimeout(() => resolve({ name: 'Segmentation 1' }), 3000) + ); + + toast.promise(promise(), { + loading: 'Loading Segmentation...', + success: data => `${data.name} has been added`, + error: 'Error', + }); + }; + + // Handler to trigger a toast with description + const triggerDescriptionToast = () => { + toast.success('Completed', { + description: 'This is a detailed description of the success message.', + }); + }; + + // Handler to trigger a toast with an action button + const triggerActionButtonToast = () => { + toast.info('No active segmentation detected', { + description: 'Create a segmentation before using the Brush', + }); + }; + + // Handler to trigger a toast with a cancel button + const triggerCancelButtonToast = () => { + toast.error('No active segmentation detected', { + description: 'Create a segmentation before using the Brush', + }); + }; + + // Handler to trigger a toast with both action and cancel buttons + const triggerCombinedToast = () => { + toast.warning('Warning!', { + description: 'This is a warning with both action and cancel buttons.', + action: ( + + ), + cancel: ( + + ), + }); + }; + + return ( + + Simple message: +
+ + + + + +
+ Message with details: +
+ + + + +
+ +
+ ); +} diff --git a/platform/docs/src/pages/components/ToolButtonListShowcase.tsx b/platform/docs/src/pages/components/ToolButtonListShowcase.tsx new file mode 100644 index 0000000..eccae8f --- /dev/null +++ b/platform/docs/src/pages/components/ToolButtonListShowcase.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { + ToolButtonList, + ToolButton, + ToolButtonListDefault, + ToolButtonListDropDown, + ToolButtonListItem, + ToolButtonListDivider, +} from '../../../../ui-next/src/components/ToolButton'; +import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip'; + +import ShowcaseRow from './ShowcaseRow'; + +/** + * ToolButtonListShowcase component displays ToolButtonList variants and examples + */ +export default function ToolButtonListShowcase() { + return ( + + + console.debug(\`Clicked \${itemId}\`)} + /> + + + + console.debug('Selected Length')} + > + Length + + console.debug('Selected Bidirectional')} + > + Bidirectional + + + + `} + > +
+ + + + console.debug(`Clicked ${itemId}`)} + /> + + + + console.debug('Selected Length')} + > + Length + + console.debug('Selected Bidirectional')} + > + Bidirectional + + console.debug('Selected Annotation')} + > + Annotation + + + + +
+
+ ); +} diff --git a/platform/docs/src/pages/components/ToolButtonShowcase.tsx b/platform/docs/src/pages/components/ToolButtonShowcase.tsx new file mode 100644 index 0000000..b3dcf10 --- /dev/null +++ b/platform/docs/src/pages/components/ToolButtonShowcase.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip'; +import ToolButton from '../../../../ui-next/src/components/ToolButton/ToolButton'; +import ShowcaseRow from './ShowcaseRow'; + +/** + * ToolButtonShowcase component displays ToolButton variants and examples + */ +export default function ToolButtonShowcase() { + return ( + console.debug(\`Clicked \${itemId}\`)} +/> + `} + > +
+ + console.debug(`Clicked ${itemId}`)} + /> + console.debug(`Clicked ${itemId}`)} + /> + console.debug(`Clicked ${itemId}`)} + /> + +
+
+ ); +} diff --git a/platform/docs/src/pages/help.md b/platform/docs/src/pages/help.md new file mode 100644 index 0000000..3610b39 --- /dev/null +++ b/platform/docs/src/pages/help.md @@ -0,0 +1,29 @@ +# Help + +We all need a little help sometimes. Don't let a few roadblocks stand in the way +of you building something awesome. + +## Get Support + +If you're a developer looking to contribute code, documentation, or discussion; +we are more than happy to help provide clarification and answer questions via +[GitHub issues][gh-issues] or our [community forum][google-group]. Regular +contributors may also be invited to join our Slack Group to streamline +discussion. + +For bug reports and feature requests (including incomplete or confusing +documentation), [GitHub issues][gh-issues] continue to be your best avenue of +communication. + +Complex issues specific to your organization/situation are still okay to post, +but they're less likely to receive a response. Unfortunately, we have limited +resources and must be judicious with how we allocate them. If you find yourself +in this situation and in need of assistance, it may be in your best interest to +pursue paid support. + +If you need additional help, please [reach out to us](https://ohif.org/get-support) to get more information on +how we can help you. + + +[gh-issues]: https://github.com/OHIF/Viewers/issues/ +[google-group]: https://groups.google.com/forum/#!forum/cornerstone-platform diff --git a/platform/docs/src/pages/patterns.tsx b/platform/docs/src/pages/patterns.tsx new file mode 100644 index 0000000..3099957 --- /dev/null +++ b/platform/docs/src/pages/patterns.tsx @@ -0,0 +1,211 @@ +import React, { useState } from 'react'; +import '../css/custom.css'; + +import Layout from '@theme/Layout'; +import { Button } from '../../../ui-next/src/components/Button'; +import { Icons } from '../../../ui-next/src/components/Icons'; +import { Card, CardHeader, CardTitle, CardDescription } from '../../../ui-next/src/components/Card'; + +interface ShowcaseRowProps { + title: string; + description?: string; + children: React.ReactNode; + code: string; +} + +export default function ComponentShowcase() { + // Update function to handle paths correctly + const openLinkInNewWindow = (url: string) => { + // Remove leading dot if present to fix production paths + const cleanUrl = url.startsWith('.') ? url.substring(1) : url; + window.open(cleanUrl, '_blank', 'noopener,noreferrer'); + }; + + return ( + +
+ + +
+

Patterns

+ + +
+ Uses the Data Row component to displays a list of segments. The current + "Segmentation" is chosen with a Select above the current list. +
+ +
+ } + code={` +aaa + `} + > +
+
+ Segmentation Panel +
+ + + +
+ Uses the Data Row component to displays a list of measurements. A custom "Label" + starts each row with measurement data appearing on the secondary row +
+ +
+ } + code={` +aaa + `} + > +
+
+ Measurements Panel +
+ + + +
+ ); +} + +function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) { + const [showCode, setShowCode] = useState(false); + + return ( +
+
+
+

{title}

+
+ +
+
+
+ {description &&

{description}

} +
+
+
{children}
+
+
+ {showCode && ( +
+          {code}
+        
+ )} +
+ ); +} + +// function ShowcaseRow({ title, description, children, code }: ShowcaseRowProps) { +// const [showCode, setShowCode] = useState(false); + +// return ( +//
+//
+//
+//

{title}

+// {description &&

{description}

} +//
+// +//
+//
{children}
+// {showCode && ( +//
+//           {code}
+//         
+// )} +//
+// ); +// } diff --git a/platform/docs/src/pages/patterns/DataRowExample.tsx b/platform/docs/src/pages/patterns/DataRowExample.tsx new file mode 100644 index 0000000..16127bf --- /dev/null +++ b/platform/docs/src/pages/patterns/DataRowExample.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { DataRow } from '../../../../ui-next/src/components/DataRow'; +import { Button } from '../../../../ui-next/src/components/Button'; +import { Icons } from '../../../../ui-next/src/components/Icons'; + +// Mock data to demonstrate DataRow usage +const mockData = [ + { + id: 1, + title: 'Segment 1', + description: 'Description for Segment 1', + optionalField: 'Optional Info 1', + colorHex: '#FF5733', + details: 'Secondary details or text', + }, + { + id: 2, + title: 'Segment 2', + description: 'Description for Segment 2', + optionalField: 'Optional Info 2', + colorHex: '#33C1FF', + details: 'Secondary details or text', + }, + { + id: 3, + title: 'Segment 3', + description: 'Description for Segment 3', + optionalField: 'Optional Info 3', + colorHex: '#5533FF', + details: 'Secondary details or text', + }, +]; + +// Mock action options map +const actionOptionsMap = { + 'ROI Tools': ['Edit', 'Delete', 'View'], +}; + +interface DataItem { + id: number; + title: string; + description: string; + optionalField?: string; + colorHex?: string; + details?: string; + series?: string; +} + +interface ListGroup { + type: string; + items: DataItem[]; +} + +const DataRowExample: React.FC = () => { + const [selectedRowId, setSelectedRowId] = React.useState(null); + + const handleAction = (id: string, action: string) => { + console.log(`Action "${action}" triggered for item with id: ${id}`); + // Implement actual action logic here + }; + + const handleRowSelect = (id: string) => { + setSelectedRowId(prevSelectedId => (prevSelectedId === id ? null : id)); + }; + + return ( +
+ {mockData.map((item, index) => { + const compositeId = `ROI Tools-${item.id}-panel`; // Ensure unique composite ID + return ( + handleAction(compositeId, action)} + isSelected={selectedRowId === compositeId} + onSelect={() => handleRowSelect(compositeId)} + /> + ); + })} +
+ ); +}; + +export default DataRowExample; diff --git a/platform/docs/src/pages/patterns/index.tsx b/platform/docs/src/pages/patterns/index.tsx new file mode 100644 index 0000000..c2b5ef3 --- /dev/null +++ b/platform/docs/src/pages/patterns/index.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import Layout from '@theme/Layout'; +import { useHistory } from '@docusaurus/router'; + +export default function Patterns() { + const history = useHistory(); + + return ( + +

Patterns

+ + + +
+ ); +} diff --git a/platform/docs/src/pages/patterns/patterns-measurements.tsx b/platform/docs/src/pages/patterns/patterns-measurements.tsx new file mode 100644 index 0000000..1daf39a --- /dev/null +++ b/platform/docs/src/pages/patterns/patterns-measurements.tsx @@ -0,0 +1,134 @@ +import React, { useState } from 'react'; +import { Button } from '../../../../ui-next/src/components/Button'; +import { Icons } from '../../../../ui-next/src/components/Icons'; +import { + Accordion, + AccordionItem, + AccordionTrigger, + AccordionContent, +} from '../../../../ui-next/src/components/Accordion'; +import { DataRow } from '../../../../ui-next/src/components/DataRow'; +import { actionOptionsMap, dataList } from '../../../../ui-next/assets/data'; +import BrowserOnly from '@docusaurus/BrowserOnly'; +import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip'; + +interface DataItem { + id: number; + title: string; + description: string; + optionalField?: string; + colorHex?: string; + details?: string; + series?: string; +} + +interface ListGroup { + type: string; + items: DataItem[]; +} + +export default function Measurements() { + const [selectedRowId, setSelectedRowId] = useState(null); + const handleAction = (id: string, action: string) => { + console.log(`Action "${action}" triggered for item with id: ${id}`); + // Implement actual action logic here + }; + const handleRowSelect = (id: string) => { + setSelectedRowId(prevSelectedId => (prevSelectedId === id ? null : id)); + }; + + const organSegmentationGroup = dataList.find( + listGroup => listGroup.type === 'Organ Segmentation' + ); + const roiToolsGroup = dataList.find(listGroup => listGroup.type === 'ROI Tools'); + + if (!organSegmentationGroup || !roiToolsGroup) { + return null; // Avoid rendering until these groups are ready. + } + + return ( + + {() => ( +
+ + {/* Simulated Panel List for "Segmentation" */} +
+ + {/* Segmentation Tools */} + + + Measurements + + +
+
2024-Jan-01
+
+ Study title lorem ipsum +
+
+ +
+
+ + +
+
+
+ {roiToolsGroup.items.map((item, index) => { + const compositeId = `${roiToolsGroup.type}-${item.id}-panel`; // Ensure unique composite ID + return ( + handleAction(compositeId, action)} + isSelected={selectedRowId === compositeId} + onSelect={() => handleRowSelect(compositeId)} + /> + ); + })} +
+
+
+ + {/* Additional Findings */} + + + Additional Findings + + +
+
+
+
+
+
+
+ )} +
+ ); +} diff --git a/platform/docs/src/pages/patterns/patterns-segmentation.tsx b/platform/docs/src/pages/patterns/patterns-segmentation.tsx new file mode 100644 index 0000000..c78b6c2 --- /dev/null +++ b/platform/docs/src/pages/patterns/patterns-segmentation.tsx @@ -0,0 +1,311 @@ +'use client'; + +import React, { useState } from 'react'; + +import { DataRow } from '../../../../ui-next/src/components/DataRow'; +import { Button } from '../../../../ui-next/src/components/Button'; +import { + Select, + SelectValue, + SelectTrigger, + SelectContent, + SelectItem, +} from '../../../../ui-next/src/components/Select'; +import { Icons } from '../../../../ui-next/src/components/Icons'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuLabel, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, + DropdownMenuPortal, +} from '../../../../ui-next/src/components/DropdownMenu'; +import { + Accordion, + AccordionItem, + AccordionTrigger, + AccordionContent, +} from '../../../../ui-next/src/components/Accordion'; +import { Slider } from '../../../../ui-next/src/components/Slider'; +import { Switch } from '../../../../ui-next/src/components/Switch'; +import { Label } from '../../../../ui-next/src/components/Label'; +import { Input } from '../../../../ui-next/src/components/Input'; +import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs'; +import { actionOptionsMap, dataList } from '../../../../ui-next/assets/data'; +import { TooltipProvider } from '../../../../ui-next/src/components/Tooltip'; + +interface DataItem { + id: number; + title: string; + description: string; + optionalField?: string; + colorHex?: string; + details?: string; + series?: string; +} + +interface ListGroup { + type: string; + items: DataItem[]; +} + +export default function SegmentationPanel() { + const [selectedRowId, setSelectedRowId] = useState(null); + const [selectedTab, setSelectedTab] = useState('Fill & Outline'); + const handleAction = (id: string, action: string) => { + console.log(`Action "${action}" triggered for item with id: ${id}`); + // Implement actual action logic here + }; + + // Handle row selection + const handleRowSelect = (id: string) => { + setSelectedRowId(prevSelectedId => (prevSelectedId === id ? null : id)); + }; + + const organSegmentationGroup = dataList.find( + (listGroup: ListGroup) => listGroup.type === 'Organ Segmentation' + ); + + if (!organSegmentationGroup) { + return
Organ Segmentation data not found.
; + } + + return ( +
+
+ + + {/* Segmentation Tools */} + + + Segmentation Tools + + +
+
+
+ + {/* Segmentation List */} + + + Segmentation List + + +
+ {/* Header Controls */} +
+ + + + + + + + Create New Segmentation + + + Manage Current Segmentation + + + Remove from Viewport + + + + Rename + + + + + Export & Download + + + + Export DICOM SEG + Download DICOM SEG + Download DICOM RTSTRUCT + + + + + + + Delete + + + + + +
+ + {/* Appearance Settings */} + + +
+ + Appearance Settings +
+
+ +
+
+ {/* Display Label with Selected Tab */} +
Show: {selectedTab}
+ {/* Tabs Controls */} + + + + + + + + + + + + + +
+ {/* Opacity Slider */} +
+ + + +
+ {/* Border Slider */} +
+ + + +
+ {/* Sync Changes Switch */} +
+ + +
+
+ {/* Display Inactive Segmentations Switch */} +
+ + +
+ {/* Additional Opacity Slider */} +
+ + + +
+
+
+
+ {/* Action Buttons */} +
+ + +
+
+ + {/* Data Rows */} +
+ {organSegmentationGroup.items.map((item, index) => { + const compositeId = `${organSegmentationGroup.type}-${item.id}-panel`; // Ensure unique composite ID + return ( + handleAction(compositeId, action)} + isSelected={selectedRowId === compositeId} + onSelect={() => handleRowSelect(compositeId)} + /> + ); + })} +
+
+
+
+
+
+
+ ); +} diff --git a/platform/docs/src/pages/patterns/patterns-split-panel.tsx b/platform/docs/src/pages/patterns/patterns-split-panel.tsx new file mode 100644 index 0000000..3fdc7b7 --- /dev/null +++ b/platform/docs/src/pages/patterns/patterns-split-panel.tsx @@ -0,0 +1,43 @@ +import React, { useState } from 'react'; + +import { DataRow } from '../../../../ui-next/src/components/DataRow'; +import { Button } from '../../../../ui-next/src/components/Button'; +import { + Select, + SelectValue, + SelectTrigger, + SelectContent, + SelectItem, +} from '../../../../ui-next/src/components/Select'; +import { Icons } from '../../../../ui-next/src/components/Icons'; +import { + Accordion, + AccordionItem, + AccordionTrigger, + AccordionContent, +} from '../../../../ui-next/src/components/Accordion'; +import { Slider } from '../../../../ui-next/src/components/Slider'; +import { Switch } from '../../../../ui-next/src/components/Switch'; +import { Label } from '../../../../ui-next/src/components/Label'; +import { Input } from '../../../../ui-next/src/components/Input'; +import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs'; +import { actionOptionsMap, dataList } from '../../../../ui-next/assets/data'; + +interface DataItem { + id: number; + title: string; + description: string; + optionalField?: string; + colorHex?: string; + details?: string; + series?: string; +} + +interface ListGroup { + type: string; + items: DataItem[]; +} + +export default function SplitPanel() { + return
hellosssssss
; +} diff --git a/platform/docs/src/pages/patterns/patterns-tmtv.tsx b/platform/docs/src/pages/patterns/patterns-tmtv.tsx new file mode 100644 index 0000000..6dc5586 --- /dev/null +++ b/platform/docs/src/pages/patterns/patterns-tmtv.tsx @@ -0,0 +1,393 @@ +import React, { useState } from 'react'; + +import { Button } from '../../../../ui-next/src/components/Button'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, + DropdownMenuPortal, +} from '../../../../ui-next/src/components/DropdownMenu'; +import { Icons } from '../../../../ui-next/src/components/Icons/Icons'; +import { DataRow } from '../../../../ui-next/src/components/DataRow'; +import { actionOptionsMap, dataList } from '../../../../ui-next/assets/data'; + +import { + Accordion, + AccordionItem, + AccordionTrigger, + AccordionContent, +} from '../../../../ui-next/src/components/Accordion/Accordion'; +import { Slider } from '../../../../ui-next/src/components/Slider'; +import { Switch } from '../../../../ui-next/src/components/Switch'; +import { Label } from '../../../../ui-next/src/components/Label'; +import { Input } from '../../../../ui-next/src/components/Input'; +import { Tabs, TabsList, TabsTrigger } from '../../../../ui-next/src/components/Tabs'; +import BrowserOnly from '@docusaurus/BrowserOnly'; + +interface DataItem { + id: number; + title: string; + description: string; + optionalField?: string; + colorHex?: string; + details?: string; + series?: string; +} + +interface ListGroup { + type: string; + items: DataItem[]; +} + +export default function TMTVPatterns() { + const [selectedRowId, setSelectedRowId] = useState(null); + const [selectedTab, setSelectedTab] = useState('Fill & Outline'); + + const handleAction = (id: string, action: string) => { + console.log(`Action "${action}" triggered for item with id: ${id}`); + // Implement actual action logic here + }; + + // Handle row selection + const handleRowSelect = (id: string) => { + setSelectedRowId(prevSelectedId => (prevSelectedId === id ? null : id)); + }; + + // Find the "TMTV2" group + const tmv2Group = dataList.find((listGroup: ListGroup) => listGroup.type === 'TMTV2'); + + // Find the "TMTV1" group + const tmvGroup = dataList.find((listGroup: ListGroup) => listGroup.type === 'TMTV1'); + + // Check if both groups exist + if (!tmv2Group) { + return
TMTV2 data not found.
; + } + + if (!tmvGroup) { + return
TMTV1 data not found.
; + } + + return ( + + {() => ( +
+
+ + {/* Segmentation Tools */} + + + Segmentation Tools + + +
+
+
+ {/* Segmentation List */} + + + Segmentation List + + + {/* Appearance Settings */} + + +
+ + Appearance Settings +
+
+ +
+
+ {/* Display Label with Selected Tab */} +
Show: {selectedTab}
+ {/* Tabs Controls */} + + + + + + + + + + + + + +
+ {/* Opacity Slider */} +
+ + + +
+ {/* Border Slider */} +
+ + + +
+ {/* Sync Changes Switch */} +
+ + +
+
+ {/* Display Inactive Segmentations Switch */} +
+ + +
+ {/* Additional Opacity Slider */} +
+ + + +
+
+
+
+ {/* TMTV1 Group */} + + +
+ {/* Left Group: DropdownMenu and TMTV1 Label */} +
+ + + + + + + + Add Segment + + + + + Remove from Viewport + + + + Rename + + + + Hide or Show all Segments + + + + + Export & Download + + + + Export DICOM SEG + Download DICOM SEG + Download DICOM RTSTRUCT + + + + + + + Delete + + + +
TMTV1 Segmentation
+
+
+ +
+
+
+ + {/* Data Rows for TMTV1 */} +
+ {tmvGroup.items.map((item, index) => { + const compositeId = `${tmvGroup.type}-${item.id}-panel`; // Ensure unique composite ID + return ( + handleAction(compositeId, action)} + isSelected={selectedRowId === compositeId} + onSelect={() => handleRowSelect(compositeId)} + /> + ); + })} +
+
+
+ {/* TMTV2 Group */} + + +
+
+ + + + + + + + Add Segment + + + + + Remove from Viewport + + + + Rename + + + + Hide or Show all Segments + + + + + Export & Download + + + + Export DICOM SEG + Download DICOM SEG + Download DICOM RTSTRUCT + + + + + + + Delete + + + + +
TMTV2 Segmentation
+
+
+ +
+
+
+ + {/* Data Rows for TMTV2 */} +
+ {tmv2Group.items.map((item, index) => { + const compositeId = `${tmv2Group.type}-${item.id}-panel`; // Ensure unique composite ID + return ( + handleAction(compositeId, action)} + isSelected={selectedRowId === compositeId} + onSelect={() => handleRowSelect(compositeId)} + /> + ); + })} +
+
+
+ {/* Footer or Additional Information */} +
+ TMTV + 21.555 mL +
+
+
+
+
+
+ )} +
+ ); +} diff --git a/platform/docs/src/pages/versions.js b/platform/docs/src/pages/versions.js new file mode 100644 index 0000000..62f1da2 --- /dev/null +++ b/platform/docs/src/pages/versions.js @@ -0,0 +1,57 @@ +import React from 'react'; +import Layout from '@theme/Layout'; +import Link from '@docusaurus/Link'; + +export default function Versions() { + const versions = [ + { + version: 'Version 1', + status: 'deprecated', + description: 'Built with Meteor as a full stack application.', + }, + { + version: 'Version 2', + status: 'deprecated', + description: 'Front end image viewer built with React', + }, + { + version: 'Version 3.x-beta', + status: 'master branch', + description: 'With latest bug fixes and features but not yet released (released under beta)', + }, + { + version: 'Version 3.x', + status: 'release branch', + description: 'Released version of the OHIF platform which is more stable and tested', + }, + ]; + + return ( + +
+

Versions

+ +

+ As we are increasing the efforts to make the OHIF platform more robust and up-to-date with + the latest software engineering practices, here we are listing the versions of the OHIF + platform that we are currently supporting, and the versions that have been deprecated. +

+ +

Product Version

+ +

Currently we have four product versions:

+ +
    + {versions.map((item, index) => ( +
  • + {item.version} ({item.status}): {item.description} +
  • + ))} +
+
+
+ ); +} diff --git a/platform/docs/src/utils/getMockedStudies.js b/platform/docs/src/utils/getMockedStudies.js new file mode 100644 index 0000000..e7d1b7d --- /dev/null +++ b/platform/docs/src/utils/getMockedStudies.js @@ -0,0 +1,16 @@ +import studyListMock from '../mocks/studyList'; + +/** Values can be env vars */ +const DEFAULT_MOCKED_STUDIES_LIMIT = 1000; + +/** + * Method to get a mocked study list + * @param {number} items Number of studies to be loaded + * @returns {array} Study list + */ +const getMockedStudies = (items = 50) => { + const num = items > DEFAULT_MOCKED_STUDIES_LIMIT ? DEFAULT_MOCKED_STUDIES_LIMIT : items; + return new Array(num).fill(studyListMock.studies[0]); +}; + +export default getMockedStudies; diff --git a/platform/docs/src/utils/index.js b/platform/docs/src/utils/index.js new file mode 100644 index 0000000..659d89d --- /dev/null +++ b/platform/docs/src/utils/index.js @@ -0,0 +1,7 @@ +import getMockedStudies from './getMockedStudies'; + +const utils = { getMockedStudies }; + +export { getMockedStudies }; + +export default utils; diff --git a/platform/docs/static/.nojekyll b/platform/docs/static/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/platform/docs/static/img/favicon.ico b/platform/docs/static/img/favicon.ico new file mode 100644 index 0000000..faaa2cf Binary files /dev/null and b/platform/docs/static/img/favicon.ico differ diff --git a/platform/docs/static/img/mgh-logo.png b/platform/docs/static/img/mgh-logo.png new file mode 100644 index 0000000..47b85da Binary files /dev/null and b/platform/docs/static/img/mgh-logo.png differ diff --git a/platform/docs/static/img/netlify-color-accent.svg b/platform/docs/static/img/netlify-color-accent.svg new file mode 100644 index 0000000..0efc282 --- /dev/null +++ b/platform/docs/static/img/netlify-color-accent.svg @@ -0,0 +1,22 @@ + + + + netlify-callout-vertical-color-accent + Created with Sketch. + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/docs/static/img/ohif-logo-light.svg b/platform/docs/static/img/ohif-logo-light.svg new file mode 100644 index 0000000..a0414e2 --- /dev/null +++ b/platform/docs/static/img/ohif-logo-light.svg @@ -0,0 +1,15 @@ + + + ohif-logo + + \ No newline at end of file diff --git a/platform/docs/static/img/ohif-logo.svg b/platform/docs/static/img/ohif-logo.svg new file mode 100644 index 0000000..c0a8760 --- /dev/null +++ b/platform/docs/static/img/ohif-logo.svg @@ -0,0 +1,15 @@ + + + ohif-logo + + \ No newline at end of file diff --git a/platform/docs/static/img/patterns-measurements.png b/platform/docs/static/img/patterns-measurements.png new file mode 100644 index 0000000..5c1870e Binary files /dev/null and b/platform/docs/static/img/patterns-measurements.png differ diff --git a/platform/docs/static/img/patterns-segmentation.png b/platform/docs/static/img/patterns-segmentation.png new file mode 100644 index 0000000..8eec0d4 Binary files /dev/null and b/platform/docs/static/img/patterns-segmentation.png differ diff --git a/platform/docs/tailwind.config.js b/platform/docs/tailwind.config.js new file mode 100644 index 0000000..7e858df --- /dev/null +++ b/platform/docs/tailwind.config.js @@ -0,0 +1,116 @@ +module.exports = { + // Don't purge any tailwind classes, usefull for debugging + // ...(process.env.NODE_ENV === 'development' && { + // safelist: [{ pattern: /.*/ }], + // }), + content: [ + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + './src/**/*.{ts,tsx}', + '../ui-next/**/*.{ts,tsx,js, jsx}', + ], + prefix: '', + theme: { + fontFamily: { + inter: ['Inter', 'sans-serif'], + }, + fontSize: { + xxs: '0.625rem', // 10px + xs: '0.6875rem', // 11px + sm: '0.75rem', // 12px + base: '0.8125rem', // 13px + lg: '0.875rem', // 14px + xl: '1rem', // 16px + // 2xl and above will be updated in an upcoming version + '2xl': '1.5rem', + '3xl': '1.875rem', + '4xl': '2.25rem', + '5xl': '3rem', + '6xl': '4rem', + }, + fontWeight: { + hairline: '100', + thin: '200', + light: '300', + normal: '400', + medium: '500', + semibold: '600', + bold: '700', + extrabold: '800', + black: '900', + }, + extend: { + colors: { + highlight: 'hsl(var(--highlight))', + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + keyframes: { + 'accordion-down': { + from: { height: '0' }, + to: { height: 'var(--radix-accordion-content-height)' }, + }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: '0' }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + }, + bkg: { + low: '#050615', + med: '#090C29', + full: '#041C4A', + }, + info: { + primary: '#FFFFFF', + secondary: '#7BB2CE', + }, + actions: { + primary: '#348CFD', + highlight: '#5ACCE6', + hover: 'rgba(52, 140, 253, 0.2)', + }, + }, + }, + plugins: [require('tailwindcss-animate')], +}; diff --git a/platform/docs/versioned_docs/version-3.9/README.md b/platform/docs/versioned_docs/version-3.9/README.md new file mode 100644 index 0000000..9b5a6f7 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/README.md @@ -0,0 +1,115 @@ +--- +id: Introduction +slug: / +sidebar_position: 1 +--- + +The [Open Health Imaging Foundation][ohif-org] (OHIF) Viewer is an open source, +web-based, medical imaging platform. It aims to provide a core framework for +building complex imaging applications. + +Key features: + +- Designed to load large radiology studies as quickly as possible. Retrieves + metadata ahead of time and streams in imaging pixel data as needed. +- Leverages [Cornerstone3D](https://github.com/cornerstonejs/cornerstone3D-beta) for decoding, + rendering, and annotating medical images. +- Works out-of-the-box with Image Archives that support [DICOMWeb][dicom-web]. + Offers a Data Source API for communicating with archives over proprietary API + formats. +- Provides a plugin framework for creating task-based workflow modes which can + reuse core functionality. +- Beautiful user interface (UI) designed with extensibility in mind. UI + components available in a reusable component library built with React.js and + Tailwind CSS + + + + +
+
+ + + +| | | | +| :-: | :--- | :--- | +| Measurement tracking | Measurement Tracking | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5) | +| Segmentations | Labelmap Segmentations | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046) | +| Hanging Protocols | Fusion and Custom Hanging protocols | [Demo](https://viewer.ohif.org/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463) | +| Volume Rendering | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) | +| PDF | PDF | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.317377619501274872606137091638706705333) | +| RTSTRUCT | RT STRUCT | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.5962.99.1.2968617883.1314880426.1493322302363.3.0) | +| 4D | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) | +| VIDEO | Video | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.96975534054447904995905761963464388233) | +| microscopy | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.141277760791347900862109212450152067508) | + + + +## Where to next? + +The Open Health Imaging Foundation intends to provide an imaging viewer +framework which can be easily extended for specific uses. If you find yourself +unable to extend the viewer for your purposes, please reach out via our [GitHub +issues][gh-issues]. We are actively seeking feedback on ways to improve our +integration and extension points. + +Check out these helpful links: + +- Ready to dive into some code? Check out our + [Getting Started Guide](./development/getting-started.md). +- We're an active, vibrant community. + [Learn how you can be more involved.](./development/contributing.md) +- Feeling lost? Read our [help page](/help). + +## Citing OHIF + +To cite the OHIF Viewer in an academic publication, please cite + +> _Open Health Imaging Foundation Viewer: An Extensible Open-Source Framework +> for Building Web-Based Imaging Applications to Support Cancer Research_ +> +> Erik Ziegler, Trinity Urban, Danny Brown, James Petts, Steve D. Pieper, Rob +> Lewis, Chris Hafey, and Gordon J. Harris _JCO Clinical Cancer Informatics_, no. 4 (2020), 336-345, DOI: +> [10.1200/CCI.19.00131](https://www.doi.org/10.1200/CCI.19.00131) + +This article is freely available on Pubmed Central: https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7259879/ + + +or, for Lesion Tracker of OHIF v1, please cite: + +> _LesionTracker: Extensible Open-Source Zero-Footprint Web Viewer for Cancer +> Imaging Research and Clinical Trials_ +> +> Trinity Urban, Erik Ziegler, Rob Lewis, Chris Hafey, Cheryl Sadow, Annick D. +> Van den Abbeele and Gordon J. Harris _Cancer Research_, November 1 2017 (77) (21) e119-e122 DOI: +> [10.1158/0008-5472.CAN-17-0334](https://www.doi.org/10.1158/0008-5472.CAN-17-0334) + +This article is freely available on Pubmed Central. +https://pubmed.ncbi.nlm.nih.gov/29092955/ + + +**Note:** If you use or find this repository helpful, please take the time to +star this repository on Github. This is an easy way for us to assess adoption, +and it can help us obtain future funding for the project. + +## License + +MIT ยฉ [OHIF](https://github.com/OHIF) + +  + + + + +[ohif-org]: https://www.ohif.org +[ohif-demo]: http://viewer.ohif.org/ +[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb +[gh-issues]: https://github.com/OHIF/Viewers/issues + diff --git a/platform/docs/versioned_docs/version-3.9/assets/designs/architecture-diagram b/platform/docs/versioned_docs/version-3.9/assets/designs/architecture-diagram new file mode 100644 index 0000000..bbf6cf5 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/designs/architecture-diagram differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/designs/canny-full.fig b/platform/docs/versioned_docs/version-3.9/assets/designs/canny-full.fig new file mode 100644 index 0000000..8756e9f Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/designs/canny-full.fig differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/designs/cloud.svg b/platform/docs/versioned_docs/version-3.9/assets/designs/cloud.svg new file mode 100644 index 0000000..ad04389 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/assets/designs/cloud.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/platform/docs/versioned_docs/version-3.9/assets/designs/embedded-viewer-diagram b/platform/docs/versioned_docs/version-3.9/assets/designs/embedded-viewer-diagram new file mode 100644 index 0000000..182ad23 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/designs/embedded-viewer-diagram differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/designs/nginx-image-archive.fig b/platform/docs/versioned_docs/version-3.9/assets/designs/nginx-image-archive.fig new file mode 100644 index 0000000..460ae95 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/designs/nginx-image-archive.fig differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/designs/npm-logo-red.svg b/platform/docs/versioned_docs/version-3.9/assets/designs/npm-logo-red.svg new file mode 100644 index 0000000..8e4aac5 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/assets/designs/npm-logo-red.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/platform/docs/versioned_docs/version-3.9/assets/designs/scope-of-project.fig b/platform/docs/versioned_docs/version-3.9/assets/designs/scope-of-project.fig new file mode 100644 index 0000000..5eb82e5 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/designs/scope-of-project.fig differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/designs/user-access-control-request-flow.fig b/platform/docs/versioned_docs/version-3.9/assets/designs/user-access-control-request-flow.fig new file mode 100644 index 0000000..8982a8f Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/designs/user-access-control-request-flow.fig differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/OHIF-e2e-test-studies.png b/platform/docs/versioned_docs/version-3.9/assets/img/OHIF-e2e-test-studies.png new file mode 100644 index 0000000..4a58a18 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/OHIF-e2e-test-studies.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/SR-exported.png b/platform/docs/versioned_docs/version-3.9/assets/img/SR-exported.png new file mode 100644 index 0000000..fc477ad Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/SR-exported.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/WORKFLOW_DEPLOY.png b/platform/docs/versioned_docs/version-3.9/assets/img/WORKFLOW_DEPLOY.png new file mode 100644 index 0000000..3e562a7 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/WORKFLOW_DEPLOY.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/WORKFLOW_PR_CHECKS.png b/platform/docs/versioned_docs/version-3.9/assets/img/WORKFLOW_PR_CHECKS.png new file mode 100644 index 0000000..f9c4a56 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/WORKFLOW_PR_CHECKS.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png b/platform/docs/versioned_docs/version-3.9/assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png new file mode 100644 index 0000000..54b0aa3 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/WORKFLOW_RELEASE.png b/platform/docs/versioned_docs/version-3.9/assets/img/WORKFLOW_RELEASE.png new file mode 100644 index 0000000..f3c2a80 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/WORKFLOW_RELEASE.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/add-extension.png b/platform/docs/versioned_docs/version-3.9/assets/img/add-extension.png new file mode 100644 index 0000000..bb4955e Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/add-extension.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/add-mode.png b/platform/docs/versioned_docs/version-3.9/assets/img/add-mode.png new file mode 100644 index 0000000..6f1a162 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/add-mode.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/browser-console-non-secure-context.png b/platform/docs/versioned_docs/version-3.9/assets/img/browser-console-non-secure-context.png new file mode 100644 index 0000000..3fb4f1b Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/browser-console-non-secure-context.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/cli-search-no-verbose.png b/platform/docs/versioned_docs/version-3.9/assets/img/cli-search-no-verbose.png new file mode 100644 index 0000000..40b5113 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/cli-search-no-verbose.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/cli-search-with-verbose.png b/platform/docs/versioned_docs/version-3.9/assets/img/cli-search-with-verbose.png new file mode 100644 index 0000000..b15713b Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/cli-search-with-verbose.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/clock-mode.png b/platform/docs/versioned_docs/version-3.9/assets/img/clock-mode.png new file mode 100644 index 0000000..68ea6dc Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/clock-mode.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/clock-mode1.png b/platform/docs/versioned_docs/version-3.9/assets/img/clock-mode1.png new file mode 100644 index 0000000..7b0375d Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/clock-mode1.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/cornerstone-tools-link.gif b/platform/docs/versioned_docs/version-3.9/assets/img/cornerstone-tools-link.gif new file mode 100644 index 0000000..22fde7a Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/cornerstone-tools-link.gif differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/cors-browser-console-errors.png b/platform/docs/versioned_docs/version-3.9/assets/img/cors-browser-console-errors.png new file mode 100644 index 0000000..0b8d062 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/cors-browser-console-errors.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/cors-network-panel-errors.png b/platform/docs/versioned_docs/version-3.9/assets/img/cors-network-panel-errors.png new file mode 100644 index 0000000..3820b76 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/cors-network-panel-errors.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/create-extension.png b/platform/docs/versioned_docs/version-3.9/assets/img/create-extension.png new file mode 100644 index 0000000..a682649 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/create-extension.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/create-mode.png b/platform/docs/versioned_docs/version-3.9/assets/img/create-mode.png new file mode 100644 index 0000000..3ca4e26 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/create-mode.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/custom-logo.png b/platform/docs/versioned_docs/version-3.9/assets/img/custom-logo.png new file mode 100644 index 0000000..ea3c9ac Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/custom-logo.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/customizable-overlay.jpeg b/platform/docs/versioned_docs/version-3.9/assets/img/customizable-overlay.jpeg new file mode 100644 index 0000000..e166f24 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/customizable-overlay.jpeg differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/data-source-configuration-ui.png b/platform/docs/versioned_docs/version-3.9/assets/img/data-source-configuration-ui.png new file mode 100644 index 0000000..f04956e Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/data-source-configuration-ui.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/dcm4chee-upload.gif b/platform/docs/versioned_docs/version-3.9/assets/img/dcm4chee-upload.gif new file mode 100644 index 0000000..e0e94f1 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/dcm4chee-upload.gif differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/demo-4d.webp b/platform/docs/versioned_docs/version-3.9/assets/img/demo-4d.webp new file mode 100644 index 0000000..0d00c2c Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/demo-4d.webp differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/demo-measurements.webp b/platform/docs/versioned_docs/version-3.9/assets/img/demo-measurements.webp new file mode 100644 index 0000000..d6aaf88 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/demo-measurements.webp differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/demo-pdf.webp b/platform/docs/versioned_docs/version-3.9/assets/img/demo-pdf.webp new file mode 100644 index 0000000..3119a12 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/demo-pdf.webp differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/demo-ptct.webp b/platform/docs/versioned_docs/version-3.9/assets/img/demo-ptct.webp new file mode 100644 index 0000000..ec0a528 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/demo-ptct.webp differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/demo-rtstruct.webp b/platform/docs/versioned_docs/version-3.9/assets/img/demo-rtstruct.webp new file mode 100644 index 0000000..7a985ff Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/demo-rtstruct.webp differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/demo-segmentation.webp b/platform/docs/versioned_docs/version-3.9/assets/img/demo-segmentation.webp new file mode 100644 index 0000000..2f798c3 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/demo-segmentation.webp differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/demo-video.webp b/platform/docs/versioned_docs/version-3.9/assets/img/demo-video.webp new file mode 100644 index 0000000..f3b3cdb Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/demo-video.webp differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/demo-volume-rendering.webp b/platform/docs/versioned_docs/version-3.9/assets/img/demo-volume-rendering.webp new file mode 100644 index 0000000..c74915f Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/demo-volume-rendering.webp differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/demo-volumeRendering.png b/platform/docs/versioned_docs/version-3.9/assets/img/demo-volumeRendering.png new file mode 100644 index 0000000..4c508ab Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/demo-volumeRendering.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/dicom-json-public.png b/platform/docs/versioned_docs/version-3.9/assets/img/dicom-json-public.png new file mode 100644 index 0000000..2d77daf Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/dicom-json-public.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/dicom-json.png b/platform/docs/versioned_docs/version-3.9/assets/img/dicom-json.png new file mode 100644 index 0000000..8eed743 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/dicom-json.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/docker-pacs.png b/platform/docs/versioned_docs/version-3.9/assets/img/docker-pacs.png new file mode 100644 index 0000000..ad33ebe Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/docker-pacs.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/e2e-cypress-final.png b/platform/docs/versioned_docs/version-3.9/assets/img/e2e-cypress-final.png new file mode 100644 index 0000000..49b3a41 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/e2e-cypress-final.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/e2e-cypress.png b/platform/docs/versioned_docs/version-3.9/assets/img/e2e-cypress.png new file mode 100644 index 0000000..89ccc3e Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/e2e-cypress.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/embedded-viewer-diagram.png b/platform/docs/versioned_docs/version-3.9/assets/img/embedded-viewer-diagram.png new file mode 100644 index 0000000..426cb7a Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/embedded-viewer-diagram.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/filtering-worklist.png b/platform/docs/versioned_docs/version-3.9/assets/img/filtering-worklist.png new file mode 100644 index 0000000..47ab317 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/filtering-worklist.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/github-readme-branches-Jun2024.png b/platform/docs/versioned_docs/version-3.9/assets/img/github-readme-branches-Jun2024.png new file mode 100644 index 0000000..4129cc4 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/github-readme-branches-Jun2024.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/google-create-credentials.png b/platform/docs/versioned_docs/version-3.9/assets/img/google-create-credentials.png new file mode 100644 index 0000000..e6534fe Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/google-create-credentials.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/google-enable-apis.png b/platform/docs/versioned_docs/version-3.9/assets/img/google-enable-apis.png new file mode 100644 index 0000000..434bf77 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/google-enable-apis.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/google-healthcare-service-agent-warning.png b/platform/docs/versioned_docs/version-3.9/assets/img/google-healthcare-service-agent-warning.png new file mode 100644 index 0000000..c98ddca Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/google-healthcare-service-agent-warning.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/google-manually-add-scopes.png b/platform/docs/versioned_docs/version-3.9/assets/img/google-manually-add-scopes.png new file mode 100644 index 0000000..9500b85 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/google-manually-add-scopes.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/google-oauth-consent-steps.png b/platform/docs/versioned_docs/version-3.9/assets/img/google-oauth-consent-steps.png new file mode 100644 index 0000000..67d4a42 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/google-oauth-consent-steps.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/google-projects-drop-down.png b/platform/docs/versioned_docs/version-3.9/assets/img/google-projects-drop-down.png new file mode 100644 index 0000000..fb20310 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/google-projects-drop-down.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/google-provided-accounts-checkbox.png b/platform/docs/versioned_docs/version-3.9/assets/img/google-provided-accounts-checkbox.png new file mode 100644 index 0000000..e129854 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/google-provided-accounts-checkbox.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/hangingProtocolExample.png b/platform/docs/versioned_docs/version-3.9/assets/img/hangingProtocolExample.png new file mode 100644 index 0000000..ce9e18c Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/hangingProtocolExample.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/iframe-basic.png b/platform/docs/versioned_docs/version-3.9/assets/img/iframe-basic.png new file mode 100644 index 0000000..25ea207 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/iframe-basic.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/iframe-headers.png b/platform/docs/versioned_docs/version-3.9/assets/img/iframe-headers.png new file mode 100644 index 0000000..27d649d Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/iframe-headers.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/jwt-explained.png b/platform/docs/versioned_docs/version-3.9/assets/img/jwt-explained.png new file mode 100644 index 0000000..f26509a Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/jwt-explained.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/keycloak-default-theme.png b/platform/docs/versioned_docs/version-3.9/assets/img/keycloak-default-theme.png new file mode 100644 index 0000000..0ea77f9 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/keycloak-default-theme.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/keycloak-ohif-theme.png b/platform/docs/versioned_docs/version-3.9/assets/img/keycloak-ohif-theme.png new file mode 100644 index 0000000..ad060f2 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/keycloak-ohif-theme.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/large-pt-ct.jpeg b/platform/docs/versioned_docs/version-3.9/assets/img/large-pt-ct.jpeg new file mode 100644 index 0000000..9999e24 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/large-pt-ct.jpeg differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/locizeSponsor.svg b/platform/docs/versioned_docs/version-3.9/assets/img/locizeSponsor.svg new file mode 100644 index 0000000..1139aa2 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/assets/img/locizeSponsor.svg @@ -0,0 +1,187 @@ + + + + Custom Preset 2 Copy + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/locked-sr.png b/platform/docs/versioned_docs/version-3.9/assets/img/locked-sr.png new file mode 100644 index 0000000..3c6ba7d Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/locked-sr.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/measurement-panel-1.png b/platform/docs/versioned_docs/version-3.9/assets/img/measurement-panel-1.png new file mode 100644 index 0000000..cafeae7 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/measurement-panel-1.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/measurement-panel-prompt.png b/platform/docs/versioned_docs/version-3.9/assets/img/measurement-panel-prompt.png new file mode 100644 index 0000000..12d21a0 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/measurement-panel-prompt.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/measurement-panel-tracked.png b/platform/docs/versioned_docs/version-3.9/assets/img/measurement-panel-tracked.png new file mode 100644 index 0000000..9075869 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/measurement-panel-tracked.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/measurement-temporary.png b/platform/docs/versioned_docs/version-3.9/assets/img/measurement-temporary.png new file mode 100644 index 0000000..9d46fd3 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/measurement-temporary.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/measurements-prevNext.png b/platform/docs/versioned_docs/version-3.9/assets/img/measurements-prevNext.png new file mode 100644 index 0000000..d4bd71b Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/measurements-prevNext.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/memory-profiling-regular.png b/platform/docs/versioned_docs/version-3.9/assets/img/memory-profiling-regular.png new file mode 100644 index 0000000..dd87a34 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/memory-profiling-regular.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/microscopy.webp b/platform/docs/versioned_docs/version-3.9/assets/img/microscopy.webp new file mode 100644 index 0000000..e348e38 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/microscopy.webp differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/migration-modes.png b/platform/docs/versioned_docs/version-3.9/assets/img/migration-modes.png new file mode 100644 index 0000000..2e51605 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/migration-modes.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/migration-split-button.png b/platform/docs/versioned_docs/version-3.9/assets/img/migration-split-button.png new file mode 100644 index 0000000..3bfc15b Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/migration-split-button.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/mode-archs.png b/platform/docs/versioned_docs/version-3.9/assets/img/mode-archs.png new file mode 100644 index 0000000..f931818 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/mode-archs.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/mode-clock.png b/platform/docs/versioned_docs/version-3.9/assets/img/mode-clock.png new file mode 100644 index 0000000..d855edb Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/mode-clock.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/mode-template.png b/platform/docs/versioned_docs/version-3.9/assets/img/mode-template.png new file mode 100644 index 0000000..9442411 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/mode-template.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/nginx-image-archive.png b/platform/docs/versioned_docs/version-3.9/assets/img/nginx-image-archive.png new file mode 100644 index 0000000..f1ac061 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/nginx-image-archive.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/ohif-cli-list.png b/platform/docs/versioned_docs/version-3.9/assets/img/ohif-cli-list.png new file mode 100644 index 0000000..a992f01 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/ohif-cli-list.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/ohif-non-secure-context.png b/platform/docs/versioned_docs/version-3.9/assets/img/ohif-non-secure-context.png new file mode 100644 index 0000000..b4ff6d0 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/ohif-non-secure-context.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/ohif-pacs-keycloak.png b/platform/docs/versioned_docs/version-3.9/assets/img/ohif-pacs-keycloak.png new file mode 100644 index 0000000..e95d2af Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/ohif-pacs-keycloak.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/open-graph.png b/platform/docs/versioned_docs/version-3.9/assets/img/open-graph.png new file mode 100644 index 0000000..5b881ab Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/open-graph.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/overview.png b/platform/docs/versioned_docs/version-3.9/assets/img/overview.png new file mode 100644 index 0000000..d504f5b Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/overview.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/panel-module-left-right.png b/platform/docs/versioned_docs/version-3.9/assets/img/panel-module-left-right.png new file mode 100644 index 0000000..fdbb558 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/panel-module-left-right.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/panel-module-v3.png b/platform/docs/versioned_docs/version-3.9/assets/img/panel-module-v3.png new file mode 100644 index 0000000..83f9e19 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/panel-module-v3.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/panelmodule-icon.png b/platform/docs/versioned_docs/version-3.9/assets/img/panelmodule-icon.png new file mode 100644 index 0000000..b1e4c53 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/panelmodule-icon.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/preferSizeOverAccuracy.png b/platform/docs/versioned_docs/version-3.9/assets/img/preferSizeOverAccuracy.png new file mode 100644 index 0000000..253414c Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/preferSizeOverAccuracy.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/progressDropdown.png b/platform/docs/versioned_docs/version-3.9/assets/img/progressDropdown.png new file mode 100644 index 0000000..263e99a Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/progressDropdown.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/reference-lines-from-start.png b/platform/docs/versioned_docs/version-3.9/assets/img/reference-lines-from-start.png new file mode 100644 index 0000000..6e63ee4 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/reference-lines-from-start.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/restore-exported-sr.png b/platform/docs/versioned_docs/version-3.9/assets/img/restore-exported-sr.png new file mode 100644 index 0000000..7aeca26 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/restore-exported-sr.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/scope-of-project.png b/platform/docs/versioned_docs/version-3.9/assets/img/scope-of-project.png new file mode 100644 index 0000000..6daac8b Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/scope-of-project.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/self-signed-cert-advanced-warning.png b/platform/docs/versioned_docs/version-3.9/assets/img/self-signed-cert-advanced-warning.png new file mode 100644 index 0000000..6f0c98b Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/self-signed-cert-advanced-warning.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/self-signed-cert-warning.png b/platform/docs/versioned_docs/version-3.9/assets/img/self-signed-cert-warning.png new file mode 100644 index 0000000..41df65f Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/self-signed-cert-warning.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/services-data.png b/platform/docs/versioned_docs/version-3.9/assets/img/services-data.png new file mode 100644 index 0000000..e5251ed Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/services-data.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/services-measurements.png b/platform/docs/versioned_docs/version-3.9/assets/img/services-measurements.png new file mode 100644 index 0000000..900419a Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/services-measurements.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/services-ui.png b/platform/docs/versioned_docs/version-3.9/assets/img/services-ui.png new file mode 100644 index 0000000..34c3bf1 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/services-ui.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/services.png b/platform/docs/versioned_docs/version-3.9/assets/img/services.png new file mode 100644 index 0000000..569c046 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/services.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/surge-deploy.gif b/platform/docs/versioned_docs/version-3.9/assets/img/surge-deploy.gif new file mode 100644 index 0000000..545f068 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/surge-deploy.gif differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/template-extension-files.png b/platform/docs/versioned_docs/version-3.9/assets/img/template-extension-files.png new file mode 100644 index 0000000..465c2a9 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/template-extension-files.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/template-mode-files.png b/platform/docs/versioned_docs/version-3.9/assets/img/template-mode-files.png new file mode 100644 index 0000000..0c44ca9 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/template-mode-files.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/template-mode-ui.png b/platform/docs/versioned_docs/version-3.9/assets/img/template-mode-ui.png new file mode 100644 index 0000000..c63d172 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/template-mode-ui.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/toolbar-module.png b/platform/docs/versioned_docs/version-3.9/assets/img/toolbar-module.png new file mode 100644 index 0000000..5753669 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/toolbar-module.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/toolbarModule-layout.png b/platform/docs/versioned_docs/version-3.9/assets/img/toolbarModule-layout.png new file mode 100644 index 0000000..8190b5f Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/toolbarModule-layout.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/toolbarModule-nested-buttons.png b/platform/docs/versioned_docs/version-3.9/assets/img/toolbarModule-nested-buttons.png new file mode 100644 index 0000000..1a85837 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/toolbarModule-nested-buttons.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/toolbarModule-zoom.png b/platform/docs/versioned_docs/version-3.9/assets/img/toolbarModule-zoom.png new file mode 100644 index 0000000..00acfca Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/toolbarModule-zoom.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/toolbox-modal.png b/platform/docs/versioned_docs/version-3.9/assets/img/toolbox-modal.png new file mode 100644 index 0000000..ce97f73 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/toolbox-modal.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/tracked-not-tracked.png b/platform/docs/versioned_docs/version-3.9/assets/img/tracked-not-tracked.png new file mode 100644 index 0000000..d61b36d Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/tracked-not-tracked.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/tracking-workflow1.png b/platform/docs/versioned_docs/version-3.9/assets/img/tracking-workflow1.png new file mode 100644 index 0000000..d5b5959 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/tracking-workflow1.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/tracking-workflow2.png b/platform/docs/versioned_docs/version-3.9/assets/img/tracking-workflow2.png new file mode 100644 index 0000000..988e56d Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/tracking-workflow2.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/tracking-workflow3.png b/platform/docs/versioned_docs/version-3.9/assets/img/tracking-workflow3.png new file mode 100644 index 0000000..c62fde7 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/tracking-workflow3.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/ui-modal.gif b/platform/docs/versioned_docs/version-3.9/assets/img/ui-modal.gif new file mode 100644 index 0000000..599964e Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/ui-modal.gif differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/ui-services.png b/platform/docs/versioned_docs/version-3.9/assets/img/ui-services.png new file mode 100644 index 0000000..dd53063 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/ui-services.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/uploader.gif b/platform/docs/versioned_docs/version-3.9/assets/img/uploader.gif new file mode 100644 index 0000000..69aff80 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/uploader.gif differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-access-control-request-flow.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-access-control-request-flow.png new file mode 100644 index 0000000..573c835 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-access-control-request-flow.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-hotkeys-default.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-hotkeys-default.png new file mode 100644 index 0000000..7621c4f Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-hotkeys-default.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-hotkeys.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-hotkeys.png new file mode 100644 index 0000000..389aa31 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-hotkeys.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-measurement-export.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-measurement-export.png new file mode 100644 index 0000000..1ce6174 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-measurement-export.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-open-viewer.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-open-viewer.png new file mode 100644 index 0000000..5e2b29c Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-open-viewer.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-study-filter.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-study-filter.png new file mode 100644 index 0000000..05d0c4b Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-study-filter.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-study-list.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-study-list.png new file mode 100644 index 0000000..4d58959 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-study-list.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-study-next.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-study-next.png new file mode 100644 index 0000000..b082eed Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-study-next.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-study-panel.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-study-panel.png new file mode 100644 index 0000000..42db7c2 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-study-panel.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-study-summary.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-study-summary.png new file mode 100644 index 0000000..e5a5aad Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-study-summary.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-studyist-modespecific.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-studyist-modespecific.png new file mode 100644 index 0000000..bf878bc Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-studyist-modespecific.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-toolbar-download-icon.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-toolbar-download-icon.png new file mode 100644 index 0000000..ca7eef5 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-toolbar-download-icon.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-toolbar-extra.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-toolbar-extra.png new file mode 100644 index 0000000..15632fd Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-toolbar-extra.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-toolbar-preset.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-toolbar-preset.png new file mode 100644 index 0000000..5a24014 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-toolbar-preset.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-toolbarDownload.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-toolbarDownload.png new file mode 100644 index 0000000..d39c9cb Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-toolbarDownload.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-viewer-layout.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-viewer-layout.png new file mode 100644 index 0000000..7111088 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-viewer-layout.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-viewer-main.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-viewer-main.png new file mode 100644 index 0000000..1ef3e76 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-viewer-main.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-viewer-toolbar-measurements.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-viewer-toolbar-measurements.png new file mode 100644 index 0000000..2cb7fd7 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-viewer-toolbar-measurements.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-viewer-toolbar.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-viewer-toolbar.png new file mode 100644 index 0000000..fc36c58 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-viewer-toolbar.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/user-viewer.png b/platform/docs/versioned_docs/version-3.9/assets/img/user-viewer.png new file mode 100644 index 0000000..c79dce1 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/user-viewer.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/viewportModule-layout.png b/platform/docs/versioned_docs/version-3.9/assets/img/viewportModule-layout.png new file mode 100644 index 0000000..58af0ad Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/viewportModule-layout.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/viewportModule.png b/platform/docs/versioned_docs/version-3.9/assets/img/viewportModule.png new file mode 100644 index 0000000..0542337 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/viewportModule.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/webgl-int16.png b/platform/docs/versioned_docs/version-3.9/assets/img/webgl-int16.png new file mode 100644 index 0000000..1a9abd8 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/webgl-int16.png differ diff --git a/platform/docs/versioned_docs/version-3.9/assets/img/webgl-report-norm16.png b/platform/docs/versioned_docs/version-3.9/assets/img/webgl-report-norm16.png new file mode 100644 index 0000000..766938b Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/assets/img/webgl-report-norm16.png differ diff --git a/platform/docs/versioned_docs/version-3.9/configuration/_category_.json b/platform/docs/versioned_docs/version-3.9/configuration/_category_.json new file mode 100644 index 0000000..0aea174 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/configuration/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Configuration", + "position": 4 +} diff --git a/platform/docs/versioned_docs/version-3.9/configuration/configurationFiles.md b/platform/docs/versioned_docs/version-3.9/configuration/configurationFiles.md new file mode 100644 index 0000000..fc0c14e --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/configuration/configurationFiles.md @@ -0,0 +1,403 @@ +--- +sidebar_position: 1 +sidebar_label: Configuration Files +--- + +# Config files + +After following the steps outlined in +[Getting Started](./../development/getting-started.md), you'll notice that the +OHIF Viewer has data for several studies and their images. You didn't add this +data, so where is it coming from? + +By default, the viewer is configured to connect to a Amazon S3 bucket that is hosting +a Static WADO server (see [Static WADO DICOMWeb](https://github.com/RadicalImaging/static-dicomweb)). +By default we use `default.js` for the configuration file. You can change this by setting the `APP_CONFIG` environment variable +and select other options such as `config/local_orthanc.js` or `config/google.js`. + + +## Configuration Files + +The configuration for our viewer is in the `platform/app/public/config` +directory. Our build process knows which configuration file to use based on the +`APP_CONFIG` environment variable. By default, its value is +[`config/default.js`][default-config]. The majority of the viewer's features, +and registered extension's features, are configured using this file. + +The simplest way is to update the existing default config: + +```js title="platform/app/public/config/default.js" +window.config = { + routerBasename: '/', + extensions: [], + modes: [], + showStudyList: true, + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'dcmjs DICOMWeb Server', + name: 'DCM4CHEE', + wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado', + qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + qidoSupportsIncludeField: true, + supportsReject: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: true, + omitQuotationForMultipartRequest: true, + }, + }, + ], + defaultDataSourceName: 'dicomweb', +}; +``` + +> As you can see a new change in `OHIF-v3` is the addition of `dataSources`. You +> can build your own datasource and map it to the internal data structure of +> OHIFโ€™s > metadata and enjoy using other peoples developed mode on your own +> data! +> +> You can read more about data sources at +> [Data Source section in Modes](../platform/modes/index.md) + +The configuration can also be written as a JS Function in case you need to +inject dependencies like external services: + +```js +window.config = ({ servicesManager } = {}) => { + const { UIDialogService } = servicesManager.services; + return { + cornerstoneExtensionConfig: { + tools: { + ArrowAnnotate: { + configuration: { + getTextCallback: (callback, eventDetails) => UIDialogService.create({... + } + } + }, + }, + routerBasename: '/', + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'dcmjs DICOMWeb Server', + name: 'DCM4CHEE', + wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado', + qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + qidoSupportsIncludeField: true, + supportsReject: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: true, + omitQuotationForMultipartRequest: true, + }, + }, + ], + defaultDataSourceName: 'dicomweb', + }; +}; +``` + + + + + +## Configuration Options + + +Here are a list of some options available: +- `disableEditing`: If true, it disables editing in OHIF, hiding edit buttons in segmentation + panel and locking already stored measurements. +- `maxNumberOfWebWorkers`: The maximum number of web workers to use for + decoding. Defaults to minimum of `navigator.hardwareConcurrency` and + what is specified by `maxNumberOfWebWorkers`. Some windows machines require smaller values. +- `acceptHeader` : accept header to request specific dicom transfer syntax ex : [ 'multipart/related; type=image/jls; q=1', 'multipart/related; type=application/octet-stream; q=0.1' ] +- `investigationalUseDialog`: This should contain an object with `option` value, it can be either `always` which always shows the dialog once per session, `never` which never shows the dialog, or `configure` which shows the dialog once and won't show it again until a set number of days defined by the user, if it's set to configure, you are required to add an additional property `days` which is the number of days to wait before showing the dialog again. +- `groupEnabledModesFirst`: boolean, if set to true, all valid modes for the study get grouped together first, then the rest of the modes. If false, all modes are shown in the order they are defined in the configuration. +- `experimentalStudyBrowserSort`: boolean, if set to true, you will get the experimental StudyBrowserSort component in the UI, which displays a list of sort functions that the displaySets can be sorted by, the sort reflects in all part of the app including the thumbnail/study panel. These sort functions are defined in the customizationModule and can be expanded by users. +- `disableConfirmationPrompts`: boolean, if set to true, it skips confirmation prompts for measurement tracking and hydration. +- `showPatientInfo`: string, if set to 'visible', the patient info header will be shown and its initial state is expanded. If set to 'visibleCollapsed', the patient info header will be shown but it's initial state is collapsed. If set to 'disabled', the patient info header will never be shown, and if set to 'visibleReadOnly', the patient info header will be shown and always expanded. +- `requestTransferSyntaxUID` : Request a specific Transfer syntax from dicom web server ex: 1.2.840.10008.1.2.4.80 (applied only if acceptHeader is not set) +- `omitQuotationForMultipartRequest`: Some servers (e.g., .NET) require the `multipart/related` request to be sent without quotation marks. Defaults to `false`. If your server doesn't require this, then setting this flag to `true` might improve performance (by removing the need for preflight requests). Also note that +if auth headers are used, a preflight request is required. +- `maxNumRequests`: The maximum number of requests to allow in parallel. It is an object with keys of `interaction`, `thumbnail`, and `prefetch`. You can specify a specific number for each type. +- `modesConfiguration`: Allows overriding modes configuration. + - Example config: + ```js + modesConfiguration: { + '@ohif/mode-longitudinal': { + displayName: 'Custom Name', + routeName: 'customRouteName', + routes: [ + { + path: 'customPath', + layoutTemplate: () => { + /** Custom Layout */ + return { + id: ohif.layout, + props: { + leftPanels: [tracked.thumbnailList], + rightPanels: [dicomSeg.panel, tracked.measurements], + rightPanelClosed: true, + viewports: [ + { + namespace: tracked.viewport, + displaySetsToDisplay: [ohif.sopClassHandler], + }, + ], + }, + }; + }, + }, + ], + } + }, + ``` + Note: Although the mode configuration is passed to the mode factory function, it is up to the particular mode itself if its going to use it to allow overwriting its original configuration e.g. + ```js + function modeFactory({ modeConfiguration }) { + return { + id, + routeName: 'viewer', + displayName: 'Basic Viewer', + ... + onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => { + ... + }, + /** + * This mode allows its configuration to be overwritten by + * destructuring the modeConfiguration value from the mode fatory function + * at the end of the mode configuration definition. + */ + ...modeConfiguration, + }; + } + ``` +- `showLoadingIndicator`: (default to true), if set to false, the loading indicator will not be shown when navigating between studies. +- `useNorm16Texture`: (default to false), if set to true, it will use 16 bit data type for the image data wherever possible which has + significant impact on reducing the memory usage. However, the 16Bit textures require EXT_texture_norm16 extension in webGL 2.0 (you can check if you have it here https://webglreport.com/?v=2). In addition to the extension, there are reported problems for Intel Macs that might cause the viewer to crash. In summary, it is great a configuration if you have support for it. +- `useSharedArrayBuffer` (default to 'TRUE', options: 'AUTO', 'FALSE', 'TRUE', note that these are strings), for volume loading we use sharedArrayBuffer to be able to + load the volume progressively as the data arrives (each webworker has the shared buffer and can write to it). However, there might be certain environments that do not support sharedArrayBuffer. In that case, you can set this flag to false and the viewer will use the regular arrayBuffer which might be slower for large volume loading. +- `supportsWildcard`: (default to false), if set to true, the datasource will support wildcard matching for patient name and patient id. +- `allowMultiSelectExport`: (default to false), if set to true, the user will be able to select the datasource to export the report to. +- `activateViewportBeforeInteraction`: (default to true), if set to false, tools can be used directly without the need to click and activate the viewport. +- `autoPlayCine`: (default to false), if set to true, data sets with the DICOM frame time tag (i.e. (0018,1063)) will auto play when displayed +- `addWindowLevelActionMenu`: (default to true), if set to false, the window level action menu item is NOT added to the viewport action corners +- `dangerouslyUseDynamicConfig`: Dynamic config allows user to pass `configUrl` query string. This allows to load config without recompiling application. If the `configUrl` query string is passed, the worklist and modes will load from the referenced json rather than the default .env config. If there is no `configUrl` path provided, the default behaviour is used and there should not be any deviation from current user experience.
+Points to consider while using `dangerouslyUseDynamicConfig`:
+ - User have to enable this feature by setting `dangerouslyUseDynamicConfig.enabled:true`. By default it is `false`. + - Regex helps to avoid easy exploit. Default is `/.*/`. Setup your own regex to choose a specific source of configuration only. + - System administrators can return `cross-origin: same-origin` with OHIF files to disallow any loading from other origin. It will block read access to resources loaded from a different origin to avoid potential attack vector. + - Example config: + ```js + dangerouslyUseDynamicConfig: { + enabled: false, + regex: /.*/ + } + ``` + > Example 1, to allow numbers and letters in an absolute or sub-path only.
+`regex: /(0-9A-Za-z.]+)(\/[0-9A-Za-z.]+)*/`
+Example 2, to restricts to either hosptial.com or othersite.com.
+`regex: /(https:\/\/hospital.com(\/[0-9A-Za-z.]+)*)|(https:\/\/othersite.com(\/[0-9A-Za-z.]+)*)/`
+Example usage:
+`http://localhost:3000/?configUrl=http://localhost:3000/config/example.json`
+- `onConfiguration`: Currently only available for DicomWebDataSource, this option allows the interception of the data source configuration for dynamic values e.g. values coming from url params or query params. Here is an example of building the dicomweb datasource configuration object with values that are based on the route url params: + ``` + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'gcpdicomweb', + configuration: { + friendlyName: 'GCP DICOMWeb Server', + name: 'gcpdicomweb', + qidoSupportsIncludeField: false, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: false, + supportsWildcard: false, + singlepart: 'bulkdata,video,pdf', + onConfiguration: (dicomWebConfig, options) => { + const { params } = options; + const { project, location, dataset, dicomStore } = params; + const pathUrl = `https://healthcare.googleapis.com/v1/projects/${project}/locations/${location}/datasets/${dataset}/dicomStores/${dicomStore}/dicomWeb`; + return { + ...dicomWebConfig, + wadoRoot: pathUrl, + qidoRoot: pathUrl, + wadoUri: pathUrl, + wadoUriRoot: pathUrl, + }; + }, + }, + }, + ``` +This configuration would allow the user to build a dicomweb configuration from a GCP healthcare api path e.g. http://localhost:3000/projects/your-gcp-project/locations/us-central1/datasets/your-dataset/dicomStores/your-dicom-store/study/1.3.6.1.4.1.1234.5.2.1.1234.1234.123123123123123123123123123123 + + +:::note +You can stack multiple panel components on top of each other by providing an array of panel components in the `rightPanels` or `leftPanels` properties. + +For instance we can use + +``` +rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.measurements]] +``` + +This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other. + +::: + +### Study Prefetcher + +You can enable the study prefetcher so that OHIF loads the next/previous series/display sets +based on the proximity to the current series/display set. This can be useful to improve the user experience + + +```js + studyPrefetcher: { + /* Enable/disable study prefetching service (default: false) */ + enabled: true, + /* Number of displaysets to be prefetched (default: 2)*/ + displaySetCount: 2, + /** + * Max number of concurrent prefetch requests (default: 10) + * High numbers may impact on the time to load a new dropped series because + * the browser will be busy with all prefetching requests. As soon as the + * prefetch requests get fulfilled the new ones from the new dropped series + * are sent to the server. + * + * TODO: abort all prefetch requests when a new series is loaded on a viewport. + * (need to add support for `AbortController` on Cornerstone) + * */ + maxNumPrefetchRequests: 10, + /* Display sets loading order (closest (deafult), downward or upward) */ + order: 'closest', + }, + +``` + +### More on Accept Header Configuration +In the previous section we showed that you can modify the `acceptHeader` +configuration to request specific dicom transfer syntax. By default +we use `acceptHeader: ['multipart/related; type=application/octet-stream; transfer-syntax=*']` for the following +reasons: + +- **Ensures Optimal Transfer Syntax**: By allowing the server to select the transfer syntax, + the client is more likely to receive the image in a syntax that's well-suited for fast transmission + and rendering. This might be the original syntax the image was stored in or another syntax that the server deems efficient. + +- **Avoids Transcoding**: Transcoding (converting from one transfer syntax to another) can be a resource-intensive process. + Since the OHIF Viewer supports all transfer syntaxes, it is fine to accept any transfer syntax (transfer-syntax=*). + This allows the server to send the images in their stored syntax, avoiding the need for costly on-the-fly conversions. + This approach not only saves server resources but also reduces response times by leveraging the viewer's capability to handle various syntaxes directly. + +- **Faster Data Transfer**: Compressed transfer syntaxes generally result in smaller file sizes compared + to uncompressed ones. Smaller files transmit faster over the network, leading to quicker load + times for the end-user. By accepting any syntax, the client can take advantage of compression when available. + +However, if you would like to get compressed data in a specific transfer syntax, you can modify the `acceptHeader` configuration or +`requestTransferSyntaxUID` configuration. + +## Default Initial Query +The default initial query for the worklist can be set as a session key in the configuration file. +For example, the following configuration automatically searches for patient names containing `Test`. + +```javascript +if (!window.location.search) { + window.sessionStorage.setItem( + 'queryFilterValues', + JSON.stringify({ + patientName: 'Test', + }) + ); +} +``` + +### Query For Studies From Today +Querying for a computed date range can be done by computing the date range +in the config file, see e2e.js for an example, reproduced below. To use this, +enter the query criteria '?today' as the full search criteria. + +```javascript +if (window.location.search === '?today') { + const now = new Date(); + const month = now.getMonth() + 1; + const day = now.getDate(); + window.sessionStorage.setItem( + 'queryFilterValues', + JSON.stringify({ + studyDate: { + startDate: `${now.getFullYear()}${month < 10 ? '0' + month : month}${day < 10 ? '0' + day : day}`, + endDate: null, + }, + }) + ); +} +``` + + +## Environment Variables + +We use environment variables at build and dev time to change the Viewer's +behavior. We can update the `HTML_TEMPLATE` to easily change which extensions +are registered, and specify a different `APP_CONFIG` to connect to an +alternative data source (or even specify different default hotkeys). + +| Environment Variable | Description | Default | +| -------------------- | -------------------------------------------------------------------------------------------------- | ------------------- | +| `HTML_TEMPLATE` | Which [HTML template][html-templates] to use as our web app's entry point. Specific to PWA builds. | `index.html` | +| `PUBLIC_URL` | The route relative to the host that the app will be served from. Specific to PWA builds. | `/` | +| `APP_CONFIG` | Which [configuration file][config-file] to copy to output as `app-config.js` | `config/default.js` | +| `PROXY_TARGET` | When developing, proxy requests that match this pattern to `PROXY_DOMAIN` | `undefined` | +| `PROXY_DOMAIN` | When developing, proxy requests from `PROXY_TARGET` to `PROXY_DOMAIN` | `undefined` | +| `OHIF_PORT` | The port to run the webpack server on for PWA builds. | `3000` | + +You can also create a new config file and specify its path relative to the build +output's root by setting the `APP_CONFIG` environment variable. You can set the +value of this environment variable a few different ways: + +- ~[Add a temporary environment variable in your shell](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-temporary-environment-variables-in-your-shell)~ + - Previous `react-scripts` functionality that we need to duplicate with + `dotenv-webpack` +- ~[Add environment specific variables in `.env` file(s)](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-development-environment-variables-in-env)~ + - Previous `react-scripts` functionality that we need to duplicate with + `dotenv-webpack` +- Using the `cross-env` package in a npm script: + - `"build": "cross-env APP_CONFIG=config/my-config.js react-scripts build"` + +After updating the configuration, `yarn run build` to generate updated build +output. + + + + +[dcmjs-org]: https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado +[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb +[storescu]: https://support.dcmtk.org/docs/storescu.html +[webpack-proxy]: https://webpack.js.org/configuration/dev-server/#devserverproxy +[orthanc-docker-compose]: https://github.com/OHIF/Viewers/tree/master/platform/app/.recipes/Nginx-Orthanc + +[dcm4chee]: https://github.com/dcm4che/dcm4chee-arc-light +[dcm4chee-docker]: https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker +[orthanc]: https://www.orthanc-server.com/ +[orthanc-docker]: https://book.orthanc-server.com/users/docker.html +[dicomcloud]: https://github.com/DICOMcloud/DICOMcloud +[dicomcloud-install]: https://github.com/DICOMcloud/DICOMcloud#running-the-code +[osirix]: https://www.osirix-viewer.com/ +[horos]: https://www.horosproject.org/ +[default-config]: https://github.com/OHIF/Viewers/blob/master/platform/app/public/config/default.js +[html-templates]: https://github.com/OHIF/Viewers/tree/master/platform/app/public/html-templates +[config-files]: https://github.com/OHIF/Viewers/tree/master/platform/app/public/config + diff --git a/platform/docs/versioned_docs/version-3.9/configuration/dataSources/_category_.json b/platform/docs/versioned_docs/version-3.9/configuration/dataSources/_category_.json new file mode 100644 index 0000000..fe1e3e8 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/configuration/dataSources/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Data Sources", + "position": 2 +} diff --git a/platform/docs/versioned_docs/version-3.9/configuration/dataSources/configuration-ui.md b/platform/docs/versioned_docs/version-3.9/configuration/dataSources/configuration-ui.md new file mode 100644 index 0000000..1ff70f4 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/configuration/dataSources/configuration-ui.md @@ -0,0 +1,177 @@ +--- +sidebar_position: 6 +sidebar_label: Configuration UI +--- + +# Configuration UI + +OHIF provides for a generic mechanism for configuring a data source. This is +most useful for those organizations with several data sources +that share common (path) hierarchies. For example, an organization may have several DICOM stores +in the Google Cloud Healthcare realm where each is organized into various projects, +location, data sets and DICOM stores. + +By implementing the `BaseDataSourceConfigurationAPI` and +`BaseDataSourceConfigurationAPIItem` in an [OHIF extension](../../platform/extensions/index.md), a data source can +be made configurable via the generic UI as is depicted below for a +Google Cloud Healthcare data source. + +![Data source configuration UI](../../assets/img/data-source-configuration-ui.png) + +:::tip +A datasource root URI can be [fully or partially specified](../../deployment/google-cloud-healthcare.md#configuring-google-cloud-healthcare-as-a-datasource-in-ohif) +in the OHIF configuration file. +::: + +## `BaseDataSourceConfigurationAPIItem` interface + +Each (path) item of a data source is represented by an instance of this interface. +At the very least each of these items must expose two properties: + +|Property |Description| +|---------|-----------| +|id|a string that uniquely identifies the item| +|name|a human readable name for the item| + +Note that information such as where in the path hierarchy the item exists +has been omitted, but can be added in any concrete class that might implement this +interface. For example, the the Google Cloud Healthcare implementation of this +interface (`GoogleCloudDataSourceConfigurationAPIItem`) adds an `itemType` +(i.e. projects, locations, datasets, or dicomStores) and `url`. + +## `BaseDataSourceConfigurationAPI` interface + +The implementation of this interface is at the heart of the configuration process. +It possesses several methods for building up a data source path based on various +`BaseDataSourceConfigurationAPIItem` objects that are set via calls to the `setCurrentItem` +method. + +The constructor for the concrete class implementation should accept whatever +parameters are necessary for configuring the data source. One argument +to the constructor must be the string identifying the name of the data source +to be configured. Furthermore, considering that the `ExtensionManager` possesses +API to configure and update data sources, it too will likely be an argument to +the constructor. See [Creation via Customization Module](#creation-via-customization-module) +for more information on how the constructor is invoked via a factory method. + +For an example implementation of this interface see `GoogleCloudDataSourceConfigurationAPI`. + +### Interface Methods + +Each of the following subsections lists a method of the interface with a description +detailing what the method should do. + +#### `getItemLabels` + +Gets the i18n labels (i.e. the i18n lookup keys) for each of the configurable items +of the data source configuration API. For example, for the Google Cloud Healthcare +API, this would be `['Project', 'Location', 'Data set', 'DICOM store']`. + +Besides the configurable item labels themselves, several other string look ups +are used base on EACH of the labels returned by this method. +For instance, for the label `{itemLabel}`, the following strings are fetched for +translation... +1. No `{itemLabel}` available + - used to indicate no such items are available + - for example, for Google, No Project available would be 'No projects available' +2. Select `{itemLabel}` + - used to direct selection of the item + - for example, for Google, Select Project would be 'Select a project' +3. Error fetching `{itemLabel}` list + - used to indicate an error occurred fetching the list of items + - usually accompanied by the error itself + - for example, for Google, Error fetching Project list would be 'Error fetching projects' +4. Search `{itemLabel}` list + - used as the placeholder text for filtering a list of items + - for example, for Google, Search Project list would be 'Search projects' + +#### `initialize` + +Initializes the cloud server API and returns the top-level sub-items +that can be chosen to begin the process of configuring a data source. +For example, for the Google Cloud Healthcare API, this would perform the initial request +to fetch the top level projects for the logged in user account. + +#### `setCurrentItem` + +Sets the current path item that is passed as an argument to the method and +returns the sub-items of that item +that can be further chosen to configure a data source. +When setting the last configurable item of the data source (path), this method +returns an empty list AND configures the active data source with the selected +items path. + +For example, for the Google Cloud Healthcare API, this would take the current item +(say a data set) and queries and returns its sub-items (i.e. all of the DICOM stores +contained in that data set). Furthermore, whenever the item to set is a DICOM store, +the Google Cloud Healthcare API implementation would update the OHIF data source +associated with this instance to point to that DICOM store. + +#### `getConfiguredItems` + +Gets the list of items currently configured for the data source associated with +this API instance. The resultant array must be the same length as the result of +`getItemLabels`. Furthermore the items returned should correspond (index-wise) +with the labels returned from `getItemLabels`. + +## Creation via Customization Module + +The generic UI (i.e. `DataSourceConfigurationComponent`) uses the +[OHIF UI customization service](../../platform/services/ui/customization-service.md) to +instantiate the `BaseDataSourceConfigurationAPI` instance to configure a data source. + +A UI configurable data source should have a `configurationAPI` field as part of +its `configuration` in the OHIF config file. The `configurationAPI` value is the +customization id of the customization module that provides the factory method +to instantiate the `BaseDataSourceConfigurationAPI` instance. + +For example, the following is a snippet of a Google Cloud Healthcare data source configuration. + +```js + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'google-dicomweb', + configuration: { + name: 'GCP', + wadoUriRoot: 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/...', + ... + configurationAPI: 'ohif.dataSourceConfigurationAPI.google', + ... + }, + }, + ] +``` + +This suggests that the factory method is provided by the `'ohif.dataSourceConfigurationAPI.google'` +customization module. That customization module is provided by the `default` extension's +`getCustomizationModule` and looks something like the following snippet of code. Notice that +the factory method's name MUST be `factory` and accept one argument - the data source name. +Furthermore note how the constructor is invoked with anything required by the concrete configuration +API class. + +```js +export default function getCustomizationModule({ + servicesManager, + extensionManager, +}) { + return [ + { + name: 'default', + value: [ + { + // The factory for creating an instance of a BaseDataSourceConfigurationAPI for Google Cloud Healthcare + id: 'ohif.dataSourceConfigurationAPI.google', + factory: (dataSourceName: string) => + new GoogleCloudDataSourceConfigurationAPI( + dataSourceName, + servicesManager, + extensionManager + ), + }, + ], + }, + ]; +} + +``` diff --git a/platform/docs/versioned_docs/version-3.9/configuration/dataSources/dicom-json.md b/platform/docs/versioned_docs/version-3.9/configuration/dataSources/dicom-json.md new file mode 100644 index 0000000..84e7be2 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/configuration/dataSources/dicom-json.md @@ -0,0 +1,194 @@ +--- +sidebar_position: 3 +sidebar_label: DICOM JSON +--- + +# DICOM JSON + +You can launch the OHIF Viewer with a JSON file which points to a DICOMWeb +server as well as a list of study and series instance UIDs along with metadata. + +An example would look like + +`https://viewer.ohif.org/viewer/dicomjson?url=https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001.json` + +As you can see the url to the location of the JSON file is passed in the query +after the `dicomjson` string, which is +`https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001.json` (this +json file has been generated by OHIF team and stored in an amazon s3 bucket for +the purpose of the guide). + +## DICOM JSON sample + +Here we are using the LIDC-IDRI-0001 case which is a sample of the LIDC-IDRI +dataset. Let's have a look at the JSON file: + +### Metadata + +JSON file stores the metadata for the study level, series level and instance +level. A JSON launch file should follow the same structure as the one below. + +:::tip +You can use our script to generate the JSON file from a hosted endpoint. See +`.scripts/dicom-json-generator.js` + +You could run it like this: + +```bash +node .scripts/dicom-json-generator.js '/path/to/study/folder' 'url/to/dicom/server/folder' 'json/output/file.json' +``` + +Some modalities require additional metadata to be added to the JSON file. You can read more about the minimum amount of metadata required for the viewer to work [here](../../faq/technical#what-are-the-list-of-required-metadata-for-the-ohif-viewer-to-work). We will handle this in the script. For example, the script will add the CodeSequences for SR in order to display the measurements in the viewer. +::: + + +Note that at the instance level metadata we are storing both the `metadata` and +also the `url` for the dicom file on the dicom server. In this case we are +referring to +`dicomweb:https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm` +which is stored in another directory in our s3. (You can actually try +downloading the dicom file by opening the url in your browser). + +The URL to the script in the given example is `https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178`. This URL serves as the parent directory that contains all the series within their respective folders. + +```json +{ + "studies": [ + // first study metadata + { + "StudyInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.298806137288633453246975630178", + "StudyDate": "20000101", + "StudyTime": "", + "PatientName": "", + "PatientID": "LIDC-IDRI-0001", + "AccessionNumber": "", + "PatientAge": "", + "PatientSex": "", + "series": [ + // first series metadata + { + "SeriesInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.179049373636438705059720603192", + "SeriesNumber": 3000566, + "Modality": "CT", + "SliceThickness": 2.5, + "instances": [ + // first instance metadata + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 1, + "SOPClassUID": "1.2.840.10008.5.1.4.1.1.2", + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.703125, 0.703125], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 1, 0], + "ImagePositionPatient": [-166, -171.699997, -10], + "FrameOfReferenceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.229925374658226729607867499499", + "ImageType": ["ORIGINAL", "PRIMARY", "AXIAL"], + "Modality": "CT", + "SOPInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.262721256650280657946440242654", + "SeriesInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.179049373636438705059720603192", + "StudyInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.298806137288633453246975630178", + "WindowCenter": -600, + "WindowWidth": 1600, + "SeriesDate": "20000101" + }, + "url": "dicomweb:https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm" + }, + // second instance metadata + { + "metadata": { + "Columns": 512, + "Rows": 512, + "InstanceNumber": 2, + "SOPClassUID": "1.2.840.10008.5.1.4.1.1.2", + "PhotometricInterpretation": "MONOCHROME2", + "BitsAllocated": 16, + "BitsStored": 16, + "PixelRepresentation": 1, + "SamplesPerPixel": 1, + "PixelSpacing": [0.703125, 0.703125], + "HighBit": 15, + "ImageOrientationPatient": [1, 0, 0, 0, 1, 0], + "ImagePositionPatient": [-166, -171.699997, -12.5], + "FrameOfReferenceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.229925374658226729607867499499", + "ImageType": ["ORIGINAL", "PRIMARY", "AXIAL"], + "Modality": "CT", + "SOPInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.512235483218154065970649917292", + "SeriesInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.179049373636438705059720603192", + "StudyInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.298806137288633453246975630178", + "WindowCenter": -600, + "WindowWidth": 1600, + "SeriesDate": "20000101" + }, + "url": "dicomweb:https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-002.dcm" + } + // ..... other instances metadata + ] + } + // ... other series metadata + ], + "NumInstances": 133, + "Modalities": "CT" + } + // second study metadata + ] +} +``` + +![](../../assets/img/dicom-json.png) + +### Local Demo + +You can run OHIF with a JSON data source against you local datasets (given that +their JSON metadata is extracted). + +First you need to put the JSON file and the folder containing the dicom files +inside your `public` folder. Since files are served from your local server the +`url` for the JSON file will be `http://localhost:3000/LIDC-IDRI-0001.json` and +the dicom files will be +`dicomweb:http://localhost:3000/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm`. + +After `yarn install` and running `yarn dev` and opening the browser at +`http://localhost:3000/viewer/dicomjson?url=http://localhost:3000/LIDC-IDRI-0001.json` +will display the viewer. + +Download JSON file from +[here](https://www.dropbox.com/sh/zvkv6mrhpdze67x/AADLGK46WuforD2LopP99gFXa?dl=0) + +Sample DICOM files can be downloaded from +[TCIA](https://wiki.cancerimagingarchive.net/display/Public/LIDC-IDRI) or +directly from +[here](https://www.dropbox.com/sh/zvkv6mrhpdze67x/AADLGK46WuforD2LopP99gFXa?dl=0) + +Your public folder should look like this: + +![](../../assets/img/dicom-json-public.png) + +:::tip +It is important to URL encode the `url` query parameter especially if the `url` +parameter itself also contains query parameters. So for example, + +`http://localhost:3000/viewer/dicomjson?url=http://localhost:3000/LIDC-IDRI-0001.json?key0=val0&key1=val1` + +should be... + +`http://localhost:3000/viewer/dicomjson?url=http://localhost:3000/LIDC-IDRI-0001.json?key0=val0%26key1=val1` + +Notice the ampersand (`&`) is encoded as `%26`. +::: + +:::note +When hosting the DICOM JSON files, it is important to be aware that certain providers +do not automatically handle the 404 error and fallback to index.html. For example, Netlify +handles this, but Azure does not. Consequently, when you attempt to access a link with a +specific URL, a 404 error will be displayed. + +This issue also occurs locally, where the http-server does not handle it. However, +if you utilize the `serve` package (npx serve ./dist -c ../public/serve.json), it effectively addresses this problem. +::: diff --git a/platform/docs/versioned_docs/version-3.9/configuration/dataSources/dicom-web-proxy.md b/platform/docs/versioned_docs/version-3.9/configuration/dataSources/dicom-web-proxy.md new file mode 100644 index 0000000..faeea85 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/configuration/dataSources/dicom-web-proxy.md @@ -0,0 +1,54 @@ +--- +sidebar_position: 4 +sidebar_label: DICOMweb Proxy +--- + +# DICOMweb Proxy + +You can launch the OHIF Viewer with a url that returns a JSON file which +contains a DICOMWeb configuration. The DICOMweb Proxy constructs a DICOMweb +datasource and delegates subsequent requests for metadata and images to that. + +Usage is similar to that of the [DICOM JSON](./dicom-json.md) datasource and +might look like + +`https://viewer.ohif.org/viewer/dicomwebproxy?url=https://ohif-dicom-json-example.s3.amazonaws.com/dicomweb.json` + +The url to the location of the JSON file is passed in the query +after the `dicomwebproxy` string, which is +`https://ohif-dicom-json-example.s3.amazonaws.com/dicomweb.json` (this json file +does not exist at the moment of this writing). + +## DICOMweb JSON configuration sample + +The json returned by the url in this example contains a dicomweb configuration +(see [DICOMweb](dicom-web.md)), in a "servers" object, which is then used to +construct a dynamic DICOMweb datasource to delegate requests to. Here is an +example configuration that might be returned using the url parameter. + +```json +{ + "servers": { + "dicomWeb": [ + { + "name": "DCM4CHEE", + "wadoUriRoot": "https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado", + "qidoRoot": "https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs", + "wadoRoot": "https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs", + "qidoSupportsIncludeField": true, + "supportsReject": true, + "imageRendering": "wadors", + "thumbnailRendering": "wadors", + "enableStudyLazyLoad": true, + "supportsFuzzyMatching": true, + "supportsWildcard": true + } + ] + } +} +``` + +The DICOMweb Proxy expects the json returned by the url parameter it is invoked +with to include a servers object which contains a "dicomWeb" configuration array +as above. It will only consider the first array item in the dicomWeb +configuration. diff --git a/platform/docs/versioned_docs/version-3.9/configuration/dataSources/dicom-web.md b/platform/docs/versioned_docs/version-3.9/configuration/dataSources/dicom-web.md new file mode 100644 index 0000000..79d9e3d --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/configuration/dataSources/dicom-web.md @@ -0,0 +1,247 @@ +--- +sidebar_position: 2 +sidebar_label: DICOMweb +--- + +# DICOMweb + +## Set up a local DICOM server + +ATTENTION! Already have a remote or local server? Skip to the +[configuration section](#configuration-learn-more) below. + +While the OHIF Viewer can work with any data source, the easiest to configure +are the ones that follow the [DICOMWeb][dicom-web] spec. + +1. Choose and install an Image Archive +2. Upload data to your archive (e.g. with DCMTK's [storescu][storescu] or your + archive's web interface) +3. Keep the server running + +For our purposes, we will be using `Orthanc`, but you can see a list of +[other Open Source options](#open-source-dicom-image-archives) below. + +### Requirements + +- Docker + - [Docker for Mac](https://docs.docker.com/docker-for-mac/) + - [Docker for Windows (recommended)](https://docs.docker.com/docker-for-windows/) + - [Docker Toolbox for Windows](https://docs.docker.com/toolbox/toolbox_install_windows/) + +_Not sure if you have `docker` installed already? Try running `docker --version` +in command prompt or terminal_ + +> If you are using `Docker Toolbox` you need to change the _PROXY_DOMAIN_ +> parameter in _platform/app/package.json_ to http://192.168.99.100:8042 or +> the ip docker-machine ip throws. This is the value [`WebPack`][webpack-proxy] +> uses to proxy requests + +## Open Source DICOM Image Archives + +There are a lot of options available to you to use as a local DICOM server. Here +are some of the more popular ones: + +| Archive | Installation | +| --------------------------------------------- | ---------------------------------- | +| [DCM4CHEE Archive 5.x][dcm4chee] | [W/ Docker][dcm4chee-docker] | +| [Orthanc][orthanc] | [W/ Docker][orthanc-docker] | +| [DICOMcloud][dicomcloud] (**DICOM Web only**) | [Installation][dicomcloud-install] | +| [OsiriX][osirix] (**Mac OSX only**) | Desktop Client | +| [Horos][horos] (**Mac OSX only**) | Desktop Client | + +_Feel free to make a Pull Request if you want to add to this list._ + +Below, we will focus on `DCM4CHEE` and `Orthanc` usage: + +### Running Orthanc + +_Start Orthanc:_ + +```bash +# Runs orthanc so long as window remains open +yarn run orthanc:up +``` + +_Upload your first Study:_ + +1. Navigate to + [Orthanc's web interface](http://localhost:8042/ui/app/index.html#/) at + `http://localhost:8042/ui/app/index.html#/` in a web browser. +2. In the left you can see the upload button where you can drag and drop your DICOM files + +#### Orthanc: Learn More + +You can see the `docker-compose.yml` file this command runs at +[`/platform/app/.recipes/Nginx-Orthanc`][orthanc-docker-compose], and more on +Orthanc for Docker in [Orthanc's documentation][orthanc-docker]. + +#### Connecting to Orthanc + +Now that we have a local Orthanc instance up and running, we need to configure +our web application to connect to it. Open a new terminal window, navigate to +this repository's root directory, and run: + +```bash +# If you haven't already, enable yarn workspaces +yarn config set workspaces-experimental true + +# Restore dependencies +yarn install + +# Run our dev command, but with the local orthanc config +yarn run dev:orthanc +``` + +#### Configuration: Learn More + +> For more configuration fun, check out the +> [Essentials Configuration](../configurationFiles.md) guide. + +Let's take a look at what's going on under the hood here. `yarn run dev:orthanc` +is running the `dev:orthanc` script in our project's `package.json` (inside +`platform/app`). That script is: + +```js +cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker-nginx-orthanc.js webpack-dev-server --config .webpack/webpack.pwa.js -w +``` + +- `cross-env` sets three environment variables + - PROXY_TARGET: `/dicom-web` + - PROXY_DOMAIN: `http://localhost:8042` + - APP_CONFIG: `config/docker-nginx-orthanc.js` +- `webpack-dev-server` runs using the `.webpack/webpack.pwa.js` configuration + file. It will watch for changes and update as we develop. + +`PROXY_TARGET` and `PROXY_DOMAIN` tell our development server to proxy requests +to `Orthanc`. This allows us to bypass CORS issues that normally occur when +requesting resources that live at a different domain. + +The `APP_CONFIG` value tells our app which file to load on to `window.config`. +By default, our app uses the file at +`/platform/app/public/config/default.js`. Here is what that +configuration looks like: + +```js +window.config = { + routerBasename: '/', + extensions: [], + modes: [], + showStudyList: true, + dataSources: [ + { + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'dicomweb', + configuration: { + friendlyName: 'dcmjs DICOMWeb Server', + name: 'DCM4CHEE', + wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado', + qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', + qidoSupportsIncludeField: true, + supportsReject: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: true, + }, + }, + ], + defaultDataSourceName: 'dicomweb', +}; +``` + +### Data Source Configuration Options + +The following properties can be added to the `configuration` property of each data source. + +##### `dicomUploadEnabled` +A boolean indicating if the DICOM upload to the data source is permitted/accepted or not. A value of true provides a link on the OHIF work list page that allows for DICOM files from the local file system to be uploaded to the data source + +:::tip +The [OHIF plugin for Orthanc](https://book.orthanc-server.com/plugins/ohif.html) by default utilizes the DICOM JSON data +source and it has been discovered that only those studies uploaded to Orthanc AFTER the plugin has been installed are +available as DICOM JSON. As such, if the OHIF plugin for Orthanc is desired for studies uploaded prior to installing the plugin, +then consider switching to using [DICOMweb instead](https://book.orthanc-server.com/plugins/ohif.html#using-dicomweb). +::: + +![toolbarModule-layout](../../assets/img/uploader.gif) + +Don't forget to add the customization to the config as well + +```js +customizationService: { + dicomUploadComponent: + '@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent', +}, +``` + + +#### `singlepart` +A comma delimited string specifying which payloads the data source responds with as single part. Those not listed are considered multipart. Values that can be included here are `pdf`, `video`, `bulkdata`, `thumbnail` and `image`. + +For DICOM video and PDF it has been found that Orthanc delivers multipart, while DCM4CHEE delivers single part. Consult the DICOM conformance statement for your particular data source to determine which payload types it delivers. + +To learn more about how you can configure the OHIF Viewer, check out our +[Configuration Guide](../configurationFiles.md). + + +### DICOM PDF +See the [`singlepart`](#singlepart) data source configuration option. + +### DICOM Video +See the [`singlepart`](#singlepart) data source configuration option. + +### BulkDataURI + +The `bulkDataURI` configuration option allows the datasource to use the +bulkdata end points for retrieving metadata if originally was not included in the +response from the server. This is useful for the metadata information that +are big and can/should be retrieved in a separate request. In case the bulkData URI +is relative (instead of absolute) the `relativeResolution` option can be used to +specify the resolution of the relative URI. The possible values are `studies`, `series` and `instances`. +Certainly the knowledge of how the server is configured is required to use this option. + +```js +bulkDataURI: { + enabled: true, + relativeResolution: 'series', +}, +``` + + +### Running DCM4CHEE + +dcm4che is a collection of open source applications for healthcare enterprise +written in Java programming language which implements DICOM standard. dcm4chee +(extra 'e' at the end) is dcm4che project for an Image Manager/Image Archive +which provides storage, retrieval and other functionalities. You can read more +about dcm4chee in their website [here](https://www.dcm4che.org/) + +DCM4chee installation is out of scope for these tutorials and can be found +[here](https://github.com/dcm4che/dcm4chee-arc-light/wiki/Run-minimum-set-of-archive-services-on-a-single-host) + +An overview of steps for running OHIF Viewer using a local DCM4CHEE is shown +below: + +
+ +
+ +[dcm4chee]: https://github.com/dcm4che/dcm4chee-arc-light +[dcm4chee-docker]: + https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker +[orthanc]: https://www.orthanc-server.com/ +[orthanc-docker]: http://book.orthanc-server.com/users/docker.html +[dicomcloud]: https://github.com/DICOMcloud/DICOMcloud +[dicomcloud-install]: https://github.com/DICOMcloud/DICOMcloud#running-the-code +[osirix]: http://www.osirix-viewer.com/ +[horos]: https://www.horosproject.org/ +[default-config]: + https://github.com/OHIF/Viewers/blob/master/platform/app/public/config/default.js +[html-templates]: + https://github.com/OHIF/Viewers/tree/master/platform/app/public/html-templates +[config-files]: + https://github.com/OHIF/Viewers/tree/master/platform/app/public/config +[storescu]: http://support.dcmtk.org/docs/storescu.html +[webpack-proxy]: https://webpack.js.org/configuration/dev-server/#devserverproxy diff --git a/platform/docs/versioned_docs/version-3.9/configuration/dataSources/introduction.md b/platform/docs/versioned_docs/version-3.9/configuration/dataSources/introduction.md new file mode 100644 index 0000000..2a0add5 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/configuration/dataSources/introduction.md @@ -0,0 +1,19 @@ +--- +sidebar_position: 1 +sidebar_label: Introduction +--- + +# Data Source + +The internal data structure of OHIFโ€™s metadata follows naturalized DICOM JSON, a +format pioneered by `dcmjs`. In short DICOM metadata headers with DICOM Keywords +instead of tags and sequences as arrays, for easy development and clear code. + +Here in this section we will discuss couple of data sources that are commonly used +and OHIF has provided the implementation for them. + +## Custom Data Source +Do you have a custom data source? or a custom data that you want to use in OHIF? +You can easily write a data source to map your data to OHIFโ€™s native format. + +You can read more in the [Data Source Module](../../platform/extensions/modules/data-source.md) diff --git a/platform/docs/versioned_docs/version-3.9/configuration/dataSources/static-files.md b/platform/docs/versioned_docs/version-3.9/configuration/dataSources/static-files.md new file mode 100644 index 0000000..2741e04 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/configuration/dataSources/static-files.md @@ -0,0 +1,47 @@ +--- +sidebar_position: 5 +sidebar_label: Static Files +--- + +# Static Files + +There is a binary DICOM to static file generator, which provides easily served +binary files. The files are all compressed in order to reduce space +significantly, and are pre-computed for the files required for OHIF, so that the +performance of serving the files is just the read from disk/write to http stream +time, without any extra processing time. + +The project for the static wado files is located here: [static-wado]: +https://github.com/OHIF/static-wado + +It can be compiled with Java and Gradle, and then run against a set of dicom, in +the example located in /dicom/study1 outputting to /dicomweb, and then a server +run against that data, like this: + +```bash +git clone https://github.com/OHIF/static-wado +cd static-wado +./gradlew installDist +StaticWado/build/install/StaticWado/bin/StaticWado -d /dicomweb /dicom/study1 +cd /dicomweb +npx http-server -p 5000 --cors -g + +# you can use npx serve ./dist -l 8080 -c ../public/serve.json as an alternative to http-server +``` + +There is then a dev environment in the platform/app directory which can be +run against those files, like this: + +``` +cd platform/app +yarn dev:static +``` + +Additional studies can be added to the dicomweb by re-running the StaticWado +command. It will create a single studies.gz index file (JSON DICOM file, +compressed) containing an index of all studies created. There is then a small +extension to OHIF which performs client side indexing. + +The StaticWado command also knows how to deploy a client and dicomweb directory +to Amazon s3, which can then server files up directly. There is another build +setup build:aws in the viewer package.json to create such a deployment. diff --git a/platform/docs/versioned_docs/version-3.9/configuration/tour-demo.gif b/platform/docs/versioned_docs/version-3.9/configuration/tour-demo.gif new file mode 100644 index 0000000..f351f75 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/configuration/tour-demo.gif differ diff --git a/platform/docs/versioned_docs/version-3.9/configuration/tours.md b/platform/docs/versioned_docs/version-3.9/configuration/tours.md new file mode 100644 index 0000000..849c82f --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/configuration/tours.md @@ -0,0 +1,156 @@ +--- +sidebar_position: 3 +sidebar_label: Tours +--- + +# Configuring Tours in OHIF with Shepherd.js + +In OHIF, you can configure guided tours for users by leveraging [Shepherd.js](https://shepherdjs.dev/), a JavaScript library for building feature tours. This page explains how you can define and customize these tours within your app configuration file. + +## Overview + +Tours allow you to provide step-by-step guidance to users, explaining different features of your mode/extension or the viewer. Each tour is associated with a route and consists of several steps, each guiding the user through specific interactions in the viewer. + +### Adding a Tour to your Configuration + +Hereโ€™s an example of adding a tour to your configuration file: + +```javascript +window.config = { + tours: [ + { + id: 'basicViewerTour', + route: '/viewer', + steps: [ + { + id: 'scroll', + title: 'Scrolling Through Images', + text: 'You can scroll through the images using the mouse wheel or scrollbar.', + attachTo: { + element: '.viewport-element', + on: 'top', + }, + advanceOn: { + selector: '.cornerstone-viewport-element', + event: 'CORNERSTONE_TOOLS_MOUSE_WHEEL', + }, + }, + { + id: 'zoom', + title: 'Zooming In and Out', + text: 'You can zoom the images using the right click.', + attachTo: { + element: '.viewport-element', + on: 'left', + }, + advanceOn: { + selector: '.cornerstone-viewport-element', + event: 'CORNERSTONE_TOOLS_MOUSE_UP', + }, + }, + // Add more steps as needed + ], + tourOptions: { + useModalOverlay: true, + defaultStepOptions: { + buttons: [ + { + text: 'Skip all', + action() { + this.complete(); + }, + secondary: true, + }, + ], + }, + }, + }, + ], +}; +``` + +## Explanation of Parameters + +### `tours` Array + +Each item in the `tours` array defines a specific tour for a particular route. The object contains the following properties: + +- **`id`**: A unique identifier for the tour. This helps in tracking whether the tour has been shown. +- **`route`**: The route in the application where the tour is applicable. When the user navigates to this route, the tour can automatically trigger if it hasn't been shown before. +- **`steps`**: An array of steps that define the individual guide elements in the tour. Each step corresponds to a UI element and guides the user through interactions. +- **`tourOptions`**: An object that allows you to configure the overall behavior of the tour, such as using a modal overlay or defining default step options. + +### `steps` Array + +Each step defines a part of the tour. Here's a breakdown of the properties you can define: + +- **`id`**: A unique identifier for the step within the tour. +- **`title`**: The title of the step, which appears at the top of the tooltip for the step. +- **`text`**: The content or description of the step, explaining what the user needs to do or understand. +- **`attachTo`**: Specifies where the step should be attached in the DOM. It includes: + - `element`: A string selector or a DOM element that the step should attach to. + - `on`: Specifies the position of the tooltip relative to the element (e.g., 'top', 'left', 'bottom', 'right'). +- **`advanceOn`**: Defines an event that will automatically advance the tour to the next step. This is useful for actions like clicking a button or scrolling. + - `selector`: The CSS selector for the element that triggers the advance. + - `event`: The event name that advances the step, this can be a OHIF service event, or a cornerstone event, or any native JS event (e.g., 'click', 'CORNERSTONE_TOOLS_MOUSE_WHEEL'). +- **`beforeShowPromise`**: A function that returns a promise. When the promise resolves, the rest of the show logic for the step will execute. You can use this to ensure that the target element is ready before the step shows. + +### `tourOptions` + +The `tourOptions` object allows you to configure the overall behavior of the tour. Here's a breakdown of the available properties: + +- **`useModalOverlay`**: A boolean that, if set to `true`, places the tour steps above a darkened modal overlay. The overlay creates an opening around the target element so it can remain interactive. +- **`defaultStepOptions`**: Default options that apply to all steps in the tour. You can override these in individual steps. The following are some options available: + - `buttons`: An array of button objects that appear in the footer of each step. Each button can trigger actions like advancing the tour or skipping it. For example: + - **`text`**: The label text on the button. + - **`action`**: A function to execute when the button is clicked. You can advance the tour using `this.next()`, or complete it using `this.complete()`. + - **`secondary`**: A boolean that, when set to `true`, styles the button as secondary (often for actions like skipping). + +### `floatingUIOptions` + +You can define positioning options for the steps using **Floating UI** middleware. This helps control how the steps are positioned, especially near the browser edges. + +For example, you can ensure that the steps maintain a margin of 24px from the viewport edges by configuring `preventOverflow` middleware: + +```javascript +floatingUIOptions: { + middleware: [ + preventOverflow({ padding: 24 }), + flip(), // Allows the step to flip if it is overflowing + ] +} +``` + +### Shepherd.js Lifecycle Events + +Each step and tour can have lifecycle events like `show`, `hide`, `complete`, or `cancel`. These events allow you to hook into the tourโ€™s lifecycle to perform actions when certain events are triggered. + +For example: + +```javascript +when: { + show() { + console.log('Step shown!'); + }, + hide() { + console.log('Step hidden.'); + } +} +``` + +## Customizing Your Tour + +Once you have a basic tour in place, you can extend it with more advanced features like custom scrolling behavior, dynamic elements, and event-based step advancement. For more details, check out the [Shepherd.js documentation](https://shepherdjs.dev/). + +## Licensing +All versions below 14.0 for Shepherd.JS is under the MIT license, if you wish to use any version above 14.0, you can visit the ShepherdJS website to learn about their pricing and plans [Shepherd.js](https://www.shepherdjs.dev/) + +[LICENSE](https://github.com/shipshapecode/shepherd?tab=License-1-ov-file#readme) + +## Demo + +![Tour Demo]() + +## Conclusion + +By leveraging **Shepherd.js**, you can provide users with interactive and informative guided tours of the viewer. This can greatly improve the user experience and help users understand how to use key features. diff --git a/platform/docs/versioned_docs/version-3.9/configuration/url.md b/platform/docs/versioned_docs/version-3.9/configuration/url.md new file mode 100644 index 0000000..cac7e5a --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/configuration/url.md @@ -0,0 +1,208 @@ +--- +sidebar_position: 3 +sidebar_label: URL +--- + +# URL + +You can modify the URL at any state of the app to get the desired result. Here +are different part of the APP that you can modify: + + +## WorkList + +The WorkList can be modified by adding the following query parameters: + +### PatientName + +The patient name can be modified by adding the `PatientName` query parameter. + +```js +/?patientName=myQuery +``` + +### MRN + +The MRN can be modified by adding the `MRN` query parameter. + +```js +/?mrn=myQuery +``` + +### Description + +The description can be modified by adding the `Description` query parameter. + +```js +/?description=myQuery +``` + +### Modality + +The modality can be modified by adding the `modalities` query parameter. + +```js +/?modalities=MG +``` + +### Accession Number + +The accession number can be modified by adding the `accession` query parameter. + +```js +/?accession=myQuery +``` + +### DataSources + +If you happen to have multiple data sources configured, you can filter the +WorkList by adding the `dataSources` query parameter. + +```js +/?dataSources=orthanc +``` + +Note1: You should pass the `sourceName` of the data source in the configuration file (not the friendly name nor the name) +Note2: Make sure that the configuration file you are using actually includes that data source. You cannot use a data source from another configuration file. + + +:::tip + +You can add `sortBy` and `sortDirection` query parameters to sort the WorkList + +```js +/?patientName=myquery&sortBy=studyDate&sortDirection=ascending +``` + +::: + + +## Viewer + +The Viewer can be modified by adding the following query parameters: + + +### Mode + +As you have seen before, the Viewer can be configured to be in different modes. +Each mode registers their `id` in the URL. + +For instance + +```js +/viewer?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339 +``` + +will open the viewer in the basic (longitudinal) mode with the StudyInstanceUID +1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339. + +And if configured, the same study can be opened in the `tmtv` mode + +```js +/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339 +``` + +### StudyInstanceUIDs + +You can open more than one study in the Viewer by adding the `StudyInstanceUIDs` + + +```js +/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095722.1&StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095258.1 +``` + +:::tip + +You can use this feature to open a current and prior study in the Viewer. +Read more in the [Hanging Protocol Module](../platform/extensions/modules/hpModule.md#matching-on-prior-study-with-uid) section. You can also use commas to separate +values. + +::: + + +### SeriesInstanceUIDs + +Sometimes you need to only retrieve a specific series in a study, you can do +that by providing series level QIDO query parameters in the URL such as +SeriesInstanceUIDs. This does NOT work with instance or study +level parameters. For example: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8 +``` + +This will only open the viewer with one series (one displaySet) loaded, and no +queries made for any other series. + +Sometimes you need to only retrieve a subset of series in a study, you can do +that by providing more than one series, separated by commas. For example: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8,1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10 +``` + +This will only open the viewer with two series (two displaySets) loaded, and no +queries made for any other series. + +### initialSeriesInstanceUID + +Alternatively, sometimes you want to just open the study on a specified series, but allowing other +series to be present too. This is the same behavior can be +achieved by using the `initialSeriesInstanceUID` parameter. For example: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&initialSeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8 +``` + +This will open all the series in the study, but the viewer will start with the +series specified by the `initialSeriesInstanceUID` parameter. + + +Note that you can combine these, if you want to load a specific set of series +plus show an initial one as the first one selected, for example: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8,1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10&initialSeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10 +``` + +### initialSopInstanceUID + +You can also specify the initial SOP Instance to be displayed by using the +`initialSopInstanceUID` parameter. For example: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8&initialSopInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095501.9 +``` + +This will open the study with the filtered series, and navigate to the slice 101 +which happens to be the SOP Instance specified by the `initialSopInstanceUID` + +Note: again you can mix and match + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8,1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10&initialSeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10&initialSopInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095510.8 +``` + +You can even load the whole study and only specify the initial SOP Instance to be displayed. Although +it will take more time to match, but it works as expected. + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&initialSopInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095510.8 +``` + +### hangingProtocolId + +You can select the initial hanging protocol to apply by using the +hangingProtocolId parameter. The selected parameter must be available in a +hangingProtocolModule registration, but does not have to be active. + +For instance for loading a specific study in mpr mode from start you can use: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingProtocolId=@ohif/mnGrid +``` + +### token + +Although not recommended, you can use the token param in the URL which will inject +the token into the Authorization header of the request. diff --git a/platform/docs/versioned_docs/version-3.9/conformance.md b/platform/docs/versioned_docs/version-3.9/conformance.md new file mode 100644 index 0000000..d0ca8ba --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/conformance.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 12 +sidebar_label: DICOM Conformance Statement (NEW) +title: DICOM Conformance Statement +--- + +You can find a version that has been open sourced by Radical Imaging [in this link](https://docs.google.com/document/d/1hbDlUApX4svX33gAUGxGfD7fXXZNaBsX0hSePbc-hNA/edit?usp=sharing) diff --git a/platform/docs/versioned_docs/version-3.9/deployment/_category_.json b/platform/docs/versioned_docs/version-3.9/deployment/_category_.json new file mode 100644 index 0000000..534be1d --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/deployment/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Deployment", + "position": 3 +} diff --git a/platform/docs/versioned_docs/version-3.9/deployment/authorization.md b/platform/docs/versioned_docs/version-3.9/deployment/authorization.md new file mode 100644 index 0000000..b3589b1 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/deployment/authorization.md @@ -0,0 +1,95 @@ +--- +sidebar_position: 6 +sidebar_label: Auth +--- + +# Authorization and Authentication +The OHIF Viewer can be configured to work with authorization servers that support one or more of the OpenID-Connect authorization flows. The Viewer finds it's OpenID-Connect settings on the oidc configuration key. You can set these values in your configuration files. For instance you can take a look at our +`google.js` configuration file. + + +```js +oidc: [ + { + // ~ REQUIRED + authority: 'https://accounts.google.com', + client_id: '723928408739-k9k9r3i44j32rhu69vlnibipmmk9i57p.apps.googleusercontent.com', + redirect_uri: '/callback', + response_type: 'id_token token', + scope: 'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid + // ~ OPTIONAL + post_logout_redirect_uri: '/logout-redirect.html', + revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=', + automaticSilentRenew: true, + revokeAccessTokenOnSignout: true, + }, +], +``` + +You need to provide the following information: +- authority: The URL of the authorization server. +- client_id: The client id of your application (provided by the authorization server). +- redirect_uri: The callback URL of your application. +- response_type: The response type of the authorization flow (e.g. id_token token, [learn more about different flows](https://darutk.medium.com/diagrams-of-all-the-openid-connect-flows-6968e3990660)). +- scope: The scopes that your application needs to access +- post_logout_redirect_uri: The URL that the user will be redirected to after logout. +- revoke_uri: The URL that the user will be redirected to after logout. +- automaticSilentRenew: If true, the user will be automatically logged in after the token expires. +- revokeAccessTokenOnSignout: If true, the access token will be revoked on logout. + + + +## How it works +The Viewer uses the `userAuthenticationService` to set the OpenID-Connect settings. The `userAuthenticationService` is a singleton service that is responsible for authentication and authorization. It is initialized by the app and you can grab it +from the `servicesManager` + +```js +const userAuthenticationService = servicesManager.services.userAuthenticationService; +``` + +Then the userAuthenticationService will inject the token as Authorization header in the requests that are sent to the server (both metadata +and pixelData). + +## Token based authentication in URL +Sometimes (although not recommended), some servers like to send the token +in the query string. In this case, the viewer will automatically grab the token from the query string +and add it to the userAuthenticationService and remove it from the query string (to prevent it from being logged in the console +in future requests). + +and example would be + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.2.3.4.5.6.6.7&token=e123125jsdfahsdf +``` + + + +## Implicit Flow vs Authorization Code Flow + +The Viewer supports both the Implicit Flow and the Authorization Code Flow. The Implicit Flow is the default currently, as it is easier to set up and use. However, you can opt for better security by using the Authorization Code Flow. To do so, add `useAuthorizationCodeFlow` to the configuration and change the `response_type` from `id_token token` to `code`. + +Read more about Implicit Flow vs Authorization Code Flow [here](https://documentation.openiddict.com/guides/choosing-the-right-flow.html#:~:text=The%20implicit%20flow%20is%20similar,when%20using%20response_mode%3Dform_post%20) and [here](https://medium.com/@alysachan830/the-basics-of-oauth-2-0-authorization-code-implicit-flow-state-and-pkce-ed95d3478e1c) + +```js +oidc: [ + { + authority: 'https://accounts.google.com', + client_id: '723928408739-k9k9r3i44j32rhu69vlnibipmmk9i57p.apps.googleusercontent.com', + redirect_uri: '/callback', + scope: 'email profile openid', + post_logout_redirect_uri: '/logout-redirect.html', + revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=', + revokeAccessTokenOnSignout: true, + automaticSilentRenew: true, + // CHANGE THESE ***************************** + response_type: 'code', + useAuthorizationCodeFlow: true, + }, +], +``` + +In fact, since browsers are blocking third-party cookies, the Implicit Flow will cease functioning in the future (not specific to OHIF). Read more [here](https://support.okta.com/help/s/article/FAQ-How-Blocking-Third-Party-Cookies-Can-Potentially-Impact-Your-Okta-Environment?language=en_US). It is recommended to use the Authorization Code Flow and begin migrating to it. + +:::note +For the Authorization Code Flow, when authenticating against Google, you must add the `client_secret` to the configuration as well. Unfortunately, this seems to occur only with Google. +::: diff --git a/platform/docs/versioned_docs/version-3.9/deployment/build-for-production.md b/platform/docs/versioned_docs/version-3.9/deployment/build-for-production.md new file mode 100644 index 0000000..d1dd508 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/deployment/build-for-production.md @@ -0,0 +1,136 @@ +--- +sidebar_position: 2 +--- + +# Build for Production + +### Build Machine Requirements + +- [Node.js & NPM](https://nodejs.org/en/download/) +- [Yarn](https://yarnpkg.com/lang/en/docs/install/) +- [Git](https://www.atlassian.com/git/tutorials/install-git) + +### Getting the Code + +_With Git:_ + +```bash +# Clone the remote repository to your local machine +git clone https://github.com/OHIF/Viewers.git +``` + +More on: _[`git clone`](https://git-scm.com/docs/git-clone), +[`git checkout`](https://git-scm.com/docs/git-checkout)_ + +_From .zip:_ + +[OHIF/Viewers: master.zip](https://github.com/OHIF/Viewers/archive/master.zip) + +### Restore Dependencies & Build + +Open your terminal, and navigate to the directory containing the source files. +Next run these commands: + +```bash +# If you haven't already, enable yarn workspaces +yarn config set workspaces-experimental true + +# Restore dependencies +yarn install + +# Build source code for production +yarn run build +``` + +If everything worked as expected, you should have a new `dist/` directory in the +`platform/app/dist` folder. It should roughly resemble the following: + +```bash title="platform/app/dist/" +โ”œโ”€โ”€ app-config.js +โ”œโ”€โ”€ app.bundle.js +โ”œโ”€โ”€ app.css +โ”œโ”€โ”€ index.html +โ”œโ”€โ”€ manifest.json +โ”œโ”€โ”€ service-worker.js +โ””โ”€โ”€ ... +``` + +By default, the build output will connect to OHIF's publicly accessible PACS. If +this is your first time setting up the OHIF Viewer, it is recommended that you +test with these default settings. After testing, you can find instructions on +how to configure the project for your own imaging archive below. + +### Configuration + +The configuration for our viewer is in the `platform/app/public/config` +directory. Our build process knows which configuration file to use based on the +`APP_CONFIG` environment variable. By default, its value is +[`config/default.js`][default-config]. The majority of the viewer's features, +and registered extension's features, are configured using this file. + +The easiest way to apply your own configuration is to modify the `default.js` +file. For more advanced configuration options, check out our +[configuration essentials guide](../configuration/configurationFiles.md). + +## Next Steps + +### Deploying Build Output + +_Drag-n-drop_ + +- [Netlify: Drop](./static-assets#netlify-drop) + +_Easy_ + +- [Surge.sh](./static-assets#surgesh) +- [GitHub Pages](./static-assets#github-pages) + +_Advanced_ + +- [AWS S3 + Cloudfront](./static-assets#aws-s3--cloudfront) +- [GCP + Cloudflare](./static-assets#gcp--cloudflare) +- [Azure](./static-assets#azure) + +### Testing Build Output Locally + +A quick way to test your build output locally is to spin up a small webserver. +You can do this by running the following commands in the `dist/` output +directory: + +```bash +# Install http-server as a globally available package +yarn global add http-server + +# Change the directory to the platform/app + +# Serve the files in our current directory +npx serve ./dist -c ../public/serve.json +``` + +:::caution +In the video below notice that there is `platform/viewer` which has been renamed to `platform/app` in the latest version +::: + +
+ +
+ + +### Automating Builds and Deployments + +If you found setting up your environment and running all of these steps to be a +bit tedious, then you are in good company. Thankfully, there are a large number +of tools available to assist with automating tasks like building and deploying +web application. For a starting point, check out this repository's own use of: + +- [CircleCI][circleci]: [config.yaml][circleci-config] +- [Netlify][netlify]: [netlify.toml][netlify.toml] | + [build-deploy-preview.sh][build-deploy-preview.sh] + + +[circleci]: https://circleci.com/gh/OHIF/Viewers +[circleci-config]: https://github.com/OHIF/Viewers/blob/master/.circleci/config.yml +[netlify]: https://app.netlify.com/sites/ohif/deploys +[netlify.toml]: https://github.com/OHIF/Viewers/blob/master/platform/app/netlify.toml +[build-deploy-preview.sh]: https://github.com/OHIF/Viewers/blob/master/.netlify/build-deploy-preview.sh + diff --git a/platform/docs/versioned_docs/version-3.9/deployment/cors.md b/platform/docs/versioned_docs/version-3.9/deployment/cors.md new file mode 100644 index 0000000..135c9f9 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/deployment/cors.md @@ -0,0 +1,313 @@ +--- +sidebar_position: 7 +--- + +# Cross-Origin Information for OHIF + +This document describes various security configurations, settings and environments/contexts needed to fully leverage OHIFโ€™s capabilities. One may need some configurations while others might need ALL of them - it all depends on the environment OHIF is expected to run in. + +In particular, three of OHIFโ€™s features depend on these configurations: +- [OHIFโ€™s use of SharedArrayBuffer](#sharedarraybuffer) +- [Embedding OHIF in an iframe](#embedding-ohif-in-an-iframe) +- [XMLHttpRequests to fetch data from data sources](#cors-in-ohif) + + + +## SharedArrayBuffer +A `SharedArrayBuffer` is a JavaScript object that is similar to an `ArrayBuffer` but can be shared between web workers and the window that spawned them via the `postMessage` API. See [SharedArrayBuffer in MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) for more information. + +:::tip +To turn off Shared Array Buffer completely, just set `useSharedArrayBuffer` to `false` in the [OHIF configuration](../configuration/configurationFiles.md). But keep in mind that you will not get the performance boost that Shared Array Buffer offers for decoding and rendering big volumes where web workers write to the same memory space. +::: + +### Security Requirements + +In order to use `SharedArrayBuffer` objects in the browser, the following security conditions must be met: + +- The page must be served in a [secure context](#secure-context). +- The page must have [cross-origin isolation](#cross-origin-isolation) enabled. + +### `SharedArrayBuffer` in OHIF + +OHIF uses `SharedArrayBuffer` in its volume loader (from Cornerstone3D). It comes with the benefit of improved performance and optimization at the cost of some configuration to use it. + +As such, if the following popup is shown when launching OHIF then the OHIF server will have to be configured to permit the loading of volumetric images and data. Note that stack viewports are still available and functional even when this error is present. + +![OHIF in non-secure context](../assets/img/ohif-non-secure-context.png) + +To better determine which (if not all) of the [security requirements](#security-requirements) are lacking, have a look at the browser console. + +Output in the console similar to the following indicates that OHIF is not running in a [secure context](#secure-context). + +![browser console for non-secure context](../assets/img/browser-console-non-secure-context.png) + +Absence of the above error in the console together with the presence of the Cross Origin Isolation popup warning, likely indicates that either or both of the [COOP](#coop---cross-origin-opener-policy) and/or [COEP](#coep---cross-origin-embedder-policy) headers are not set for OHIF. + +## Embedding OHIF in an iframe + +As described [here](./iframe.md), there are cases where OHIF will be embedded in an iframe. The following links provide more information for setting up and configuring OHIF to work in an iframe: + +- [OHIF iframe documentation](./iframe.md#static-build) +- [OHIF as a Cross-origin Resource in an iframe](#ohif-as-a-cross-origin-resource-in-an-iframe) + +## Secure Context + +MDN defines a secure context as [โ€œa Window or Worker for which certain minimum standards of authentication and confidentiality are met.โ€œ](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) + +Any local URL is considered secure. The following are some examples of local URLs that are considered secureโ€ฆ +- http://localhost +- http://127.0.0.1:3000 + +URLs that are NOT local must be delivered over `https://` or `wss://` (i.e. TLS) to be considered secure. See [When is a context considered secure](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure) in MDN for more information. + +### iframes + +A page embedded in an iframe is considered secure if it itself and every one of its embedding ancestors are delivered securely. Otherwise it is deemed insecure. + +### Why does OHIF require a secure context? + +Beyond all of the inherent benefits of a secure connection, OHIF requires a secure context so that it can utilize [SharedArrayBuffer](#sharedarraybuffer) objects for volume rendering. + +### Configuring/setting up a secure context + +[Local URLs are considered secure](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure), and as such whenever OHIF is accessed via a local URL (e.g. http://localhost:3000) it is running in a secure context. For example, in a development environment using the default webpack setup, OHIF can be deployed and accessed in a secure context at http://localhost:3000. + +The best alternative is to host OHIF over HTTPS. + +:::tip +OHIF can be served over HTTPS in a variety of ways (these are just some examples). +- Website hosting services that offer HTTPS deployment (e.g,. Netlify) or offer HTTPS load balancers (AWS, Google Cloud etc.) +- Setting up a reverse proxy (e.g. `nginx`) with a self-signed certificate that forwards requests to the OHIF server + - [An OHIF Docker image can be set up this way](./docker.md#ssl). +::: + +## Origin Definition + +According to [MDN](https://developer.mozilla.org/en-US/docs/Glossary/Origin), a Web contentโ€™s origin is defined by the scheme (protocol), hostname (domain), and port of the URL used to access it. Two objects have the same origin only when the scheme, hostname, and port all match. + +## CORS - Cross-Origin Resource Sharing + +A cross-origin resource is a resource (e.g. image, JSON, etc) that is served by one origin and used/referenced by a different origin. + +CORS is the protocol utilized by web servers and browsers whereby a server of one origin identifies and/or restricts which of its resources that other origins (i.e. other than its own) a browser should allow access to. By default a browser does not permit cross-origin resource sharing. + +The CORS mechanism relies on the HTTP response headers from the server to indicate if a resource can be shared with a different origin. + +See the [MDN CORS article](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) for more information. + +### CORS HTTP Headers + +The header that mostly concerns OHIF is listed below and should be configured accordingly on the DICOMweb server or any data source that OHIF would make XMLHttpRequests to for its data. + +```http +Access-Control-Allow-Origin: `` | * +``` + +:::tip +The `Access-Control-Allow-Origin` header specifies which origins can access the served resource embedded in the response. + +Either a single, specific origin (i.e. ``) can be specified or ALL origins (i.e. *) + +See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-allow-origin) for more information. +::: + +### CORS in OHIF + +OHIF fetches and displays data and images from data sources. It invokes XMLHttpRequests to some data sources such as DICOMweb data sources to fetch the information to render. Typically, a DICOMweb server is hosted on a completely different origin than the one serving OHIF. As such, those XMLHttpRequests use CORS. + +### Troubleshooting CORS in OHIF + +The following is an example screenshot of the browser console when one of OHIFโ€™s DICOMweb data source servers is not configured for CORS. + +![CORS browser console errors](../assets/img/cors-browser-console-errors.png) + +And the following is what is in the accompanying network tab. + +![CORS browser network panel errors](../assets/img/cors-network-panel-errors.png) + +:::info +Setting the appropriate CORS header varies per server or service that is hosting the data source. What follows below is just one example to remedy the problem. +::: + +:::tip +If Orthanc is the data source running in a Docker container composed with/behind nginx. And OHIF is being served at localhost:3000. The issue can be remedied by adding either of the following to Orthancโ€™s Docker container nginx.conf file. + +```nginx +add_header 'Access-Control-Allow-Origin' 'http://localhost:3000' always; +``` + +Or + +```nginx +add_header 'Access-Control-Allow-Origin' '*' always; +``` +::: + +## COOP - Cross-Origin Opener Policy + +The COOP HTTP response header restricts the global, root document of the page from being referenced and accessed by another cross-origin document that might open the page in a window. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy) for more information. + +### Header Values Pertinent to OHIF (see [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy#syntax) for more information) + +|Value|Description| +|-----|-----------| +|same-origin|Restricts the document to be referenced by openers of the same origin only.| + +### COOP in OHIF + +COOP is required for [SharedArrayBuffer](#sharedarraybuffer) usage in OHIF. See also [Troubleshooting Cross-origin Isolation in OHIF](#troubleshooting-cross-origin-isolation-in-ohif). + +## COEP - Cross-Origin Embedder Policy + +The COEP HTTP response header restricts cross-origin documents from being embedded into a document (e.g. in an iframe, video, image, etc). See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy) for more information. + +### Header Values Pertinent to OHIF (see [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy#syntax) for more information) + +|Value|Description| +|-----|-----------| +|require-corp|Permits the document to load either of the following embedded resources:
  • Those from the same origin
  • Cross-origin resources embedded by a DOM element that has the appropriate [crossorigin attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin) set
  • Cross-origin resources with the appropriate [CORP response header](#corp---cross-origin-resource-policy)
+|credentialless|See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy#syntax) for more information| + +### COEP in OHIF + +COEP is required for [SharedArrayBuffer](#sharedarraybuffer) usage in OHIF. See also [Troubleshooting Cross-origin Isolation in OHIF](#troubleshooting-cross-origin-isolation-in-ohif). + +## Cross-origin Isolation + +Cross-origin isolation is [enabled](https://web.dev/cross-origin-isolation-guide/#enable-cross-origin-isolation) for a web page when the following COOP and COEP headers are set. +- [COOP](#coop---cross-origin-opener-policy) with `same-origin` +- [COEP](#coep---cross-origin-embedder-policy) with `require-corp` or `credentialless` + +### iframe + +An iframe is considered to have cross-origin isolation enabled if it itself has the appropriate COOP and COEP headers set as well as every one of its embedding ancestors. + +### Troubleshooting Cross-origin Isolation in OHIF + +The [SharedArrayBuffer in OHIF](#sharedarraybuffer-in-ohif) section describes how to determine if there are problems with cross-origin isolation in OHIF. If it is determined that COOP and/or COEP is indeed an issue, then the COOP and COEP headers must be set for OHIF. How to accomplish this varies per server or service that is hosting OHIF. The following are just a few examples. + +:::tip +In the default dev environment, the following can be set in the webpack.pwa.js fileโ€ฆ + +```javascript +devServer: { + headers: { + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp" + } +} +``` +::: + +:::tip +If deploying OHIF using Netlify, the Netlify configuration [file](https://docs.netlify.com/configure-builds/file-based-configuration/) can be used to configure the headers as suchโ€ฆ + +``` +[[headers]] + # Define which paths this specific [[headers]] block will cover. + for = "/*" + + [headers.values] + Cross-Origin-Opener-Policy = "same-origin" + Cross-Origin-Embedder-Policy = "require-corp" +``` +::: + +:::tip +If OHIF is served behind nginx, then the headers can be set in the nginx.conf file as follows. The [template nginx configuration file](https://github.com/OHIF/Viewers/blob/master/.docker/Viewer-v3.x/default.conf.template) for creating a [OHIF Docker image](./docker.md#building-the-docker-image) has an example of this too. +```nginx +server { + location / { + add_header Cross-Origin-Opener-Policy same-origin; + add_header Cross-Origin-Embedder-Policy require-corp; + } +} +``` +::: + +## CORP - Cross-Origin Resource Policy + +The CORP HTTP response header indicates which origins can read and use a resource. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy) for more information. + +### Header Values (see [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy#usage) for more information) + +|Value|Description| +|-----|-----------| +|same-site|Only requests from the same site can read the resource.| +|same-origin|Only requests from the same origin can read the resource.| +|cross-origin|Requests from any origin can read the resource. The value is useful and [exists](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy#relationship_to_cross-origin_embedder_policy_coep) primarily for letting documents with the [COEP require-corp value](#header-values-pertinent-to-ohif-see-mdn-for-more-information-1) know that the resource is ok to be embedded| + +### OHIF and CORP + +There are two scenarios where the CORP header is relevant to OHIF: + +- [PDF from a Cross Origin DICOMweb Data Source](#pdf-from-a-cross-origin-dicomweb-data-source) +- [OHIF as a Cross-origin Resource in an iframe](#ohif-as-a-cross-origin-resource-in-an-iframe) + +Both these scenarios stem from the fact that OHIF has to be served with the [COEP](#coep---cross-origin-embedder-policy) header to support [SharedArrayBuffer](#sharedarraybuffer). + +#### PDF from a Cross Origin DICOMweb Data Source + +There are some DICOMweb data sources (e.g. dcm4chee) whereby OHIF uses the data sourceโ€™s `/rendered` endpoint to embed a DICOM PDF document in the OHIF DOM using an `` tag. + +As specified for the [COEP require-corp value](#header-values-pertinent-to-ohif-see-mdn-for-more-information-1), a page like OHIF with COEP header `require-corp` can embed cross-origin resources in DOM elements that have the [`crossorigin` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin) OR the resource is delivered with an appropriate CORP header. The `` tag does NOT support the `crossorigin` attribute. As such, the PDF must be delivered with a CORP header. + +:::tip +Setting the CORP header varies per server or service that is hosting the data source. The following is just one example. + +For a dcm4chee DICOMweb data source composed in Docker behind nginx, the CORP header can be configured in the nginx.conf file as such: + +```nginx +add_header 'Cross-Origin-Resource-Policy' 'cross-origin' always; +``` + +If the dcm4chee server and the OHIF server are hosted on the same site, then the following would also work: +```nginx +add_header 'Cross-Origin-Resource-Policy' 'same-site' always; +``` +::: + +#### OHIF as a Cross-origin Resource in an iframe + +There are cases where [OHIF is embedded in an iframe](./iframe.md) and the embedding page is from a different origin. Again due to the [security requirements for SharedArrayBuffer](#security-requirements), [both OHIF and the embedding page](#iframe) must have the appropriate COEP header. In this scenario, OHIF is the cross-origin resource and since the ` + + + +### Troubleshooting + +_Exit code 137_ + +This means Docker ran out of memory. Open Docker Desktop, go to the `advanced` +tab, and increase the amount of Memory available. + +_Cannot create container for service X_ + +Use this one with caution: `docker system prune` + +_X is already running_ + +Stop running all containers: + +- Win: `docker ps -a -q | ForEach { docker stop $_ }` +- Linux: `docker stop $(docker ps -a -q)` + + +_Traceback (most recent call last):_ + _File "urllib3/connectionpool.py", line 670, in urlopen_ + _...._ + +Are you sure your docker is running? see explanation [here](https://github.com/docker/compose/issues/7896) + + +### Configuration + +After verifying that everything runs with default configuration values, you will +likely want to update: + +- The domain: `http://127.0.0.1` + +#### OHIF Viewer + +The OHIF Viewer's configuration is imported from a static `.js` file. The +configuration we use is set to a specific file when we build the viewer, and +determined by the env variable: `APP_CONFIG`. You can see where we set its value +in the `dockerfile` for this solution: + +`ENV APP_CONFIG=config/docker-nginx-orthanc.js` + +You can find the configuration we're using here: +`/public/config/docker-nginx-orthanc.js` + +To rebuild the `webapp` image created by our `dockerfile` after updating the +Viewer's configuration, you can run: + +- `docker-compose build` OR +- `docker-compose up --build` + +#### Other + +All other files are found in: `/docker/Nginx-Orthanc/` + +| Service | Configuration | Docs | +| ----------------- | --------------------------------- | ------------------------------------------- | +| OHIF Viewer | [dockerfile][dockerfile] | You're reading them now! | +| Nginx | [`/nginx.conf`][config-nginx] | | +| Orthanc | [`/orthanc.json`][config-orthanc] | [Here][orthanc-docs] | + +## Next Steps + +### OHIF + Dcm4chee + +You can follow the similar steps above to run OHIF Viewer with Dcm4chee PACS. + +The recipe for this setup can be found at `platform/app/.recipes/Nginx-Dcm4chee`. + + +The routes are as follows: +- `127.0.0.1` for the OHIF viewer +- `127.0.0.1/pacs` for the Dcm4chee UI + +:::info +For uploading studies, you can see the following gif for the steps: + +![alt text](../assets/img/dcm4chee-upload.gif) + +::: + +### Deploying to Production + +While you can deploy this solution to production, there is one main caveat: every user can access the app and the patient portal without any authentication. In the next step, we will add authentication with Keycloak to secure the app. + + + + +### Improving This Guide + +Here are some improvements this guide would benefit from, and that we would be +more than happy to accept Pull Requests for: + +- Add Docker caching for faster builds + + + +### Referenced Articles + +For more documentation on the software we've chosen to use, you may find the +following resources helpful: + +- [Orthanc for Docker](http://book.orthanc-server.com/users/docker.html) + +For a different take on this setup, check out the repositories our community +members put together: + +- [mjstealey/ohif-orthanc-dimse-docker](https://github.com/mjstealey/ohif-orthanc-dimse-docker) +- [trypag/ohif-orthanc-postgres-docker](https://github.com/trypag/ohif-orthanc-postgres-docker) + + + + + +[nginx]: https://www.nginx.com/resources/glossary/nginx/ +[understanding-cors]: https://medium.com/@baphemot/understanding-cors-18ad6b478e2b +[orthanc-docs]: http://book.orthanc-server.com/users/configuration.html#configuration +[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc + +[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/app/.recipes/OpenResty-Orthanc/dockerfile +[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/app/.recipes/OpenResty-Orthanc/config/nginx.conf +[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/app/.recipes/OpenResty-Orthanc/config/orthanc.json + diff --git a/platform/docs/versioned_docs/version-3.9/deployment/static-assets.md b/platform/docs/versioned_docs/version-3.9/deployment/static-assets.md new file mode 100644 index 0000000..4186a1b --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/deployment/static-assets.md @@ -0,0 +1,176 @@ +--- +sidebar_position: 3 +--- + +# Deploy Static Assets + +> WARNING! All of these solutions stand-up a publicly accessible web viewer. Do +> not hook your hosted viewer up to a sensitive source of data without +> implementing authentication. + +There are a lot of options for deploying static assets. Some services, like +`netlify` and `surge.sh`, specialize in static websites. You'll notice that +deploying with them requires much less time and effort, but comes at the cost of +less product offerings. + +While not required, it can simplify things to host your Web Viewer alongside +your image archive. Services with more robust product offerings, like +`Google Cloud`, `Microsoft's Azure`, and `Amazon Web Services (AWS)`, are able +to accommodate this setup. + +_Drag-n-drop_ + +- [Netlify: Drop](#netlify-drop) + +_Easy_ + +- [Surge.sh](#surgesh) +- [GitHub Pages](#github-pages) + +_Advanced_ + +- [Deploy Static Assets](#deploy-static-assets) + - [Drag-n-drop](#drag-n-drop) + - [Netlify Drop](#netlify-drop) + - [Easy](#easy) + - [Surge.sh](#surgesh) + - [GitHub Pages](#github-pages) + - [Advanced](#advanced) + - [AWS S3 + Cloudfront](#aws-s3--cloudfront) + - [GCP + Cloudflare](#gcp--cloudflare) + - [Azure](#azure) + +## Drag-n-drop + +### Netlify Drop + + +
+ +
+ + +_GIF demonstrating deployment with Netlify Drop_ + +1. https://app.netlify.com/drop +2. Drag your `build/` folder on to the drop target +3. ... +4. _annnd you're done_ + +**Features:** + +- Custom domains & HTTPS +- Instant Git integration +- Continuous deployment +- Deploy previews +- Access to add-ons + +(Non-free tiers include identity, FaaS, Forms, etc.) + +Learn more about [Netlify on their website](https://www.netlify.com/) + +## Easy + +### Surge.sh + +> Static web publishing for Front-End Developers. Simple, single-command web +> publishing. Publish HTML, CSS, and JS for free, without leaving the command +> line. + +![surge.sh deploy example](../assets/img/surge-deploy.gif) + +_GIF demonstrating deployment with surge_ + +```shell +# Add surge command +yarn global add surge + +# In the build directory +surge +``` + +**Features:** + +- Free custom domain support +- Free SSL for surge.sh subdomains +- pushState support for single page apps +- Custom 404.html pages +- Barrier-free deployment through the CLI +- Easy integration into your Grunt toolchain +- Cross-origin resource support +- And moreโ€ฆ + +Learn more about [surge.sh on their website](https://surge.sh/) + +### GitHub Pages + +> WARNING! While great for project sites and light use, it is not advised to use +> GitHub Pages for production workloads. Please consider using a different +> service for mission critical applications. + +> Websites for you and your projects. Hosted directly from your GitHub +> repository. Just edit, push, and your changes are live. + +This deployment strategy makes more sense if you intend to maintain your project in +a GitHub repository. It allows you to specify a `branch` or `folder` as the +target for a GitHub Page's website. As you push code changes, the hosted content +updates to reflect those changes. + +1. Head over to GitHub.com and create a new repository, or go to an existing + one. Click on the Settings tab. +2. Scroll down to the GitHub Pages section. Choose the `branch` or `folder` you + would like as the "root" of your website. +3. Fire up a browser and go to `http://username.github.io/repository` + +Configuring Your Site: + +- [Setting up a custom domain](https://help.github.com/en/articles/using-a-custom-domain-with-github-pages) +- [Setting up SSL](https://help.github.com/en/articles/securing-your-github-pages-site-with-https) + +Learn more about [GitHub Pages on its website](https://pages.github.com/) + +## Advanced + +All of these options, while using providers with more service offerings, +demonstrate how to host the viewer with their respective file storage and CDN +offerings. While you can serve your static assets this way, if you're going +through the trouble of using AWS/GCP/Azure, it's more likely you're doing so to +avoid using a proxy or to simplify authentication. + +If that is the case, check out some of our more advanced `docker` deployments +that target these providers from the left-hand sidepanel. + +These guides can be a bit longer and an update more frequently. To provide +accurate documentation, we will link to each provider's own recommended steps: + +### AWS S3 + Cloudfront + +- [Host a Static Website](https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html) +- [Speed Up Your Website with Cloudfront](https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-cloudfront-walkthrough.html) + +### GCP + Cloudflare + +- [Things to Know Before Getting Started](https://code.luasoftware.com/tutorials/google-cloud-storage/things-to-know-before-hosting-static-website-on-google-cloud-storage/) +- [Hosting a Static Website on GCP](https://cloud.google.com/storage/docs/hosting-static-website) + +### Azure + + - Deploying viewer to Azure blob storage as a static website: + Refer to [Host a static website](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website) + High level steps : + 1. Go to Azure portal and create a storage account. + 2. Under Overview->Capabilities, select Static website. + 3. Enable Static website. Set the index document as โ€˜index.htmlโ€™. + 4. Copy the primary endpoint. This will serve as the root URL for the viewer. + 5. Save. A new container named โ€˜$webโ€™ will be created. + 6. Copy OHIF viewerโ€™s build output from โ€˜platform\app\distโ€™ folder to the โ€˜$webโ€™ container. + 7. Open browser and navigate to the viewer root URL copied in the step above. It should display OHIF viewer with data from default data source. + + ![image](https://github.com/OHIF/Viewers/assets/132684122/236a574b-0f05-4d90-a721-df8720d05949) + Special consideration while accessing DicomJson data source : + โ€ข Due to the way routing is handled in react, it may error out in production when trying to display data through dicomJson data source. E.g. https://[Static Website endpoint]/viewer/dicomjson?url= https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001.json + โ€ข Resolution to this is to set error page to โ€˜index.htmlโ€™ at the website level. This will ensure that all errors are redirected to root and requests are further served from root path. + ![image](https://github.com/OHIF/Viewers/assets/132684122/87696c90-c344-489a-af15-b992434555f9) + +- [Add SSL Support](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-https-custom-domain-cdn) +- [Configure a Custom Domain](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-custom-domain-name) diff --git a/platform/docs/versioned_docs/version-3.9/deployment/user-account-control.md b/platform/docs/versioned_docs/version-3.9/deployment/user-account-control.md new file mode 100644 index 0000000..87f0235 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/deployment/user-account-control.md @@ -0,0 +1,524 @@ +--- +sidebar_position: 11 +--- +# User Account Control + + +:::danger +DISCLAIMER: We make no claims or guarantees regarding the security of this approach. If you have any doubts, please consult an expert and conduct thorough audits. +::: + +Making a viewer and its medical imaging data accessible on the open web can +provide a lot of benefits, but requires additional security to make sure +sensitive information can only be viewed by authorized individuals. Most image +archives are equipped with basic security measures, but they are not +robust/secure enough for the open web. + +This guide covers one of many potential production setups that secure our +sensitive data. + +## Overview + +This guide builds on top of our +[Nginx + Image Archive guide](./nginx--image-archive.md), +wherein we used a [`reverse proxy`](https://en.wikipedia.org/wiki/Reverse_proxy) +to retrieve resources from our image archive (Orthanc). + +To add support for "User Account Control" we introduce +[Keycloak](https://www.keycloak.org/about.html). Keycloak is an open source +Identity and Access Management solution that makes it easy to secure +applications and services with little to no code. We improve upon our +`reverse proxy` setup by integrating Keycloak and Nginx to create an +`authenticating reverse proxy`. + +> An authenticating reverse proxy is a reverse proxy that only retrieves the +> resources on behalf of a client if the client has been authenticated. If a +> client is not authenticated they can be redirected to a login page. + +This setup allows us to create a setup similar to the one pictured below: + +![userControlFlow](../assets/img/ohif-pacs-keycloak.png) + + + +**Nginx:** + +- Acts as a reverse proxy server that handles incoming requests to the domain (mydomain.com:80) and forwards them to the appropriate backend services. +- It also ensures that all requests go through the OAuth2 Proxy for authentication. + + +**OAuth2 Proxy:** + +- Serves as an intermediary that authenticates users via OAuth2. +- Works in conjunction with Keycloak to manage user sessions and authentication tokens. +- Once the user is authenticated, it allows access to specific routes (/ohif-viewer, /pacs, /pacs-admin). + +**Keycloak:** + +- An open-source identity and access management solution. +- Manages user identities, including authentication and authorization. +- Communicates with the OAuth2 Proxy to validate user credentials and provide tokens for authenticated sessions. + +**OHIF Viewer:** + +- Hosted under the route /ohif-viewer, which serves the static assets of the OHIF Viewer. + +**Orthanc/DCM4chee:** + +- PACS (Picture Archiving and Communication System) for managing medical imaging data. +Exposes two routes: +- /pacs: Accesses the DICOM web services. +- /pacs-admin: Provides administrative and explorer interfaces. + + + +## Getting Started - Orthanc + + +### Requirements + +- Docker + - [Docker for Mac](https://docs.docker.com/docker-for-mac/) + - [Docker for Windows](https://docs.docker.com/docker-for-windows/) + +_Not sure if you have `docker` installed already? Try running `docker --version` +in command prompt or terminal_ + +### Setup 1 - Trying Locally + +Navigate to the Orthanc Keycloak configuration directory: + +`cd platform\app\.recipes\Nginx-Orthanc-Keycloak` + +Due to the increased complexity of this setup, we've introduced a magic word `YOUR_DOMAIN`. Replace this word with your project IP address to follow along more easily. + +Since we are running this locally, we will use `127.0.0.1` as our IP address. + +In the `docker-compose.yml` file, replace `YOUR_DOMAIN` with `127.0.0.1`. + +In the Keycloak service: + + +Before: + +``` +KC_HOSTNAME_ADMIN_URL: http://YOUR_DOMAIN/keycloak/ +KC_HOSTNAME_URL: http://YOUR_DOMAIN/keycloak/ +``` + + +After + +``` +KC_HOSTNAME_ADMIN_URL: http://127.0.0.1/keycloak/ +KC_HOSTNAME_URL: http://127.0.0.1/keycloak/ +``` + +In the Keycloak healthcheck, replace `YOUR_DOMAIN` with `localhost`. + +In the Nginx config, change: + +``` +server_name YOUR_DOMAIN; +``` + +to: + +``` +server_name 127.0.0.1; +``` + +Since we're not using SSL, remove the following lines from the Nginx config file and create one server instead of two: + +Before (two servers one for http and one for https): + +``` +server { + listen 80; + server_name YOUR_DOMAIN; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + server_name YOUR_DOMAIN; + + ssl_certificate /etc/letsencrypt/live/ohifviewer.duckdns.org/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/ohifviewer.duckdns.org/privkey.pem; + + root /var/www/html; +``` + +After (merging both servers into one only http server): + +``` +server { + listen 80; + server_name 127.0.0.1; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + root /var/www/html; +``` + +In OAuth2-proxy configuration at `oauth2-proxy.cfg` + +Before: + +``` +redirect_url="http://YOUR_DOMAIN/oauth2/callback" +oidc_issuer_url="http://YOUR_DOMAIN/keycloak/realms/ohif" +``` + +After: + +``` +redirect_url="http://127.0.0.1/oauth2/callback" +oidc_issuer_url="http://127.0.0.1/keycloak/realms/ohif" +``` + +Finally, in the docker-nginx-orthanc-keycloak config file that lives in `platform/app/public/config/docker-nginx-orthanc-keycloak.js`, replace `YOUR_DOMAIN` with + +Before: + +``` +wadoUriRoot: 'http://YOUR_DOMAIN/pacs', +qidoRoot: 'http://YOUR_DOMAIN/pacs', +wadoRoot: 'http://YOUR_DOMAIN/pacs', +``` + +After: + +``` +wadoUriRoot: 'http://127.0.0.1/pacs', +qidoRoot: 'http://127.0.0.1/pacs', +wadoRoot: 'http://127.0.0.1/pacs', +``` + +:::note +This is the config that is used inside the dockerfile to build the viewer, look at dockerfile + +`ENV APP_CONFIG=config/docker-nginx-orthanc-keycloak.js` +::: + +Run the following command to start the services: + +``` +docker-compose up --build +``` + + +You can watch the following video, which will guide you through the process of setting up Orthanc with keycloak and OHIF locally. + +We have set up two predefined users in Keycloak: + +- `user: admin password: admin` - Has access to keycloak portal for managing users and clients +- `user: viewer password: viewer` - Has access to the OHIF Viewer but not the pacs-admin +- `user: pacsadmin password: pacsadmin` - Has access to both the pacs-admin for uploading and the OHIF Viewer + +You can navigate to: + +- `http://127.0.0.1` - This will redirect you to `http://127.0.0.1/ohif-viewer`, prompting you to log in with Keycloak using either user +- `http://127.0.0.1/pacs-admin` - Only the `pacsadmin` user can access this route, while the `viewer` user cannot +- + +
+ +
+ + +### Step 2 - Trying via a Server + +Now that you have successfully set up Orthanc with Keycloak and OHIF locally, you can deploy it to a server. While you can rent a server from any provider, this tutorial will demonstrate the process using Linode as an example. + +You can watch the following video, which will guide you through the process. + +Some notes: + +- Since this is a remote machine we need to clone the repo +- Typically a Linux machine, you need to download and install Docker on it +- Use the Visual Studio Code Remote SSH extension to connect to the server +- Use docker extension in Visual Studio Code to manage the containers +- The public IP address of the server now becomes the YOUR_DOMAIN and is used in the configuration files. + +Still we have not set up SSL, so we will use HTTP instead of HTTPS. + +We should use the same one server configuration as we did locally for Nginx (but with the new server IP address) + +:::info +Don't forget to change the `docker-ngix-orthanc-keycloak.js` file to use the new server IP address. +::: + +After you run `docker compose up --build` you can navigate to the server IP address and see the viewer will not work... + +We have encountered some strange issues with the Keycloak service not allowing non-HTTPS connections (around 10:00). To resolve this, we need to modify the Keycloak configuration to permit HTTPS. This requires accessing the container and making the necessary changes. + +After accessing the container shell + +``` +cd /opt/keycloak/bin + +./kcadm.sh config credentials --server http://localhost:8080 --realm master --user admin +./kcadm.sh update realms/master -s sslRequired=NONE +``` + +After we need to change some configurations in the Keycloak UI to enable the connection in the server + +Navigate to + +``` +http://IP_ADDRESS/keycloak +``` + +which will redirect you to the Keycloak login page + +0. login with the admin user `admin` and password `admin` +1. From the top left drop down menu, select `ohif` realm +2. Go to `Clients` and select `ohif_viewer` +3. In the `Access Settings` change all instances of `http://127.0.0.1` to `http://IP_ADDRESS` + 1. Root URL: `http://IP_ADDRESS` + 2. Home URL: `http://IP_ADDRESS` + 3. Valid Redirect URIs: `http://IP_ADDRESS/oauth2/callback` + 4. Valid post logout URIs: `*` + 5. Web Origins: `http://IP_ADDRESS` + 6. Admin URL: `http://IP_ADDRESS` + +Now if you navigate to the IP address it should work !! + + +
+ +
+ +### Step 3 - Adding SSL and Deploying to Production + +Now we'll add an SSL certificate to our server to enable HTTPS. We'll use Let's Encrypt to generate the SSL certificate. + +Let's Encrypt requires a domain name, so we'll use a free domain name service like DuckDNS (duckdns.org). Follow these steps: + +1. Visit https://www.duckdns.org/ and create an account. +2. Create a free domain name and point it to your server's IP address. + +You can watch a video guide for this process if needed. + +Replace `YOUR_DOMAIN` with your new domain name in the `docker-compose.yml` file and all other config files, as we did previously. + +Next, we'll add HTTPS support. Add the following lines to the Nginx config file: + +(Note: We'll have both HTTP and HTTPS servers, and the server IP will use HTTPS) +``` +server { + listen 80; + server_name https://IP_ADDRESS; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + server_name https://IP_ADDRESS; + + ssl_certificate /etc/letsencrypt/live/ohifviewer.duckdns.org/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/ohifviewer.duckdns.org/privkey.pem; + + root /var/www/html; +``` + +Don't forget to replace `YOUR_DOMAIN` with the new domain name in the `docker-nginx-orthanc-keycloak.js` file. + +:::info +Remember to include `https://` when adding the domain name to the configurations. +::: + +Now, we need to add a certificate. Let's assume we have the domain name `hospital.duckdns.org` and the email we registered with DuckDNS is `your_email@example.com`. + +``` + docker run -it --rm --name certbot \ + -v ./config/letsencrypt:/etc/letsencrypt \ + -v ./config/certbot:/var/www/certbot \ + -p 80:80 \ + certbot/certbot certonly \ + --standalone \ + --preferred-challenges http \ + --email your_email@example.com \ + --agree-tos \ + --no-eff-email \ + -d hospital.duckdns.org +``` + +:::note +Replace "hospital.duckdns.org" with your domain name and update the email address accordingly. +::: + +:::warning +DuckDNS is suitable for testing and demonstration purposes only. For production environments, use a proper domain name and SSL certificate to ensure security. +::: + +If you follow these steps, you'll encounter the error `invalid parameter: redirect_uri` when attempting to log in to Keycloak. This occurs because the redirect URL isn't set up correctly in the Keycloak client configuration. To resolve this, we need to log in and adjust these settings. + +Navigate to: + +``` +http://IP_ADDRESS/keycloak +``` + +Log in using the admin credentials: +- Username: `admin` +- Password: `admin` + +Replace all IP addresses with the new domain name, using HTTPS. + +
+ +
+ + + + + + +## Getting Started - DCM4CHEE + + + + +You can follow the same steps as above to set up DCM4CHEE. The only difference is that you need to navigate to the correct directory. `platform\app\.recipes\Nginx-Dcm4chee-Keycloak` + +You can watch the following video, which will guide you through the process of setting up DCM4CHEE. + + +
+ +
+ + + +## Troubleshooting + + +_invalid parameter: redirect_uri_ + +This means the redirect URL isn't set up correctly in the Keycloak client configuration. To resolve this, log in to Keycloak and adjust the settings in the correct client (ohif_viewer) and correct realm (ohif). + +_Exit code 137_ + +This means Docker ran out of memory. Open Docker Desktop, go to the `advanced` +tab, and increase the amount of Memory available. + +_Cannot create container for service X_ + +Use this one with caution: `docker system prune` + +_X is already running_ + +Stop running all containers: + +- Win: `docker ps -a -q | ForEach { docker stop $_ }` +- Linux: `docker stop $(docker ps -a -q)` + + +#### OHIF Viewer + +The OHIF Viewer's configuration is imported from a static `.js` file. The +configuration we use is set to a specific file when we build the viewer, and +determined by the env variable: `APP_CONFIG`. You can see where we set its value +in the `dockerfile` for this solution: + +`ENV APP_CONFIG=config/docker-nginx-orthanc-keycloak.js` + +You can find the configuration we're using here: +`/public/config/docker-nginx-orthanc-keycloak.js` + +To rebuild the `webapp` image created by our `dockerfile` after updating the +Viewer's configuration, you can run: + +- `docker-compose build` OR +- `docker-compose up --build` + + + +## Next Steps + +### Keycloak Theming + +The `Login` screen for the `ohif-viewer` client is using a Custom Keycloak +theme. You can find the source files for it in +`platform/app/.recipes/deprecated-recipes/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes`. You can see how +we add it to Keycloak in the `docker-compose` file, and you can read up on how +to leverage custom themes in +[Keycloak's own docs](https://www.keycloak.org/docs/latest/server_development/index.html#_themes). + +| Default Theme | OHIF Theme | +| ---------------------------------------------------------------------- | ---------------------------------------------------------------- | +| ![Keycloak Default Theme](../assets/img/keycloak-default-theme.png) | ![Keycloak OHIF Theme](../assets/img/keycloak-ohif-theme.png) | + + + + + +## Resources + +### Referenced Articles + +The inspiration for our setup was driven largely by these articles: + +- [Securing Nginx with Keycloak](https://edhull.co.uk/blog/2018-06-06/keycloak-nginx) +- [Authenticating Reverse Proxy with Keycloak](https://eclipsesource.com/blogs/2018/01/11/authenticating-reverse-proxy-with-keycloak/) +- [Securing APIs with Kong and Keycloak](https://www.jerney.io/secure-apis-kong-keycloak-1/) + +For more documentation on the software we've chosen to use, you may find the +following resources helpful: + +- [Orthanc for Docker](http://book.orthanc-server.com/users/docker.html) +- [OpenResty Guide](http://www.staticshin.com/programming/definitely-an-open-resty-guide/) +- [Lua Ngx API](https://openresty-reference.readthedocs.io/en/latest/Lua_Nginx_API/) +- [Auth0: Picking a Grant Type](https://auth0.com/docs/api-auth/which-oauth-flow-to-use) + +We chose to use a generic OpenID Connect library on the client, but it's worth +noting that Keycloak comes packaged with its own: + +- [oidc-client-js](https://github.com/IdentityModel/oidc-client-js/wiki) +- [Keycloak JavaScript Adapter](https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter) + +If you're not already drowning in links, here are some good security resources +for OAuth: + +- [Diagrams of OpenID Connect Flows](https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660) +- [KeyCloak: OpenID Connect Flows](https://www.keycloak.org/docs/latest/securing_apps/index.html#authorization-code) + +For a different take on this setup, check out the repositories our community +members put together: + +- [mjstealey/ohif-orthanc-dimse-docker](https://github.com/mjstealey/ohif-orthanc-dimse-docker) +- [trypag/ohif-orthanc-postgres-docker](https://github.com/trypag/ohif-orthanc-postgres-docker) + + + + + +[orthanc-docs]: http://book.orthanc-server.com/users/configuration.html#configuration +[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc + +[config]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/src/config.js +[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/dockerfile +[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/nginx.conf +[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/orthanc.json +[config-keycloak]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/ohif-keycloak-realm.json + diff --git a/platform/docs/versioned_docs/version-3.9/development/_category_.json b/platform/docs/versioned_docs/version-3.9/development/_category_.json new file mode 100644 index 0000000..8627cac --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/development/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Development", + "position": 5 +} diff --git a/platform/docs/versioned_docs/version-3.9/development/android-ios-debugging.md b/platform/docs/versioned_docs/version-3.9/development/android-ios-debugging.md new file mode 100644 index 0000000..67c4273 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/development/android-ios-debugging.md @@ -0,0 +1,80 @@ +--- +sidebar_position: 12 +sidebar_label: Android & iOS Debugging +--- + +# Android & iOS Debugging for OHIF using Emulators + +This guide covers how to debug the OHIF viewer on Android and iOS emulators using Chrome DevTools and Safari Web Inspector, respectively. You can use these tools to inspect elements, debug JavaScript, and view console logs for the web content running on the emulators. + +## Android Emulator Setup with Android Studio + +### Prerequisites: +- Install [Android Studio](https://developer.android.com/studio) +- Ensure you have a recent Android SDK and Emulator installed via Android Studio +- Google Chrome installed on your machine + +### Steps to Run Android Emulator: + +1. **Launch Android Studio:** + - Open Android Studio and create a new project if you don't already have one. + - Once your IDE opens up, click on the **Device Manager** icon in the right-side toolbar. + +2. **Create a Virtual Device (if necessary):** + - If you donโ€™t have an existing virtual device, click **Create Virtual Device**. + - Choose a device model (e.g., Pixel series) and click **Next**. + - Select a system image with the required Android API version and click **Next**. + - Finish the setup by clicking **Finish**. + +3. **Start the Android Emulator:** + - Once the device is created, click the **Play** button next to the virtual device to start the emulator. + +4. **Open a Browser on the Emulator:** + - Once the emulator is running, open the **Chrome** app on the virtual device. + - Navigate to the OHIF Viewer URL to view the application. The URL will be 10.0.2.2:3000, you can read more about it [here](https://developer.android.com/studio/run/emulator-networking). + +5. **Debug Using Chrome DevTools:** + - On your development machine, open Google Chrome. + - Type `chrome://inspect` in the Chrome address bar and hit **Enter**. + - You will see your Android device listed under **Remote Target**. + - Click **Inspect** to open DevTools for the browser on the Android emulator. + +6. **Happy Debugging!:** + - You can now use Chrome DevTools to inspect elements, debug JavaScript, and view console logs directly from the emulatorโ€™s browser. + +### Video Tutorial + + + +--- + +## iOS Emulator Setup with Xcode + +### Prerequisites: +- Install [Xcode](https://developer.apple.com/xcode/) from the Mac App Store. +- Ensure you have the latest iOS SDK. + +### Steps to Run iOS Emulator: + +1. **Launch Xcode:** + - Open Xcode and navigate to **Xcode > Settings**. + - Go to the **Platform** tab and ensure you have an iOS simulator installed for the version of iOS you need. If not you can do so using the + button. + +2. **Start the iOS Simulator:** + - Open Xcode and navigate to **Xcode > Open Developer Tools > Simulator**. + - Select your device from the list of available simulators and click on it. + +3. **Open a Browser on the Simulator:** + - Run the **Safari** browser + +4. **Connect Safari DevTools to the iOS Simulator:** + - On your development machine, open **Safari** on your Mac. + - Click **Develop** in the menu bar and select your simulator under **Devices**. + - You will see the web pages open on the iOS simulator. Select the page to open the inspector. + +5. **Happy Debugging!:** + - You can now use the Safari Web Inspector to inspect elements, debug JavaScript, and view logs for the OHIF Viewer on the iOS simulator. + +### Video Tutorial + + diff --git a/platform/docs/versioned_docs/version-3.9/development/architecture.md b/platform/docs/versioned_docs/version-3.9/development/architecture.md new file mode 100644 index 0000000..f191f16 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/development/architecture.md @@ -0,0 +1,205 @@ +--- +sidebar_position: 2 +sidebar_label: Architecture +--- + +# Architecture + +In order to achieve a platform that can support various workflows and be +extensible for the foreseeable future we went through extensive planning of +possible use cases and decided to significantly change and improve the +architecture. + +Below, we aim to demystify that complexity by providing insight into how +`OHIF Platform` is architected, and the role each of its dependent libraries +plays. + +## Overview + +The [OHIF Medical Image Viewing Platform][viewers-project] is maintained as a +[`monorepo`][monorepo]. This means that this repository, instead of containing a +single project, contains many projects. If you explore our project structure, +you'll see the following: + +```bash +โ”‚ +โ”œโ”€โ”€ extensions +โ”‚ โ”œโ”€โ”€ default # default functionalities +โ”‚ โ”œโ”€โ”€ cornerstone # 2D/3D images w/ Cornerstonejs +โ”‚ โ”œโ”€โ”€ cornerstone-dicom-sr # Structured reports +โ”‚ โ”œโ”€โ”€ measurement-tracking # measurement tracking +โ”‚ โ””โ”€โ”€ dicom-pdf # View DICOM wrapped PDFs in viewport +| # and many more ... +โ”‚ +โ”œโ”€โ”€ modes +โ”‚ โ””โ”€โ”€ longitudinal # longitudinal measurement tracking mode +| โ””โ”€โ”€ basic-dev-mode # basic viewer with Cornerstone (a developer focused mode) +| # and many more +โ”‚ +โ”œโ”€โ”€ platform +โ”‚ โ”œโ”€โ”€ core # Business Logic +โ”‚ โ”œโ”€โ”€ i18n # Internationalization Support +โ”‚ โ”œโ”€โ”€ ui # React component library +โ”‚ โ””โ”€โ”€ app # Connects platform and extension projects +โ”‚ +โ”œโ”€โ”€ ... # misc. shared configuration +โ”œโ”€โ”€ lerna.json # MonoRepo (Lerna) settings +โ”œโ”€โ”€ package.json # Shared devDependencies and commands +โ””โ”€โ”€ README.md +``` + +OHIF v3 is composed of the following components, described in detail in further +sections: + +- `@ohif/app`: The core framework that controls extension registration, mode + composition and routing. +- `@ohif/core`: A library of useful and reusable medical imaging functionality + for the web. +- `@ohif/ui`: A library of reusable components to build OHIF-styled applications + with. +- `Extensions`: A set of building blocks for building applications. The OHIF org + maintains a few core libraries. +- `Modes`: Configuration objects that tell @ohif/app how to compose + extensions to build applications on different routes of the platform. + +## Extensions + +The `extensions` directory contains many packages that provide essential +functionalities such as rendering, study/series browsers, measurement tracking +that modes can consume to enable a certain workflow. Extensions have had their +behavior changed in `OHIF-v3` and their api is expanded. In summary: + +> In `OHIF-v3`, extensions no longer automatically hook themselves to the app. +> Now, registering an extension makes its component available to `modes` that +> wish to use them. Basically, extensions in `OHIF-v3` are **building blocks** +> for building applications. + +OHIF team maintains several high value and commonly used functionalities in its +own extensions. For a list of extensions maintained by OHIF, +[check out this helpful table](../platform/extensions/index.md#maintained-extensions). +As an example `default` extension provides a default viewer layout, a +study/series browser and a datasource that maps to a DICOMWeb compliant backend. + +[Click here to read more about extensions!](../platform/extensions/index.md) + +## Modes + +The `modes` directory contains workflows that can be registered with OHIF within +certain `routes`. The mode will get used once the user opens the viewer on the +registered route. + +OHIF extensions were designed to provide certain core functionalities for +building your viewer. However, often in medical imaging we face a specific use +case in which we are using some core functionalities, adding our specific UI, +and use it in our workflows. Previously, to achieve this you had to create an +extension to add have such feature. `OHIF-v3` introduces `Modes` to enable +building such workflows by re-using the core functionalities from the +extensions. + +Some common workflows may include: + +- Measurement tracking for lesions +- Segmentation of brain abnormalities +- AI probe mode for detecting prostate cancer + +In the mentioned modes above, they will share the same core rendering module +that the `default` extension provides. However, segmentation mode will require +segmentation tools which is not needed for the other two. As you can see, modes +are a layer on top of extensions, that you can configure in order to achieve +certain workflows. + +To summarize the difference between extensions and modes in `OHIF-v3` and +extensions in `OHIF-v2` + +> - `Modes` are configuration objects that tell _@ohif/app_ how to compose +> extensions to build applications on different routes of the platform. +> - In v2 extensions are โ€œpluginsโ€ that add functionality to a core viewer. +> - In v3 extensions are building blocks that a mode uses to build an entire +> viewer layout. + +[Click here to read more about modes!](../platform/modes/index.md) + +## Platform + +### `@ohif/app` + +This library is the core library which consumes modes and extensions and builds +an application. Extensions can be passed in as app configuration and will be +consumed and initialized at the appropriate time by the application. Upon +initialization the viewer will consume extensions and modes and build up the +route desired, these can then be accessed via the study list, or directly via +url parameters. + +Upon release modes will also be plugged into the app via configuration, but this +is still an area which is under development/discussion, and they are currently +pulled from the window in beta. + +Future ideas for this framework involve only adding modes and fetching the +required extension versions at either runtime or build time, but this decision +is still up for discussion. + +### `@ohif/core` + +OHIF core is a carefully maintained and tested set of web-based medical imaging +functions and classes. This library includes managers and services used from +within the viewer app. + +OHIF core is largely similar to the @ohif/core library in v2, however a lot of +logic has been moved to extensions: however all logic about DICOMWeb and other +data fetching mechanisms have been pulled out, as these now live in extensions, +described later. + +### `@ohif/ui` + +Firstly, a large time-consumer/barrier for entry we discovered was building new +UI in a timely manner that fit OHIFโ€™s theme. For this reason we have built a new +UI component library which contains all the components one needs to build their +own viewer. + +These components are presentational only, so you can reuse them with whatever +logic you desire. As the components are presentational, you may swap out +@ohif/ui for a custom UI library with conforming API if you wish to white label +the viewer. The UI library is here to make development easier and quicker, but +it is not mandatory for extension components to use. + +[Check out our component library!](https://ui.ohif.org/) + +## Overview of the architecture + +OHIF-v3 architecture can be seen in the following figure. We will explore each +piece in more detail. + +![mode-archs](../assets/img/mode-archs.png) + +## Common Questions + +> Can I create my own Viewer using Vue.js or Angular.js? + +You can, but you will not be able to leverage as much of the existing code and +components. `@ohif/core` could still be used for business logic, and to provide +a model for extensions. `@ohif/ui` would then become a guide for the components +you would need to recreate. + +> When I want to implement a functionality, should it be in the mode or in an +> extension? + +This is a great question. Modes are designed to consume extensions, so you +should implement your functionality in one of the modules of your new extension, +and let the mode consume it. This way, in the future, if you needed another mode +that utilizes the same functionality, you can easily hook the extension to the +new mode as well. + + + + +[monorepo]: https://github.com/OHIF/Viewers/issues/768 +[viewers-project]: https://github.com/OHIF/Viewers +[viewer-npm]: https://www.npmjs.com/package/@ohif/app +[pwa]: https://developers.google.com/web/progressive-web-apps/ +[configuration]: ../configuration/configurationFiles.md +[extensions]: ../platform/extensions/index.md +[core-github]: https://github.com/OHIF/viewers/platform/core +[ui-github]: https://github.com/OHIF/Viewers/tree/master/platform/ui + diff --git a/platform/docs/versioned_docs/version-3.9/development/continuous-integration.md b/platform/docs/versioned_docs/version-3.9/development/continuous-integration.md new file mode 100644 index 0000000..48ad4c4 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/development/continuous-integration.md @@ -0,0 +1,90 @@ +--- +sidebar_position: 8 +sidebar_label: Continuous Integration +--- + +# Continuous Integration (CI) + +This repository uses `CircleCI` and `Netlify` for Continuous integration. + +## Deploy Previews + +[Netlify Deploy previews][deploy-previews] are generated for every pull request. +They allow pull request authors and reviewers to "Preview" the OHIF Viewer as if +the changes had been merged. + +Deploy previews can be configured by modifying the `netlify.toml` file in the +root of the repository. Some additional scripts/assets for netlify are included +in the root `.netlify` directory. + +## Workflows + +[CircleCI Workflows][circleci-workflows] are a set of rules for defining a +collection of jobs and their run order. They are self-documenting and their +configuration can be found in our CircleCI configuration file: +`.circleci/config.yml`. + +### Workflow: PR_CHECKS + +The PR_CHECKS workflow (Pull Request Checks) runs our automated unit and +end-to-end tests for every code check-in. These tests must all pass before code +can be merged to our `master` branch. + +![PR_CHECKS](../assets/img/WORKFLOW_PR_CHECKS.png) + +### Workflow: PR_OPTIONAL_DOCKER_PUBLISH + +The PR_OPTIONAL_DOCKER_PUBLISH workflow allows for "manual approval" to publish +the pull request as a tagged docker image. This is helpful when changes need to +be tested with the Google Adapter before merging to `master`. + +![PR_Workflow](../assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png) + +> NOTE: This workflow will fail unless it's for a branch on our `upstream` +> repository. If you need this functionality, but the branch is from a fork, +> merge the changes to a short-lived `feature/` branch on `upstream` + +### Workflow: DEPLOY + +The DEPLOY workflow deploys the OHIF Viewer when changes are merged to master. +It uses the Netlify CLI to deploy assets created as part of the repository's PWA +Build process (`yarn run build`). The workflow allows for "Manual Approval" to +promote the build to `STAGING` and `PRODUCTION` environments. + +![WORKFLOW_DEPLOY](../assets/img/WORKFLOW_DEPLOY.png) + +| Environment | Description | URL | +| ----------- | ---------------------------------------------------------------------------------- | --------------------------------------------- | +| Development | Always reflects latest changes on `master` branch. | [Netlify][netlify-dev] / [OHIF][ohif-dev] | +| Staging | For manual testing before promotion to prod. Keeps development workflow unblocked. | [Netlify][netlify-stage] / [OHIF][ohif-stage] | +| Production | Stable, tested, updated less frequently. | [Netlify][netlify-prod] / [OHIF][ohif-prod] | + +### Workflow: RELEASE + +The RELEASE workflow publishes our `npm` packages, updated documentation, and +`docker` image when changes are merged to master. `Lerna` and "Semantic Commit +Syntax" are used to independently version and publish the many packages in our +monorepository. If a new version is cut/released, a Docker image is created. +Documentation is generated with `gitbook` and pushed to our `gh-pages` branch. +GitHub hosts the `gh-pages` branch with GitHub Pages. + +- Platform Packages: https://github.com/ohif/viewers/#platform +- Extension Packages: https://github.com/ohif/viewers/#extensions +- Documentation: https://docs.ohif.org/ + +![WORKFLOW_RELEASE](../assets/img/WORKFLOW_RELEASE.png) + + + + +[deploy-previews]: https://www.netlify.com/blog/2016/07/20/introducing-deploy-previews-in-netlify/ +[circleci-workflows]: https://circleci.com/docs/2.0/workflows/ +[netlify-dev]: https://ohif-dev.netlify.com +[netlify-stage]: https://ohif-stage.netlify.com +[netlify-prod]: https://ohif-prod.netlify.com +[ohif-dev]: https://viewer-dev.ohif.org +[ohif-stage]: https://viewer-stage.ohif.org +[ohif-prod]: https://viewer-prod.ohif.org + diff --git a/platform/docs/versioned_docs/version-3.9/development/contributing.md b/platform/docs/versioned_docs/version-3.9/development/contributing.md new file mode 100644 index 0000000..62c5934 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/development/contributing.md @@ -0,0 +1,152 @@ +--- +sidebar_position: 5 +sidebar_label: Contributing +--- + +# Contributing + +## How can I help? + +Fork the repository, make your change and submit a pull request. If you would +like to discuss the changes you intend to make to clarify where or how they +should be implemented, please don't hesitate to create a new issue. At a +minimum, you may want to read the following documentation: + +- [Getting Started](/development/getting-started.md) +- [Architecture](./architecture.md) + +Pull requests that are: + +- Small +- [Well tested](./testing.md) +- Decoupled + +Are much more likely to get reviewed and merged in a timely manner. + +## When changes impact multiple repositories + +While this can be tricky, we've tried to reduce how often this situation crops +up this with our [recent switch to a monorepo][monorepo]. Our maintained +extensions, ui components, internationalization library, and business logic can +all be developed by simply running `yarn run dev` from the repository root. + +Testing the viewer with locally developed, unpublished package changes from a +package outside of the monorepo is most common with extension development. Let's +demonstrate how to accomplish this with two commonly forked extension +dependencies: + +### `cornerstone-tools` + +On your local file system: + +```bash title="/my-projects/" +โ”œโ”€โ”€ cornerstonejs/cornerstone-tools +โ””โ”€โ”€ ohif/viewers +``` + +- Open a terminal/shell +- Navigate to `cornerstonejs/cornerstone-tools` + - `yarn install` + - [`yarn link`](https://yarnpkg.com/en/docs/cli/link) + - `yarn run dev` + +* Open a new terminal/shell +* Navigate to `ohif/viewers` (the root of ohif project) + - `yarn install` + - [`yarn link cornerstone-tools`](https://yarnpkg.com/en/docs/cli/link) + - `yarn run dev` + +As you make changed to `cornerstone-tools`, and it's output is rebuilt, you +should see the following behavior: + +![tools](..//assets/img/cornerstone-tools-link.gif) + +If you wish to stop using your local package, run the following commands in the +`ohif/viewers` repository root: + +- `yarn unlink cornerstone-tools` +- `yarn install --force` + + + +#### Other linkage notes + +We're still working out some of the kinks with local package development as +there are a lot of factors that can influence the behavior of our development +server and bundler. If you encounter issues not addressed here, please don't +hesitate to reach out on GitHub. + +Sometimes you might encounter a situation where the linking doesn't work as +expected. This might happen when there are multiple linked packages with the +same name. You can [remove][unlink] the linked packages inside yarn and try +again. + +## Any guidance on submitting changes? + +While we do appreciate code contributions, triaging and integrating contributed +code changes can be very time consuming. Please consider the following tips when +working on your pull requests: + +- Functionality is appropriate for the repository. Consider creating a GitHub + issue to discuss your suggested changes. +- The scope of the pull request is not too large. Please consider separate pull + requests for each feature as big pull requests are very time consuming to + understand. + +We will provide feedback on your pull requests as soon as possible. Following +the tips above will help ensure your changes are reviewed. + + + + + + +[example-url]: https://deploy-preview-237--ohif.netlify.com/viewer/?url=https://s3.eu-central-1.amazonaws.com/ohif-viewer/sampleDICOM.json +[pr-237]: https://github.com/OHIF/Viewers/pull/237 +[monorepo]: https://github.com/OHIF/Viewers/issues/768 +[unlink]: https://stackoverflow.com/questions/58459698/is-there-a-command-to-unlink-all-yarn-packages-yarn-unlink-all + diff --git a/platform/docs/versioned_docs/version-3.9/development/getting-started.md b/platform/docs/versioned_docs/version-3.9/development/getting-started.md new file mode 100644 index 0000000..32214b6 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/development/getting-started.md @@ -0,0 +1,133 @@ +--- +sidebar_position: 1 +sidebar_label: Getting Started +--- + +# Getting Started + +## Setup + +### Fork & Clone + +If you intend to contribute back changes, or if you would like to pull updates +we make to the OHIF Viewer, then follow these steps: + +- [Fork][fork-a-repo] the [OHIF/Viewers][ohif-viewers-repo] repository +- [Create a local clone][clone-a-repo] of your fork + - `git clone https://github.com/YOUR-USERNAME/Viewers` +- Add OHIF/Viewers as a [remote repository][add-remote-repo] labeled `upstream` + - Navigate to the cloned project's directory + - `git remote add upstream https://github.com/OHIF/Viewers.git` + +With this setup, you can now [sync your fork][sync-changes] to keep it +up-to-date with the upstream (original) repository. This is called a "Triangular +Workflow" and is common for Open Source projects. The GitHub blog has a [good +graphic that illustrates this setup][triangular-workflow]. + + +### Private + +Alternatively, if you intend to use the OHIF Viewer as a starting point, and you +aren't as concerned with syncing updates, then follow these steps: + +1. Navigate to the [OHIF/Viewers][ohif-viewers] repository +2. Click `Clone or download`, and then `Download ZIP` +3. Use the contents of the `.zip` file as a starting point for your viewer + +> NOTE: It is still possible to sync changes using this approach. However, +> submitting pull requests for fixes and features are best done with the +> separate, forked repository setup described in "Fork & Clone" + +## Developing + + +### Branches + +#### `master` branch - The latest dev (beta) release + +- `master` - The latest dev release + +This is typically where the latest development happens. Code that is in the master branch has passed code reviews and automated tests, but it may not be deemed ready for production. This branch usually contains the most recent changes and features being worked on by the development team. It's often the starting point for creating feature branches (where new features are developed) and hotfix branches (for urgent fixes). + +Each package is tagged with beta version numbers, and published to npm such as `@ohif/ui@3.6.0-beta.1` + +### `release/*` branches - The latest stable releases +Once the `master` branch code reaches a stable, release-ready state, we conduct a comprehensive code review and QA testing. Upon approval, we create a new release branch from `master`. These branches represent the latest stable version considered ready for production. + +For example, `release/3.5` is the branch for version 3.5.0, and `release/3.6` is for version 3.6.0. After each release, we wait a few days to ensure no critical bugs. If any are found, we fix them in the release branch and create a new release with a minor version bump, e.g., 3.5.1 in the `release/3.5` branch. + +Each package is tagged with version numbers and published to npm, such as `@ohif/ui@3.5.0`. Note that `master` is always ahead of the `release` branch. We publish docker builds for both beta and stable releases. + +Here is a schematic representation of our development workflow: + +![alt text](../assets/img/github-readme-branches-Jun2024.png) + + + +### Requirements + +- [Node.js & NPM](https://nodejs.org/en/) +- [Yarn](https://yarnpkg.com/en/) +- Yarn workspaces should be enabled: + - `yarn config set workspaces-experimental true` + +### Kick the tires + +Navigate to the root of the project's directory in your terminal and run the +following commands: + +```bash +# Restore dependencies +yarn install + +# Start local development server +yarn run dev +``` + +You should see the following output: + +```bash +@ohif/app: i ๏ฝขwds๏ฝฃ: Project is running at http://localhost:3000/ +@ohif/app: i ๏ฝขwds๏ฝฃ: webpack output is served from / +@ohif/app: i ๏ฝขwds๏ฝฃ: Content not from webpack is served from D:\code\ohif\Viewers\platform\viewer +@ohif/app: i ๏ฝขwds๏ฝฃ: 404s will fallback to /index.html + +# And a list of all generated files +``` + +### ๐ŸŽ‰ Celebrate ๐ŸŽ‰ + +
+ +
+ +### Building for Production + +> More comprehensive guides for building and publishing can be found in our +> [deployment docs](./../deployment/index.md) + +```bash +# Build static assets to host a PWA +yarn run build +``` + +## Troubleshooting + +- If you receive a _"No Studies Found"_ message and do not see your studies, try + changing the Study Date filters to a wider range. +- If you see a 'Loading' message which never resolves, check your browser's + JavaScript console inside the Developer Tools to identify any errors. + + + + +[fork-a-repo]: https://help.github.com/en/articles/fork-a-repo +[clone-a-repo]: https://help.github.com/en/articles/fork-a-repo#step-2-create-a-local-clone-of-your-fork +[add-remote-repo]: https://help.github.com/en/articles/fork-a-repo#step-3-configure-git-to-sync-your-fork-with-the-original-spoon-knife-repository +[sync-changes]: https://help.github.com/en/articles/syncing-a-fork +[triangular-workflow]: https://github.blog/2015-07-29-git-2-5-including-multiple-worktrees-and-triangular-workflows/#improved-support-for-triangular-workflows +[ohif-viewers-repo]: https://github.com/OHIF/Viewers/ +[ohif-viewers]: https://github.com/OHIF/Viewers + diff --git a/platform/docs/versioned_docs/version-3.9/development/link.md b/platform/docs/versioned_docs/version-3.9/development/link.md new file mode 100644 index 0000000..f8eaebe --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/development/link.md @@ -0,0 +1,63 @@ +--- +sidebar_position: 9 +sidebar_label: Local Linking +--- + +# Introduction + +Local linking allows you to develop and test a library in the context of an application before it's published or when you encounter +a bug that you suspect is related to a library. With Yarn, this can be achieved through the yarn link command. + +The general procedure is as follows: + + +Link the Library: + +```sh +cd /path/to/library +yarn link +``` + +This command will create a symlink in a global directory for the library. + + +Link to the Application: + +```sh +cd /path/to/application +yarn link "library-name" +``` + +Creates a symlink from the global directory to the application's node_modules. + + +# Tutorial for linking Cornerstone3D to OHIF + +Below we demonstrate how to link Cornerstone3D to OHIF Viewer. This is useful for testing and debugging Cornerstone3D in the context of OHIF Viewer. + +
+ +
+ +:::tip +Since `@cornerstonejs/tools` depends on `@cornerstonejs/core`, if you need the changes +you made in `@cornerstonejs/core` to be reflected in `@cornerstonejs/tools`, you need to +also link `@cornerstonejs/core` to `@cornerstonejs/tools`. + +```sh +cd /path/to/cornerstonejs-core +# for the core +yarn link + +cd /path/to/cornerstonejs-tools +yarn link "@cornerstonejs/core" + +# for the tools +yarn link + +# inside OHIF +cd /path/to/OHIFViewer +yarn link "@cornerstonejs/core" +yarn link "@cornerstonejs/tools" +``` +::: diff --git a/platform/docs/versioned_docs/version-3.9/development/ohif-cli.md b/platform/docs/versioned_docs/version-3.9/development/ohif-cli.md new file mode 100644 index 0000000..038f331 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/development/ohif-cli.md @@ -0,0 +1,312 @@ +--- +sidebar_position: 3 +sidebar_label: OHIF CLI +--- + +# OHIF Command Line Interface + +OHIF-v3 architecture has been re-designed to enable building applications that +are easily extensible to various use cases (Modes) that behind the scene would +utilize desired functionalities (Extensions) to reach the goal of the use case. +Now, the question is _how to create/remove/install/uninstall an extension and/or +mode?_ + +You can use the `cli` script that comes with the OHIF monorepo to achieve these +goals. + +:::note Info +In the long-term, we envision our `cli` tool to be a separate installable +package that you can invoke anywhere on your local system to achieve the same +goals. In the meantime, `cli` will remain as part of the OHIF monorepo and needs +to be invoked using the `yarn` command. +::: + + +## CLI Installation + +You don't need to install the `cli` currently. You can use `yarn` to invoke its +commands. + +## Commands + +:::note Important +All commands should run from the root of the monorepo. +::: + + +There are various commands that can be used to interact with the OHIF-v3 CLI. If +you run the following command, you will see a list of available commands. + +``` +yarn run cli --help +``` + +which will output + +``` +OHIF CLI + +Options: + -V, --version output the version number + -h, --help display help for command + +Commands: + create-extension Create a new template extension + create-mode Create a new template Mode + add-extension [version] Adds an ohif extension + remove-extension removes an ohif extension + add-mode [version] Removes an ohif mode + remove-mode Removes an ohif mode + link-extension Links a local OHIF extension to the Viewer to be used for development + unlink-extension Unlinks a local OHIF extension from the Viewer + link-mode Links a local OHIF mode to the Viewer to be used for development + unlink-mode Unlinks a local OHIF mode from the Viewer + list List Added Extensions and Modes + search [options] Search NPM for the list of Modes and Extensions + help [command] display help for command +``` + +As seen there are commands for you such as: `create-extension`, `create-mode`, +`add-extension`, `remove-extension`, `add-mode`, `remove-mode`, +`link-extension`, `unlink-extension`, `link-mode`, `unlink-mode`, `list`, +`search`, and `help`. Here we will go through each of the commands and describe +them. + +### create-mode + +If you need to create a new mode, you can use the `create-mode` command. This +command will create a new mode template in the directory that you specify. +The command will ask you couple of information/questions in order +to properly create the mode metadata in the `package.json` file. + +```bash +yarn run cli create-mode +``` + +
+ +![image](../assets/img/create-mode.png) + + +
+ +Note 1: Some questions have a default answer, which is indicated inside the +parenthesis. If you don't want to answer the question, just hit enter. It will +use the default answer. + +Note 2: As you see in the questions, you can initiate a git repository for the +new mode right away by answering `Y` (default) to the question. + +Note 3: Finally, as indicated by the green lines at the end, `create-mode` command only +create the mode template. You will need to link the mode to the Viewer in order +to use it. See the [`link-mode`](#link-mode) command. + +If we take a look at the directory that we created, we will see the following +files: + +
+ +![image](../assets/img/mode-template.png) + +
+ + +### create-extension + +Similar to the `create-mode` command, you can use the `create-extension` +command to create a new extension template. This command will create a new +extension template in the directory that you specify the path. + +```bash +yarn run cli create-extension +``` + + +Note: again similar to the `create-extension` command, you need to manually link +the extension to the Viewer in order to use it. See the +[`link-mode`](#link-mode) command. + + +### link-extension + +`link-extension` command will link a local OHIF extension to the Viewer. This +command will utilize `yarn link` to achieve so. + +```bash +yarn run cli link-extension +``` + +### unlink-extension + +There might be situations where you want to unlink an extension from the Viewer +after some developments. `unlink-extension` command will do so. + +```bash +ohif-cli unlink-extension +``` + + + +### link-mode + +Similar to the `link-extension` command, `link-mode` command will link a local +OHIF mode to the Viewer. + +```bash +yarn run cli link-mode +``` + +### unlink-mode + +Similar to the `unlink-extension` command, `unlink-mode` command will unlink a +local OHIF mode from the Viewer. + +```bash +ohif-cli unlink-mode +``` + +### add-mode + +OHIF is a modular viewer. This means that you can install (add) different modes +to the viewer if they are published online . `add-mode` command will add a new mode to +the viewer. It will look for the mode in the NPM registry and installs it. This +command will also add the extension dependencies that the mode relies on to the +Viewer (if specified in the peerDependencies section of the package.json). + +:::note Important +`cli` will validate the npm package before adding it to the Viewer. An OHIF mode +should have `ohif-mode` as one of its keywords. +::: + +Note: If you don't specify the version, the latest version will be used. + +```bash +yarn run cli add-mode [version] +``` + +For instance `@ohif-test/mode-clock` is an example OHIF mode that we have +published to NPM. This mode basically has a panel that shows the clock :) + +We can add this mode to the Viewer by running the following command: + +```bash +yarn run cli add-mode @ohif-test/mode-clock +``` + +After installation, the Viewer has a new mode! + + +![image](../assets/img/add-mode.png) + + +Note: If the mode has an extension peerDependency (in this case @ohif-test/extension-clock), +`cli` will automatically add the extension to the Viewer too. + +The result + +![image](../assets/img/clock-mode.png) +![image](../assets/img/clock-mode1.png) + +### add-extension + +This command will add an OHIF extension to the Viewer. It will look for the +extension in the NPM registry and install it. + +```bash +yarn run cli add-extension [version] +``` + + +### remove-mode + +This command will remove the mode from the Viewer and also remove the extension +dependencies that the mode relies on from the Viewer. + +```bash +yarn run cli remove-mode +``` + + +### remove-extension + +Similar to the `remove-mode` command, this command will remove the extension +from the Viewer. + +```bash +yarn run cli remove-extension +``` + +### list + +`list` command will list all the installed extensions and modes in +the Viewer. It uses the `PluginConfig.json` file to list the installed +extensions and modes. + +```bash +yarn run cli list +``` + +an output would look like this: + +
+ +![image](../assets/img/ohif-cli-list.png) + +
+ +### search + +Using `search` command, you can search for OHIF extensions and modes +in the NPM registry. This tool can accept a `--verbose` flag to show more +information about the results. + +```bash +yarn run cli search [--verbose] +``` + +
+ +![image](../assets/img/cli-search-no-verbose.png) + +
+ +with the verbose flag `ohif-cli search --verbose` you will achieve the following +output: + +
+ +![image](../assets/img/cli-search-with-verbose.png) + +
+ + +## PluginConfig.json + +To make all the above commands work, we have created a new file called `PluginConfig.json` which contains the +information needed to run the commands. You **don't need to (and should not)** +edit/update/modify this file as it is automatically generated by the CLI. You +can take a look at what this file contains by going to +`platform/app/PluginConfig.json` in your project's root directory. In short, +this file tracks and stores all the extensions/modes and the their version that +are currently being used by the viewer. + +## Private NPM Repos + +For the `yarn cli` to view private NPM repos, create a read-only token with the +following steps and export it as an environmental variable. You may also export +an existing npm token. +``` +npm login +npm token create --read-only +export NPM_TOKEN= +``` + +## External dependencies +The ohif-cli will add the path to the external dependencies to the webpack config, +so that you can install them in your project and use them in your custom +extensions and modes. To achieve this ohif-cli will update the webpack.pwa.js +file in the platform/app directory. + +## Video tutorials +See the [Video Tutorials](./video-tutorials.md) for videos of some the above +commands in action. diff --git a/platform/docs/versioned_docs/version-3.9/development/our-process.md b/platform/docs/versioned_docs/version-3.9/development/our-process.md new file mode 100644 index 0000000..263135b --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/development/our-process.md @@ -0,0 +1,156 @@ +--- +sidebar_position: 6 +sidebar_label: Issue & PR Triage Process +--- + +# Our Process + +Our process is a living, breathing thing. We strive to have regular +[retrospectives][retrospective] that help us shape and adapt our process to our +team's current needs. This document attempts to capture the broad strokes of +that process in an effort to: + +- Strengthen community member involvement and understanding +- Welcome feedback and helpful suggestions + +## Issue Triage + +[GitHub issues][gh-issues] are the best way to provide feedback, ask questions, +and suggest changes to the OHIF Viewer's core team. Community issues generally +fall into one of three categories, and are marked with a `triage` label when +created. + +| Issue Template Name | Description | +| ---------------------- | ---------------------------------------------------------------------------------------- | +| Community: Report ๐Ÿ› | Describe a new issue; Provide steps to reproduce; Expected versus actual result? | +| Community: Request โœ‹ | Describe a proposed new feature. Why should it be implemented? What is the impact/value? | +| Community: Question โ“ | Seek clarification or assistance relevant to the repository. | + +_table 1. issue template names and descriptions_ + +Issues that require `triage` are akin to support tickets. As this is often our +first contact with would-be adopters and contributors, it's important that we +strive for timely responses and satisfactory resolutions. We attempt to +accomplish this by: + +1. Responding to issue requiring `triage` at least once a week +2. Create new "official issues" from "community issues" +3. Provide clear guidance and next steps (when applicable) +4. Regularly clean up old (stale) issues + +> ๐Ÿ–‹ Less obviously, patterns in the issues being reported can highlight areas +> that need improvement. For example, users often have difficulty navigating +> CORS issues when deploying the OHIF Viewer -- how do we best reduce our ticket +> volume for this issue? + +### Backlogged Issues + +Community issues serve as vehicles of discussion that lead us to "backlogged +issues". Backlogged issues are the distilled and actionable information +extracted from community issues. They contain the scope and requirements +necessary for hand-off to a core-team (or community) contributor ^\_^ + +| Category | Description | Labels | +| -------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Bugs | An issue with steps that produce a bug (an unexpected result). | [Bug: Verified ๐Ÿ›][label-bug] | +| Stories | A feature/enhancement with a clear benefit, boundaries, and requirements. | [Story ๐Ÿ™Œ][label-story] | +| Tasks | Changes that improve [UX], [DX], or test coverage; but don't impact application behavior | [Task: CI/Tooling ๐Ÿค–][label-tooling], [Task: Docs ๐Ÿ“–][label-docs], [Task: Refactor ๐Ÿ› ][label-refactor], [Task: Tests ๐Ÿ”ฌ][label-tests] | + +_table 2. backlogged issue types ([full list of labels][gh-labels])_ + +## Issue Curation (["backlog grooming"][groom-backlog]) + +If a [GitHub issue][gh-issues] has a `bug`, `story`, or `task` label; it's on +our backlog. If an issue is on our backlog, it means we are, at the very least, +committed to reviewing any community drafted Pull Requests to complete the +issue. If you're interested in seeing an issue completed but don't know where to +start, please don't hesitate to leave a comment! + +While we don't yet have a long-term or quarterly road map, we do regularly add +items to our ["Active Development" GitHub Project Board][gh-board]. Items on +this project board are either in active development by Core Team members, or +queued up for development as in-progress items are completed. + +> ๐Ÿ–‹ Want to contribute but not sure where to start? Check out [Up for +> grabs][label-grabs] issues and our [Contributing +> documentation][contributing-docs] + +## Contributions (Pull Requests) + +Incoming Pull Requests (PRs) are triaged using the following labels. Code review +is performed on all PRs where the bug fix or added functionality is deemed +appropriate: + +| Labels | Description | +| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| **Classification** | | +| [PR: Bug Fix][label-bug] | Filed to address a Bug. | +| [PR: Draft][draft] | Filed to gather early feedback from the core team, but which is not intended for merging in the short term. | +| **Review Workflow** | | +| [PR: Awaiting Response ๐Ÿ’ฌ][awaiting-response] | The core team is waiting for additional information from the author. | +| [PR: Awaiting Review ๐Ÿ‘€][awaiting-review] | The core team has not yet performed a code review. | +| [PR: Awaiting Revisions ๐Ÿ–Š][awaiting-revisions] | Following code review, this label is applied until the author has made sufficient changes. | +| **QA** | | +| [PR: Awaiting User Cases ๐Ÿ’ƒ][awaiting-stories] | The PR code changes need common language descriptions of impact to end users before the review can start | +| [PR: No UX Impact ๐Ÿ™ƒ][no-ux-impact] | The PR code changes do not impact the user's experience | + +We rely on GitHub Checks and integrations with third party services to evaluate +changes in code quality and test coverage. Tests must pass and User cases must +be present (when applicable) before a PR can be merged to master, and code +quality and test coverage must not be changed by a significant margin. For some +repositories, visual screenshot-based tests are also included, and video +recordings of end-to-end tests are stored for later review. + +[You can read more about our continuous integration efforts here](/development/continuous-integration.md) + +## Releases + +Releases are made automatically based on the type of commits which have been +merged (major.minor.patch). Releases are automatically pushed to NPM. Release +notes are automatically generated. Users can subscribe to GitHub and NPM +releases. + +We host development, staging, and production environments for the Progressive +Web Application version of the OHIF Viewer. [Development][ohif-dev] always +reflects the latest changes on our master branch. [Staging][ohif-stage] is used +to regression test a release before a bi-weekly deploy to our [Production +environment][ohif-prod]. + +Important announcements are made on GitHub, tagged as Announcement, and pinned +so that they remain at the top of the Issue page. + +The Core team occasionally performs full manual testing to begin the process of +releasing a Stable version. Once testing is complete, the known issues are +addressed and a Stable version is released. + + + + +[groom-backlog]: https://www.agilealliance.org/glossary/backlog-grooming +[retrospective]: https://www.atlassian.com/team-playbook/plays/retrospective +[gh-issues]: https://github.com/OHIF/Viewers/issues/new/choose +[gh-labels]: https://github.com/OHIF/Viewers/labels + +[label-story]: https://github.com/OHIF/Viewers/labels/Story%20%3Araised_hands%3A +[label-tooling]: https://github.com/OHIF/Viewers/labels/Task%3A%20CI%2FTooling%20%3Arobot%3A +[label-docs]: https://github.com/OHIF/Viewers/labels/Task%3A%20Docs%20%3Abook%3A +[label-refactor]: https://github.com/OHIF/Viewers/labels/Task%3A%20Refactor%20%3Ahammer_and_wrench%3A +[label-tests]: https://github.com/OHIF/Viewers/labels/Task%3A%20Tests%20%3Amicroscope%3A +[label-bug]: https://github.com/OHIF/Viewers/labels/Bug%3A%20Verified%20%3Abug%3A + +[draft]: https://github.com/OHIF/Viewers/labels/PR%3A%20Draft +[awaiting-response]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20Response%20%3Aspeech_balloon%3A +[awaiting-review]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20Review%20%3Aeyes%3A +[awaiting-stories]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20UX%20Stories%20%3Adancer%3A +[awaiting-revisions]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20Revisions%20%3Apen%3A +[no-ux-impact]: https://github.com/OHIF/Viewers/labels/PR%3A%20No%20UX%20Impact%20%3Aupside_down_face%3A + +[ohif-dev]: https://viewer-dev.ohif.org +[ohif-stage]: https://viewer-stage.ohif.org +[ohif-prod]: https://viewer.ohif.org +[gh-board]: https://github.com/OHIF/Viewers/projects/4 +[label-grabs]: https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs+%3Araising_hand_woman%3A%22 +[contributing-docs]: ./contributing.md + diff --git a/platform/docs/versioned_docs/version-3.9/development/playwright-testing.md b/platform/docs/versioned_docs/version-3.9/development/playwright-testing.md new file mode 100644 index 0000000..6798723 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/development/playwright-testing.md @@ -0,0 +1,159 @@ +--- +sidebar_position: 11 +sidebar_label: Playwright Testing +--- + +# Writing PlayWright Tests + +Our Playwright tests are written using the Playwright test framework. We use these tests to test our OHIF Viewer and ensure that it is working as expected. + +In this guide, we will show you how to write Playwright tests for the OHIF Viewer. + +## Using a specific study and mode + +If you would like to use a specific study, you can use the `studyInstanceUID` property to reference the study you would like to visit. for example, if you would like to use the study with StudyInstanceUID `2.16.840.1.114362.1.11972228.22789312658.616067305.306.2` and the mode `Basic Viewer`, you can use the following code snippet: + +```ts +import { test } from '@playwright/test'; +import { visitStudy, checkForScreenshot, screenShotPaths } from './utils/index.js'; + +test.beforeEach(async ({ page }) => { + const studyInstanceUID = '2.16.840.1.114362.1.11972228.22789312658.616067305.306.2'; + const mode = 'Basic Viewer'; + await visitStudy(page, studyInstanceUID, mode); +}); + +test.describe('Some Test', async () => { + test('should do something.', async ({ page }) => { + // Your test code here... + }); +}); + +``` + +## Screenshots + +A good way to check your tests is working as expected is to capture screenshots at different stages of the test. You can use our `checkForScreenshot` function located in `tests/utils/checkForScreenshot.ts` to capture screenshots. You should also plan your screenshots in advance, screenshots need to be defined in the `tests/utils/screenshotPaths.ts` file. For example, if you would to capture a screenshot after a measurement is added, you can define a screenshot path like this: + +```ts +const screenShotPaths = { + your_test_name: { + measurementAdded: 'measurementAdded.png', + measurementRemoved: 'measurementRemoved.png', + }, +}; +``` + +It's okay if the screenshot doesn't exist yet, this will be dealt with in the next step. Once you have defined your screenshot path, you can use the `checkForScreenshot` function in your test to capture the screenshot. For example, if you would like to capture a screenshot of the page after a measurement is added, you can use the following code snippet: + +```ts +import { test } from '@playwright/test'; +import { + visitStudy, + checkForScreenshot, + screenshotPath, +} from './utils/index.js'; + +test.beforeEach(async ({ page }) => { + const studyInstanceUID = '2.16.840.1.114362.1.11972228.22789312658.616067305.306.2'; + const mode = 'Basic Viewer'; + await visitStudy(page, studyInstanceUID, mode); +}); + +test.describe('Some test', async () => { + test('should do something', async ({ page }) => { + // Your test code here to add a measurement + await checkForScreenshot( + page, + page, + screenshotPath.your_test_name.measurementAdded + ); + }); +}); +``` + +The test will automatically fail the first time you run it, it will however generate the screenshot for you, you will notice 3 new entries in the `tests/screenshots` folder, under `chromium/your-test.spec.js/measurementAdded.png`, `firefox/your-test.spec.js/measurementAdded.png` and `webkit/your-test.spec.js/measurementAdded.png` folders. You can now run the test again and it will use those screenshots to compare against the current state of the example. Please verify that the ground truth screenshots are correct before committing them or testing against them. + +## Simulating mouse drags + +If you would like to simulate a mouse drag, you can use the `simulateDrag` function located in `tests/utils/simulateDrag.ts`. You can use this function to simulate a mouse drag on an element. For example, if you would like to simulate a mouse drag on the `cornerstone-canvas` element, you can use the following code snippet: + +```ts +import { + visitStudy, + checkForScreenshot, + screenShotPaths, + simulateDrag, +} from './utils/index.js'; + +test.beforeEach(async ({ page }) => { + const studyInstanceUID = '2.16.840.1.114362.1.11972228.22789312658.616067305.306.2'; + const mode = 'Basic Viewer'; + await visitStudy(page, studyInstanceUID, mode); +}); + +test.describe('Some Test', async () => { + test('should do something..', async ({ + page, + }) => { + const locator = page.locator('.cornerstone-canvas'); + await simulateDrag(page, locator); + }); +}); +``` + +Our simulate drag utility can simulate a drag on any element, and avoid going out of bounds. It will calculuate the bounding box of the element and ensure that the drag stays within the bounds of the element. This should be good enough for most tools, and better than providing custom x, and y coordinates which can be error prone and make the code difficult to maintain. + +## Running the tests + +After you have wrote your tests, you can run them by using the following command: + +```bash +yarn test:e2e:ci +``` + +If you want to use headed mode, you can use the following command: + +```bash +yarn test:e2e:headed +``` + +You will see the test results in your terminal, if you want an indepth report, you can use the following command: + +```bash +yarn playwright show-report tests/playwright-report +``` + +## Serving the viewer manually for development + +By default, when you run the tests, it will call the `yarn start` command to serve the viewer first, then run the tests, if you would like to serve the viewer manually, you can use the same command. The viewer will be available at `http://localhost:3000`. This could speed up your development process since playwright will skip this step and use the existing server on port 3000. + +## Accessing services, managers, configs and cornerstone in your tests + +If you would like to access the cornerstone3D, services, or command managers in your tests, you can use the `page.evaluate` function to access them. For example, if you would like to access the `services` so you can show a UI notifcation using the uiNotifcationService, you can use the following code snippet: + +```ts + await page.evaluate(({ services }: AppTypes.Test) => { + const { uiNotificationService } = services; + uiNotificationService.show({ + title: 'Test', + message: 'This is a test', + type: 'info', + }); + }, await page.evaluateHandle('window')); + ``` + +## Playwright VSCode Extension and Recording Tests + +If you are using VSCode, you can use the Playwright extension to help you write your tests. The extension provides a test runner and many great features such as picking a locator using your mouse, recording a new test, and more. You can install the extension by searching for `Playwright` in the extensions tab in VSCode or by visiting the [Playwright extension page](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright). + +
+ +
+ + +
+ +
diff --git a/platform/docs/versioned_docs/version-3.9/development/testing.md b/platform/docs/versioned_docs/version-3.9/development/testing.md new file mode 100644 index 0000000..23f2397 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/development/testing.md @@ -0,0 +1,217 @@ +--- +sidebar_position: 7 +sidebar_label: Testing +--- + +# Running Tests for OHIF + +We introduce here various test types that is available for OHIF, and how to run +each test in order to make sure your contribution hasn't broken any existing +functionalities. Idea and philosophy of each testing category is discussed in +the second part of this page. + +## Unit test + +To run the unit test: + +```bash +yarn run test:unit:ci +``` + +Note: You should have already installed all the packages with `yarn install`. + +Running unit test will generate a report at the end showing the successful and +unsuccessful tests with detailed explanations. + +## End-to-end test +For running the OHIF e2e test you need to run the following steps: + +- Open a new terminal, and from the root of the OHIF mono repo, run the following command: + + ```bash + yarn test:data + ``` + + This will download the required data to run the e2e tests (it might take a while). + The `test:data` only needs to be run once and checks the data out. Read more about + test data [below](#test-data). + +- Run the viewer with e2e config + + ```bash + APP_CONFIG=config/e2e.js yarn start + ``` + + You should be able to see test studies in the study list + + ![OHIF-e2e-test-studies](../assets/img/OHIF-e2e-test-studies.png) + +- Open a new terminal inside the OHIF project, and run the e2e cypress test + + ```bash + yarn test:e2e + ``` + + You should be able to see the cypress window open + + ![e2e-cypress](../assets/img/e2e-cypress.png) + + Run the tests by clicking on the `Run #number integration tests` . + + A new window will open, and you will see e2e tests being executed one after + each other. + + ![e2e-cypress-final](../assets/img/e2e-cypress-final.png) + + ## Test Data + The testing data is stored in two OHIF repositories. The first contains the + binary DICOM data, at [viewer-testdata](https://github.com/OHIF/viewer-testdata.git) + while the second module contains data in the DICOMweb format, installed as a submodule + into OHIF in the `testdata` directory. This is retrieved via the command + ```bash + yarn test:data + ``` + or the equivalent command `git submodule update --init` + When adding new data, run: + ``` + npm install -g dicomp10-to-dicomweb + mkdicomweb -d dicomweb dcm + ``` + to update the local dicomweb submodule in viewer-testdata. Then, commit + that data and update the submodules used in OHIF and in the viewer-testdata + parent modules. + + All data MUST be fully anonymized and allowed to be used for open access. + Any attributions should be included in the DCM directory. + +## Testing Philosophy + +> Testing is an opinionated topic. Here is a rough overview of our testing +> philosophy. See something you want to discuss or think should be changed? Open +> a PR and let's discuss. + +You're an engineer. You know how to write code, and writing tests isn't all that +different. But do you know why we write tests? Do you know when to write one, or +what kind of test to write? How do you know if a test is a _"good"_ test? This +document's goal is to give you the tools you need to make those determinations. + +Okay. So why do we write tests? To increase our... **CONFIDENCE** + +- If I do a large refactor, does everything still work? +- If I changed some critical piece of code, is it safe to push to production? + +Gaining the confidence we need to answer these questions after every change is +costly. Good tests allow us to answer them without manual regression testing. +What and how we choose to test to increase that confidence is nuanced. + +## Further Reading: Kinds of Tests + +Test's buy us confidence, but not all tests are created equal. Each kind of test +has a different cost to write and maintain. An expensive test is worth it if it +gives us confidence that a payment is processed, but it may not be the best +choice for asserting an element's border color. + +| Test Type | Example | Speed | Cost | +| ----------- | ------------------------------------------------------------------------ | ---------------- | ------------------------------------------------------------------------ | +| Static | `addNums(1, '2')` called with `string`, expected `int`. | :rocket: Instant | :money_with_wings: | +| Unit | `addNums(1, 2)` returns expected result `3` | :airplane: Fast | :money_with_wings::money_with_wings: | +| Integration | Clicking "Sign In", navigates to the dashboard (mocked network requests) | :running: Okay | :money_with_wings::money_with_wings::money_with_wings: | +| End-to-end | Clicking "Sign In", navigates to the dashboard (no mocks) | :turtle: Slow | :money_with_wings::money_with_wings::money_with_wings::money_with_wings: | + +- :rocket: Speed: How quickly tests run +- :money_with_wings: Cost: Time to write, and to debug when broken (more points + of failure) + +### Static Code Analysis + +Modern tooling gives us this "for free". It can catch invalid regular +expressions, unused variables, and guarantee we're calling methods/functions +with the expected parameter types. + +Example Tooling: + +- [ESLint][eslint-rules] +- [TypeScript][typescript-docs] or [Flow][flow-org] + +### Unit Tests + +The building blocks of our libraries and applications. For these, you'll often +be testing a single function or method. Conceptually, this equates to: + +_Pure Function Test:_ + +- If I call `sum(2, 2)`, I expect the output to be `4` + +_Side Effect Test:_ + +- If I call `resetViewport(viewport)`, I expect `cornerstone.reset` to be called + with `viewport` + +#### When to use + +Anything that is exposed as public API should have unit tests. + +#### When to avoid + +You're actually testing implementation details. You're testing implementation +details if: + +- Your test does something that the consumer of your code would never do. + - IE. Using a private function +- A refactor can break your tests + +### Integration Tests + +We write integration tests to gain confidence that several units work together. +Generally, we want to mock as little as possible for these tests. In practice, +this means only mocking network requests. + +### End-to-End Tests + +These are the most expensive tests to write and maintain. Largely because, when +they fail, they have the largest number of potential points of failure. So why +do we write them? Because they also buy us the most confidence. + +#### When to use + +Mission critical features and functionality, or to cover a large breadth of +functionality until unit tests catch up. Unsure if we should have a test for +feature `X` or scenario `Y`? Open an issue and let's discuss. + +### General + +- [Assert(js) Conf 2018 Talks][assert-js-talks] + - [Write tests. Not too many. Mostly integration.][kent-talk] - Kent C. Dodds + - [I see your point, butโ€ฆ][gleb-talk] - Gleb Bahmutov +- [Static vs Unit vs Integration vs E2E Testing][kent-blog] - Kent C. Dodds + (Blog) + +### End-to-end Testing w/ Cypress + +- [Getting Started](https://docs.cypress.io/guides/overview/why-cypress.html) + - Be sure to check out `Getting Started` and `Core Concepts` +- [Best Practices](https://docs.cypress.io/guides/references/best-practices.html) +- [Example Recipes](https://docs.cypress.io/examples/examples/recipes.html) + + + + +[eslint-rules]: https://eslint.org/docs/rules/ +[mini-pacs]: https://github.com/OHIF/viewer-testdata +[typescript-docs]: https://www.typescriptlang.org/docs/home.html +[flow-org]: https://flow.org/ + +[assert-js-talks]: https://www.youtube.com/playlist?list=PLZ66c9_z3umNSrKSb5cmpxdXZcIPNvKGw +[kent-talk]: https://www.youtube.com/watch?v=Fha2bVoC8SE +[gleb-talk]: https://www.youtube.com/watch?v=5FnalKRjpZk +[kent-blog]: https://kentcdodds.com/blog/unit-vs-integration-vs-e2e-tests + +[testing-trophy]: https://twitter.com/kentcdodds/status/960723172591992832?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E960723172591992832&ref_url=https%3A%2F%2Fkentcdodds.com%2Fblog%2Fwrite-tests +[aaron-square]: https://twitter.com/Carofine247/status/966727489274961920 +[gleb-pyramid]: https://twitter.com/Carofine247/status/966764532046684160/photo/3 +[testing-pyramid]: https://dojo.ministryoftesting.com/dojo/lessons/the-mobile-test-pyramid +[testing-dorito]: https://twitter.com/denvercoder/status/960752578198843392 +[testing-dorito-img]: https://pbs.twimg.com/media/DVVHXycUMAAcN-F?format=jpg&name=4096x4096 + diff --git a/platform/docs/versioned_docs/version-3.9/development/types.md b/platform/docs/versioned_docs/version-3.9/development/types.md new file mode 100644 index 0000000..a6af5df --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/development/types.md @@ -0,0 +1,85 @@ +--- +sidebar_position: 10 +sidebar_label: Global Types +--- + +# Extending App Types and Services in Your Application + +This documentation provides an overview and examples on how to use and extend `withAppTypes`, integrate custom properties, and add services in the global namespace of the application. This helps in enhancing the application's modularity and extensibility. + +## Overview of `withAppTypes` + +The `withAppTypes` function is a TypeScript utility that extends the base properties of components or modules with the application's core service and manager types. It allows for a more flexible and type-safe way to pass around core functionality and custom properties. + +### Using `withAppTypes` + +`withAppTypes` can be enhanced using generics to include custom properties. This is particularly useful for passing additional data or configurations specific to your component or service. + +### Extending with Custom Properties + +You can extend `withAppTypes` to include custom properties by defining an interface for the props you need. For example: + +```typescript +interface ColorbarProps { + viewportId: string; + displaySets: Array; + colorbarProperties: ColorbarProperties; +} + +export function Colorbar({ + viewportId, + displaySets, + commandsManager, // injected type + servicesManager, // injected type + colorbarProperties, +}: withAppTypes): ReactElement { + // Component logic here +} +``` + +In this example, `ColorbarProps` is a custom interface that extends the application types through `withAppTypes`. + +## Typing the custom extensions's new services + +Extensions can define additional services that integrate seamlessly into the application's global service architecture, and will be available on the ServicesManager for use across the application. + +### Adding the extension's services Types + +Declare your service in the global namespace and use it across your application as demonstrated below: + +`extensions/my-extension/src/types/whatever.ts` + +```typescript +declare global { + namespace AppTypes { + // only add if you need direct access to the service ex. AppTypes.MicroscopyService + export type MicroscopyService = MicroscopyServiceType; + // add to the global Services interface, and to withAppTypes + export interface Services { + microscopyService?: MicroscopyServiceType; + } + } +} +``` + +Doing the above adds the `microscopyService` to the global Services interface, which ServicesManager uses by default `public services: AppTypes.Services = {};` to type services, and is also used by withAppTypes to inject services into components. +You will also get access to the seperate services via `AppTypes.YourServiceName` in your application. + + +```typescript +export function CustomComponent({ + servicesManager, +}: withAppTypes): ReactElement { + const { microscopyService } = servicesManager.services; + microscopyService.someMethod(); // auto completation available + +} +``` + +```typescript +export function CustomComponent2( + microscopyService: AppTypes.MicroscopyService, +): ReactElement { + microscopyService.someMethod(); // auto completation available +} +``` diff --git a/platform/docs/versioned_docs/version-3.9/development/video-tutorials.md b/platform/docs/versioned_docs/version-3.9/development/video-tutorials.md new file mode 100644 index 0000000..482967e --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/development/video-tutorials.md @@ -0,0 +1,68 @@ +--- +sidebar_position: 4 +sidebar_label: Video Tutorials +--- + +# Video Tutorials + +## Creating, Linking and Publishing OHIF Modes and Extensions + +The [OHIF CLI](./ohif-cli.md) facilitates the creation, linkage and publication +of OHIF modes and extensions. The videos below walk through how to use the CLI for +- creating modes and extensions +- linking local modes and extensions +- publishing modes and extensions to NPM +- adding published modes and extensions to OHIF +- submitting a mode to OHIF + +The videos build on top of one another whereby the mode and extension created +in each of the first two videos are published to NPM and then the published +entities are added to OHIF. + +### Creating and Linking a Mode + +The first video demonstrates the creation and linkage of a mode. +
+ +
+ +### Creating and Linking an Extension + +The second video creates and links an extension. The mode from the first +video is modified to reference the extension. +
+ +
+ +### Publishing an Extension to NPM + +The third video shows how the extension created in the second video can +be published to NPM. +
+ +
+ +### Publishing a Mode to NPM + +The fourth video shows how the mode created in the first video can be +published to NPM. +
+ +
+ +### Adding a Mode from NPM + +The fifth video adds the mode and extension published in NPM to OHIF. Note +that since the mode references the extension both are added with one CLI +command. +
+ +
+ +### Submitting a Mode to OHIF + +The sixth video demonstrates how a mode can be submitted to OHIF to have it +appear in OHIF's mode gallery. +
+ +
diff --git a/platform/docs/versioned_docs/version-3.9/development/webWorkers.md b/platform/docs/versioned_docs/version-3.9/development/webWorkers.md new file mode 100644 index 0000000..7fa3bed --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/development/webWorkers.md @@ -0,0 +1,191 @@ +--- +sidebar_position: 13 +sidebar_label: Web Workers +--- +# Web Worker Implementation Guide + +## Overview +Web Workers enable running computationally intensive tasks in background threads without blocking the UI. This guide explains how to implement them step by step. + +## Basic Setup + +### 1. Create Your Worker File +First, create a worker file with your background tasks: + +```javascript +// myWorker.js +import { expose } from 'comlink'; + +const obj = { + // Simple task + basicCalculation({ data }) { + // Your computation here + return result; + }, + + // Task with progress updates + longRunningTask({ data }, progressCallback) { + const total = data.length; + + for (let i = 0; i < total; i++) { + // Your processing logic + + if (progressCallback) { + const progress = Math.round((i / total) * 100); + progressCallback(progress); + } + } + + return result; + } +}; + +expose(obj); +``` + +### 2. Register the Worker + +In the main thread, can be your service, commands module, etc. + +```javascript +import { getWebWorkerManager } from '@cornerstonejs/core'; + +const workerManager = getWebWorkerManager(); + +// Define worker creation function +const workerFn = () => { + return new Worker( + new URL('./myWorker.js', import.meta.url), + { name: 'my-worker' } + ); +}; + +// Registration options +const options = { + maxWorkerInstances: 1, // Number of concurrent workers + autoTerminateOnIdle: { + enabled: true, + idleTimeThreshold: 3000, // Terminate after 3s idle + }, +}; + +// Register the worker +workerManager.registerWorker('my-worker', workerFn, options); +``` + +:::info +It is recommended to register the worker in top of the commands module. So that it +gets registered before any commands that need to use the worker. +::: + +### 3. Execute Tasks + +```javascript +// Basic execution +try { + const result = await workerManager.executeTask( + 'my-worker', + 'basicCalculation', + { data: myData } + ); +} catch (error) { + console.error('Task failed:', error); +} + +// Execution with progress callback +try { + const result = await workerManager.executeTask( + 'my-worker', + 'longRunningTask', + { data: myData }, + { + callbacks: [ + (progress) => { + console.log(`Progress: ${progress}%`); + } + ] + } + ); +} catch (error) { + console.error('Task failed:', error); +} +``` + +## Progress Events (Optional) + +If you want to show progress in your UI as a loading spinner, you can implement a progress event system: + +### 1. Publish Progress Events + +```javascript +// Helper to trigger progress events +const publishProgress = (eventTarget, progress, taskId) => { + triggerEvent(eventTarget, 'WEB_WORKER_PROGRESS', { + progress, // number 0-100 + type: 'YOUR_TASK_TYPE', // can be any string identifier + id: taskId, // unique task identifier + }); +}; + +// Usage in your application +async function runTaskWithProgress(data) { + // Start progress + publishProgress(eventTarget, 0, data.id); + + try { + const result = await workerManager.executeTask( + 'my-worker', + 'longRunningTask', + { data }, + { + callbacks: [ + (progress) => { + publishProgress(eventTarget, progress, data.id); + } + ] + } + ); + + // Complete progress + publishProgress(eventTarget, 100, data.id); + + return result; + } catch (error) { + console.error('Task failed:', error); + throw error; + } +} +``` + +Note: Publishing the `WEB_WORKER_PROGRESS` event on Cornerstone's `eventTarget` will automatically trigger the built-in loading spinner. This gives users visual feedback while your worker runs in the background. + + +## Multiple Methods in One Worker + +You can define multiple related methods in a single worker file: + +```javascript +// complexWorker.js +import { expose } from 'comlink'; + +const obj = { + processingMethod1({ data }, progressCallback) { + // Implementation + }, + + processingMethod2({ data }, progressCallback) { + // Implementation + }, + + processingMethod3({ data }, progressCallback) { + // Implementation + }, + + // Shared helper methods + _internalHelper() { + // Helper logic + } +}; + +expose(obj); +``` diff --git a/platform/docs/versioned_docs/version-3.9/faq/_category_.json b/platform/docs/versioned_docs/version-3.9/faq/_category_.json new file mode 100644 index 0000000..d83af03 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/faq/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "FAQ", + "position": 13 +} diff --git a/platform/docs/versioned_docs/version-3.9/faq/faq-measure-1.png b/platform/docs/versioned_docs/version-3.9/faq/faq-measure-1.png new file mode 100644 index 0000000..86d92dd Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/faq/faq-measure-1.png differ diff --git a/platform/docs/versioned_docs/version-3.9/faq/faq-measure-2.png b/platform/docs/versioned_docs/version-3.9/faq/faq-measure-2.png new file mode 100644 index 0000000..3feef69 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/faq/faq-measure-2.png differ diff --git a/platform/docs/versioned_docs/version-3.9/faq/faq-measure-4.png b/platform/docs/versioned_docs/version-3.9/faq/faq-measure-4.png new file mode 100644 index 0000000..4198eda Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/faq/faq-measure-4.png differ diff --git a/platform/docs/versioned_docs/version-3.9/faq/faq-measure-5.png b/platform/docs/versioned_docs/version-3.9/faq/faq-measure-5.png new file mode 100644 index 0000000..1dcdc42 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/faq/faq-measure-5.png differ diff --git a/platform/docs/versioned_docs/version-3.9/faq/faq-measure3.png b/platform/docs/versioned_docs/version-3.9/faq/faq-measure3.png new file mode 100644 index 0000000..3231771 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/faq/faq-measure3.png differ diff --git a/platform/docs/versioned_docs/version-3.9/faq/general.md b/platform/docs/versioned_docs/version-3.9/faq/general.md new file mode 100644 index 0000000..2c01de7 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/faq/general.md @@ -0,0 +1,82 @@ +--- +id: general +--- + + + +# General FAQ + + +## How do I report a bug? + +Navigate to our [GitHub Repository][new-issue], and submit a new bug report. +Follow the steps outlined in the [Bug Report Template][bug-report-template]. + +## How can I request a new feature? + +At the moment we are in the process of defining our roadmap and will do our best +to communicate this to the community. If your requested feature is on the +roadmap, then it will most likely be built at some point. If it is not, you are +welcome to build it yourself and [contribute it](../development/contributing.md). +If you have resources and would like to fund the development of a feature, +please [contact us](https://ohif.org/get-support). + + +## Who should I contact about Academic Collaborations? + +[Gordon J. Harris](https://www.dfhcc.harvard.edu/insider/member-detail/member/gordon-j-harris-phd/) +at Massachusetts General Hospital is the primary contact for any academic +collaborators. We are always happy to hear about new groups interested in using +the OHIF framework, and may be able to provide development support if the +proposed collaboration has an impact on cancer research. + +## Does OHIF offer support? + +yes, you can contact us for more information [here](https://ohif.org/get-support) + + +## Does The OHIF Viewer have [510(k) Clearance][501k-clearance] from the U.S. F.D.A or [CE Marking][ce-marking] from the European Commission? + +**NO.** The OHIF Viewer is **NOT** F.D.A. cleared or CE Marked. It is the users' +responsibility to ensure compliance with applicable rules and regulations. The +[License](https://github.com/OHIF/Viewers/blob/master/LICENSE) for the OHIF +Platform does not prevent your company or group from seeking F.D.A. clearance +for a product built using the platform. + +If you have gone this route (or are going there), please let us know because we +would be interested to hear about your experience. + +## Is there a DICOM Conformance Statement for the OHIF Viewer? + +Yes, check it here [DICOM Conformance Statement](https://docs.google.com/document/d/1hbDlUApX4svX33gAUGxGfD7fXXZNaBsX0hSePbc-hNA/edit?usp=sharing) + +## Is The OHIF Viewer [HIPAA][hipaa-def] Compliant? + +**NO.** The OHIF Viewer **DOES NOT** fulfill all of the criteria to become HIPAA +Compliant. It is the users' responsibility to ensure compliance with applicable +rules and regulations. + +## Could you provide me with a particular study from the OHIF Viewer Demo? + +You can check out the studies that we have put in this [Dropbox link](https://www.dropbox.com/scl/fo/66xidsx13pn0zf3b9cbfq/ADaCgn7aT29WMlnTdT_WRXM?rlkey=rratvx6g4kfxnswjdbupewjye&dl=0) + + + + + + +[general]: general +[technical]: technical +[report-bug]: how-do-i-report-a-bug +[new-feature]: how-can-i-request-a-new-feature +[commercial-support]: does-ohif-offer-commercial-support +[academic]: who-should-i-contact-about-academic-collaborations +[fda-clearance]: does-the-ohif-viewer-have-510k-clearance-from-the-us-fda-or-ce-marking-from-the-european-commission +[hipaa]: is-the-ohif-viewer-hipaa-compliant +[501k-clearance]: https://www.fda.gov/MedicalDevices/DeviceRegulationandGuidance/HowtoMarketYourDevice/PremarketSubmissions/PremarketNotification510k/ +[ce-marking]: https://ec.europa.eu/growth/single-market/ce-marking_en +[hipaa-def]: https://en.wikipedia.org/wiki/Health_Insurance_Portability_and_Accountability_Act +[new-issue]: https://github.com/OHIF/Viewers/issues/new/choose +[bug-report-template]: https://github.com/OHIF/Viewers/issues/new?assignees=&labels=Bug+Report+%3Abug%3A&template=---bug-report.md&title= diff --git a/platform/docs/versioned_docs/version-3.9/faq/index.md b/platform/docs/versioned_docs/version-3.9/faq/index.md new file mode 100644 index 0000000..e44af0f --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/faq/index.md @@ -0,0 +1,81 @@ +--- +id: index +--- + + +# General FAQ + + +## How do I report a bug? + +Navigate to our [GitHub Repository][new-issue], and submit a new bug report. +Follow the steps outlined in the [Bug Report Template][bug-report-template]. + +## How can I request a new feature? + +At the moment we are in the process of defining our roadmap and will do our best +to communicate this to the community. If your requested feature is on the +roadmap, then it will most likely be built at some point. If it is not, you are +welcome to build it yourself and [contribute it](../development/contributing.md). +If you have resources and would like to fund the development of a feature, +please [contact us](https://ohif.org/get-support). + + +## Who should I contact about Academic Collaborations? + +[Gordon J. Harris](https://www.dfhcc.harvard.edu/insider/member-detail/member/gordon-j-harris-phd/) +at Massachusetts General Hospital is the primary contact for any academic +collaborators. We are always happy to hear about new groups interested in using +the OHIF framework, and may be able to provide development support if the +proposed collaboration has an impact on cancer research. + +## Does OHIF offer support? + +yes, you can contact us for more information [here](https://ohif.org/get-support) + + +## Does The OHIF Viewer have [510(k) Clearance][501k-clearance] from the U.S. F.D.A or [CE Marking][ce-marking] from the European Commission? + +**NO.** The OHIF Viewer is **NOT** F.D.A. cleared or CE Marked. It is the users' +responsibility to ensure compliance with applicable rules and regulations. The +[License](https://github.com/OHIF/Viewers/blob/master/LICENSE) for the OHIF +Platform does not prevent your company or group from seeking F.D.A. clearance +for a product built using the platform. + +If you have gone this route (or are going there), please let us know because we +would be interested to hear about your experience. + +## Is there a DICOM Conformance Statement for the OHIF Viewer? + +Yes, check it here [DICOM Conformance Statement](https://docs.google.com/document/d/1hbDlUApX4svX33gAUGxGfD7fXXZNaBsX0hSePbc-hNA/edit?usp=sharing) + +## Is The OHIF Viewer [HIPAA][hipaa-def] Compliant? + +**NO.** The OHIF Viewer **DOES NOT** fulfill all of the criteria to become HIPAA +Compliant. It is the users' responsibility to ensure compliance with applicable +rules and regulations. + +## Could you provide me with a particular study from the OHIF Viewer Demo? + +You can check out the studies that we have put in this [Dropbox link](https://www.dropbox.com/scl/fo/66xidsx13pn0zf3b9cbfq/ADaCgn7aT29WMlnTdT_WRXM?rlkey=rratvx6g4kfxnswjdbupewjye&dl=0) + + + + + + +[general]: #general +[technical]: #technicalรŸหš +[report-bug]: #how-do-i-report-a-bug +[new-feature]: #how-can-i-request-a-new-feature +[commercial-support]: #does-ohif-offer-commercial-support +[academic]: #who-should-i-contact-about-academic-collaborations +[fda-clearance]: #does-the-ohif-viewer-have-510k-clearance-from-the-us-fda-or-ce-marking-from-the-european-commission +[hipaa]: #is-the-ohif-viewer-hipaa-compliant +[501k-clearance]: https://www.fda.gov/MedicalDevices/DeviceRegulationandGuidance/HowtoMarketYourDevice/PremarketSubmissions/PremarketNotification510k/ +[ce-marking]: https://ec.europa.eu/growth/single-market/ce-marking_en +[hipaa-def]: https://en.wikipedia.org/wiki/Health_Insurance_Portability_and_Accountability_Act +[new-issue]: https://github.com/OHIF/Viewers/issues/new/choose +[bug-report-template]: https://github.com/OHIF/Viewers/issues/new?assignees=&labels=Bug+Report+%3Abug%3A&template=---bug-report.md&title= diff --git a/platform/docs/versioned_docs/version-3.9/faq/study-sorting.png b/platform/docs/versioned_docs/version-3.9/faq/study-sorting.png new file mode 100644 index 0000000..46d2be1 Binary files /dev/null and b/platform/docs/versioned_docs/version-3.9/faq/study-sorting.png differ diff --git a/platform/docs/versioned_docs/version-3.9/faq/technical.md b/platform/docs/versioned_docs/version-3.9/faq/technical.md new file mode 100644 index 0000000..4b446e5 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/faq/technical.md @@ -0,0 +1,365 @@ +# Technical FAQ + + + +## Why do I keep seeing a Cross Origin Isolation warning +If you encounter a warning while running OHIF indicating that your application is not cross-origin isolated, it implies that volume rendering, such as MPR, will not function properly since they depend on Shared Array Buffers. To resolve this issue, we recommend referring to our comprehensive guide on Cross Origin Isolation available at [our dedicated cors page](../deployment/cors.md). + +## What if my setup does not support the Shared Array Buffers API? +You can simply disable that by adding the `useSharedArrayBuffer: 'FALSE'` (notice the string FALSE), and the volumes will only use a regular +array buffer which is a bit slower but will work on all browsers. + + +## Viewer opens but does not show any thumbnails + +Thumbnails may not appear in your DICOMWeb application for various reasons. This guide focuses on one primary scenario, which is you are using +the `supportsWildcard: true` in your configuration file while your sever does not support it. +One + +For instance for the following filtering in the worklist tab we send this request + +![](../assets/img/filtering-worklist.png) + +`https://d33do7qe4w26qo.cloudfront.net/dicomweb/studies?PatientName=*Head*&limit=101&offset=0&fuzzymatching=false&includefield=00081030%2C00080060` + +Which our server can respond properly. If your server does not support this type of filtering, you can disable it by setting `supportsWildcard: false` in your configuration file, +or edit your server code to support it for instance something like + +```js +Pseudocode: +For each filter in filters: + if filter.value contains "*": + Convert "*" to SQL LIKE wildcard ("%") + Add "metadataField LIKE ?" to query + else: + Add "metadataField = ?" to query +``` + + + +## What are the list of required metadata for the OHIF Viewer to work? + + +### Mandatory + +**All Modalities** + +- `StudyInstanceUID`, `SeriesInstanceUID`, `SOPInstanceUID`: Unique identifiers for the study, series, and object. +- `PhotometricInterpretation`: Describes the color space of the image. +- `Rows`, `Columns`: Image dimensions. +- `PixelRepresentation`: Indicates how pixel data should be interpreted. +- `Modality`: Type of modality (e.g., CT, MR, etc.). +- `PixelSpacing`: Spacing between pixels. +- `BitsAllocated`: Number of bits allocated for each pixel sample. +- `SOPClassUID`: Specifies the DICOM service class of the object (though you might be able to render without it for most regular images datasets, but it is pretty normal to have it) + +**Rendering** + +You need to have the following tags for the viewer to render the image properly, otherwise you should +use the windowing tools to adjust the image to your liking: + +- `RescaleIntercept`, `RescaleSlope`: Values used for rescaling pixel values for visualization. +- `WindowCenter`, `WindowWidth`: Windowing parameters for display. + +**Some Datasets** + +- `InstanceNumber`: Useful for sorting instances (without it the instances might be out of order) + +**For MPR (Multi-Planar Reformatting) rendering and tools** + +- `ImagePositionPatient`, `ImageOrientationPatient`: Position and orientation of the image in the patient. + +**SEG (Segmentation)** + +- `FrameOfReferenceUID` for handling segmentation layers. +- sequences + - `ReferencedSeriesSequence` + - `SharedFunctionalGroupsSequence` + - `PerFrameFunctionalGroupsSequence` + +**RTSTRUCT (Radiotherapy Structure)** + +- `FrameOfReferenceUID` for handling segmentation layers. +- sequences + - `ROIContourSequence` + - `StructureSetROISequence` + - `ReferencedFrameOfReferenceSequence` + +**US (Ultrasound)** + +- `NumberOfFrames`: Number of frames in a multi-frame image. +- `SequenceOfUltrasoundRegions`: For measurements. +- `FrameTime`: Time between frames if specified. + +**SR (Structured Reporting)** + +- Various sequences for encoding the report content and template. + - `ConceptNameCodeSequence` + - `ContentSequence` + - `ContentTemplateSequence` + - `CurrentRequestedProcedureEvidenceSequence` + - `ContentTemplateSequence` + - `CodingSchemeIdentificationSequence` + +**PT with SUV Correction (Positron Tomography Standardized Uptake Value)** + +- Sequences and tags related to radiopharmaceuticals, units, corrections, and timing. + - `RadiopharmaceuticalInformationSequence` + - `SeriesDate` + - `SeriesTime` + - `CorrectedImage` + - `Units` + - `DecayCorrection` + - `AcquisitionDate` + - `AcquisitionTime` + - `PatientWeight` + +**PDF** + +- `EncapsulatedDocument`: Contains the PDF document. + +**Video** + +- `NumberOfFrames`: Video frame count . + + +### Optional +There are various other optional tags that will add to the viewer experience, but are not required for basic functionality. These include: +Patient Information, Study Information, Series Information, Instance Information, and Frame Information. + + +## How do I handle large volumes for MPR and Volume Rendering + +Currently there are two ways to handle large volumes for MPR and Volume Rendering if that does not +fit in the memory of the client machine. + +### `useNorm16Texture` + +WebGL officially supports only 8-bit and 32-bit data types. For most images, 8 bits are not enough, and 32 bits are too much. However, we have to use the 32-bit data type for volume rendering and MPR, which results in suboptimal memory consumption for the application. + +Through [EXT_texture_norm16](https://registry.khronos.org/webgl/extensions/EXT_texture_norm16/) , WebGL can support 16 bit data type which is ideal +for most images. You can look into the [webgl report](https://webglreport.com/?v=2) to check if you have that extension enabled. + +![](../assets/img/webgl-report-norm16.png) + + +This is a flag that you can set in your [configuration file](../configuration/configurationFiles.md) to force usage of 16 bit data type for the volume rendering and MPR. This will reduce the memory usage by half. + + +For instance for a large pt/ct study + +![](../assets/img/large-pt-ct.jpeg) + +Before (without the flag) the app shows 399 MB of memory usage + +![](../assets/img/memory-profiling-regular.png) + + +After (with flag, running locally) the app shows 249 MB of memory usage + + +![](../assets/img/webgl-int16.png) + +:::note +Using the 16 bit texture (if supported) will not have any effect in the rendering what so ever, and pixelData +would be exactly shown as it is. For datasets that cannot be represented with 16 bit data type, the flag will be ignored +and the 32 bit data type will be used. + + +Read more about these discussions in our PRs +- https://github.com/Kitware/vtk-js/pull/2058 +::: + + +:::warning +Although the support for 16 bit data type is available in WebGL, in some settings (e.g., Intel-based Macos) there seems +to be still some issues with it. You can read and track bugs below. + +- https://bugs.chromium.org/p/chromium/issues/detail?id=1246379 +- https://bugs.chromium.org/p/chromium/issues/detail?id=1408247 +::: + +### `preferSizeOverAccuracy` + +This is another flag that you can set in your [configuration file](../configuration/configurationFiles.md) to force the usage of the `half_float` data type for volume rendering and MPR. The main reason to choose this option over `useNorm16Texture` is its broader support across hardware and browsers. However, it is less accurate than the 16-bit data type and may lead to some rendering artifacts. + +```js +Integers between 0 and 2048 can be exactly represented (and also between โˆ’2048 and 0) +Integers between 2048 and 4096 round to a multiple of 2 (even number) +Integers between 4096 and 8192 round to a multiple of 4 +Integers between 8192 and 16384 round to a multiple of 8 +Integers between 16384 and 32768 round to a multiple of 16 +Integers between 32768 and 65519 round to a multiple of 32 +``` + +As you see in the ranges above 2048 there will be inaccuracies in the rendering. + +Memory snapshot after enabling `preferSizeOverAccuracy` for the same study as above + +![](../assets/img/preferSizeOverAccuracy.png) + + +## How to dynamically load a measurement + +You can dynamically load a measurement by using a combination of `MeasurementService` and `CornerstoneTools` Annotation API. Here, we will demonstrate this with an example of loading a `Rectangle` measurement. + +![alt text](faq-measure-1.png) + +So if we look at the terminal and get the measurement service we can see there is one measurement + +![alt text](faq-measure-2.png) + +However, this is the `mapped` cornerstone measurement inside OHIF, and it has additional information such as `geReport` and `source`, which are internal details of OHIF Viewers that you don't need to worry about. + +we can call the `cornerstoneTools` api to grab the raw annotation data with the `uid` + +`cornerstoneTools.annotation.state.getAnnotation("ea45a45c-0731-47d4-9438-d2a53ffea4ff")` + +![alt text](faq-measure3.png) + + + + +:::note +Note: There is a `pointsInShape` attribute inside the data that stores the points within the annotation for some tools like `Rectangle` and `EllipticalRoi`. However, you can remove that attribute as well. +::: + +For the sake of this example, I have extracted those keys and uploaded them to our server for fetching. + +` +https://ohif-assets.s3.us-east-2.amazonaws.com/ohif-faq/rectangle-roi.json +` + +Now, let's discuss how to load this measurement dynamically and programmatically. + +There are numerous places in OHIF where you can add annotations, but we always recommend having your own extensions and modes to maintain full control over your custom API. + +For this example, I will add the logic in the `longitudinal` mode. However, as mentioned, you can create your own extension and mode, and either use `onModeEnter` or other lifecycle hooks to add annotations. Learn more about lifecycle hooks [here](../platform/extensions/lifecycle.md). + + +Of course, you need to load the appropriate measurement for each study. However, for simplicity's sake, I will hardcode the URL in this example. + +```js +import * as cs3dTools from '@cornerstonejs/tools'; + +onModeEnter: function ({ servicesManager, extensionManager, commandsManager }: withAppTypes) { + // rest of logic + + const annotationResponse = await fetch( + 'https://ohif-assets.s3.us-east-2.amazonaws.com/ohif-faq/rectangle-roi.json' + ); + + const annotationData = await annotationResponse.json(); + + cs3dTools.annotation.state.addAnnotation(annotationData); +}, +``` + +As you can see, we use the CornerstoneTools API to add the annotation. Since OHIF has mappers set up for CornerstoneTools (`extensions/cornerstone/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.ts`), it will automatically map the annotation to the OHIF measurement service. + +If you refresh the viewer, you'll see the measurement loaded on the image. + +![alt text](faq-measure-4.png) + +But if you notice it does not appear on the right panel, the reason is that the right panel is the tracking measurement panel. You can switch to a non-tracking measurement by changing + +`rightPanels: [dicomSeg.panel, tracked.measurements],` + +to + +`rightPanels: [dicomSeg.panel, '@ohif/extension-default.panelModule.measure'],` + +which then it will look like + +![alt text](faq-measure-5.png) + + + +## How do I sort the series in the study panel by a specific value + +You need to enable the experimental StudyBrowserSort component by setting the `experimentalStudyBrowserSort` to true in your config file. This will add a dropdown in the study panel to sort the series by a specific value. This component is experimental +since we are re-deigning the study panel and it might change in the future, but the functionality will remain the same. + +```js +{ + experimentalStudyBrowserSort: true, +} +``` +The component will appear in the study panel and will allow you to sort the series by a specific value. It comes with 3 default sorting functions, Series Number, Series Image Count, and Series Date. + +You can sort the series in the study panel by a specific value by adding a custom sorting function in the customizationModule, you can use the existing customizationModule in `extensions/default/src/getCustomizationModule.tsx` or create your own in your extension. + +The value to be used for the entry is `studyBrowser.sortFunctions` and should be under the `default` key. + +### Example + +```js +export default function getCustomizationModule({ servicesManager, extensionManager }) { + return [ + { + name: 'default', + value: [ + + { + id: 'studyBrowser.sortFunctions', + values: [ + { + label: 'Series Number', + sortFunction: (a, b) => { + return a?.SeriesNumber - b?.SeriesNumber; + }, + }, + // Add more sort functions as needed + ], + }, + ], + }, + ]; +} +``` + +### Explanation +This function will be retrieved by the StudyBrowserSort component and will be used to sort all displaySets, it will reflect in all parts of the app since it works at the displaySetService level, which means the thumbnails in the study panel will also be sorted by the desired value. +You can define multiple functions and pick which sort to use via the dropdown in the StudyBrowserSort component that appears in the study panel. + + +## How can i change the sorting of the thumbnail / study panel / study browser +We are currently redesigning the study panel and the study browser. During this process, you can enable our undesigned component via the `experimentalStudyBrowserSort` flag. This will look like: + +![alt text](study-sorting.png) + +You can also add your own sorting functions by utilizing the `customizationService` and adding the `studyBrowser.sortFunctions` key, as shown below: + +``` +customizationService.addModeCustomizations([ + { + id: 'studyBrowser.sortFunctions', + values: [{ + label: 'Series Images', + sortFunction: (a, b) => { + return a?.numImageFrames - b?.numImageFrames; + }, + }], + }, +]); +``` + +:::note +Notice the arrays and objects, the values are arrays +::: + + +## How do I change the cine auto mount behavior + +You can change the cine auto mount behavior by adding the `autoCineModalities` mode customization, the value is an array of modalities that should be mounted with cine. + +By default the viewer will mount with cine enabled for `OT` and `US` modalities. + +```js +customizationService.addModeCustomizations([ + { + id: 'autoCineModalities', + modalities: ['OT', 'US'], + }, +]); +``` diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/0-general.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/0-general.md new file mode 100644 index 0000000..45701fc --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/0-general.md @@ -0,0 +1,288 @@ +--- +id: 0-general +title: General +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# No SharedArrayBuffer anymore! + +We have streamlined the process of loading volumes without sacrificing speed by eliminating the need for shared array buffers. This change resolves issues across various frameworks, where previously, specific security headers were required. Now, you can remove any previously set headers, which lowers the barrier for adopting Cornerstone 3D in frameworks that didn't support those headers. Shared array buffers are no longer necessary, and all related headers can be removed. + +You can remove `Cross-Origin-Opener-Policy` and `Cross-Origin-Embedder-Policy` from your custom headers if you don't need them in other +aspects of your app. + +# React 18 Migration Guide +As we upgrade to React 18, we're making some exciting changes to improve performance and developer experience. This guide will help you navigate the key updates and ensure your custom extensions and modes are compatible with the new version. +What's Changing? + + + + +```md +- React 17 +- Using `defaultProps` +- `babel-inline-svg` for SVG imports +``` + + + + +```md +- React 18 +- Default parameters for props +- `svgr` for SVG imports +``` + + + + + +## Update React version: +In your custom extensions and modes, change the version of react and react-dom to ^18.3.1. + +## Replace defaultProps with default parameters: + + + + +```jsx +const MyComponent = ({ prop1, prop2 }) => { + return
{prop1} {prop2}
+} + +MyComponent.defaultProps = { + prop1: 'default value', + prop2: 'default value' +} +``` + +
+ + +```jsx +const MyComponent = ({ prop1 = 'default value', prop2 = 'default value' }) => { + return
{prop1} {prop2}
+} +``` +
+
+ +## Update SVG imports: + +You might need to update your SVG imports to use the `ReactComponent` syntax, if you want to use the old Icon component. However, we have made a significant change to how we handle Icons, read the UI Migration Guide for more information. + + + + +```javascript +import arrowDown from './../../assets/icons/arrow-down.svg'; +``` + + + + +```javascript +import { ReactComponent as arrowDown } from './../../assets/icons/arrow-down.svg'; +``` + + + + +--- + +## Polyfill.io + +We have removed the Polyfill.io script from the Viewer. If you require polyfills, you can add them to your project manually. This change primarily affects Internet Explorer, which Microsoft has already [ended support for](https://learn.microsoft.com/en-us/lifecycle/faq/internet-explorer-microsoft-edge#is-internet-explorer-11-the-last-version-of-internet-explorer-). + + + +--- + +## Crosshairs + +They now have new colors in their associated viewports in the MPR view. However, you can turn this feature off. + +To disable it, remove the configuration from the `initToolGroups` in your mode. + +``` +{ + configuration: { + viewportIndicators: true, + viewportIndicatorsConfig: { + circleRadius: 5, + xOffset: 0.95, + yOffset: 0.05, + }, + } +} +``` + +--- + + +## useAuthorizationCodeFlow + +`useAuthorizationCodeFlow` config is deprecated + +now internally we detect the authorizationCodeFlow if the response_type is equal to `code` + +you can remove the config from the appConfig + +--- + +## StackScrollMouseWheel -> StackScroll Tool + Mouse bindings + +If you previously used: + +```js +{ toolName: toolNames.StackScrollMouseWheel, bindings: [] } +``` + +in your `initToolGroups`, you should now use: + +```js +{ + toolName: toolNames.StackScroll, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], +} +``` + +This change allows for more flexible mouse bindings and keyboard combinations. + +## VolumeRotateMouseWheel -> VolumeRotate Tool + Mouse bindings + +Before: + +```js +{ + toolName: toolNames.VolumeRotateMouseWheel, + configuration: { + rotateIncrementDegrees: 5, + }, +}, +``` + +Now: + +```js +{ + toolName: toolNames.VolumeRotate, + bindings: [{ mouseButton: Enums.MouseBindings.Wheel }], + configuration: { + rotateIncrementDegrees: 5, + }, +}, +``` + +--- + +## SidePanel auto switch if open + +In `basic viewer` mode, if the side panel is open and the segmentation panel is active, adding a measurement will automatically switch to the measurement panel. This switch won't occur if the side panel is closed. To enable or disable this feature, adjust your mode configuration accordingly. + +```js +panelService.addActivatePanelTriggers('your.panel.id', [ +{ + sourcePubSubService: segmentationService, + sourceEvents: [segmentationService.EVENTS.SEGMENTATION_ADDED], +}, +]) + +panelService.addActivatePanelTriggers('your.panel.id', [ + { + sourcePubSubService: measurementService, + sourceEvents: [ + measurementService.EVENTS.MEASUREMENT_ADDED, + measurementService.EVENTS.RAW_MEASUREMENT_ADDED, + ], + }, +]) +``` + +--- + +## DicomUpload + +The DICOM upload functionality in OHIF has been refactored to use the standard customization service pattern. Now you don't need to put + +`customizationService: { dicomUploadComponent: '@ohif/extension-cornerstone.customizationModule.cornerstoneDicomUploadComponent', },` + +in your config, we will automatically add that if you have `dicomUploadEnabled` + +--- + +## Viewport and Modality Support for Toolbar Buttons + +Previously, toolbar buttons had limited support for disabling themselves based on the active viewport type (e.g., `volume3d`, `video`, `sr`) or the modality of the displayed data (e.g., `US`, `SM`). This led to inconsistencies and sometimes enabled tools in contexts where they weren't applicable. + +The new implementation introduces more robust and flexible evaluators to control the enabled/disabled state of toolbar buttons based on viewport types and modalities. + +**Key Changes** + +1. **New Evaluators:** New evaluators have been added to the `getToolbarModule`: + - `evaluate.viewport.supported`: Disables a button if the active viewport's type is listed in the `unsupportedViewportTypes` property. + - `evaluate.modality.supported`: Disables a button based on the modalities of the displayed data. It checks for both `unsupportedModalities` (exclusion) and `supportedModalities` (inclusion). +2. **Removal of Legacy Evaluators:** + - Evaluators such as `evaluate.not.sm`, `evaluate.action.not.video`, `evaluate.not3D`, and `evaluate.isUS` have been removed. Migrate your toolbar button definitions to use the new evaluators mentioned above. + + +**Replace Legacy Evaluators:** + - Replace `evaluate.not.sm` with: + + ```json + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['sm'], + } + ``` + + - Replace `evaluate.action.not.video` with: + + ```json + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['video'], + } + ``` + + - Replace `evaluate.not3D` with: + + ```json + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d'], + } + ``` + + - Replace `evaluate.isUS` with: + + ```json + { + name: 'evaluate.modality.supported', + supportedModalities: ['US'], + } + ``` + +
+Example Migration + +Before: + +```json +evaluate: ['evaluate.cine', 'evaluate.not3D'], +``` + +After + +```json +evaluate: [ + 'evaluate.cine', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d'], + }, +], +``` +
diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/1-Architecture.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/1-Architecture.md new file mode 100644 index 0000000..c8a92ef --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/1-Architecture.md @@ -0,0 +1,48 @@ +--- +id: seg-new-arch +title: New Architecture +--- + + +## New Architecture + +* **Viewport-Centric Architecture** + * Previous: Segmentations were tied to toolGroups + * Now: Segmentations are tied directly to viewports + * Impact: More granular control but requires significant code changes + +* **Representation Management** + * Previous: Required managing segmentation representation UIDs + * Now: Uses simpler segmentationId + type combination + * Impact: Simplified but requires API updates + + + +If you are not familiar with the difference between a segmentation and a segmentation representation, below + +
+Read More + +In Cornerstone3DTools, we have decoupled the concept of a Segmentation from a Segmentation Representation. This means that from one Segmentation we can create multiple Segmentation Representations. For instance, a Segmentation Representation of a 3D Labelmap, can be created from a Segmentation data, and a Segmentation Representation of a Contour can be created from the same Segmentation data. This way we have decouple the presentational aspect of a Segmentation from the underlying data. + + +Similar relationship structure has been adapted in popular medical imaging softwares such as 3D Slicer with the addition of polymorph segmentation. + +- https://github.com/PerkLab/PolySeg +- https://www.slicer.org/ + + + +
+ + + + +### Architecture Overview + +The new architecture in Cornerstone3D 2.0 makes a clear distinction between: + +* A segmentation (the data structure containing segments) +* A segmentation representation (how that segmentation is visualized in a specific viewport) + +Let's now review what has changed diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/2-segmentationService-basic.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/2-segmentationService-basic.md new file mode 100644 index 0000000..06601b7 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/2-segmentationService-basic.md @@ -0,0 +1,433 @@ +--- +id: seg-api +title: SegmentationService API +--- + + + +Below we will review the changes to the API of the `SegmentationService` + +# SegmentationService API + +## Events + +SEGMENTATION_UPDATED -> SEGMENTATION_MODIFIED + + +Just a rename to match the cornerstone terminology + +## VolumeId vs SegmentationId + +Previously, we used the SegmentationId as the VolumeId for volume-based segmentations, which led to confusion and issues. + +Now, we have two separate IDs: one for the segmentation and one for the volume. + +`segmentationService.getLabelmapVolume(segmentationId)` will return the volume associated with the segmentation. + +If your code uses `cache.getVolume(segmentationId)`, update it to use the new `getLabelmapVolume` method. + + +## getSegmentation(segmentationId) + +remains the same it will return the segmentation object = cornerstone segmentation object with the following properties: + +```js +/** + * Global Segmentation Data which is used for the segmentation + */ +type Segmentation = { + /** segmentation id */ + segmentationId: string; + /** segmentation label */ + label: string; + segments: { + [segmentIndex: number]: Segment; + }; + /** + * Representations of the segmentation. Each segmentation "can" be viewed + * in various representations. For instance, if a DICOM SEG is loaded, the main + * representation is the labelmap. However, for DICOM RT the main representation + * is contours, and other representations can be derived from the contour (currently + * only labelmap representation is supported) + */ + representationData: RepresentationsData; + /** + * Segmentation level stats, Note each segment can have its own stats + * This is used for caching stats for the segmentation level + */ + cachedStats: { [key: string]: unknown }; +}; + +export type Segment = { + /** segment index */ + segmentIndex: number; + /** segment label */ + label: string; + /** is segment locked for editing */ + locked: boolean; + /** cached stats for the segment, e.g., pt suv mean, max etc. */ + cachedStats: { [key: string]: unknown }; + /** is segment active for editing, at the same time only one segment can be active for editing */ + active: boolean; +}; +``` + + +
+Compared to Cornerstone3D 1.x + +Previously this function was returning this + +```js +export type Segmentation = { + segmentationId: string; + type: Enums.SegmentationRepresentations; + label: string; + activeSegmentIndex: number; + segmentsLocked: Set; + cachedStats: { [key: string]: number }; + segmentLabels: { [key: string]: string }; + representationData: SegmentationRepresentationData; +}; + +``` + +As you can see `segmentLabels`, `segmentsLocked`, `activeSegmentIndex`, are all gathered under the new `segments` object. We now have support for per segment cachedStats as well. + +
+ +--- + +## getSegmentations + +It provides all segmentations in the state. Previously, it accepted a `filterNonhydrated` flag, but since we've moved away from hydration and every loaded segmentation is now hydrated by default, it returns all segmentations. + + + + +--- + +## getActiveSegmentation + + +After migrating to viewport-specific segmentations, different viewports can have distinct active segmentations for editing. The panel will always display the active segmentation when the active viewport changes. + +Before (3.8) + +```js +// Returns full segmentation object +public getActiveSegmentation(): Segmentation { + const segmentations = this.getSegmentations(); + return segmentations.find(segmentation => segmentation.isActive); +} +``` + +After (3.9) + +```js +public getActiveSegmentation(viewportId: string): Segmentation | null { + return cstSegmentation.activeSegmentation.getActiveSegmentation(viewportId); +} +``` + +
+Key Changes + +1. **Viewport Specificity** + - Before: Global active segmentation across all tool groups + - After: Active segmentation per viewport +2. **Required Parameters** + - Before: No parameters needed + - After: Requires viewportId parameter +
+ + +
+Migration Examples + +**Before:** + +```js +// Get active segmentation +const activeSegmentation = segmentationService.getActiveSegmentation(); +if (activeSegmentation) { + console.log('Active segmentation:', activeSegmentation.segmentationId); + console.log('Active segment:', activeSegmentation.activeSegmentIndex); +} +``` + +**After:** + +```js +// Get active segmentation for specific viewport +const activeSegmentation = segmentationService.getActiveSegmentation('viewport1'); + +``` + +
+ +--- + +## getToolGroupIdsWithSegmentation + +is now -> `getViewportIdsWithSegmentation` as you guessed + + + +## setActiveSegmentationForToolGroup + +-> setActiveSegmentation + + + +**Before (OHIF 3.8)** + +```js +setActiveSegmentationForToolGroup( + segmentationId: string, + toolGroupId?: string, + suppressEvents?: boolean +): void +``` + +**After (OHIF 3.9)** + +```js +setActiveSegmentation( + viewportId: string, + segmentationId: string +): void +``` + +
+Migration Examples + +1. **Basic Usage Update** + + ```js + // Before - OHIF 3.8 + segmentationService.setActiveSegmentationForToolGroup( + segmentationId, + toolGroupId + ); + // After - OHIF 3.9 + segmentationService.setActiveSegmentation( + viewportId, + segmentationId + ); + ``` + +
+ + + +--- + + +## addSegment + +The `addSegment` method in OHIF 3.9 has been updated to handle segmentation properties in a viewport-centric way, removing tool group dependencies and simplifying the configuration structure. + + +**Before (OHIF 3.8)** + +```js +addSegment( + segmentationId: string, + config: { + segmentIndex?: number; + toolGroupId?: string; + properties?: { + label?: string; + color?: ohifTypes.RGB; + opacity?: number; + visibility?: boolean; + isLocked?: boolean; + active?: boolean; + }; + } +): void +``` + +**After (OHIF 3.9)** + +```js +addSegment( + segmentationId: string, + config: { + segmentIndex?: number; + label?: string; + isLocked?: boolean; + active?: boolean; + color?: csTypes.Color; + visibility?: boolean; + } +): void +``` + +
+Key Changes + +1. **Configuration Structure** + - Removed double nested `properties` object + - Configuration options now at top level + - Removed `toolGroupId` parameter + - Removed `opacity` parameter (now part of color) +2. **Segment Index Generation** + - Changed from length-based to max-value-based indexing + - More reliable for non-sequential segment indices +3. **Color Handling** + - Color now includes alpha channel (opacity) + - Applied to all relevant viewports automatically +
+ + + + +
+Migration Examples + +1. **Basic Segment Creation** + + ```js + // Before - OHIF 3.8 + segmentationService.addSegment(segmentationId, { + properties: { + label: 'Segment 1' + } + }); + // After - OHIF 3.9 + segmentationService.addSegment(segmentationId, { + label: 'Segment 1' + }); + ``` + +2. **Creating Segment with Color** + + ```js + // Before - OHIF 3.8 + segmentationService.addSegment(segmentationId, { + properties: { + color: [255, 0, 0], + opacity: 255 + } + }); + // After - OHIF 3.9 + segmentationService.addSegment(segmentationId, { + color: [255, 0, 0, 255] // RGB + Alpha + }); + ``` + +3. **Setting Visibility and Lock Status** + + ```js + // Before - OHIF 3.8 + segmentationService.addSegment(segmentationId, { + toolGroupId: 'myToolGroup', + properties: { + visibility: true, + isLocked: true + } + }); + // After - OHIF 3.9 + segmentationService.addSegment(segmentationId, { + visibility: true, + isLocked: true + }); + ``` + +4. **Complete Configuration Example** + + ```js + // Before - OHIF 3.8 + segmentationService.addSegment(segmentationId, { + segmentIndex: 1, + toolGroupId: 'myToolGroup', + properties: { + label: 'Tumor', + color: [255, 0, 0], + opacity: 200, + visibility: true, + isLocked: false, + active: true + } + }); + // After - OHIF 3.9 + segmentationService.addSegment(segmentationId, { + segmentIndex: 1, + label: 'Tumor', + color: [255, 0, 0, 200], // RGB + Alpha + visibility: true, + isLocked: false, + active: true + }); + ``` + +
+ + + + +
+Important Changes + +1. **Tool Group Removal** + ```js + // Before - OHIF 3.8 + segmentationService.addSegment(segmentationId, { + toolGroupId: 'myToolGroup' + // ... other properties + }); + // After - OHIF 3.9 + // No tool group needed - automatically applies to all relevant viewports + segmentationService.addSegment(segmentationId, { + // ... properties + }); + ``` + +2. **Segment Index Generation** + ```js + // Before - OHIF 3.8 + // Used array length + segmentIndex = segmentation.segments.length === 0 ? 1 : segmentation.segments.length; + // After - OHIF 3.9 + // Uses highest existing index + 1 + segmentIndex = Math.max(...Object.keys(csSegmentation.segments).map(Number)) + 1; + ``` + +3. **Color and Opacity** + ```js + // Before - OHIF 3.8 + segmentationService.addSegment(segmentationId, { + properties: { + color: [255, 0, 0], + opacity: 200 + } + }); + + // After - OHIF 3.9 + segmentationService.addSegment(segmentationId, { + color: [255, 0, 0, 200] // Combined color and opacity + }); + ``` + +
+ + +--- + +--- + +## getActiveSegment + +now requires viewportId, since we have moved away from global active segmentation to viewport specific one + +**API Changes** + +```js +// Before +getActiveSegment(): Segment + +// After +getActiveSegment(viewportId: string): Segment | null +``` diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/3-segmentationserice-representation.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/3-segmentationserice-representation.md new file mode 100644 index 0000000..5222592 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/3-segmentationserice-representation.md @@ -0,0 +1,189 @@ +--- +id: seg-representation +title: Segmentation Representations +--- + + + + +## Segmentation Representation Management API + +```js +addSegmentationRepresentationToToolGroup +removeSegmentationRepresentationFromToolGroup +getSegmentationRepresentationsForToolGroup +``` + +In Cornerstone3D 2.0, segmentation representation management has shifted from a tool group-centric approach to a viewport-centric approach. This architectural change provides better control over segmentation rendering and simplifies the mental model for managing segmentations. + + +### Adding Segmentation Representations + +**Before (3.8)**: + +```js +// Tool group-based approach +await segmentation.addSegmentationRepresentationToToolGroup( + toolGroupId, + segmentationId, + hydrateSegmentation, + csToolsEnums.SegmentationRepresentations.Labelmap +); +``` + +**After (3.9)**: + +```js +// Viewport-centric approach +await segmentation.addSegmentationRepresentation( + viewportId, + { + segmentationId: segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap, + } +); +``` + +### Removing Segmentation Representations + +**Before** : + +```js +// Remove specific representations from a tool group +segmentation.removeSegmentationRepresentationFromToolGroup( + toolGroupId, + [segmentationRepresentationUID] +); +// Remove all representations from a tool group +segmentation.removeSegmentationRepresentationFromToolGroup(toolGroupId); +``` + +**After** + +```js +// Remove specific representation from a viewport +segmentation.removeSegmentationRepresentation( + viewportId, + { + segmentationId: segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap + } +); +// Remove all representations from a viewport +segmentation.removeSegmentationRepresentations(viewportId); +``` + +### Getting Segmentation Representations + +**Before**: + +```js +// Get representations for a tool group +const representations = segmentation.getSegmentationRepresentationsForToolGroup(toolGroupId); +``` + +**After** : + +```js +// Get all representations for a viewport +const representations = segmentation.getSegmentationRepresentations(viewportId); + +// Get specific type of representations +const labelmapReps = segmentation.getSegmentationRepresentations(viewportId, { + type: csToolsEnums.SegmentationRepresentations.Labelmap +}); + +// Get representations for specific segmentation +const segmentationReps = segmentation.getSegmentationRepresentations(viewportId, { + segmentationId: segmentationId +}); + +// Get specific representation +const representation = segmentation.getSegmentationRepresentation(viewportId, { + segmentationId: segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap +}); +``` + +### Understanding the Specifier Pattern + +The Cornerstone3D 2.0 (OHIF 3.9) API introduces a "specifier" pattern that provides more flexible and precise control over segmentation representations. A specifier is an object that can include: + +```js +type Specifier = { + segmentationId?: string; // The ID of the segmentation + type?: SegmentationRepresentations; // The type of representation (Labelmap, Contour, etc.) +} +``` + +The specifier pattern allows for: + +1. **Precise Targeting**: You can target specific segmentations and representation types + - Allows direct access to individual segmentations + - Enables filtering by representation type + +2. **Flexible Querying**: You can get all representations of a certain type or for a specific segmentation + - Query by segmentation ID + - Query by representation type + - Combine queries for specific needs + +3. **Granular Control**: You can manage representations at different levels of specificity + - Viewport level control + - Segmentation level control + - Individual representation type control + +### Examples of Specifier Usage + +```js +// Get all labelmap representations in a viewport +const labelmaps = segmentation.getSegmentationRepresentations(viewportId, { + type: csToolsEnums.SegmentationRepresentations.Labelmap +}); + +// Get all representations of a specific segmentation (including contour, labelmap, surface) +const segReps = segmentation.getSegmentationRepresentations(viewportId, { + segmentationId: 'seg123' +}); + +// Get a specific representation +const specificRep = segmentation.getSegmentationRepresentation(viewportId, { + segmentationId: 'seg123', + type: csToolsEnums.SegmentationRepresentations.Labelmap +}); +``` + +
+Benefits of the New Approach + +1. **Direct Viewport Control**: + - Each viewport can have its own unique representation configuration + - No need to create separate tool groups for different viewport representations +2. **Simpler Mental Model**: + - Representations are directly tied to where they're displayed + - No intermediate tool group layer to manage +3. **More Flexible Rendering**: + - Each viewport can render the same segmentation differently + - Better support for multiple views of the same data +4. **Improved Type Safety**: + - Specifier pattern provides better TypeScript support + - More explicit API with clearer intentions +
+ + +
+Migration Tips + +1. **Replace Tool Group References**: + - Search your codebase for `toolGroupId` references in segmentation code + - Replace with appropriate `viewportId` references +2. **Update Event Handlers**: + - Update any code listening for segmentation events + - Events now include viewportId instead of toolGroupId +3. **Review Representation Management**: + - Identify where you manage segmentation representations + - Convert to using the new viewport-centric methods +4. **Consider Viewport Context**: + - Think about segmentation representation in terms of viewport display + - Use specifiers to target specific representations when needed + +
diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-creation.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-creation.md new file mode 100644 index 0000000..c70021e --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-creation.md @@ -0,0 +1,215 @@ +--- +id: seg-creation +title: Segmentation Creation +--- + +## createEmptySegmentationForViewport + +is now `createLabelmapForViewport` to align with other segmentation creation methods. + +Run it using `commandsManager.runCommand('createLabelmapForViewport', {viewportId})`. + +## createSegmentationForDisplaySet + +is now -> `createLabelmapForDisplaySet` + +Since we are moving towards segmentations be contours as well, this is renamed to clearly state the purpose. +Since OHIF 3.9 introduced Stack Segmentation support, we no longer generate a volume-based labelmap or convert the viewport to a volume viewport by default. Our default creation is now stack-based. + +API Changes +- `createSegmentationForDisplaySet` has been renamed to `createLabelmapForDisplaySet`. +- Pass a `displaySet` object instead of a `displaySetInstanceUID`. This change enhances type safety and flexibility, accommodating future updates to the `displaySetService`. + +**Before (OHIF 3.8)** + +```js +async createSegmentationForDisplaySet( + displaySetInstanceUID: string, + options?: { + segmentationId: string; + FrameOfReferenceUID: string; + label: string; + } +): Promise +``` + +**After (OHIF 3.9)** + +```js +// Method 1: Display Set Based +async createLabelmapForDisplaySet( + displaySet: DisplaySet, + options?: { + segmentationId?: string; + label: string; + segments?: { + [segmentIndex: number]: Partial + }; + } +): Promise +``` + + +
+Migration Examples + + +```js +// Before - OHIF 3.8 +const segmentationId = await segmentationService.createSegmentationForDisplaySet( + displaySetInstanceUID, + { + label: 'My Segmentation' + } +); +``` + +```js +// After - OHIF 3.9 +// Option 1: If you have a display set UID +const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + +const segmentationId = await segmentationService.createLabelmapForDisplaySet( + displaySet, + { + label: 'My Segmentation' + } +); +``` + +
+ +--- + +## createSegmentationForRTDisplaySet + + +**Before (OHIF 3.8)** + +```js +async createSegmentationForRTDisplaySet( + rtDisplaySet, + segmentationId?: string, + suppressEvents = false +): Promise +``` + +**After (OHIF 3.9)** + +```js +async createSegmentationForRTDisplaySet( + rtDisplaySet, + options: { + segmentationId?: string; + type: SegmentationRepresentations; // not required, defaults to Contour + } +): Promise +``` + + +
+Migration Examples + +if you were not passing segmentationId, you don't need to change anything + + +```js +// Before - OHIF 3.8 +const segmentationId = await segmentationService.createSegmentationForRTDisplaySet( + rtDisplaySet +); + +// After - OHIF 3.9 +const segmentationId = await segmentationService.createSegmentationForRTDisplaySet( + rtDisplaySet, +); +``` + +if you were passing segmentationId, you need to update the API to pass an options object and set the segmentationId in there. + +```js +// Before - OHIF 3.8 +const segmentationId = await segmentationService.createSegmentationForRTDisplaySet( + rtDisplaySet, + 'custom-id', +); +// After - OHIF 3.9 +const segmentationId = await segmentationService.createSegmentationForRTDisplaySet( + rtDisplaySet, + { + segmentationId: 'custom-id', + type: csToolsEnums.SegmentationRepresentations.Contour + } +); +``` + +
+ +--- + + +## createSegmentationForSEGDisplaySet Changes + +**Before (OHIF 3.8)** + +```js +async createSegmentationForSEGDisplaySet( + segDisplaySet, + segmentationId?: string, + suppressEvents = false +): Promise +``` + +**After (OHIF 3.9)** + +```js +async createSegmentationForSEGDisplaySet( + segDisplaySet, + options: { + segmentationId?: string; + type: SegmentationRepresentations; // not required, defaults to Labelmap + } +): Promise +``` + +
+Migration Examples + +1. **Basic Usage Update** + + ``` + // Before - OHIF 3.8 + const segmentationId = await segmentationService.createSegmentationForSEGDisplaySet( + segDisplaySet + ); + // After - OHIF 3.9 + const segmentationId = await segmentationService.createSegmentationForSEGDisplaySet( + segDisplaySet, + { + type: csToolsEnums.SegmentationRepresentations.Labelmap + } + ); + ``` + +2. **Custom Configuration** + + ``` + // Before - OHIF 3.8 + const segmentationId = await segmentationService.createSegmentationForSEGDisplaySet( + segDisplaySet, + 'custom-id', + false + ); + // After - OHIF 3.9 + const segmentationId = await segmentationService.createSegmentationForSEGDisplaySet( + segDisplaySet, + { + segmentationId: 'custom-id', + type: csToolsEnums.SegmentationRepresentations.Labelmap + } + ); + ``` +
+ + +--- diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-modification.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-modification.md new file mode 100644 index 0000000..ebe16f7 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/4-segmentationserice-modification.md @@ -0,0 +1,193 @@ +--- +id: seg-service-mod +title: SegmentationService Modifications +--- + + +--- + + +## Segmentation Representation Management API + +```js +addSegmentationRepresentationToToolGroup +removeSegmentationRepresentationFromToolGroup +getSegmentationRepresentationsForToolGroup +``` + +In Cornerstone3D 2.0, segmentation representation management has shifted from a tool group-centric approach to a viewport-centric approach. This architectural change provides better control over segmentation rendering and simplifies the mental model for managing segmentations. + + +### Adding Segmentation Representations + +**Before (3.8)**: + +```js +// Tool group-based approach +await segmentation.addSegmentationRepresentationToToolGroup( + toolGroupId, + segmentationId, + hydrateSegmentation, + csToolsEnums.SegmentationRepresentations.Labelmap +); +``` + +**After (3.9)**: + +```js +// Viewport-centric approach +await segmentation.addSegmentationRepresentation( + viewportId, + { + segmentationId: segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap, + } +); +``` + +### Removing Segmentation Representations + +**Before** : + +```js +// Remove specific representations from a tool group +segmentation.removeSegmentationRepresentationFromToolGroup( + toolGroupId, + [segmentationRepresentationUID] +); +// Remove all representations from a tool group +segmentation.removeSegmentationRepresentationFromToolGroup(toolGroupId); +``` + +**After** + +```js +// Remove specific representation from a viewport +segmentation.removeSegmentationRepresentation( + viewportId, + { + segmentationId: segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap + } +); +// Remove all representations from a viewport +segmentation.removeSegmentationRepresentations(viewportId); +``` + +### Getting Segmentation Representations + +**Before**: + +```js +// Get representations for a tool group +const representations = segmentation.getSegmentationRepresentationsForToolGroup(toolGroupId); +``` + +**After** : + +```js +// Get all representations for a viewport +const representations = segmentation.getSegmentationRepresentations(viewportId); + +// Get specific type of representations +const labelmapReps = segmentation.getSegmentationRepresentations(viewportId, { + type: csToolsEnums.SegmentationRepresentations.Labelmap +}); + +// Get representations for specific segmentation +const segmentationReps = segmentation.getSegmentationRepresentations(viewportId, { + segmentationId: segmentationId +}); + +// Get specific representation +const representation = segmentation.getSegmentationRepresentation(viewportId, { + segmentationId: segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap +}); +``` + +### Understanding the Specifier Pattern + +The Cornerstone3D 2.0 (OHIF 3.9) API introduces a "specifier" pattern that provides more flexible and precise control over segmentation representations. A specifier is an object that can include: + +```js +type Specifier = { + segmentationId?: string; // The ID of the segmentation + type?: SegmentationRepresentations; // The type of representation (Labelmap, Contour, etc.) +} +``` + +The specifier pattern allows for: + +1. **Precise Targeting**: You can target specific segmentations and representation types + - Allows direct access to individual segmentations + - Enables filtering by representation type + +2. **Flexible Querying**: You can get all representations of a certain type or for a specific segmentation + - Query by segmentation ID + - Query by representation type + - Combine queries for specific needs + +3. **Granular Control**: You can manage representations at different levels of specificity + - Viewport level control + - Segmentation level control + - Individual representation type control + +### Examples of Specifier Usage + +```js +// Get all labelmap representations in a viewport +const labelmaps = segmentation.getSegmentationRepresentations(viewportId, { + type: csToolsEnums.SegmentationRepresentations.Labelmap +}); + +// Get all representations of a specific segmentation (including contour, labelmap, surface) +const segReps = segmentation.getSegmentationRepresentations(viewportId, { + segmentationId: 'seg123' +}); + +// Get a specific representation +const specificRep = segmentation.getSegmentationRepresentation(viewportId, { + segmentationId: 'seg123', + type: csToolsEnums.SegmentationRepresentations.Labelmap +}); +``` + +
+Benefits of the New Approach + +1. **Direct Viewport Control**: + - Each viewport can have its own unique representation configuration + - No need to create separate tool groups for different viewport representations +2. **Simpler Mental Model**: + - Representations are directly tied to where they're displayed + - No intermediate tool group layer to manage +3. **More Flexible Rendering**: + - Each viewport can render the same segmentation differently + - Better support for multiple views of the same data +4. **Improved Type Safety**: + - Specifier pattern provides better TypeScript support + - More explicit API with clearer intentions +
+ + +
+Migration Tips + +1. **Replace Tool Group References**: + - Search your codebase for `toolGroupId` references in segmentation code + - Replace with appropriate `viewportId` references +2. **Update Event Handlers**: + - Update any code listening for segmentation events + - Events now include viewportId instead of toolGroupId +3. **Review Representation Management**: + - Identify where you manage segmentation representations + - Convert to using the new viewport-centric methods +4. **Consider Viewport Context**: + - Think about segmentation representation in terms of viewport display + - Use specifiers to target specific representations when needed + +
+ + +--- diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/5-segmentationserice-style.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/5-segmentationserice-style.md new file mode 100644 index 0000000..266206b --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/5-segmentationserice-style.md @@ -0,0 +1,362 @@ +--- +id: seg-style +title: SegmentationService Style +--- + + +## Style + + +### setSegmentVisibility + +since visibility is viewport concern and representation is what is being toggled -> + +**Before (OHIF 3.8)** + +```js +setSegmentVisibility( + segmentationId: string, + segmentIndex: number, + isVisible: boolean, + toolGroupId?: string +): void +``` + +**After (OHIF 3.9)** + +```js +setSegmentVisibility( + viewportId: string, + segmentationId: string, + segmentIndex: number, + isVisible: boolean, + type?: SegmentationRepresentations +): void +``` + +
+Migration Example + +```js +// Before +segmentationService.setSegmentVisibility( + 'segmentation1', + 1, + true, + 'toolGroup1' +); +// After +segmentationService.setSegmentVisibility( + 'viewport1', + 'segmentation1', + 1, + true +); +``` + +**Getting Viewport IDs** + +When you need to update visibility across multiple viewports: + +```js +// Before +const toolGroupIds = ['toolGroup1', 'toolGroup2']; +toolGroupIds.forEach(toolGroupId => { + segmentationService.setSegmentVisibility( + 'segmentation1', + 1, + true, + toolGroupId + ); +}); +// After +const viewportIds = segmentationService.getViewportIdsWithSegmentation('segmentation1'); +viewportIds.forEach(viewportId => { + segmentationService.setSegmentVisibility( + viewportId, + 'segmentation1', + 1, + true + ); +}); +``` + + +
+ + +### get/set Configuration -> get/setStyle + +The segmentation configuration system has been completely redesigned: + +- Moved from global/toolGroup configuration to viewport-specific styles +- Split rendering of inactive segmentations into separate API +- More granular control over styles at different levels (global, segmentation, viewport, segment) + + +**Before (OHIF 3.8)** + +```js +interface SegmentationConfig { + brushSize: number; + brushThresholdGate: number; + fillAlpha: number; + fillAlphaInactive: number; + outlineWidthActive: number; + renderFill: boolean; + renderInactiveSegmentations: boolean; + renderOutline: boolean; + outlineOpacity: number; + outlineOpacityInactive: number; +} +``` + +**After (OHIF 3.9)** + +```js +// Style Types +interface StyleSpecifier { + viewportId?: string; + segmentationId?: string; + type: SegmentationRepresentations; + segmentIndex?: number; +} +interface LabelmapStyle { + renderOutline: boolean; + outlineWidth: number; + renderFill: boolean; + fillAlpha: number; + outlineAlpha: number; + // .... +} +// Functions +getStyle(specifier: StyleSpecifier): LabelmapStyle | ContourStyle | SurfaceStyle; +setStyle(specifier: StyleSpecifier, style: LabelmapStyle | ContourStyle | SurfaceStyle): void; +setRenderInactiveSegmentations(viewportId: string, renderInactive: boolean): void; +getRenderInactiveSegmentations(viewportId: string): boolean; +``` + + +**Before:** + +```js +// Get global configuration +const config = segmentationService.getConfiguration(); +console.log(config.fillAlpha, config.renderOutline); +// Get tool group specific config +const toolGroupConfig = segmentationService.getConfiguration('toolGroup1'); +``` + +**After:** + +```js +// Get global style for labelmap +const labelmapStyle = segmentationService.getStyle({ + type: SegmentationRepresentations.Labelmap +}); +// Get viewport-specific style +const viewportStyle = segmentationService.getStyle({ + viewportId: 'viewport1', + type: SegmentationRepresentations.Labelmap +}); +// Get segmentation-specific style +const segmentationStyle = segmentationService.getStyle({ + segmentationId: 'seg1', + type: SegmentationRepresentations.Labelmap +}); +// Get segment-specific style +const segmentStyle = segmentationService.getStyle({ + segmentationId: 'seg1', + type: SegmentationRepresentations.Labelmap, + segmentIndex: 1 +}); +``` + + + +**Setting Configuration/Style** + +**Before:** + +```js +segmentationService.setConfiguration({ + fillAlpha: 0.5, + outlineWidthActive: 2, + renderOutline: true, + renderFill: true, + renderInactiveSegmentations: true +}); +``` + +**After:** + +```js +// Set global style +segmentationService.setStyle( + { type: SegmentationRepresentations.Labelmap }, + { + fillAlpha: 0.5, + outlineWidth: 2, + renderOutline: true, + renderFill: true + } +); +// Set viewport-specific style +segmentationService.setStyle( + { + viewportId: 'viewport1', + type: SegmentationRepresentations.Labelmap + }, + { + fillAlpha: 0.5, + outlineWidth: 2 + } +); +// Handle inactive segmentations separately +segmentationService.setRenderInactiveSegmentations('viewport1', true); +``` + + +
+Migration Examples + +**Combining Multiple Style Settings** + +**Before:** + +```js +segmentationService.setConfiguration({ + fillAlpha: 0.5, + fillAlphaInactive: 0.2, + outlineWidthActive: 2, + outlineOpacity: 1, + outlineOpacityInactive: 0.5, + renderOutline: true, + renderFill: true, + renderInactiveSegmentations: true +}); +``` + +**After:** + +```js +// Set base style +segmentationService.setStyle( + { type: SegmentationRepresentations.Labelmap }, + { + fillAlpha: 0.5, + outlineWidth: 2, + outlineAlpha: 1, + renderOutline: true, + renderFill: true + } +); +``` + +
+ + + +**Set inactive rendering per viewport** + +```js +segmentationService.setRenderInactiveSegmentations('viewport1', true); +// Set style for inactive segments if needed +segmentationService.setStyle( + { + viewportId: 'viewport1', + type: SegmentationRepresentations.Labelmap, + segmentationId: 'seg1' + }, + { + fillAlpha: 0.2, + outlineAlpha: 0.5 + } +); +``` + +--- + + + +## setSegmentRGBAColor , setSegmentOpacity, setSegmentRGBA +Previously, the SegmentationService had multiple redundant methods for setting colors and opacity (`setSegmentRGBA`, `setSegmentColor`, `setSegmentOpacity`). This led to confusion and potential state inconsistencies between the service and Cornerstone.js Tools. + +The old methods (`setSegmentRGBA`, `setSegmentRGBA`, and `setSegmentOpacity`) are now removed. + + +1. Replace `setSegmentRGBAColor`, `setSegmentRGBA`, and `setSegmentOpacity` calls: Replace all instances of the old methods with the new `setSegmentColor` method. Note that you now need to provide the `viewportId` as the first argument since segment color is managed per viewport and representation in cornerstone3D. + + +**Before** + +```js +// Old API: +segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, rgbaColor, toolGroupId); +segmentationService.setSegmentRGBA(segmentationId, segmentIndex, rgbaColor, toolGroupId); +segmentationService.setSegmentOpacity(segmentationId, segmentIndex, opacity, toolGroupId); +``` + +**After** + +```js +// New API: +segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color); // color is an array of [red, green, blue, alpha] +``` + +The new `color` argument is an array representing the RGBA color, where the alpha component determines the opacity. Since the Cornerstone Tools library handles segment color per viewport and representation, we require the `viewportId` as an argument now. + + + +2. **Retrieve Segment Color using** `getSegmentColor`: The new `getSegmentColor` provides a way to fetch the color of a segment within a specific viewport. + +```js +const color = segmentationService.getSegmentColor(viewportId, segmentationId, segmentIndex); //returns [r, g, b, a] +``` + + +--- + + + +## ToggleSegmentationVisibility + +In Cornerstone3D v2.x, `toggleSegmentationVisibility` has been replaced with `toggleSegmentationRepresentationVisibility`. This change reflects the fact that +a representation is what is being toggled, not the segmentation. + + +**Before (OHIF 3.8)** + +```js +// Toggle visibility for a segmentation globally +segmentationService.toggleSegmentationVisibility(segmentationId); +``` + +**After (OHIF 3.9)** + +```js +// Toggle visibility for a segmentation representation in a specific viewport +segmentationService.toggleSegmentationRepresentationVisibility(viewportId, { + segmentationId: segmentationId, + type: csToolsEnums.SegmentationRepresentations.Labelmap +}); +``` + +**Migration Steps** + +1. Update all calls to `toggleSegmentationVisibility` to use `toggleSegmentationRepresentationVisibility` +2. Add the required `viewportId` parameter +3. Add a `type` parameter specifying the representation type (e.g., Labelmap, Contour) +4. If you were toggling visibility across all viewports, you'll need to loop through the viewports: + + +
+Additional Notes + + +- Each viewport can now have independent visibility settings for the same segmentation +- The visibility state is specific to the representation type (Labelmap, Contour, etc.) +- To check current visibility, use `getSegmentationRepresentationVisibility(viewportId, { segmentationId, type })` +
+ +--- diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/6-segmentationserice-other.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/6-segmentationserice-other.md new file mode 100644 index 0000000..1236787 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/6-segmentationserice-other.md @@ -0,0 +1,374 @@ +--- +id: seg-other +title: Other Changes +--- + + + + +## addOrUpdateSegmentation + +This was a public method but there is a good chance you were not using it + + +**Before (OHIF 3.8)** + +```js +// Before +addOrUpdateSegmentation( + segmentation: Segmentation, + suppressEvents = false, + notYetUpdatedAtSource = false +): string +``` + +**After** + +```js +addOrUpdateSegmentation( + segmentationInput: SegmentationPublicInput | Partial +) +``` + +### Data Structure Changes + +The segmentation object that was used previously was a custom segmentation object that was used internally by the SegmentationService. But +we have moved to the cornerstone public segmentation input type. + +**Before:** + +```js +const segmentation = { + id: 'segmentation1', + type: SegmentationRepresentations.Labelmap, + isActive: true, + activeSegmentIndex: 1, + segments: [ + { + segmentIndex: 1, + color: [255, 0, 0], + isVisible: true, + isLocked: false, + opacity: 255 + } + ], + label: 'Segmentation 1', + cachedStats: {}, + representationData: { + LABELMAP: { + volumeId: 'volume1', + referencedVolumeId: 'reference1' + } + } +}; +``` + + +**After:** + +This matches the cornerstone public segmentation input type. + +```js +const segmentationInput = { + segmentationId: 'segmentation1', + representation: { + type: SegmentationRepresentations.Labelmap, + data: { + imageIds: segmentationImageIds, + referencedVolumeId: 'reference1' + } + }, + config: { + label: 'Segmentation 1', + segments: { + 1: { + label: 'Segment 1', + active: true, + locked: false + } + } + } +}; +``` + +
+Migration Examples + + +```js +// Before +const newSegmentation = { + id: 'seg1', + type: SegmentationRepresentations.Labelmap, + segments: [...], + representationData: { + LABELMAP: { + volumeId: 'volume1', + referencedVolumeId: 'reference1' + } + } +}; +segmentationService.addOrUpdateSegmentation(newSegmentation); + +// After +segmentationService.addOrUpdateSegmentation({ + segmentationId: 'seg1', + representation: { + type: SegmentationRepresentations.Labelmap, + data: { + imageIds: segmentationImageIds, + referencedVolumeId: 'reference1' + } + }, + config: { + segments: { + 1: { + label: 'Segment 1', + active: true + } + } + } +}); +``` + + +**Updating Existing Segmentation** + +```js +// Before +const updatedSegmentation = { + ...existingSegmentation, + segments: [...modifiedSegments], + activeSegmentIndex: 2 +}; +segmentationService.addOrUpdateSegmentation(updatedSegmentation); + +// After +segmentationService.addOrUpdateSegmentation({ + segmentationId: 'seg1', + config: { + segments: { + 2: { active: true }, + } + } +}); +``` + +
+ + +## loadSegmentationsForViewport + +same as addOrUpdateSegmentation, you should pass in the new segmentation data structure. + +For instance + +**Before** + +```js +const segmentations = [ + { + id: '1', + label: 'Segmentations', + segments: labels.map((label, index) => ({ + segmentIndex: index + 1, + label + })), + isActive: true, + activeSegmentIndex: 1, + }, +]; + +commandsManager.runCommand('loadSegmentationsForViewport', { + segmentations, +}); +``` + + + +**After** + +```js + +const labels = ['Segment 1', 'Segment 2', 'Segment 3']; + +const segmentations = [ + { + segmentationId: '1', + representation: { + type: Enums.SegmentationRepresentations.Labelmap, + }, + config: { + label: 'Segmentations', + segments: labels.reduce((acc, label, index) => { + acc[index + 1] = { + label, + active: index === 0, // First segment is active + locked: false, + }; + return acc; + }, {}), + }, + }, +]; + +commandsManager.runCommand('loadSegmentationsForViewport', { + segmentations, +}); +``` + + +--- + + + + +## highlightSegment + +**Before (OHIF 3.8)** + +```js +// Before (v1.x) +highlightSegment( + segmentationId: string, + segmentIndex: number, + toolGroupId?: string, + alpha = 0.9, + animationLength = 750, + hideOthers = true, + highlightFunctionType = 'ease-in-out' +) + +``` + +**After (OHIF 3.9)** + +```js +highlightSegment( + segmentationId: string, + segmentIndex: number, + viewportId?: string, // notice viewportId instead of toolGroupId + alpha = 0.9, + animationLength = 750, + hideOthers = true, + highlightFunctionType = 'ease-in-out' +) +``` + +
+Key Changes + +1. Removed `toolGroupId` in favor of `viewportId` +2. If no viewportId is provided, highlights in all relevant viewports + +
+ +
+Migration Examples + +**Basic Usage** + +```js +// Before +segmentationService.highlightSegment( + 'seg1', + 1, + 'toolGroup1', + 0.9, + 750, + true, +); +// After +segmentationService.highlightSegment( + 'seg1', + 1, + 'viewport1', + 0.9, + 750, + true +); +``` + +**Highlighting in Multiple Views** + +```js +// Before +const toolGroupIds = ['toolGroup1', 'toolGroup2']; +toolGroupIds.forEach(toolGroupId => { + segmentationService.highlightSegment( + 'seg1', + 1, + toolGroupId + ); +}); +// After - Method 1: Let service handle multiple viewports +segmentationService.highlightSegment('seg1', 1); +// After - Method 2: Explicitly specify viewports +const viewportIds = ['viewport1', 'viewport2']; +viewportIds.forEach(viewportId => { + segmentationService.highlightSegment( + 'seg1', + 1, + viewportId + ); +}); +``` +
+ +--- + +## jumpToSegmentCenter + +**Before (OHIF 3.8)** + +```js +jumpToSegmentCenter( + segmentationId: string, + segmentIndex: number, + toolGroupId?: string, + highlightAlpha = 0.9, + highlightSegment = true, + animationLength = 750, + highlightHideOthers = false, + highlightFunctionType = 'ease-in-out' +) +``` + +**After (OHIF 3.9)** + +```js +jumpToSegmentCenter( + segmentationId: string, + segmentIndex: number, + viewportId? string, // notice viewportId instead of toolGroupId + highlightAlpha = 0.9, + highlightSegment = true, + animationLength = 750, + highlightHideOthers = false, + highlightFunctionType = 'ease-in-out' +) +``` + +
+Key Changes + +1. Removed `toolGroupId` parameter infavor of viewportId +2. Automatically handles relevant viewports if `viewportId` not provided + + +``` +// Before +segmentationService.jumpToSegmentCenter( + 'seg1', + 1, + 'toolGroup1' +); +// After +segmentationService.jumpToSegmentCenter( + 'seg1', + 1, + 'viewportId1' +); +``` + +
diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/index.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/index.md new file mode 100644 index 0000000..0eea04b --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/1-segmentation/index.md @@ -0,0 +1,11 @@ +--- +id: segmentation-index +title: Segmentation +sidebar_position: 1 +--- + +:::info +This migration involves significant architectural changes to the segmentation system. While we typically aim for incremental updates, the shift from a tool group-centric to a viewport-centric architecture was necessary to support OHIF 3.9's advanced visualization capabilities, and more flexible segmentation handling. + +Don't worry - we'll guide you through each change step by step! +::: diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/2-Renamings.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/2-Renamings.md new file mode 100644 index 0000000..6c0763f --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/2-Renamings.md @@ -0,0 +1,33 @@ +--- +id: 2-renamings +title: Renamings +sidebar_position: 2 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + +## Panel Measurements + +The panel in the default extension is renamed from `measure` to `panelMeasurement` to be more consistent with the rest of the extensions. + +**Action Needed** + +Update any references to the `measure` panel to `panelMeasurement` in your code. + +Find and replace + + + + @ohif/extension-default.panelModule.measure + + + @ohif/extension-cornerstone.panelModule.panelMeasurement + + + +## addIcon from ui + +The addIcon from the ui package has had a version added in the default extension as +`utils.addIcon` which adds to both `ui` and `ui-next`. diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/3-DataSources.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/3-DataSources.md new file mode 100644 index 0000000..2d14adf --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/3-DataSources.md @@ -0,0 +1,40 @@ +--- +id: 3-data-sources +title: Data Sources +sidebar_position: 3 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## BulkDataURI Configuration + +We've updated the configuration for BulkDataURI to provide more flexibility and control. This guide will help you migrate from the old configuration to the new one. + +### What's Changing? + + + + +```javascript +useBulkDataURI: false, +``` + + + + +```javascript +bulkDataURI: { + enabled: true, + // Additional configuration **options** +}, +``` + + + + + +**Additional Notes:** +- The new configuration allows for more granular control over BulkDataURI behavior. +- You can now add custom URL prefixing logic using the startsWith and prefixWith properties. +- This change enables easier correction of retrieval URLs, especially in scenarios where URLs pass through multiple systems. diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/4-Measurements.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/4-Measurements.md new file mode 100644 index 0000000..d4b89fa --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/4-Measurements.md @@ -0,0 +1,43 @@ +--- +title: Measurements +--- + + +## Display Text + + +Previously, `displayText` for measurements was often a simple string or an array of strings. This approach made it difficult to distinguish between primary measurement values (e.g., length, area) and secondary information (e.g., series number, instance number). It also limited styling options for differentiating these types of information. + +The new approach introduces a structured object for `displayText`, consisting of `primary` and `secondary` arrays. This separation allows for better organization and presentation of measurement information. The `primary` array is intended for the main measurement values (on the left), while the `secondary` array is for contextual information like series and instance numbers (on the right) + +### Migration Steps + +If you have custom measurement tools or modify existing ones, you need to update the `getDisplayText` functions within the `measurementServiceMappings` to return a structured object in the new format. + +**Update Measurement Mappings:** If your extension defines custom measurement tools or modifies existing ones, update the `getDisplayText` functions within the `measurementServiceMappings` to return a structured object in the new format. + +```js +// Old Implementation (example for Length tool) +function getDisplayText(mappedAnnotations, displaySet, customizationService) { + // ... + return `${roundedLength} ${unit} (S: ${SeriesNumber}${instanceText}${frameText})`; +} +// New Implementation +function getDisplayText(mappedAnnotations, displaySet) { + // ... + return { + primary: [`${roundedLength} ${unit}`], // Primary measurement value + secondary: [`S: ${SeriesNumber}${instanceText}${frameText}`], // Secondary information + }; +} +``` + +--- + +### selected property + +`selected` property on measurements is now renamed to `isSelected` to match the rest of `isLocked` , `isVisible` naming convention. + +Migration: you probably don't need to perform any migration + +--- diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/4-ViewportActionCorner.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/4-ViewportActionCorner.md new file mode 100644 index 0000000..7b85daf --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/4-ViewportActionCorner.md @@ -0,0 +1,40 @@ +--- +id: viewport-action-corner +title: ViewportActionCorner +--- + + + + +## Key Changes and Rationale + +Previously, the `ViewportActionCornersService` used the `setComponent` or `setComponents` methods to add components to viewport corners. These methods, when used with multiple components, would essentially overwrite existing components at the same location, unless great care was taken with the `indexPriority` property. This made it difficult to reliably position multiple components within the same corner. + +The new approach introduces the methods `addComponent` and `addComponents`, which insert components into the viewport corners based on an optional `indexPriority` property and provide predictable ordering based on the relative `indexPriority` of the components already at the corner. If no `indexPriority` is given, components are added to the end (for the left side) or the beginning (for the right side) by default. + +### Migration Steps + +**Update Component Addition Methods:** Replace calls to `setComponent` and `setComponents` with `addComponent` and `addComponents`, respectively. + +```js +// Old API +viewportActionCornersService.setComponent({ + viewportId, + id: 'myComponent', + component: , + location: viewportActionCornersService.LOCATIONS.topRight +}); +``` + +**New API** + +```js +viewportActionCornersService.addComponent({ + viewportId, + id: 'myComponent', + component: , + location: viewportActionCornersService.LOCATIONS.topRight, + indexPriority: 1, // indexPriority is now optional and determines placement order within the corner +}); + +``` diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/5-StateSyncService.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/5-StateSyncService.md new file mode 100644 index 0000000..8505a34 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/5-StateSyncService.md @@ -0,0 +1,343 @@ +--- +id: state-sync-service +title: StateSyncService +--- + + +## Migrating from StateSyncService to Zustand Stores + +The `StateSyncService` has been deprecated in favor of more modern and efficient state management using Zustand stores. This migration guide outlines the reasons for the change and provides step-by-step instructions on how to migrate your extension or mode from using `StateSyncService` to Zustand. + +## Why Migrate? + +The `StateSyncService` had limitations: + +- **Limited Reactivity:** Updates weren't always reactive, requiring manual re-renders. +- **Lack of Granularity:** It stored large chunks of state, hindering performance. +- **Complexity:** Managing and syncing state across components was cumbersome. + +Zustand offers several advantages: + +- **Lightweight and Fast:** Zustand is a minimal and performant state management library. +- **Granular Control:** Create individual stores for specific data, improving reactivity and performance. +- **Simplified API:** Easy-to-use hooks for subscribing and updating state. + +## Migration Steps: + +1. **Identify State to Migrate:** Determine which parts of your extension or mode rely on the `StateSyncService`. Typical examples include: + - **Viewport Presentations:** LUT and position information for viewports. + - **Layout State:** Custom grid layouts and one-up toggling. + - **Synchronizers:** State for cross-viewport synchronization. + - **UI State:** UI-specific settings. +2. **Replace StateSyncService Usage:** In your extension or mode: + - **Import Zustand Stores:** Import the new stores you created. + - **Replace** `getState()` and `store()`: Use the Zustand hooks (`useStore`, `set`, `get`) to access and update state in your components. + - **Handle Presentation IDs:** Implement logic for generating and managing presentation IDs within your stores or relevant components. This can involve using unique keys based on viewport options, display sets, and unique indices. See the `presentationUtils.ts` file for example implementations. + - **Rehydrate State:** On mode entry, rehydrate your Zustand stores with any relevant persisted state from localStorage or other storage mechanisms. + - **Clear State on Mode Exit:** Ensure you clear your Zustand stores appropriately on mode exit to prevent memory leaks. + + + +### `LutPresentationStore` + + +**Before (StateSyncService):** + +```js +const stateSyncService = servicesManager.services.stateSyncService; +const lutPresentationStore = stateSyncService.getState().lutPresentationStore; +const lutPresentation = lutPresentationStore[presentationId]; +// ...to update +stateSyncService.store({ + lutPresentationStore: { + ...lutPresentationStore, + [presentationId]: newLutPresentation, + }, +}); +``` + +**After (Zustand):** + +```js +import { useLutPresentationStore } from '../stores/useLutPresentationStore'; +const { lutPresentationStore, setLutPresentation } = useLutPresentationStore(); +const lutPresentation = lutPresentationStore[presentationId]; +// ...to update +setLutPresentation(presentationId, newLutPresentation); +``` + +The `getPresentationId` for `lutPresentationStore` was previously registered in `platform/core`. Now, the Zustand store provides this functionality. + +```js +// Fetch getPresentationId functions from respective Zustand stores +const { getPresentationId: getLutPresentationId } = useLutPresentationStore.getState(); + +// Register presentation id providers +viewportGridService.addPresentationIdProvider('lutPresentationId', getLutPresentationId); +``` + + +--- + +### `PositionPresentationStore` + +**Before (StateSyncService):** + +```js +const stateSyncService = servicesManager.services.stateSyncService; +const positionPresentationStore = stateSyncService.getState().positionPresentationStore; +const positionPresentation = positionPresentationStore[presentationId]; +// ...to update +stateSyncService.store({ + positionPresentationStore: { + ...positionPresentationStore, + [presentationId]: newPositionPresentation, + }, +}); +``` + +**After (Zustand):** + +```js +import { usePositionPresentationStore } from '../stores/usePositionPresentationStore'; +const { positionPresentationStore, setPositionPresentation } = usePositionPresentationStore(); +const positionPresentation = positionPresentationStore[presentationId]; +// ...to update +setPositionPresentation(presentationId, newPositionPresentation); +``` + +Similar to lutPresentationId, the PositionPresentationId is also registered from outside + +```js + + const { getPresentationId: getPositionPresentationId } = usePositionPresentationStore.getState(); + + // register presentation id providers + viewportGridService.addPresentationIdProvider( + 'positionPresentationId', + getPositionPresentationId + ); +``` + +--- + +### `ViewportGridStore` + +**Before (StateSyncService):** + +```js +const stateSyncService = servicesManager.services.stateSyncService; +const viewportGridStore = stateSyncService.getState().viewportGridStore; +const gridState = viewportGridStore[storeId]; +// ...to update +stateSyncService.store({ + viewportGridStore: { + ...viewportGridStore, + [storeId]: newGridState, + }, +}); +``` + +**After (Zustand):** + +```js +import { useViewportGridStore } from '../stores/useViewportGridStore'; +const { viewportGridState, setViewportGridState } = useViewportGridStore(); +const gridState = viewportGridState[storeId]; +// ...to update +setViewportGridState(storeId, newGridState); +``` + +--- + +### `DisplaySetSelectorStore` + + +**Before (StateSyncService):** + +```js +const stateSyncService = servicesManager.services.stateSyncService; +const displaySetSelectorMap = stateSyncService.getState().displaySetSelectorMap; +const displaySetUID = displaySetSelectorMap[selectorKey]; +// ...to update +stateSyncService.store({ + displaySetSelectorMap: { + ...displaySetSelectorMap, + [selectorKey]: newDisplaySetUID, + }, +}); +``` + +**After (Zustand):** + +```js +import { useDisplaySetSelectorStore } from '../stores/useDisplaySetSelectorStore'; +const { displaySetSelectorMap, setDisplaySetSelector } = useDisplaySetSelectorStore(); +const displaySetUID = displaySetSelectorMap[selectorKey]; +// ...to update +setDisplaySetSelector(selectorKey, newDisplaySetUID); +``` + +--- + +### `HangingProtocolStageIndexStore` + + +**Before (StateSyncService):** + +```js +const stateSyncService = servicesManager.services.stateSyncService; +const hangingProtocolStageIndexMap = stateSyncService.getState().hangingProtocolStageIndexMap; +const hpInfo = hangingProtocolStageIndexMap[cacheId]; +// ...to update +stateSyncService.store({ + hangingProtocolStageIndexMap: { + ...hangingProtocolStageIndexMap, + [cacheId]: newHpInfo, + }, +}); +``` + +**After (Zustand):** + +```js +import { useHangingProtocolStageIndexStore } from '../stores/useHangingProtocolStageIndexStore'; +const { hangingProtocolStageIndexMap, setHangingProtocolStageIndex } = useHangingProtocolStageIndexStore(); +const hpInfo = hangingProtocolStageIndexMap[cacheId]; +// ...to update +setHangingProtocolStageIndex(cacheId, newHpInfo); +``` + +--- + +### `ToggleHangingProtocolStore` + + +**Before (StateSyncService):** + +```js +const stateSyncService = servicesManager.services.stateSyncService; +const toggleHangingProtocol = stateSyncService.getState().toggleHangingProtocol; +const previousHpInfo = toggleHangingProtocol[storedHanging]; +// ...to update +stateSyncService.store({ + toggleHangingProtocol: { + ...toggleHangingProtocol, + [storedHanging]: newHpInfo, + }, +}); +``` + +**After (Zustand):** + +```js +import { useToggleHangingProtocolStore } from '../stores/useToggleHangingProtocolStore'; +const { toggleHangingProtocol, setToggleHangingProtocol } = useToggleHangingProtocolStore(); +const previousHpInfo = toggleHangingProtocol[storedHanging]; +// ...to update +setToggleHangingProtocol(storedHanging, newHpInfo); +``` + +--- + +### `ToggleOneUpViewportGridStore` + + +**Before (StateSyncService):** + +```js +const stateSyncService = servicesManager.services.stateSyncService; +const toggleOneUpViewportGridStore = stateSyncService.getState().toggleOneUpViewportGridStore; +const previousGridState = toggleOneUpViewportGridStore.layout; // Assuming layout was a property +// ...to update +stateSyncService.store({ + toggleOneUpViewportGridStore: newGridState, +}); +``` + +**After (Zustand):** + +```js +import { useToggleOneUpViewportGridStore } from '../stores/useToggleOneUpViewportGridStore'; +const { toggleOneUpViewportGridStore, setToggleOneUpViewportGridStore } = useToggleOneUpViewportGridStore(); +const previousGridState = toggleOneUpViewportGridStore; // No nested layout property +// ...to update +setToggleOneUpViewportGridStore(newGridState); +``` + +--- + +### `UIStateStore` + + +**Before (StateSyncService):** + +```js +const stateSyncService = servicesManager.services.stateSyncService; +const uiState = stateSyncService.getState().uiStateStore[someUIKey]; +// ...to update +stateSyncService.store({ + uiStateStore: { + ...stateSyncService.getState().uiStateStore, + [someUIKey]: newUIState, + }, +}); +``` + +**After (Zustand):** + +```js +import { useUIStateStore } from '../stores/useUIStateStore'; +const { uiState, setUIState } = useUIStateStore(); +const currentUIState = uiState[someUIKey]; +// ...to update +setUIState(someUIKey, newUIState); +``` + +--- + +### `ViewportsByPositionStore` + + +**Before (StateSyncService):** + +```js +const stateSyncService = servicesManager.services.stateSyncService; +const viewportsByPosition = stateSyncService.getState().viewportsByPosition; +const cachedViewport = viewportsByPosition[positionId]; +// ...to update +stateSyncService.store({ + viewportsByPosition: { + ...viewportsByPosition, + [positionId]: newViewport, + }, +}); +``` + +**After (Zustand):** + +```js +import { useViewportsByPositionStore } from '../stores/useViewportsByPositionStore'; +const { viewportsByPosition, setViewportsByPosition } = useViewportsByPositionStore(); +const cachedViewport = viewportsByPosition[positionId]; +// ...to update +setViewportsByPosition(positionId, newViewport); +``` + +--- + +### `SegmentationPresentationStore` + +**After (Zustand):** + +```js +import { useSegmentationPresentationStore } from '../stores/useSegmentationPresentationStore'; +const { segmentationPresentationStore, setSegmentationPresentation } = + useSegmentationPresentationStore(); +// ...to update +setSegmentationPresentation(presentationId, newSegmentationPresentation); +// You likely have functions within the store like: +// addSegmentationPresentation +// setSegmentationVisibility +// etc. +``` diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/6-RTSTRUCT.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/6-RTSTRUCT.md new file mode 100644 index 0000000..f13bf91 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/6-RTSTRUCT.md @@ -0,0 +1,18 @@ +--- +id: 6-rtstruct +title: RTSTRUCT +sidebar_position: 6 +--- + + + +# RTStructure Set has transitioned from VTK actors to SVG. + +We have transitioned from VTK-based rendering to SVG-based rendering for RTStructure Set contours. This change should not require any modifications to your codebase. We anticipate improved stability and speed in our contour rendering. + +As a result of this update, viewports rendering RTStructure Sets will no longer convert to volume viewports. Instead, they will remain as stack viewports. + + +Read more in Pull Requests: +- https://github.com/OHIF/Viewers/pull/4074 +- https://github.com/OHIF/Viewers/pull/4157 diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/7-UI.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/7-UI.md new file mode 100644 index 0000000..71e625d --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/7-UI.md @@ -0,0 +1,149 @@ +--- +title: UI +--- + + + + +## New Components + +You can explore our new playground at `docs.ohif.org/ui` to see the latest components and their properties. We haven't provided a migration guide yet because the old components are still available. Feel free to update your codebase, including custom extensions and UI, to use the new Button, Dropdown, Icons, and other new components from `@ohif/ui-next`. The old methods (importing from `@ohif/ui`) will continue to work for now. However, the new components have a slightly different API, and we plan to deprecate the old components in a future release, as we see the new ones as the future of OHIF. + + + + +## `UINotificationService` + + +We've switched our custom notification service to the Sonner component from https://sonner.emilkowal.ski/ + +### 1. Toast Positions (Kebab-Case) + +Toast positions are now defined using kebab-case instead of camelCase. For instance, `topRight` becomes `top-right`, `bottomRight` becomes `bottom-right`, etc. Ensure your position strings are updated accordingly. + +**Old API:** + +```js +uiNotificationService.show({ + title: 'My Title', + message: 'My Message', + duration: 3000, + position: 'topRight', + type: 'error', + autoClose: true, +}); +``` + + +**New API:** + +```js +uiNotificationService.show({ + title: 'My Title', + message: 'My Message', + duration: 3000, + position: 'top-right', // Note the change to kebab-case + type: 'error', + autoClose: true, +}); +``` + +### 2. Promise Support + +The `show()` method now supports promises, enabling you to display loading notifications and automatically update them based on the promise's resolution or rejection. This significantly simplifies asynchronous operation feedback. + +**Example:** + +```js +const myPromise = someAsyncOperation(); +const notificationId = uiNotificationService.show({ + title: 'Loading Data', + message: 'Fetching data from server...', + type: 'info', + promise: myPromise, + promiseMessages: { + loading: 'Fetching...', + success: (data) => `Data loaded: ${data.length} items`, // Access promise result + error: (error) => `Failed to load data: ${error.message}`, // Access error details + }, +}); +// Optionally hide notification manually if needed +// myPromise.finally(() => uiNotificationService.hide(notificationId)); +``` + +### 3. `hide()` API Change + +The `hide()` method no longer takes an options object. It only accepts the notification ID as a string argument. + +**Old API:** + +```js +uiNotificationService.hide({ id: notificationId }); +``` + +**New API:** + +```js +uiNotificationService.hide(notificationId); +``` + + +--- + + +## Viewport Pane Tailwindcss class + +Previously, when targeting the viewport pane to add custom CSS, you likely used `group-hover:visible` with the viewportPane having a `group` class. + +The naming was confusing as we added more groups, so we renamed it to `group/pane`. Now you can apply `group-hover/pane` for better clarity. + + +--- + +## Header Component + + +Header Component has been refactored in the @ohif/ui-next package. + + +**Before** + + +```js +function Header({ + children, + menuOptions, + isReturnEnabled, + onClickReturnButton, + isSticky, + WhiteLabeling, + showPatientInfo, + servicesManager, + Secondary, + appConfig, + ...props +}: withAppTypes): ReactNode +``` + +**After** + +```js +function Header({ + children, + menuOptions, + isReturnEnabled, + onClickReturnButton, + isSticky, + WhiteLabeling, + PatientInfo, + Secondary, + ...props +}: HeaderProps): ReactNode +``` + +The `PatientInfo` component is now preferred, and the `showPatientInfo` prop has been removed. The previous method depended on `servicesManager`, which was cumbersome because the UI shouldn't need to interact with `servicesManager`. + +All the DropDown and Icons are now in the @ohif/ui-next package. + + +--- diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/8-Refactorings.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/8-Refactorings.md new file mode 100644 index 0000000..7554a18 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/8-Refactorings.md @@ -0,0 +1,120 @@ +--- +title: Refactoring +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + + +## Panel Segmentation + +is now moved from `@ohif/extension-cornerstone-dicom-seg` to `@ohif/extension-cornerstone`. + + +The cornerstone extension now provides the panelSegmentation feature, which was previously part of the cornerstone-dicom-seg extension. This change is logical as panelSegmentation handles more than just DICOM. It can process various formats, including custom formats from the backend and potentially NIFTI format in the future. + + +Before in your modes you were using + +```js +'@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation', +``` + + +Now you should use it via + + +```js +'@ohif/extension-cornerstone.panelModule.panelSegmentation', +``` + +--- + +## `callInputDialog` and `colorPickerDialog` and `showLabelAnnotationPopup` + +Due to the excessive number of `callInputDialog` instances, we centralized them. You can now import them from `@ohif/extension-default`. + + +```js +import { showLabelAnnotationPopup, callInputDialog, colorPickerDialog } from '@ohif/extension-default'; +``` + + +--- + +## disableEditing + +The configuration has moved from appConfig to allow more precise control over component disabling. To disable editing for segmentation and measurements, add the following settings: + + +**Before: ** + +```js +customizationService.addModeCustomizations([ + { + id: 'segmentation.panel', + disableEditing: true, + }, +]); +``` + +**Now ** + +```js +customizationService.addModeCustomizations([ + // To disable editing in the SegmentationTable + { + id: 'panelSegmentation.disableEditing', + disableEditing: true, + }, + // To disable editing in the MeasurementTable + { + id: 'PanelMeasurement.disableEditing', + disableEditing: true, + }, +]) +``` + + +--- + +## Customization Ids + +The primary reason for this migration is to improve modularity and maintainability in configuration management, as we plan to focus more on the customization service in the near future. + +**Before** + +```js +customizationService.addModeCustomizations([ + { + id: 'segmentation.panel', + segmentationPanelMode: 'expanded', + addSegment: false, + onSegmentationAdd: () => { + commandsManager.run('createNewLabelmapFromPT'); + }, + }, +]); +``` + + +**Now** + +```js +customizationService.addModeCustomizations([ + { + id: 'panelSegmentation.tableMode', + mode: 'expanded', + }, + { + id: 'panelSegmentation.onSegmentationAdd', + onSegmentationAdd: () => { + commandsManager.run('createNewLabelmapFromPT'); + }, + }, +]); + +``` diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/9-other.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/9-other.md new file mode 100644 index 0000000..5bd8b73 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/9-other.md @@ -0,0 +1,99 @@ +--- +title: Other Changes +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + +## External Libraries +Some libraries are loaded via dynamic import. You can provide a global function +`browserImport` the allows loading of dynamic imports without affecting the +webpack build. This import looks like: + +```html + +``` + +and belongs in the root html file for your application. +You then need to remove `dependencies` on the external import, and add a reference +to the external import in your `pluginConfig.json` file. + +### Example plugin config for `dicom-microscopy-viewer` +The example below imports the `dicom-microscopy-viewer` for use as an external +dependency. The example is part of the default `pluginConfig.json` file. + +```json + "public": [ + { + "directory": "./platform/public" + }, + { + "packageName": "dicom-microscopy-viewer", + "importPath": "/dicom-microscopy-viewer/dicomMicroscopyViewer.min.js", + "globalName": "dicomMicroscopyViewer", + "directory": "./node_modules/dicom-microscopy-viewer/dist/dynamic-import" + } + ] +``` + +This defines two directory modules, whose contents are copied unchanged to the +output build directory. It then defines the `dicom-microscopy-viewer` using +the `packageName` element as being a module which is imported dynamically. +Then, the import path passed into the browserImportFunction above is +specified, and then how to access the import itself, via the `window.dicomMicroscopyViewer` +global name reference. + +### Referencing External Imports +The appConfig either defines or has a default peerImport function which can be +used to load references to the modules defined in the pluginConfig file. See +the example in `init.tsx` for the cornerstone extension for how this is passed +into CS3D for loading the whole slide imaging library. + + + +--- + + + +--- + + +--- + + +## Use of ViewReference for navigation +When navigating to measurements and storing/remembering navigation positions, +the `viewport.getViewReference` is used to get a position, and `viewport.isReferenceViewable` +used to check if a reference can be applied, and finally `viewport.setViewReference` to +navigate to a view. Note that this changes the behaviour of navigation between +MPR and Stack viewports, and also enables navigation of video and microscopy +viewports in CS3D. This can cause some unexpected behaviour depending on how the +frame of reference values are configured to allow for navigation. + +The isReferenceViewable is used to determine when a view or measurement can be +shown on a given view. For stack versus volume viewports, this can cause unexpected +behaviour to be seen depending on how the view reference was fetched. + +### `getViewReference` with `forFrameOfReference` +When a view reference is fetched with the for frame of reference flag set to true, +a reference will be returned which can be displayed on any viewport containing +the same frame of reference and encompassing the given FOR and able to display the required +orientation. Without this flag, a view reference is returned which will be +displayed on a stack with the given image id, or a volume containing said image id +or the specified volume. + +### `isReferenceViewable` with navigation and/or orientation +The is reference viewable will return false unless the given reference is directly +viewable in the viewport as is. However, it can be passed various flags to determine +whether the reference could be displayed if the viewport was modified in various ways, +for example, by changing the position or orientation of the viewport. This allows +checking for degrees of closeness so that the correct viewport can be chosen. + +Note that this may result in displaying a measurement from one viewport on a completely +different viewport, for example, showing a Probe tool from the stack viewport on +an MPR view. diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/index.md b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/index.md new file mode 100644 index 0000000..56e414e --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/3p8-to-3p9/index.md @@ -0,0 +1,7 @@ +--- +id: 3p8-to-3p9 +title: 3.8 -> 3.9 +sidebar_position: 1 +--- + +Here are the changes you need to make to migrate from 3.8 to 3.9. diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/_category_.json b/platform/docs/versioned_docs/version-3.9/migration-guide/_category_.json new file mode 100644 index 0000000..e4b82ff --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Migration Guides", + "position": 11 +} diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/from-3p7-to-3p8.md b/platform/docs/versioned_docs/version-3.9/migration-guide/from-3p7-to-3p8.md new file mode 100644 index 0000000..b07d8ff --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/from-3p7-to-3p8.md @@ -0,0 +1,212 @@ +--- +sidebar_position: 2 +sidebar_label: 3.7 -> 3.8 +--- + +# Migration Guide + +There are two main things that need to be taken care of. + + +## New Toolbar Button definitions + +### Update Active Tool Handling +The concept of `activeTool` and its associated getter and setter has been removed. The active tool should now be derived from the toolGroup and the viewport. + + +**Action Needed** + +Remove any code that sets the default tool using `toolbarService.setDefaultTool()` and activates the tool using +`toolbarService.recordInteraction()`. For example, the following code should be removed: + +```javascript +let unsubscribe; +toolbarService.setDefaultTool({ + groupId: "WindowLevel", + itemId: "WindowLevel", + interactionType: "tool", + commands: [ + { + commandName: "setToolActive", + commandOptions: { + toolName: "WindowLevel", + }, + context: "CORNERSTONE", + }, + ], +}); + +const activateTool = () => { + toolbarService.recordInteraction(toolbarService.getDefaultTool()); + + unsubscribe(); +}; + +({ unsubscribe } = toolGroupService.subscribe( + toolGroupService.EVENTS.VIEWPORT_ADDED, + activateTool +)); +``` + + + +Instead, focus on defining the buttons and their placement in the toolbar using `toolbarService.addButtons()` and `toolbarService.createButtonSection()`. For example: + +```javascript +toolbarService.addButtons([...toolbarButtons, ...moreTools]); +toolbarService.createButtonSection("primary", [ + "MeasurementTools", + "Zoom", + "WindowLevel", + "Pan", + "Capture", + "Layout", + "MPR", + "Crosshairs", + "MoreTools", +]); +``` + + +### Update Button Definitions +The concept of button types (toggle, action, tool) has been removed. Buttons are now defined using a simplified object-based definition. + +**Action Needed** + +Update your button definitions to use the new object-based format and remove the `type` property. Use the `uiType` property for the top-level UI type definition. For example: + +```javascript +// Old Implementation +{ + id: 'Capture', + type: 'ohif.action', + props: { + icon: 'tool-capture', + label: 'Capture', + type: 'action', + commands: [ + { + commandName: 'showDownloadViewportModal', + commandOptions: {}, + context: 'CORNERSTONE', + }, + ], + }, +}, +``` + +is now + +```javascript +// New Implementation +{ + id: 'Capture', + uiType: 'ohif.radioGroup', + props: { + icon: 'tool-capture', + label: 'Capture', + commands: [ + { + commandName: 'showDownloadViewportModal', + context: 'CORNERSTONE', + }, + ], + evaluate: 'evaluate.action', + }, +}, +``` + +### Add Evaluators to Button Definitions +Introduce the ๏ปฟevaluate property in your button definitions to determine the state of the button based on the app context. + +**Action Needed** + +Add the appropriate `evaluate` property to each button definition. For example: + - Use `evaluate.cornerstoneTool` if the button should be highlighted only when it is the active primary tool (left mouse). + - Use `evaluate.cornerstoneTool.toggle` if the tool is a toggle tool (like reference lines or image overlay). + +Refer to the `modes/longitudinal/src/toolbarButtons.ts` file for examples of using the `evaluate` property. + +Additional Resources + + - For more information on the new toolbar module and its usage, refer to the [Toolbar documentation](../platform/extensions/modules/toolbar.md). + - Consult the updated button definitions in `modes/longitudinal/src/toolbarButtons.ts` for examples of the new object-based button definition format and the usage of evaluators. + +### Tool listeners + +Some tools can be configured to listen to events to trigger, for example + +```ts +createButton({ + id: 'ReferenceLines', + icon: 'tool-referenceLines', + label: 'Reference Lines', + tooltip: 'Show Reference Lines', + commands: 'toggleEnabledDisabledToolbar', + listeners: { + [ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: + ReferenceLinesListeners, + [ViewportGridService.EVENTS.VIEWPORTS_READY]: + ReferenceLinesListeners, + }, + evaluate: 'evaluate.cornerstoneTool.toggle', + }), +``` + +If you have a custom viewport component, and you are overriding the ```onElementEnabled``` handler, than ensure to call ```viewportGridService.setViewportIsReady(viewportId, true)``` in your own handler so that eventually the ```VIEWPORTS_READY``` event fires as expected, if you are not modifying the handler, then an existing handler that is automatically passed down via the props will call that for you, it is passed down from ```ViewportGrid.tsx``` + +```ts + 1 ? viewportLabel : ''} + viewportId={viewportId} + dataSource={dataSource} + viewportOptions={viewportOptions} + displaySetOptions={displaySetOptions} + needsRerendering={displaySetsNeedsRerendering} + isHangingProtocolLayout={isHangingProtocolLayout} + onElementEnabled={() => { + viewportGridService.setViewportIsReady(viewportId, true); + }} +/> + +``` + +## Toolbar Service + +toolbarService.init is not a function. + +**Action Needed** +remove the call to toolbarService.init() from your codebase. + + + +## leftPanelDefaultClosed and rightPanelDefaultClosed + +Now they are renamed to `leftPanelClosed` and `rightPanelClosed` respectively. + + +## StudyInstanceUID in the URL param + +Previously there were two params that you could choose: seriesInstanceUID and seriesInstanceUIDs, they have been replaced with seriesInstanceUIDs so even if you would like to filter one series use ``seriesInstanceUIDs` + + +## UI + +### Header +Header in @ohif/ui now needs servicesManager and appConfig as input. + + +### Panels +Left and right panel lists are no longer injected into the LayoutTemplate, and have been moved to a PanelService where you have to fetch them from. + +If you're using the main layout, you're fine. However, if you have a custom layout, you'll need to update it. To get the panels, see the + +`extensions/default/src/ViewerLayout/index.tsx` + + + + +## Refactoring + +- TimingEnum (and I guess all enums exported from OHIF core have now moved from Types to Enums export). diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/from-v2.md b/platform/docs/versioned_docs/version-3.9/migration-guide/from-v2.md new file mode 100644 index 0000000..2df1768 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/from-v2.md @@ -0,0 +1,996 @@ +--- +sidebar_position: 3 +sidebar_label: 2.x -> 3.5 +--- + +# Migration Guide + +On this page, we will provide a guide to migrating from OHIF v2 to v3. Please note +that this document is a work in progress and will be updated as we move forward. +This document is not meant to be used as a migration recipe but as a migration overview. + + +# Introduction + +## Importance of Migration + +- Enhanced UX: the new design and UI of OHIF v3 provides a more intuitive and user-friendly experience. + OHIF v3 adds an improved side panels and toolbar, and a new layout system that lets you customize + the layout of your application. +- Improved Performance: OHIF v3 leverages the new Cornerstone3D rendering and tooling libraries, which + significantly improve performance and provide a more robust and stable foundation for your application + for rendering and interacting with medical images. Some of the new advanced features in Cornerstone3D + include: OffScreen Rendering and GPU Acceleration for all viewports, streaming of the volume data, 3D annotations and measurements, + sharing tool states between viewports and more. +- Improved Customizability: With addition of Modes and Extensions, OHIF v3 provides a more modular + and customizable framework for building medical imaging applications, this will let you + focus on your use case and not worry about the underlying infrastructure and also have less worry + to keep up to date with the latest changes. +- Community driven Modes: OHIF v3 provides a gallery of modes that you can use as a starting point + for your application. These +- Future-Proofing: By migrating to v3, you align your application with the latest advancements in the OHIF framework, ensuring ongoing support, updates, and access to new features. +- Community Support: OHIF v3 benefits from an active community of developers and contributors who provide valuable support, bug fixes, and continuous improvements. + +## Migration Timeframe + +The duration of the migration process can vary depending on factors such as the complexity of custom changes made in v2, familiarity with v3's architecture, and the size of the codebase. +If you don't have any custom changes in v2, the migration process should be relatively straightforward. If you have custom changes, you will need to update them to work with the new architecture and new +rendering and tooling engines. + + +## Complexity and Pain Points + +Certain scenarios can make the migration process more complex and potentially introduce pain points: + +- Extensive Customizations: If your v2 implementation includes extensive custom changes and overrides, adapting those customizations to the new structure and APIs of v3 may require additional effort and careful refactoring. +- UI Customizations: Since in OHIF v3 we moved our component library to Tailwind CSS + if you have any custom UI components, you will need to migrate them to Tailwind CSS too, and this might be a bit time consuming. +- Hardware requirements: Since Cornerstone3D uses WebGL for rendering volumeViewport (although it has + a CPU rendering fallback), you need to make sure that your target hardware supports WebGL. You can check + if your hardware supports WebGL [here](https://get.webgl.org/). Also regarding the GPU requirements, you can check the tier of your GPU [here](https://pmndrs.github.io/detect-gpu/), if it is tier 1 and above, you + should be good to go. + +## Summary of Changes + +OHIF v3 is a major re-architecture of the OHIF v2 to make it more modular and +easier to maintain. The main differences are: + +- platform/viewer (@ohif/viewer) has been renamed to platform/app (@ohif/app) (explanation below) +- Extensions are available to be used by modes on request, but are still injected as module components. +- To use the modules provided by the extensions, you need to write a [Mode](../platform/modes/index.md). Modes +are configuration objects that will be used by the viewer to load the modules. This lets users to be able to use common extensions with different configurations, and enhances the customizability of the viewer. +- App configuration structure is different, mainly the `servers` is renamed to `dataSources`. +- Apps can be customized significantly more than previously by providing configuration code int he customizationModule section. +- The viewer UI is completely re-written in Tailwind CSS for better maintainability, although it is a WIP but + already provides a better user experience. +- cornerstone-core and cornerstone-tools are removed and OHIF v3 is using the new Cornerstone3D rendering library and tools. Moving to Cornerstone3D has enabled us to provide a more robust and stable foundation + for 3D rendering and 3D annotations and measurements. In addition, Cornerstone3D provides APIs to load + and stream data into a volume which has huge performance benefits. +- A new CLI tool to help you create extensions and modes (more [here](../development/ohif-cli.md)) +- redux store has been removed and replaced with a simpler state management system via React Context API. + +New significant additions that might be useful for you that weren't available in OHIF v2: +- [OHIF CLI](../development/ohif-cli.md) +- [New Rendering Engine and Toolings](https://www.cornerstonejs.org/) +- [Modes](../platform/modes/index.md) +- [Mode Gallery](https://ohif.org/modes) +- [Layouts](../platform/extensions/modules/layout-template.md) +- [Data Sources](../platform/extensions/modules/data-source.md) +- [Hanging Protocols](../platform/services/data/HangingProtocolService.md) +- [URL Params](../configuration/url.md) + +## Platform/viewer (@ohif/viewer) -> platform/app (@ohif/app) + + +To ensure proper versioning of OHIF v3, we have made a decision to rename the platform/viewer to platform/app. Previously, the platform/viewer package followed software engineering versioning (currently at v4.12.51). However, going forward, we aim to align the versioning of platform/app with the product version (e.g., v3.4.0, v3.5.0, etc.). + +Since the platform/viewer (@ohif/viewer) is already at v4.12.51, we opted to rename it as platform/app to enable versioning in accordance with the product versioning approach. If you were utilizing any exports from @ohif/viewer, please update them to use @ohif/app instead. + + +## Configuration + +:::tip +There are various configurations available to customize the viewer. Each configuration is represented by a custom-tailored object that should be used with the viewer to work effectively with a specific server. Here are some examples of configuration files found in the platform/app/public/config directory. Some server-specific configurations that you should be aware are: `supportsWildcard`, `bulkDataURI`, `omitQuotationForMultipartRequest`, `staticWado` (Read more about them [here](../configuration/configurationFiles.md)). + +- default.js: This is our default configuration designed for our main server, which uses a Static WADO datasource hosted on Amazon S3. +- local_orthanc.js: Use this configuration when working with our local Orthanc server. +- local_dcm4chee.js: This configuration is intended for our local dcm4chee server. +- netlify.js: This configuration is the same as default.js and is used for deployment on Netlify. +- google.js: Use this configuration to run the viewer against the Google Health API. +::: + +OHIF v3 has a new configuration structure. The main difference is that the `servers` is renamed to `dataSources` and the configuration is now asynchronous. Datasources are more abstract and +far more capable than servers. Read more about dataSources [here](../platform/extensions/modules/data-source.md). + +- `StudyPrefetcher` is only available in OHIF v3.9 beta and will be available in the next stable 3.9 release. +- The `servers` object has been replaced with a `dataSources` array containing objects representing different data sources. +- The cornerstoneExtensionConfig property has been removed, you should use `customizationService` instead (you can read more [here](../platform/services/ui/customization-service.md)) +- The maxConcurrentMetadataRequests property has been removed in favor of `maxNumRequests` +- The hotkeys array has been updated with different command names and options, and some keys have been removed. +- New properties have been added, including `maxNumberOfWebWorkers`, `omitQuotationForMultipartRequest`, `showWarningMessageForCrossOrigin`, `showCPUFallbackMessage`, `showLoadingIndicator`, `strictZSpacingForVolumeViewport`. +- you should see if `supportsWildcard` is supported in your server, some servers don't support it and you need to make it false. + +## Modes + +As mentioned briefly above, modes are configuration objects that will be used by the viewer to load extensions. +This lets users to be able to use common extensions with different configurations. So as OHIF developers can focus on creating extensions while +you as the user can focus on creating modes having your own use case and configuration/initialization logic in mind. + +Separating the configuration from the extensions also makes it so that you can +have multiple modes in a single application each focusing on certain tasks. For example, you can have a mode for segmentation which uses specific panels and tools which you don't need +for a mode that will be used for reading (read more about modes [here](../platform/modes/index.md)) + +:::info +Previously, the viewer was designed around registered extensions. If you had a specific use case, you had to duplicate the viewer code and incorporate your customizations through extensions. However, with the introduction of a new layer of abstraction called Modes, you no longer need to fork the viewer. + +Modes provide a flexible approach where you can create your own mode and utilize the necessary extensions within that mode. This eliminates the need for duplicating the viewer codebase. + +Furthermore, Modes offer the advantage of having multiple applications within a single viewer. For instance, you can have a mode dedicated to segmentation tasks and another mode focused on reading. Each mode can have its own unique configuration, initialization logic, layout, tools, and hanging protocols. This ensures a cleaner user interface in the viewer and an improved user experience overall. +::: + +Upon entering a mode, the Viewer will register its declared extensions and load them. And you +can specify which modules you need from each extension in the mode configuration. For instance + +```js + +const ohif = { + layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', + sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', + measurements: '@ohif/extension-default.panelModule.measure', + thumbnailList: '@ohif/extension-default.panelModule.seriesList', +}; + +const cs3d = { + viewport: '@ohif/extension-cornerstone.viewportModule.cornerstone', +}; + +const tmtv = { + hangingProtocol: '@ohif/extension-tmtv.hangingProtocolModule.ptCT', + petSUV: '@ohif/extension-tmtv.panelModule.petSUV', + ROIThresholdPanel: '@ohif/extension-tmtv.panelModule.ROIThresholdSeg', +}; + +function modeFactory({ modeConfiguration }) { + routes: [ + { + path: 'tmtv', + layoutTemplate: ({ location, servicesManager }) => { + return { + id: ohif.layout, + props: { + // leftPanels: [ohif.thumbnailList], + rightPanels: [tmtv.ROIThresholdPanel, tmtv.petSUV], + viewports: [ + { + namespace: cs3d.viewport, + displaySetsToDisplay: [ohif.sopClassHandler], + }, + ], + }, + }; + }, + }, + ], +} +``` + +In the example above, we are using the `tmtv` mode which is a mode for reading PET/CT scans +and as you can see we are specifying the layout, the panels and the viewports that we need +for this mode. The `tmtv` mode is using the `cs3d` extension for rendering and the `ohif` extension. As you see you can reference the modules from the extensions using the `namespace` via strings. So for instance, if you need to use the `viewportModule` from the `@ohif/extension-cornerstone` you can use `@ohif/extension-cornerstone.viewportModule.cornerstone` as the namespace. + +:::tip +`ExtensionManager` will register and load the modules from the extensions and make them available to the viewer by their namespaces. +::: + +Below you can see a screen shot from the demo showcasing 3 modes for the opened study. + +![Alt text](../assets/img/migration-modes.png) + +:::tip +How do I decide certain thing should go inside a mode or extension, Here are some considerations to help you make the decision: + +- **Functionality Scope**: If the functionality is specific to a particular use case or task within your viewer, it is often best suited to be included within a mode. Modes allow you to create customized configurations, layouts, panels, tools, and other components specific to a particular task or workflow. This includes which tool to be active by default, which panels to be displayed, and which layout to be used. + +- **Reusability**: If the functionality can be used across multiple modes, it is better to implement it as an extension. Extensions provide a modular approach where you can encapsulate and share functionality across different modes. For instance, if you have a custom panel that you want to use in multiple modes, you can implement it as an extension and include it in + the mode configuration. + +- **Complexity**: If the functionality requires significant customizations, complex logic, or extensive modifications to the viewer's core behavior, it might be better suited as an extension. + +- **New Service**: If you are writing a new service, it is preferable to implement it as an extension. Services are used to provide a common interface for interacting with external systems and data sources. +There is a new way to register new services which are extendible by other extensions. + +Remember that there is no strict rule for deciding between modes and extensions. It's a matter of understanding the specific requirements of your application. + +::: + + +## Routes + +In OHIF v2 a study was loaded and mounted on `/viewer/:studyInstanceUID` route. In OHIF v3 +we have reworked the route registration to enable more sophisticated routing. Now, Modes are tied to specific routes in the viewer, and multiple modes/routes can be present within a single application, making "routes" configuration the most important part of mode configuration. + +- Routes with a dataSourceName: `{mode.id}/{dataSourceName}` +- Routes without a dataSourceName: `{mode.id}` which uses the default dataSourceName + +This makes a mode flexible enough to be able to connect to multiple datasources +without rebuild of the app for use cases such as reading from one PACS and +writing to another. + +
+ +Can I register a custom route to OHIF v3? + + +Yes, you can take advantage of the customizationService and register your own routes. +see [custom routes](../platform/services/ui/customization-service.md#customroutes) + + +
+ + +## DICOM Endpoints + +In OHIF v3 there is a new end point that your DICOM server should be able to respond to +`WADO-RS GET studies/{studyInstanceUid}/series` + +This is used in the viewer for fetching the series list for a study to use for the hanging protocol. + +## LifeCycle Hooks + +OHIF v2 had `preRegistration` hook for extensions for initialization. In OHIF v3 you have +even more control using `onModeEnter` and `onModeExit` hooks on the extensions and on the modes. + +- `preRegistration`: is called before the extension is registered to the viewer. So very early in the lifecycle of the viewer. +- `onModeEnter` is called when the mode is entered (component on the route is mounted, e.g., when you click on the mode to enter it) +- `onModeExit` is called when the mode is exited (component on the route is unmounted, e.g., when you navigate back to the worklist) + +## Extensions + +Since extensions in OHIF v2 were the main way of customizing the viewer, we will spend some time +below to explain how you can migrate your extensions to OHIF v3. + +### Default Extension + +Lots of common functionalities in the platform/core has been moved inside +the `@ohif/extension-default` extension. This extension is loaded by default +in the viewer and it provides the following functionalities: + +- common datasources such as DICOMWeb, DICOMLocal, and DICOMJSON datasource. +- default measurement panel and panel study browser +- common toolbar button layouts +- common hanging protocol configurations + +
+ +how can I integrate to my google health api? is there support for that? + + +You can right now, take a look into our google configuration that we use for our QA located at +`config/google.js`. Also we have some exciting UI changes coming up for the next release +that will make it easier to integrate with google health api. +
+ +
+ +Is there any recommendation for PACS integration + + +You can take a look at open source PACS such as dcm4chee or orthanc. We have support for them. Also +we have a new static wado datasource that you can use to take benefit of new deduplicated metadata +and caching features. + +
+ +### Cornerstone Extension + +In OHIF v2, the Cornerstone extension provided modules like Cornerstone ViewportModule, ToolbarModule, and CommandsModule for controlling viewport actions. +It relied on `react-cornerstone-viewport` for rendering viewports, `cornerstone-tools` for tools, and `cornerstone-core` for core functionalities. + +However, in OHIF v3, there have been significant changes. The rendering and tooling logic has been migrated to a new library called [`Cornerstone3D`](https://github.com/cornerstonejs/cornerstone3D-beta/). This means that all viewport rendering and tool functionalities are now handled by Cornerstone3D. + +Additionally, in OHIF v3, the native support for 3D functionalities previously provided by the `vtk` extension has been integrated into Cornerstone3D. As a result, any vtkjs logic is encapsulated on CS3D. Things now are much more cleaner and simpler. + +To migrate from OHIF v2 to OHIF v3: + +#### Loading + +Previously we used `cornerstone-wado-image-loader` for loading images. However, we have fully switched the a new +library called `@cornerstonejs/dicom-image-loader` which is a fork of `cornerstone-wado-image-loader` with typescript support and bug fixes. +We have deprecated `cornerstone-wado-image-loader` and you should also switch to `@cornerstonejs/dicom-image-loader` as well. +The process is very simple, you can follow this [PR](https://github.com/OHIF/Viewers/pull/3339) to see how we have migrated. + +There is also a new loader and package `@cornerstonejs/streaming-image-volume-loader`, which provides streaming of the image data +into a volume using web workers and web assembly. You can look into the cornerstone documentation and read more about the +volumeViewport and volumeLoader. + + +#### Rendering + +The significant difference between cornerstone-core and cornerstone3D is that cornerstone3D fully utilizes +[vtk.js](https://kitware.github.io/vtk-js/) for rendering, however in cornerstone-core we used a mix of webGL and vtk +for rendering. While you don't need to do a migration for this, you should be aware that the rendering is now fully performed in the +world coordinate system and the image is placed in the world coordinate system using the `imagePositionPatient` and `imageOrientationPatient` +attributes of the image. This means that you can now share the tool states between multiple viewports and you can also +use the same tool states for 2D and 3D viewports. + +:::tip + +In OHIF v3, we have removed the OHIF's vtk extension and migrated all the 3D functionalities to Cornerstone3D. + +Also you need to remove any dependencies on `react-cornerstone-viewport`, `cornerstone-tools`, and `cornerstone-core`. +::: + +#### Tools + +If you don't have any custom tools, you most likely won't need to make any changes as have tried +to migrate all the tools from `cornerstone-tools` to Cornerstone3D (except `ROIWindowLevel` which is work in progress right now). + +Cornerstone3D has moved the coordinate system of tools to the world coordinate system enabling sharing +tool states between multiple viewports, and as a result the toolData is now stored in the world coordinate system as well. +So to migrate your tools, you will need to update your toolData to be stored in the world coordinate system. You can look +into the simplest tool for instance LengthTool in both `cornerstone-tools` and `cornerstone3D` to see the difference. + + + +By following these steps, you can leverage the improved rendering and tooling capabilities of Cornerstone3D and eliminate the need for the old ohif's vtk extension in OHIF v3. + + +
+ +Is there any name standard for modes and extensions? + + +No naming standard, you can have your organization name as a prefix for your modes and extensions as we +do for ohif (`@ohif/extension-*` and `@ohif/mode-*`). + +
+ + +
+ +What happens if I have create a mode with same name as existing one + + +You shouldn't. Modes are configuration objects that you can simply. There is no real use case +for creating a mode with same name as existing one. If you do so, the last one will override the previous one + + +
+ + +
+ +How to remove an "core" extension/mode? + + +You can use the OHIF cli tool to add/remove/link and unlink extensions and modes. You can find more information +about the cli tool [here](../development/ohif-cli.md) + +
+ +
+ +If I have vtkjs implementation how can I port it? Should I create a specific extension for that? + + +Cornerstone3D has support for some vtk.js actor and mappers including imageData, polyData and volume. If you have another +implementation of vtk.js actor or mapper, you might be able to use `viewport.addActor` to include it in the rendering +pipeline, but depending on the implementation and how much it interfere with the cornerstone3D rendering pipeline, you might +not get the expected result. + +
+ + + +### DICOM Segmentation & DICOM RT + +In OHIF v3, the equivalent extensions for RT and SEG exists with similar logic, but with various improvements such as +enhanced ui/ux for segmentation panel, faster loading and interaction, and better support for multiple viewports, +animations for jump to segment, volumetric rendering, and more. Additionally, OHIF v3 introduces new functionalities with the SEG Viewport and RT Viewport. + +:::tip + +In OHIF v3, Segmentation objects +are loading using the frame of reference by default which means that if there are two viewports that are using the same frame of reference, +if you load a segmentation (labelmap or RT) which lives in the same frame of reference, it will be loaded in both viewports. +::: + +When loading a series that contains SEG (Segmentation) or RT (RT Structure Set) data, the viewport will automatically +switch to the corresponding SEG or RT viewport. The user will then be prompted to decide whether to load the segmentation +or RT structure set into the viewer. This new feature addresses a common use case in which there are multiple segmentation +series in a study, and the user only wants to load specific ones. In OHIF v3, the Segmentations are all loaded +as 3D volumes and as a result a volume viewport is used to display them. (Stack Segmentation in Cornerstone3D is still a +work in progress.) + +In OHIF v2, the user had to load all the segmentation series and then manually delete the ones they didn't want to see. +However, in OHIF v3, the user has more control. The temporary SEG or RT viewport does not immediately load (hydrate) +the segmentation or RT structure set. Instead, the user can decide which ones to load, reducing unnecessary +loading and providing a more efficient workflow. + +This enhancement in OHIF v3 allows users to selectively load specific segmentations or RT structure sets, +improving the usability and efficiency of the viewer when working with multiple SEG or RT series. + + + + +
+ +Can I load one seg in one viewport and another in another viewport? + + +If there is another viewport in the grid that is using the same frame of reference, the segmentation will be loaded in that viewport as well. + +However, since we split the concept of `load` (`hydration`) and `preview`, you can use the preview (not load), which +makes sure the SEG is contained within the viewport, but it is not hydrated so you cannot edit it. + +In future however, we will add more controls over, hiding the segmentation in other viewports via UI, however, you can +right now do it via code. + + + +
+ +
+ +Does it support nifti? + + +Nifit support for both image and segmentation is coming soon. We are working on it. + + +
+ +### DICOM SR + +In OHIF v2, DICOM SR functionality was integrated into the Cornerstone extension. However, in OHIF v3, DICOM SR is now a separate extension. The DICOM SR extension in OHIF v3 retains the same loading and hydrating logic using dcmjs adapters. Additionally, it introduces a new type of viewport called the SR Viewport, which is used to display SR data. + +Similar to the temporary SEG and RT viewports, when a SR display set is selected in OHIF v3, the user is prompted to decide whether to load the SR data into the viewer and initiate the tracking. The SR viewport allows the user to switch between different measurements within the SR instance by utilizing the arrow buttons located at the top of the viewport. + +:::tip +This separation of DICOM SR into its own extension in OHIF v3 provides a dedicated viewport type for SR data and offers enhanced functionality for interacting with SR measurements within the viewer. +::: + + +### DICOM Tag Browser + +In OHIF v2, the DICOM Tag Browser was a separate extension that provided a dedicated user interface for exploring DICOM tags. However, in OHIF v3, we have integrated the DICOM Tag Browser functionality into the `default` extension. + +The DICOM Tag Browser is a powerful tool for debugging and inspecting DICOM metadata, and we wanted to make it easily accessible to users. As a result, it is now available as a toolbar icon within the `default` extension. This allows users to conveniently access the DICOM Tag Browser directly from the toolbar, eliminating the need for a separate extension. + + +
+ +Now that dicom tag is integrated back to default extension, how can I port my code that was implemented in the old extension? Should I create an extension or change directly into default? + + +If you have a custom tag browser, you have two options, either modify the default tag browser (if you think the features +you added is useful for everyone, feel free to open a PR!), or create your own extension with your custom tag browser +which then you can add to the toolbar. + + + +
+ + +### DICOM HTML + +Since we have added graphical overlay of DICOM SR in OHIF v3, we have temporarily downgraded the priority of displaying DICOM HTML within the viewer. While DICOM HTML support is not available in the current version of OHIF v3, we acknowledge its importance and plan to reintroduce this functionality in future updates. + + + +
+ +is there any easy way for supporting my own dicom html viewer? Should I use extension? + + +Yes, you can write your own sopClassHandler and custom viewport in your custom extensions. +After, you need to associate that with the viewport that you +will use in the mode configuration, this way when that sopClassUID is requested it will use your custom viewport. + + + +
+ + + +### DICOM Microscopy + +In OHIF v2, the DICOM microscopy engine was based on an older version of the [DICOM microscopy viewer](https://github.com/ImagingDataCommons/dicom-microscopy-viewer) maintained by our friends at IDC (Imaging Data Commons). However, in OHIF v3, we have upgraded to the latest version of the DICOM microscopy viewer. This new version offers significant improvements in terms of robustness and performance, providing users with an enhanced microscopy viewing experience. + +One notable addition in the latest DICOM microscopy viewer is the support for annotations within the whole slide images (SM images). This feature allows users to annotate and mark specific regions of interest directly within the microscopy images. + +:::tip +Looking ahead, our future plans include adding DICOM SR (Structured Reporting) support for export of annotations in microscopy images. While we will enhance our support for SM images (color profiles etc.), we recommend utilizing the [SLIM Viewer](https://github.com/ImagingDataCommons/slim) developed by IDC for more sophisticated microscopy use cases. +::: + + + +## Extension Modules + + +v3 Extension is likely the same as in v2. Extensions can (like before) have +modules exported via `get{ModuleName}Module` (e.g., `getViewportModule`). + +:::info +There are new +types of modules that can be exported from extensions (such as `HangingProtocolModule`, `LayoutModule`, read more about +modules in v3 [here](../platform/extensions/index.md)). +::: + +The main difference between v3 and v2 is that exported modules were represented as a single object, whereas in OHIF v3, they are +represented as an array of objects, each having a name property. This change was implemented to +enable extensions to export multiple named submodules, providing more flexibility and modularity. + +To access these modules in OHIF v3, you can use the namespace provided by the `ExtensionManager`. For example, consider the following code snippet + + +```js +getUtilityModule({ servicesManager }) { + return [ + { + name: 'common', + exports: { + getCornerstoneLibraries: () => { + return { cornerstone, cornerstoneTools }; + }, + getEnabledElement, + dicomLoaderService, + registerColormap, + }, + }, + { + name: 'core', + exports: { + Enums: cs3DEnums, + }, + }, + { + name: 'tools', + exports: { + toolNames, + Enums: cs3DToolsEnums, + }, + }, + ]; +}, +``` + + +In this example, the extension is exporting multiple submodules named 'common', +'core', and 'tools'. To access the 'common' submodule provided by the @ohif/extension-cornerstone extension, +you can use the following code: + +```js +extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.common' +); +``` + +This allows you to access the specific submodule provided by the extension and utilize its functionalities within your application. + + +
+ +How can I have a lazy-loaded component and import it from another extension? + + +If an extension is exporting a component, you can import it from another extension. For example, if you have an extension that exports a component called `MyComponent`, you can import it from another extension like this: + +```js +import { MyComponent } from '@ohif/extension-my-extension'; +``` + + +
+ + +### ToolbarModule + +In OHIF v2, the toolbarModule was used to add buttons to the toolbar. For example, the following code snippet demonstrates adding a zoom tool button to the toolbar: + +In OHIF v2 + +```js +{ + id: 'Zoom', + label: 'Zoom', + icon: 'search-plus', + // + type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE, + commandName: 'setToolActive', + commandOptions: { toolName: 'Zoom' }, +}, +``` + +However, in OHIF v3, the toolbarModule has been repurposed to define different button types. For instance, OHIF v3 introduces the ohif.radioGroup and ohif.splitButton button types, which provide more flexibility in defining toolbar buttons for each mode. + + + +```js +{ + name: 'ohif.radioGroup', + defaultComponent: ToolbarButton, + clickHandler: () => {}, +}, +{ + name: 'ohif.splitButton', + defaultComponent: ToolbarSplitButton, + clickHandler: () => {}, +}, +``` + +To use these button types within your modes, you can define the buttons in your mode's configuration. In the onModeEnter hook, you can add the defined buttons to the toolbar using the toolbarService. Here's an example of how to add buttons to the toolbar: + + + +```js +// toolbar button +{ + id: 'Zoom', + type: 'ohif.radioGroup', + props: { + type: 'tool', + icon: 'tool-zoom', + label: 'Zoom', + commands: _createSetToolActiveCommands('Zoom'), + }, +}, +``` + +and in `onModeEnter` + +```js +onModeEnter: ({ servicesManager, extensionManager, commandsManager }) => { + const { + toolbarService, + toolGroupService, + } = servicesManager.services; + + // Init tool groups (see cornerstone3D for more details) + initToolGroups(extensionManager, toolGroupService, commandsManager); + + toolbarService.addButtons(toolbarButtons); + toolbarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'WindowLevel', + 'Pan', + 'Capture', + 'Layout', + 'Crosshairs', + 'MoreTools', + ]); +}, +``` + +By using the updated toolbarModule in OHIF v3, you can define and add toolbar buttons specific to each mode, providing greater flexibility and customization options for the toolbar configuration. + +An example of split button icon in v3 is shown below + +![Alt text](../assets/img/migration-split-button.png) + +
+ +Is the tool state shared between two different modes? + +No, the tool state is not shared between different modes in OHIF v3. Each mode operates independently and maintains its own tool state. + +
+ +
+ +I have a custom icon. How can I add it to the toolbar? + + +You need to first register it via `addIcon` in the src/components/Icon, and then you can +referenced it by name in the toolbar configuration for mode +
+ + +
+ +Can I change the toolbar's location? Can I add a secondary toolbar? + +Not in our default layout, but you can write your own layout in your custom extension +and use it instead of the default one. + +
+ +
+ +Can I have different tool sets for each viewport? + + +We don't have fully support for this yet, but we have plans for it. Basically, the plan +is to use the viewport action bar in the top of the viewport to provide viewport-specific +tool sets. +
+ +
+ +Are all tools from v2 support in v3? + + +Almost all with the exception of ROIWindow, but we have plans to add it in the future. However, there are +much more tools in v3 that are not available in v2 such as referenceLines, Stack Image Sync, and +Calibration tool. +
+ +### CommandsModule + +The structure of the commands module is the same as before. The only difference is that +we use Cornerstone3D for rendering and tools. So, if you have a custom command that you were +using in the v2, you need to migrate it to the new Cornerstone3D API. + +You can visit the migration guide for cornerstone [here](https://www.cornerstonejs.org/docs/migrationGuides). + +### PanelModule + +Previously in OHIF v2 you had + +```js +return { + menuOptions: [ + { + icon: 'list', + label: 'Segmentations', + target: 'segmentation-panel', + stateEvent: SegmentationPanelTabUpdatedEvent, + }, + ], + components: [ + { + id: 'segmentation-panel', + component: ExtendedSegmentationPanel, + }, + ], + defaultContext: ['VIEWER'], +}; +``` + +but in OHIF v3 you have + +```js +return [ + { + name: 'panelSegmentation', + iconName: 'tab-segmentation', + iconLabel: 'Segmentation', + label: 'Segmentation', + component: wrappedPanelSegmentation, + }, +]; +``` + +
+ +How can I add my own custom panel? + +To add your own custom panel in OHIF v3, you can follow these steps: + +- Create a new React component that represents your custom panel. +- Provide it in the getPanelModule of your extension. +- Inside your mode, add the panel namespace to the mode's configuration for the layout module. + +
+ +
+ +How to enhance an existing panel? + +To enhance an existing panel in OHIF v3, you can create a new React component that extends or wraps the existing panel component. In your enhanced component, you can add additional functionality, modify the appearance, or incorporate new features specific to your use case. You can also look into the customizationService to see +how you can use the registered points to customize the panel. + +
+ +
+ +How to change the order of appearance of panels? + +To change the order of appearance of panels in OHIF v3, you can modify the panel layout configuration in the mode configuration. The panel layout configuration specifies the order and arrangement of panels within the viewer interface. + +
+ +
+ +Is there a way to change the viewer layout to present right panels on the left and the toolbar on the right? + +Not with our default layout which the default extension provides. However, you can write a new layout and provide it +in the `getLayoutModule` which you can reference in the `layout` property of the mode configuration. +
+ +### SopClassHandlerModule + +The least changed module is the SopClassHandlerModule, although this now returns +an array instead of a single instance. The purpose of this module is to create +a list of displaySets based on the metadata. OHIF App uses this module to +create one or more displaySets for each series. +The displaySet is then used to then get assigned +on each viewport and the viewport renders the image. + +The `DisplaySet` created by the handler can have a member function `addInstances` +which will update the display set with new SOP instance data, allowing the +preservation of the display set UID when required. + +Multiple display sets will be returned when different parts of the series are +to be shown separately, for example, to split scout images from volume images. + + +### ViewportModule + +In OHIF v3, viewports are tied to series of SOP Class UIDs (sopClassUIDs). Each extension provides its own viewport for specific SOP Class UIDs, and you can choose which viewports and SOP Class UIDs your mode can handle in the mode configuration. + +For example, in the longitudinal mode configuration, there are multiple viewports specified along with their associated SOP Class Handler Modules: + + +```js +viewports: [ + { + namespace: '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked', + displaySetsToDisplay: [ '@ohif/extension-default.sopClassHandlerModule.stack'], + }, + { + namespace: '@ohif/extension-cornerstone-dicom-sr.viewportModule.dicom-sr', + displaySetsToDisplay: [ '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr'], + }, + // additional viewports +], +``` + +In this example, there are six viewports specified, each identified by a unique namespace. Each viewport is associated with a specific SOP Class Handler Module through the displaySetsToDisplay property. + +To add a new viewport, you would need to create a new SOP Class Handler Module and a new Viewport Module. The SOP Class Handler Module handles the logic for loading and handling specific SOP Class UIDs, while the Viewport Module defines the rendering and behavior of the viewport. + +In addition to the viewports, the mode configuration should include and register each SOP Class Handler Module that your mode can handle: + + +```js +sopClassHandlers: [ + '@ohif/extension-default.sopClassHandlerModule.stack', + '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr', + '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video', + '@ohif/extension-dicom-pdf.sopClassHandlerModule.dicom-pdf', + '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg', + '@ohif/extension-cornerstone-dicom-rt.sopClassHandlerModule.dicom-rt', +] +``` + +Here, each SOP Class Handler Module is specified with its namespace. + +By configuring the viewports and SOP Class Handler Modules in your mode, you can define how your mode interacts with different types of DICOM data and specify the appropriate rendering and behavior for each SOP Class UID. + +## Metadata Store and Provider + +In OHIF v2, we utilized the `platform/core/classes/metadata` module, which included the classes StudyeMetadata, SeriesMetadata, and InstanceMetadata for storing metadata. However, in OHIF v3, we have replaced these classes with a more versatile metadata store called `DICOMMetadataStore`. This new metadata store is used by each datasource to store the metadata associated with studies, series, and instances. The DICOMMetadataStore API allows you to add study/series/instance metadata to the store and retrieve metadata from it. + +Although we have transitioned to using DICOMMetadataStore as the primary metadata storage mechanism, you still have access to OHIF's MetadataProvider. The MetadataProvider can be found in the same `platform/core/classes` location. The MetadataProvider is internally used to retrieve instance-based metadata based on UIDs, perform queries, and includes some legacy support for older versions of the loading logic. + + +## Build + +We have recently transitioned from bundling all the extensions and the viewer into a single bundle to a more modular approach. In this new approach, the required extensions are dynamically loaded inside a mode as needed. This change brings several advantages, including: + +- Faster build time: Bundling only the necessary extensions reduces the build time, as you no longer need to bundle all extensions upfront. +- Smaller bundle size: By loading extensions on-demand, the initial bundle size is reduced, resulting in faster page load times for users. +- Faster reload for development: During development, the incremental build process allows for faster reloads, improving developer productivity. + +This new approach does not impact the deployment process of the viewer. You can continue to follow our deployment guides, such as the [Build for Production](../deployment/build-for-production.md) guide, to deploy the viewer effectively. + + +### Script tag usage of the OHIF viewer + +With the transition to more advanced visualization, loading, and rendering techniques using WebWorkers, WASM, and WebGL, the script tag usage of the OHIF viewer has been deprecated. However, if you still prefer to use the script tag usage, it is theoretically possible to bundle all the required dependencies and utilize the script tag approach. + +An alternative option for script tag usage is to employ an `iframe`. You can utilize the iframe element to load the OHIF viewer and establish communication with it using the postMessage API. This allows you to exchange messages and data between the parent window and the iframe, enabling interaction and coordination with the OHIF viewer embedded within the iframe. + +Please note that while these alternatives exist, we recommend utilizing modern development practices and incorporating OHIF viewer within your application using a more modular and integrated approach, such as leveraging bundlers, and import statements to ensure better maintainability, extensibility, and compatibility with the OHIF ecosystem. + + +
+ +I use OHIF v2 in an iframe. Is there any impediment for v3? + +No, there is no impediment for using OHIF v3 in an iframe. OHIF v3 is designed to be compatible with iframe usage, allowing you to embed the viewer within other applications or web pages seamlessly. You can still communicate with the OHIF v3 viewer using the postMessage API to exchange information and trigger actions between the parent window and the embedded iframe. + +
+ + +
+ +Does the build support dynamic imports? How can I use it? + +Yes, the build configuration in OHIF v3 supports dynamic imports. Dynamic imports allow you to asynchronously load modules or components on demand, improving performance and reducing the initial bundle size. In fact we are using this method for our viewport components. In general you can: + +``` +import('path/to/module').then((module) => { + // Use the imported module here +}).catch((error) => { + // Handle any error that occurs during dynamic import +}); +``` + +By using dynamic imports, you can selectively load modules or components at runtime when they are needed, enhancing the efficiency and responsiveness of your application. However, note +that these components must be available at BUILD time, and cannot be updated after +build. + +
+ +
+ + +How can I enhance the existing build to consume my own webpack script? + +You can't enhance the existing build to consume your own webpack script as of now. However, you can +modify the webpack.base.js and webpakc.pwa.js files to add your own webpack script/modules if needed. + +
+ +## UI Components + +Migrating to Tailwind CSS, OHIF v3 is now able to have a component-oriented styling approach, speeding up development, ensuring consistent styling, making responsive design easier, and enabling extensibility + +We have gone through extensive re-design of each part of the UI, and we have also added new components to the OHIF viewer. + +
+ +I have a huge complex styles using native CSS, how can I reuse them? + +You can leverage the power of Tailwind CSS (https://TailwindCSS.com/) in OHIF v3 to reuse your existing styles. Tailwind CSS is a utility-first approach, allowing you to create reusable CSS classes by composing utility classes together. You can migrate your existing styles to Tailwind CSS by breaking them down into utility classes and utilizing the extensive set of predefined utilities provided by Tailwind CSS. + +
+ +
+ +How can I change the page color from being purplish to blueish? + +In OHIF v3, you can easily modify the page color by customizing the Tailwind CSS configuration. You can locate the tailwind.config.js file in your project and update the theme section, specifically the colors property, to define your desired color palette. By adjusting the values for the colors, you can change the page color to any shade of blue or other colors according to your preference. + +
+ +
+ +Can I have my own React UI component working in the application? Is there a way to use the current build for it as well? + +Yes, you can integrate your own React UI components seamlessly into the OHIF v3 application. You can even have external +UI dependencies and by creating your own component inside your extensions and importing it into the application, you can +use it as if it was part of the OHIF v3 application. + +
+ +
+ +How can I replace the existing component ui/tooltip? + +You need to write your own component, and inside your mode layout you can replace the existing component with your own. +As of now, for the tooltip component, you need to use the customizationService to customize it; however, the customizationService +requires a registration of the to-be-customized property before you can customize it. Read more about customizationService. + +
+ +
+ +How can I add/consume logos/images/icons? + + +For logos you can use the whiteLabelling inside the configuration. However, if you need a more complex UI for your toolbar +you need to create you own layout. See `getLayoutModule`. + +
+ +## Redux store + +In OHIF v3, we made the decision to move away from the Redux store and adopt a new approach utilizing React context providers and services with a pub/sub pattern. This shift was driven by the need for a more flexible and scalable architecture that better aligns with the plugin and extension system of OHIF. This offers + +- Modularity and Scalability: Context providers and services enable a modular architecture for easy addition and removal of plugins and extensions. +- Reduced Boilerplate: eliminate Redux boilerplate for simpler development. +- Flexible Pub/Sub Pattern: Services provide a pub/sub pattern for inter-component communication. + +
+ + +Now that redux store is gone, how can I access the user information? + + +You can use the `authenticationService` for that purpose. + +
diff --git a/platform/docs/versioned_docs/version-3.9/migration-guide/index.md b/platform/docs/versioned_docs/version-3.9/migration-guide/index.md new file mode 100644 index 0000000..85d2af8 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/migration-guide/index.md @@ -0,0 +1,13 @@ +--- +id: index +--- + + +import DocCardList from '@theme/DocCardList'; +import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; + +# Migration Guides + +Based on the version you are migrating from, you can find the migration guide for the latest version of the platform. + + diff --git a/platform/docs/versioned_docs/version-3.9/platform/_category_.json b/platform/docs/versioned_docs/version-3.9/platform/_category_.json new file mode 100644 index 0000000..842e4ab --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Platform", + "position": 6 +} diff --git a/platform/docs/versioned_docs/version-3.9/platform/browser-support.md b/platform/docs/versioned_docs/version-3.9/platform/browser-support.md new file mode 100644 index 0000000..fa31439 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/browser-support.md @@ -0,0 +1,48 @@ +--- +sidebar_position: 2 +--- +# Browser Support + +The browsers that we support are specified in the `.browserlistrc` file located +in the `platform/app` project. While we leverage the latest language features +when writing code, we rely on `babel` to _transpile_ our code so that it can run +in the browsers that we support. + +## In Practice + +The OHIF Viewer is capable of _running_ on: + +- IE 11 +- FireFox +- Chrome +- Safari +- Edge + +However, we do not have the resources to adequately test and maintain bug free +functionality across all of these. In order to push web based medical imaging +forward, we focus our development efforts on recent version of modern evergreen +browsers. + +Our support of older browsers equates to our willingness to review PRs for bug +fixes, and target their minimum JS support whenever possible. + +### Polyfills + +> A polyfill, or polyfiller, is a piece of code (or plugin) that provides the +> technology that you, the developer, expect the browser to provide natively. + +An example of a polyfill is that you expect `Array.prototype.filter` to exist, +but for some reason, the browser that's being used has not implemented that +language feature yet. Our earlier transpilation will rectify _syntax_ +discrepancies, but unimplemented features require a "temporary" implementation. +That's where polyfills step in. + +We previously used polyfill io, but due to a security vulnerability in the library, it's necessary to switch to alternative services. + + + + +[core-js]: https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md + diff --git a/platform/docs/versioned_docs/version-3.9/platform/environment-variables.md b/platform/docs/versioned_docs/version-3.9/platform/environment-variables.md new file mode 100644 index 0000000..1b08d54 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/environment-variables.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 3 +sidebar_label: Environment Variables +--- +# Environment Variables + +There are a number of environment variables we use at build time to influence the output application's behavior. + +```bash +# Application +NODE_ENV=< production | development > +DEBUG=< true | false > +APP_CONFIG=< relative path to application configuration file > +PUBLIC_URL=< relative path to application root - default / > +VERSION_NUMBER= +BUILD_NUM= +# i18n +USE_LOCIZE= +LOCIZE_PROJECTID= +LOCIZE_API_KEY= +``` + +## Setting Environment Variables + +- `npx cross-env` +- `.env` files +- env variables on build machine, or for terminal session diff --git a/platform/docs/versioned_docs/version-3.9/platform/extensions/_category_.json b/platform/docs/versioned_docs/version-3.9/platform/extensions/_category_.json new file mode 100644 index 0000000..b7a30d9 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/extensions/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Extensions", + "position": 9 +} diff --git a/platform/docs/versioned_docs/version-3.9/platform/extensions/extension.md b/platform/docs/versioned_docs/version-3.9/platform/extensions/extension.md new file mode 100644 index 0000000..148b82a --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/extensions/extension.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 4 +sidebar_label: Extension Manager +--- + +# Extension Manager + +## Overview + +The `ExtensionManager` is a class made available to us via the `@ohif/core` +project (platform/core). Our application instantiates a single instance of it, +and provides a `ServicesManager` and `CommandsManager` along with the +application's configuration through the appConfig key (optional). + +```js +const commandsManager = new CommandsManager(); +const servicesManager = new ServicesManager(); +const extensionManager = new ExtensionManager({ + commandsManager, + servicesManager, + appConfig, +}); +``` + +The `ExtensionManager` only has a few public members: + +- `setActiveDataSource` - Sets the active data source for the application +- `getDataSources` - Returns the registered data sources +- `getActiveDataSource` - Returns the currently active data source +- `getModuleEntry` - Returns the module entry by the give id. + +## Accessing Modules + +We use `getModuleEntry` in our `ViewerLayout` logic to find the panels based on +the provided IDs in the mode's configuration. + +For instance: +`extensionManager.getModuleEntry("@ohif/extension-measurement-tracking.panelModule.seriesList")` +accesses the `seriesList` panel from `panelModule` of the +`@ohif/extension-measurement-tracking` extension. + +```js +const getPanelData = id => { + const entry = extensionManager.getModuleEntry(id); + const content = entry.component; + + return { + iconName: entry.iconName, + iconLabel: entry.iconLabel, + label: entry.label, + name: entry.name, + content, + }; +}; +``` diff --git a/platform/docs/versioned_docs/version-3.9/platform/extensions/index.md b/platform/docs/versioned_docs/version-3.9/platform/extensions/index.md new file mode 100644 index 0000000..d9c630e --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/extensions/index.md @@ -0,0 +1,345 @@ +--- +sidebar_position: 1 +sidebar_label: Introduction +--- + +# Introduction + +We have re-designed the architecture of the `OHIF-v3` to enable building +applications that are easily extensible to various use cases (modes) that behind +the scene would utilize desired functionalities (extensions) to reach the goal +of the use case. + +Previously, extensions were โ€œadditiveโ€ and could not easily be mixed and matched +within the same viewer for different use cases. Previous `OHIF-v2` architecture +meant that any minor extension alteration usually would require the user to hard +fork. E.g. removing some tools from the toolbar of the cornerstone +extension meant you had to hard fork it, which was frustrating if the +implementation was otherwise the same as master. + +> - Developers should make packages of _reusable_ functionality as extensions, +> and can consume publicly available extensions. +> - Any conceivable radiological workflow or viewer setup will be able to be +> built with the platform through _modes_. + +Practical examples of extensions include: + +- A set of segmentation tools that build on top of the `cornerstone` viewport +- A set of rendering functionalities to volume render the data +- [See our maintained extensions for more examples of what's possible](#maintained-extensions) + +**Diagram showing how extensions are configured and accessed.** + + + +## Extension Skeleton + +An extension is a plain JavaScript object that has `id` and `version` properties, and one or +more [modules](#modules) and/or [lifecycle hooks](#lifecycle-hooks). + +```js +// prettier-ignore +export default { + /** + * Required properties. Should be a unique value across all extensions. + */ + id, + + // Lifecycle + preRegistration() { /* */ }, + onModeEnter() { /* */ }, + onModeExit() { /* */ }, + // Modules + getLayoutTemplateModule() { /* */ }, + getDataSourcesModule() { /* */ }, + getSopClassHandlerModule() { /* */ }, + getPanelModule() { /* */ }, + getViewportModule() { /* */ }, + getCommandsModule() { /* */ }, + getContextModule() { /* */ }, + getToolbarModule() { /* */ }, + getHangingProtocolModule() { /* */ }, + getUtilityModule() { /* */ }, +} +``` + +## OHIF-Maintained Extensions + +A small number of powerful extensions for popular use cases are maintained by +OHIF. They're co-located in the [`OHIF/Viewers`][viewers-repo] repository, in +the top level [`extensions/`][ext-source] directory. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ExtensionDescriptionModules
+ + default + + + Default extension provides default viewer layout, a study/series + browser, and a datasource that maps to a DICOMWeb compliant backend + commandsModule, ContextModule, DataSourceModule, HangingProtocolModule, LayoutTemplateModule, PanelModule, SOPClassHandlerModule, ToolbarModule
+ + cornerstone + + + Provides 2d and 3d rendering functionalities + ViewportModule, CommandsModule, UtilityModule
+ dicom-pdf + + Renders PDFs for a specific SopClassUID. + Viewport, SopClassHandler
+ dicom-video + + Renders DICOM Video files. + Viewport, SopClassHandler
+ cornerstone-dicom-sr + + Maintained extensions for cornerstone and visualization of DICOM Structured Reports + ViewportModule, CommandsModule, SOPClassHandlerModule
+ measurement-tracking + + Tracking measurements in the measurement panel + ContextModule,PanelModule,ViewportModule,CommandsModule
+ +## Registering of Extensions + +`viewer` starts by registering all the extensions specified inside the +`pluginConfig.json`, by default we register all extensions in the repo. + + +```js title=platform/app/pluginConfig.json +// Simplified version of the `pluginConfig.json` file +{ + "extensions": [ + { + "packageName": "@ohif/extension-cornerstone", + "version": "3.4.0" + }, + { + "packageName": "@ohif/extension-measurement-tracking", + "version": "3.4.0" + }, + // ... + ], + "modes": [ + { + "packageName": "@ohif/mode-longitudinal", + "version": "3.4.0" + } + ] +} +``` + +:::note Important +You SHOULD NOT directly register extensions in the `pluginConfig.json` file. +Use the provided `cli` to add/remove/install/uninstall extensions. Read more [here](../../development/ohif-cli.md) +::: + +The final registration and import of the extensions happen inside a non-tracked file `pluginImport.js` (this file is also for internal use only). + +After an extension gets registered within the `viewer`, +each [module](#modules) defined by the extension becomes available to the modes +via the `ExtensionManager` by requesting it via its id. +[Read more about Extension Manager](#extension-manager) + +## Lifecycle Hooks + +Currently, there are three lifecycle hook for extensions: + +[`preRegistration`](./lifecycle/#preRegistration) This hook is called once on +initialization of the entire viewer application, used to initialize the +extensions state, and consume user defined extension configuration. If an +extension defines the [`preRegistration`](./lifecycle/#preRegistration) +lifecycle hook, it is called before any modules are registered in the +`ExtensionManager`. It's most commonly used to wire up extensions to +[services](./../services/index.md) and [commands](./modules/commands.md), and to +bootstrap 3rd party libraries. + +[`onModeEnter`](./lifecycle#onModeEnter): This hook is called whenever a new +mode is entered, or a modeโ€™s data or datasource is switched. This hook can be +used to initialize data. + +[`onModeExit`](./lifecycle#onModeExit): Similarly to onModeEnter, this hook is +called when navigating away from a mode, or before a modeโ€™s data or datasource +is changed. This can be used to cache data for reuse later, but since it +isn't known which mode will be entered next, the state after exiting should be +clean, that is, the same as the state on a clean start. This is called BEFORE +service clean up, and after mode specific onModeExit handling. + +## Modules + +Modules are the meat of extensions, the `blocks` that we have been talking about +a lot. They provide "definitions", components, and filtering/mapping logic that +are then made available to modes and services. + +Each module type has a special purpose, and is consumed by our viewer +differently. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Types + Description
+ + LayoutTemplate + + Control Layout of a route
+ + DataSource + + Control the mapping from DICOM metadata to OHIF-metadata
+ + SOPClassHandler + + Determines how retrieved study data is split into "DisplaySets"
+ + Panel + + Adds left or right hand side panels
+ + Viewport + + Adds a component responsible for rendering a "DisplaySet"
+ + Commands + + Adds named commands, scoped to a context, to the CommandsManager
+ + Toolbar + + Adds buttons or custom components to the toolbar
+ + Context + + Shared state for a workflow or set of extension module definitions
+ + HangingProtocol + + Adds hanging protocol rules
+ + Utility + + Expose utility functions to the outside of extensions
+ +Tbl. Module types +with abridged descriptions and examples. Each module links to a dedicated +documentation page. + +### Contexts + +The `@ohif/app` tracks "active contexts" that extensions can use to scope +their functionality. Some example contexts being: + +- Route: `ROUTE:VIEWER`, `ROUTE:STUDY_LIST` +- Active Viewport: `ACTIVE_VIEWPORT:CORNERSTONE`, `ACTIVE_VIEWPORT:VTK` + +An extension module can use these to say "Only show this Toolbar Button if the +active viewport is a Cornerstone viewport." This helps us use the appropriate UI +and behaviors depending on the current contexts. + +For example, if we have hotkey that "rotates the active viewport", each Viewport +module that supports this behavior can add a command with the same name, scoped +to the appropriate context. When the `command` is fired, the "active contexts" +are used to determine the appropriate implementation of the rotation behavior. + + + + +[viewers-repo]: https://github.com/OHIF/Viewers +[ext-source]: https://github.com/OHIF/Viewers/tree/master/extensions +[module-types]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/extensions/MODULE_TYPES.js + diff --git a/platform/docs/versioned_docs/version-3.9/platform/extensions/installation.md b/platform/docs/versioned_docs/version-3.9/platform/extensions/installation.md new file mode 100644 index 0000000..2e5fb81 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/extensions/installation.md @@ -0,0 +1,12 @@ +--- +sidebar_position: 5 +sidebar_label: Installation +--- + +# Extension: Installation + +OHIF-v3 provides the ability to utilize external extensions. + + +You can use ohif `cli` tool to install both local and publicly published +extensions on NPM. You can read more [here](../../development/ohif-cli.md) diff --git a/platform/docs/versioned_docs/version-3.9/platform/extensions/lifecycle.md b/platform/docs/versioned_docs/version-3.9/platform/extensions/lifecycle.md new file mode 100644 index 0000000..2786888 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/extensions/lifecycle.md @@ -0,0 +1,128 @@ +--- +sidebar_position: 3 +sidebar_label: Lifecycle Hooks +--- + +# Extensions: Lifecycle Hooks + +## Overview + +Extensions can implement specific lifecycle methods. + +- preRegistration +- onModeEnter +- onModeExit + +## preRegistration + +If an extension defines the `preRegistration` lifecycle hook, it is called +before any modules are registered in the `ExtensionManager`. This hook is an +`async` function that can be used to perform: + +- initialize 3rd party libraries +- register event listeners +- add or call services +- add or call commands + +The `preRegistration` hook receives an object containing the +`ExtensionManager`'s associated `ServicesManager`, `CommandsManager`, and any +`configuration` that was provided with the extension at time of registration. + +Example `preRegistration` implementation that register a new service and make it +available in the app. We will talk more in details for creating a new service +for `OHIF-v3`. + +```js +// new service inside new extension +import MyNewService from './MyNewService'; + +export default function MyNewServiceWithServices(servicesManager) { + return { + name: 'MyNewService', + create: ({ configuration = {} }) => { + return new MyNewService(servicesManager); + }, + }; +} +``` + +and + +```js +import MyNewService from './MyNewService' + +export default { + id, + + /** + * @param {object} params + * @param {object} params.configuration + * @param {ServicesManager} params.servicesManager + * @param {CommandsManager} params.commandsManager + * @returns void + */ + async preRegistration({ servicesManager, commandsManager, configuration }) { + console.log('Wiring up important stuff.'); + + window.importantStuff = () => { + console.log(configuration); + }; + + console.log('Important stuff has been wired.'); + window.importantStuff(); + + // Registering new services + servicesManager.registerService(MyNewService(servicesManager)); + }, + }, +}; +``` + +## onModeEnter + +If an extension defines the `onModeEnter` lifecycle hook, it is called when a +new mode is enters, or a mode's data or datasource is switched. + +For instance, in DICOM structured report extension (`dicom-sr`), we are using +`onModeEnter` to re-create the displaySets after a new mode is entered. + +_Example `onModeEnter` hook implementation_ + +```js +export default { + id: '@ohif/extension-cornerstone-dicom-sr', + + onModeEnter({ servicesManager }) { + const { DisplaySetService } = servicesManager.services; + const displaySetCache = DisplaySetService.getDisplaySetCache(); + + const srDisplaySets = displaySetCache.filter( + ds => ds.SOPClassHandlerId === SOPClassHandlerId + ); + + srDisplaySets.forEach(ds => { + // New mode route, allow SRs to be hydrated again + ds.isHydrated = false; + }); + }, +}; +``` + +## onModeExit + +If an extension defines the `onModeExit` lifecycle hook, it is called when +navigating away from a mode. This hook can be used to clean up data tasks such +as unregistering services, removing annotations that do not need to be +persisted. + +_Example `onModeExit` hook implementation_ + +```js +export default { + id: 'myExampleExtension', + + onModeExit({ servicesManager, commandsManager }) { + myCacheService.purge(); + }, +}; +``` diff --git a/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/_category_.json b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/_category_.json new file mode 100644 index 0000000..c131ccd --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Modules", + "position": 3 +} diff --git a/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/commands.md b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/commands.md new file mode 100644 index 0000000..b37202b --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/commands.md @@ -0,0 +1,121 @@ +--- +sidebar_position: 2 +sidebar_label: Commands +--- +# Module: Commands + + +## Overview +`CommandsModule` includes list of arbitrary functions. These may activate tools, communicate with a server, open a modal, etc. +The significant difference between `OHIF-v3` and `OHIF-v2` is that in `v3` a `mode` defines +its toolbar, and which commands each tool call is inside in its toolDefinition + +An extension can register a Commands Module by defining a `getCommandsModule` +method. The Commands Module allows us to register one or more commands scoped to +specific [contexts](./../index.md#contexts). Commands have several unique +characteristics that make them tremendously powerful: + +- Multiple implementations for the same command can be defined +- Only the correct command's implementation will be run, dependent on the + application's "context" +- Commands are used by hotkeys, toolbar buttons and render settings + +Here is a simple example commands module: + +```js +const getCommandsModule = () => ({ + definitions: { + exampleActionDef: { + commandFn: ({ param1 }) => { + console.log(`param1's value is: ${param1}`); + }, + // storeContexts: ['viewports'], + options: { param1: 'param1' }, + context: 'VIEWER', // optional + }, + }, + defaultContext: 'ACTIVE_VIEWPORT::DICOMSR', +}); +``` + + +Each definition returned by the Commands Module is registered to the +`ExtensionManager`'s `CommandsManager`. + +> `storeContexts` has been removed in `OHIF-v3` and now modules have access to all commands and services. This change enables support for user-registered services. + +## Command Definitions + +The command definition consists of a named command (`exampleActionDef` below) and a +`commandFn`. The command name is used to call the command, and the `commandFn` +is the "command" that is actioned. + +```js +exampleActionDef: { + commandFn: ({ param1, options }) => { }, + options: { param1: 'measurement' }, + context: 'DEFAULT', +} +``` + +| Property | Type | Description | +| --------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| `commandFn` | func | The function to call when command is run. Receives `options` and `storeContexts`. | +| `options` | object | (optional) Arguments to pass at the time of calling to the `commandFn` | +| `context` | string[] or string | (optional) Overrides the `defaultContext`. Let's us know if command is currently "available" to be run. | + +## Command Behavior + + + +**If there are multiple valid commands for the application's active contexts** + +- What happens: all commands are run +- When to use: A `clearData` command that cleans up state for multiple + extensions + +**If no commands are valid for the application's active contexts** + +- What happens: a warning is printed to the console +- When to use: a `hotkey` (like "invert") that doesn't make sense for the + current viewport (PDF or HTML) + +## `CommandsManager` Public API + +If you would like to run a command in the consuming app or an extension, you can +use `CommandsManager.runCommand(commandName, options = {}, contextName)` + + +```js +// Returns all commands for a given context +commandsManager.getContext('string'); + +// Run a command, it will run all the `speak` commands in all contexts +commandsManager.runCommand('speak', { command: 'hello' }); + +// Run command, from Default context +commandsManager.runCommand('speak', { command: 'hello' }, ['DEFAULT']); +``` + +The `ExtensionManager` handles registering commands and creating contexts, so +most consumer's won't need these methods. If you find yourself using these, ask +yourself "why can't I register these commands via an extension?" + +```js +// Used by the `ExtensionManager` to register new commands +commandsManager.registerCommand('context', 'name', commandDefinition); + +// Creates a new context; clears the context if it already exists +commandsManager.createContext('string'); +``` + +### Contexts + +It is up to the consuming application to define what contexts are possible, and +which ones are currently active. As extensions depend heavily on these, we will +likely publish guidance around creating contexts, and ways to override extension +defined contexts in the near future. If you would like to discuss potential +changes to how contexts work, please don't hesitate to create a new GitHub +issue. + +[Some additional information on Contexts can be found here.](./../index.md#contexts) diff --git a/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/contextModule.md b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/contextModule.md new file mode 100644 index 0000000..35de381 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/contextModule.md @@ -0,0 +1,30 @@ +--- +sidebar_position: 9 +sidebar_label: Context +--- +# Module: Context + +## Overview +This new module type allows you to connect components via a shared context. You can create a context that two components, e.g. a viewport and a panel can use to synchronize and communicate. An extensive example of this can be seen in the longitudinal modeโ€™s custom extensions. + + + +```jsx +const ExampleContext = React.createContext(); + +function ExampleContextProvider({ children }) { + return ( + + {children} + + ); +} + +const getContextModule = () => [ + { + name: 'ExampleContext', + context: ExampleContext, + provider: ExampleContextProvider, + }, +]; +``` diff --git a/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/data-source.md b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/data-source.md new file mode 100644 index 0000000..4bdcf37 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/data-source.md @@ -0,0 +1,212 @@ +--- +sidebar_position: 3 +sidebar_label: Data Source +--- + +# Module: Data Source + +## Overview + +We have built couple of methods for fetching and mapping data into OHIFโ€™s native +format, which we call DataSources, and have provided one implementation of this +standard. + +You can make another datasource implementation which communicates to your +backend and maps to OHIFโ€™s native format, then use any existing mode on your +platform. Your data doesnโ€™t even need to be DICOM if you can map some +proprietary data to the correct format. + +The DataSource is also a place to add easy helper methods that platform-specific +extensions can call in order to interact with the backend, meaning proprietary +data interactions can be wrapped in extensions. + +```js +const getDataSourcesModule = () => [ + { + name: 'exampleDataSource', + type: 'webApi', // 'webApi' | 'local' | 'other' + createDataSource: dataSourceConfig => { + return IWebApiDataSource.create(/* */); + }, + }, +]; +``` + +Default extension provides two main data sources that are commonly used: +`dicomweb` and `dicomjson` + +```js +import { createDicomWebApi } from './DicomWebDataSource/index'; +import { createDicomJSONApi } from './DicomJSONDataSource/index'; + +function getDataSourcesModule() { + return [ + { + name: 'dicomweb', + type: 'webApi', + createDataSource: createDicomWebApi, + }, + { + name: 'dicomjson', + type: 'jsonApi', + createDataSource: createDicomJSONApi, + }, + ]; +} +``` + +## Custom DataSource + +You can add your custom datasource by creating the implementation using +`IWebApiDataSource.create` from `@ohif/core`. This factory function creates a +new "Web API" data source that fetches data over HTTP. + +```js title="platform/core/src/DataSources/IWebApiDataSource.js" +function create({ + initialize, + query, + retrieve, + store, + reject, + parseRouteParams, + deleteStudyMetadataPromise, + getImageIdsForDisplaySet, + getImageIdsForInstance, +}) { + /* */ +} +``` + +You can take a look at `dicomweb` data source implementation to get an idea +`extensions/default/src/DicomWebDataSource/index.js` but here here are some +important api endpoints that you need to implement: + +- `initialize`: This method is called when the data source is first created in the mode.tsx, it is used to initialize the data source and set the configuration. For instance, `dicomwebDatasource` uses this method to grab the StudyInstanceUID from the URL and set it as the active study, as opposed to `dicomJSONDatasource` which uses url in the browser to fetch the data and store it in a cache +- `query.studies.search`: This is used in the study panel on the left to fetch the prior studies for the same MRN which is then used to display on the `All` tab. it is also used in the Worklist to show all the studies from the server. +- `query.series.search`: This is used to fetch the series information for a given study that is expanded in the Worklist. +- `retrieve.bulkDataURI`: used to render RTSTUCTURESET in the viewport. +- `retrieve.series.metadata`: It is a crucial end point that is used to fetch series level metadata which for hanging displaySets and displaySet creation. +- `store.dicom`: If you don't need store functionality, you can skip this method. This is used to store the data in the backend. + +## Static WADO Client + +If the configuration for the data source has the value staticWado set, then it +is assumed that queries for the studies return a super-set of the studies, as it +is assumed to be returning a static list. The StaticWadoClient performs the +search functionality manually, by interpreting the query parameters and then +applying them to the returned response. This functionality may be useful for +other types of DICOMweb back ends, where they are capable of performing queries, +but don't allow for querying certain types of fields. However, that only works +as long as the size of the studies list isn't too large that client side +selection isn't too expensive. + +## DicomMetadataStore + +In `OHIF-v3` we have a central location for the metadata of studies, and they are +located in `DicomMetadataStore`. Your custom datasource can communicate with +`DicomMetadataStore` to store, and fetch Study/Series/Instance metadata. We will +learn more about `DicomMetadataStore` in services. + +## Adding a Data Source Outside a Module + +A data source can be added outside a module via `ExtensionManager.addDataSource`. +The following snippet of code demonstrates how `addDataSource` can be used to add +a new DICOMWeb data source for the Google Cloud Healthcare API and set it as the +active data source. + +```js +extensionManager.addDataSource({ + namespace: '@ohif/extension-default.dataSourcesModule.dicomweb', + sourceName: 'google', + configuration: { + friendlyName: 'dcmjs DICOMWeb Server', + name: 'GCP', + wadoUriRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + qidoRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + wadoRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + qidoSupportsIncludeField: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: false, + dicomUploadEnabled: true, + omitQuotationForMultipartRequest: true, + }, + {activate:true} +}); +``` + +## Updating a Data Source's Configuration + +An existing data source can have its configuration updated using the +`ExtensionManager.updateDataSourceConfiguration` method. The following snippet of +code demonstrates how `updateDataSourceConfiguration` can be use to update the +configuration of an existing DICOMWeb data source (named `dicomweb`) with the +configuration for a Google Cloud Healthcare API data source. + +```js +extensionManager.updateDataSourceConfiguration( "dicomweb", + { + name: 'GCP', + wadoUriRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + qidoRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + wadoRoot: + 'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb', + qidoSupportsIncludeField: true, + imageRendering: 'wadors', + thumbnailRendering: 'wadors', + enableStudyLazyLoad: true, + supportsFuzzyMatching: true, + supportsWildcard: false, + dicomUploadEnabled: true, + omitQuotationForMultipartRequest: true, + }, +); +``` + +## Merge Data Source +The built-in merge data source is a useful tool for combining results from multiple data sources. +Currently, this data source only supports merging at the series level. This means that series from data source 'A' +and series from data source 'B' will be retrieved under the same study. If the same series exists in both data sources, +the first series arrived is the one that gets stored, and any other conflicting series will be ignored. + +The merge data source is particularly useful when dealing with derived data that is generated and stored in different servers. +For example, it can be used to retrieve annotation series from one data source and input data (images) from another data source. + +A default data source can be defined as shown below. This allows defining which of the servers should be the +fallback server in case something goes wrong. + +Configuration Example: +```js +window.config = { + ... + dataSources: [ + { + sourceName: 'merge', + namespace: '@ohif/extension-default.dataSourcesModule.merge', + configuration: { + name: 'merge', + friendlyName: 'Merge dicomweb-1 and dicomweb-2 data at the series level', + seriesMerge: { + dataSourceNames: ['dicomweb-1', 'dicomweb-2'], + defaultDataSourceName: 'dicomweb-1' + }, + }, + }, + { + sourceName: 'dicomweb-1', + ... + }, + { + sourceName: 'dicomweb-2', + ... + }, + ], +}; +``` diff --git a/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/hpModule.md b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/hpModule.md new file mode 100644 index 0000000..e29f976 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/hpModule.md @@ -0,0 +1,624 @@ +--- +sidebar_position: 8 +sidebar_label: Hanging Protocol +--- +# Module: Hanging Protocol + +## Overview + +[Hanging protocols](http://dicom.nema.org/dicom/Conf-2005/Day-2_Selected_Papers/B305_Morgan_HangProto_v1.pdf) are an essential part of any radiology viewer. +OHIF uses Hanging Protocols to handle the arrangement of the images in the viewport. In +short, the registered protocols will get matched with the DisplaySets that are +available. Each protocol gets a score, and they are ranked. The +winning protocol (highest score) gets applied and its settings run for the viewports +to be arranged. + + +In `OHIF-v3` hanging protocols you can: + +- Define what layout the viewport should starts with (e.g., 2x2 layout) +- Specify the type of the viewport and its orientation (e.g., stack, volume with Sagittal view) +- Define which displaySets gets displayed in which viewport of the layout (e.g,. displaySet that has modality of 'CT' and 'SeriesDescription' of 'Coronary Arteries' gets displayed in the first viewport of the layout) +- Apply certain initial viewport settings (e.g., inverting the contrast, jumping to a specific slice, etc.) +- Add specific synchronization rules for the viewports (e.g., synchronize the zoom of the viewports of the index 1, 2 OR synchronize the VOI of the viewports of the index 2, 3) + + +Using `hangingProtocolModule` you can provide/register the protocols for OHIF to +utilize. + +Here is an example protocol which if used will hang a 1x3 layout with the first viewport showing a CT image, the second viewport showing a PT image and the third viewport showing their fusion, all in Sagittal orientations to achieve a view of + + +![](../../../assets/img/hangingProtocolExample.png) + + +```js +const oneByThreeProtocol = { + id: 'oneByThreeProtocol', + locked: true, + name: 'Default', + createdDate: '2021-02-23T19:22:08.894Z', + modifiedDate: '2022-10-04T19:22:08.894Z', + availableTo: {}, + editableBy: {}, + imageLoadStrategy: 'interleaveTopToBottom', + protocolMatchingRules: [ + { + attribute: 'ModalitiesInStudy', + constraint: { + contains: ['CT', 'PT'], + }, + }, + ], + displaySetSelectors: { + ctDisplaySet: { + seriesMatchingRules: [ + { + weight: 1, + attribute: 'Modality', + constraint: { + equals: { + value: 'CT', + }, + }, + required: true, + }, + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + ], + }, + ptDisplaySet: { + seriesMatchingRules: [ + { + attribute: 'Modality', + constraint: { + equals: 'PT', + }, + required: true, + }, + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + { + attribute: 'SeriesDescription', + constraint: { + contains: 'Corrected', + }, + }, + ], + }, + }, + stages: [ + { + id: 'hYbmMy3b7pz7GLiaT', + name: 'default', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 3, + }, + }, + viewports: [ + { + viewportOptions: { + viewportId: 'ctAXIAL', + viewportType: 'volume', + orientation: 'sagittal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [ + { + type: 'voi', + id: 'ctWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'ptAXIAL', + viewportType: 'volume', + orientation: 'sagittal', + initialImageOptions: { + preset: 'middle', + }, + }, + displaySets: [ + { + id: 'ptDisplaySet', + }, + ], + }, + { + viewportOptions: { + viewportId: 'fusionSAGITTAL', + viewportType: 'volume', + orientation: 'sagittal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [ + { + type: 'voi', + id: 'ctWLSync', + source: false, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + { + options: { + colormap: 'hsv', + voi: { + windowWidth: 5, + windowCenter: 2.5, + }, + }, + id: 'ptDisplaySet', + }, + ], + }, + ], + createdDate: '2021-02-23T18:32:42.850Z', + }, + ], + numberOfPriorsReferenced: -1, +}; + + +function getHangingProtocolModule() { + return [ + { + id: 'oneByThreeProtocol', + protocol: oneByThreeProtocol, + }, + ]; +} +``` + + +## Skeleton of a Protocol + +The skeleton of a hanging protocol is as follows: + + +### Id +unique identifier for the protocol, this id can be used inside mode configuration +to specify which protocol should be used for a specific mode. A mode can +request a protocol by its id (which makes OHIF to apply the protocol without +matching), or provides an array of ids which will +make the ProtocolEngine to choose the best matching protocol (based on +protocolMatching rules, which is next section). + +### imageLoadStrategy +The image load strategy specifies a function (by name) containing logic to re-order +the image load requests. This allows loading images viewed earlier to be done +sooner than those loaded later. The available strategies are: + +* interleaveTopToBottom to start at the top and work towards the bottom, for all series being loaded +* interleaveCenter is like top to bottom but starts at the center +* nth is a strategy that loads every nth instance, starting with the center +and end points, and then filling in progressively all along the image. This results in partial +image view very quickly. + +### protocolMatchingRules +A list of criteria for the protocol along with the provided points for ranking. + + - `weight`: weight for the matching rule. Eventually, all the registered + protocols get sorted based on the weights, and the winning protocol gets + applied to the viewer. + - `attribute`: tag that needs to be matched against. This can be either + Study-level metadata or a custom attribute such as "StudyInstanceUID", + "StudyDescription", "ModalitiesInStudy", "NumberOfStudyRelatedSeries", "NumberOfSeriesRelatedInstances" + In addition to these tags, you can also use a custom attribute that you have registered before. + We will learn more about this later. + - `from`: Indicates the source of the attribute. This allows getting values + from other objects such as the `prior` instance object instead of from the + current one. + + + + - `constraint`: the constraint that needs to be satisfied for the attribute. It accepts a `validator` which can be + [`equals`, `doesNotEqual`, `contains`, `doesNotContain`, `startsWith`, `endsWidth`] + + | Rule | Single Value | Array Value | Example | +|------|--------------|-------------|---------| +| equals | === | All members are === in same order | value = ['abc', 'def', 'GHI']
testValue = 'abc' (Fail)
= ['abc'] (Fail)
= ['abc', 'def', 'GHI'] (Valid)
= ['abc', 'GHI', 'def'] (Fail)
= ['abc', 'def'] (Fail)

value = 'Attenuation Corrected'
testValue = 'Attenuation Corrected' (Valid)
= 'Attenuation' (Fail)

value = ['Attenuation Corrected']
testValue = ['Attenuation Corrected'] (Valid)
= 'Attenuation Corrected' (Valid)
= 'Attenuation' (Fail) | +| doesNotEqual | !== | Any member is !== for the array, either in value, order, or length | value = ['abc', 'def', 'GHI']
testValue = 'abc' (Valid)
= ['abc'] (Valid)
= ['abc', 'def', 'GHI'] (Fail)
= ['abc', 'GHI', 'def'] (Valid)
= ['abc', 'def'] (Valid)

value = 'Attenuation Corrected'
testValue = 'Attenuation Corrected' (Fail)
= 'Attenuation' (Valid)

value = ['Attenuation Corrected']
testValue = ['Attenuation Corrected'] (Fail)
= 'Attenuation Corrected' (Fail)
= 'Attenuation' (Fail) | +| includes | Not allowed | Value is equal to one of the values of the array | value = ['abc', 'def', 'GHI']
testValue = ['abc'] (Valid)
= 'abc' (Fail)
= ['abc'] (Fail)
= 'dog' (Fail)
= ['att', 'abc'] (Valid)
= ['abc', 'def', 'dog'] (Valid)
= ['cat', 'dog'] (Fail)

value = 'Attenuation Corrected'
testValue = ['Attenuation Corrected', 'Corrected'] (Valid)
= ['Attenuation', 'Corrected'] (Fail)

value = ['Attenuation Corrected']
testValue = 'Attenuation Corrected' (Fail)
= ['Attenuation Corrected', 'Corrected'] (Valid)
= ['Attenuation', 'Corrected'] (Fail) | +| doesNotInclude | Not allowed | Value is not in one of the values of the array | value = ['abc', 'def', 'GHI']
testValue = 'Corr' (Valid)
= 'abc' (Fail)
= ['att', 'cor'] (Valid)
= ['abc', 'def', 'dog'] (Fail)

value = 'Attenuation Corrected'
testValue = ['Attenuation Corrected', 'Corrected'] (Fail)
= ['Attenuation', 'Corrected'] (Valid)

value = ['Attenuation Corrected']
testValue = 'Attenuation' (Fail)
= ['Attenuation Corrected', 'Corrected'] (Fail)
= ['Attenuation', 'Corrected'] (Valid) | +| containsI | String containment (case insensitive) | String containment (case insensitive) is OK for one of the rule values | value = 'Attenuation Corrected'
testValue = 'Corr' (Valid)
= 'corr' (Valid)
= ['att', 'cor'] (Valid)
= ['Att', 'Wall'] (Valid)
= ['cat', 'dog'] (Fail)

value = ['abc', 'def', 'GHI']
testValue = 'def' (Valid)
= 'dog' (Fail)
= ['gh', 'de'] (Valid)
= ['cat', 'dog'] (Fail) | +| contains | String containment (case sensitive) | String containment (case sensitive) is OK for one of the rule values | value = 'Attenuation Corrected'
testValue = 'Corr' (Valid)
= 'corr' (Fail)
= ['att', 'cor'] (Fail)
= ['Att', 'Wall'] (Valid)
= ['cat', 'dog'] (Fail)

value = ['abc', 'def', 'GHI']
testValue = 'def' (Valid)
= 'dog' (Fail)
= ['cat', 'de'] (Valid)
= ['cat', 'dog'] (Fail) | +| doesNotContain | String containment is false | String containment is false for all values of the array | value = 'Attenuation Corrected'
testValue = 'Corr' (Fail)
= 'corr' (Valid)
= ['att', 'cor'] (Valid)
= ['Att', 'Wall'] (Fail)
= ['cat', 'dog'] (Valid)

value = ['abc', 'def', 'GHI']
testValue = 'def' (Fail)
= 'dog' (Valid)
= ['cat', 'de'] (Fail)
= ['cat', 'dog'] (Valid) | +| doesNotContainI | String containment is false (case insensitive) | String containment (case insensitive) is false for all values of the array | value = 'Attenuation Corrected'
testValue = 'Corr' (Fail)
= 'corr' (Fail)
= ['att', 'cor'] (Fail)
= ['Att', 'Wall'] (Fail)
= ['cat', 'dog'] (Valid)

value = ['abc', 'def', 'GHI']
testValue = 'DEF' (Fail)
= 'dog' (Valid)
= ['cat', 'gh'] (Fail)
= ['cat', 'dog'] (Valid) | +| startsWith | Value begins with characters | Starts with one of the values of the array | value = 'Attenuation Corrected'
testValue = 'Corr' (Fail)
= 'Att' (Fail)
= ['cat', 'dog', 'Att'] (Valid)
= ['cat', 'dog'] (Fail)

value = ['abc', 'def', 'GHI']
testValue = 'deg' (Valid)
= ['cat', 'GH'] (Valid)
= ['cat', 'gh'] (Fail)
= ['cat', 'dog'] (Fail) | +| endsWith | Value ends with characters | ends with one of the value of the array | value = 'Attenuation Corrected'
testValue = 'TED' (Fail)
= 'ted' (Valid)
= ['cat', 'dog', 'ted'] (Valid)
= ['cat', 'dog'] (Fail)

value = ['abc', 'def', 'GHI']
testValue = 'deg' (Valid)
= ['cat', 'HI'] (Valid)
= ['cat', 'hi'] (Fail)
= ['cat', 'dog'] (Fail) | +| greaterThan | value is >= to rule | Not applicable | value = 30
testValue = 20 (Valid)
= 40 (Fail) | +| lessThan | value is <= to rule | Not applicable | value = 30
testValue = 40 (Valid)
= 20 (Fail) | +| range | Not applicable | 2 value requested (min and max) | value = 50
testValue = [10,60] (Valid)
= [60, 10] (Valid)
= [0, 10] (Fail)
= [70, 80] (Fail)
= 45 (Fail)
= [45] (Fail) | +| notNull | Not Applicable | Not Applicable | No value | | + A sample of the matching rule is above which matches against the study description to be PETCT + + ```js + { + id: 'wauZK2QNEfDPwcAQo', + weight: 1, + attribute: 'StudyDescription', + constraint: { + contains: { + value: 'PETCT', + }, + }, + required: false, + }, + ``` + +### `from` attribute (optional) +The `from` attribute allows you to retrieve the attribute to test from another object, such as the previous study, the overall list of studies, or another provided value from a module. + +The values provided by OHIF which you can use are: + +- `activeStudy`: to use the metadata of the active study to match +- `studies`: to use the metadata of the list of studies (all studies) to match +- `allDisplaySets`: all available display sets +- `displaySets`: if the selector has matched a study, these are the display sets for that study +- `prior`: the metadata of the first study in the list of studies that is not the active study +- `options`: during matching, we also provide an options object with the following information that you can use as the `from` value: + - `studyInstanceUIDsIndex`: the index of the study in the list of studies + - `instance`: the metadata of the instance being matched, which is exactly the displaySet.instance metadata. + + +### displaySetSelectors (mandatory) +Defines the display sets that the protocol will use for arrangement. + +```js + displaySetSelectors: { + ctDisplaySet: { + seriesMatchingRules: [ + { + weight: 1, + attribute: 'Modality', + constraint: { + equals: { + value: 'CT', + }, + }, + required: true, + }, + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + ], + }, + ptDisplaySet: { + seriesMatchingRules: [ + { + attribute: 'Modality', + constraint: { + equals: 'PT', + }, + required: true, + }, + { + weight: 1, + attribute: 'isReconstructable', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + { + attribute: 'SeriesDescription', + constraint: { + contains: 'Corrected', + }, + }, + ], + }, + } +``` + +As you see above we have specified two displaysets: 1) ctDisplaySet , 2) ptDisplaySet +The ctDisplaySet will match against all the series that are CT and reconstructable +The ptDisplaySet will match against all the series that are PT and reconstructable. + +As you see each selector is composed of an `id` as the key and a set of `seriesMatchingRules` (displaySetMatchingRules) which gives score to the displaySet +based on the matching rules. The displaySet with the highest score will be used for the `id`. + +### stages +Each protocol can define one or more stages. Each stage defines a certain layout and viewport rules. Therefore, the `stages` property is an array of objects, each object being one stage. + +### viewportStructure +Defines the layout of the viewer. You can define the number of `rows` and `columns`. There should be `rows * columns` number of +viewport configuration in the `viewports` property. Note that order of viewports are rows first then columns. + +```js +viewportStructure: { + type: 'grid', + properties: { + rows: 1, + columns: 2, + viewportOptions: [], + }, +}, +``` + +In addition to the equal viewport sizes, you can define viewports to span multiple rows or columns. + +```js +viewportStructure: { + type: 'grid', + properties: { + rows: 1, + columns: 2, + viewportOptions: [ + { + x: 0, + y: 0, + width: 1 / 4, + height: 1, + }, + { + x: 1 / 4, + y: 0, + width: 3 / 4, + height: 1, + }, + ], + }, +}, + +``` + + +### viewports +This field includes the viewports that will get hung on the viewer. + +```js +viewports: [ + { + viewportOptions: { + viewportId: 'ctAXIAL', + viewportType: 'volume', + orientation: 'sagittal', + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [ + { + type: 'voi', + id: 'ctWLSync', + source: true, + target: true, + }, + ], + }, + displaySets: [ + { + id: 'ctDisplaySet', + }, + ], + }, + // the rest +], +``` + +As you can see in the hanging protocol we defined three viewports (but only showing one of them right above). Each viewport has two properties: + +1. `viewportOptions`: defines the viewport properties such as + - `viewportId`: unique identifier for the viewport (optional) + - `viewportType`: type of the viewport (optional - options: stack, volume - default is stack + - `background`: background color of the viewport (optional) + - `orientation`: orientation of the viewport (optional - if not defined for volume -> acquisition axis) + - `toolGroupId`: tool group that will be used for the viewport (optional) + - `initialImageOptions`: initial image options (optional - can be specific imageIndex number or preset (first, middle, last)) + - `syncGroups`: sync groups for the viewport (optional) + -The `displayArea` parameter refers to the designated area within the viewport where a specific portion of the image can be displayed. This parameter is optional and allows you to choose the location of the image within the viewport. For example, in mammography images, you can display the left breast on the left side of the viewport and the right breast on the right side, with the chest wall positioned in the middle. To understand how to define the display area, you can refer to the live example provided by CornerstoneJS [here](https://www.cornerstonejs.org/live-examples/programaticpanzoom). + + +2. `displaySets`: defines the display sets that are displayed on a viewport. It is an array of objects, each object being one display set. + - `id`: id of the display set (required) + - `options` (optional): options for the display set + - voi: windowing options for the display set (optional: windowWidth, windowCenter) + - voiInverted: whether the VOI is inverted or not (optional) + - colormap: colormap for the display set (optional, it is an object with `{ name }` and optional extra `opacity` property) + - displayPreset: display preset for the display set (optional, used for 3D volume rendering. e.g., 'CT-Bone') + + +### Custom attribute +For any matching rules you can specify a custom attribute too. For instance, +if you have a timepoint attribute in for each of your studies, you can use that in the matching rules. + +```js +{ + id: 'wauZK2QNEfDPwcAQo', + weight: 1, + attribute: 'timepoint', + constraint: { + equals: { + value: 'baseline', + }, + }, + required: false, +}, +``` + +and then you need to register a callback in the HangingProtocolService to get the value for the attribute. + +```js +HangingProtocolService.addCustomAttribute( + 'timepoint', // attributeId + 'addCustomAttribute', // attributeName + study => { // callback that returns the value for the attribute + const timePoint = fetchFromMyCustomBackend(study.studyInstanceUid); + return timePoint; + } +); +``` + + +## Matching on Prior Study with UID + +Often it is desired to match a new study to a prior study (e.g., follow up on +a surgery). Since the hanging protocols run on displaySets we need to have a +way to let OHIF knows that it needs to load the prior study as well. This can +be done by specifying both StudyInstanceUIDs in the URL. The additional studies +are then accessible to the hanging protocol. Below we are +running OHIF with two studies, and a comparison hanging protocol available by +default. + +```bash +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095722.1&hangingprotocolId=@ohif/hpCompare +``` + +The `&hangingProtocolId` option forces the specific hanging protocol to be +applied, but the mode can also add the hanging protocols to the default set, +and then the best matching hanging protocol will be applied by the run method. + +To match any other studies, it is required to enable the prior matching rules +capability using: + +```javascript + // Indicate number of priors used - 0 means any number, -1 means none. + numberOfPriorsReferenced: 1, +``` + +The matching rule that allows the hanging protocol to be runnable is: + +```javascript + protocolMatchingRules: [ + { + id: 'Two Studies', + weight: 1000, + // This will generate 1.3.6.1.4.1.25403.345050719074.3824.20170125095722.1 + // since that is study instance UID in the prior from instance. + attribute: 'StudyInstanceUID', + // The 'from' attribute says where to get the 'attribute' value from. In this case + // prior means the second study in the study list. + from: 'prior', + required: true, + constraint: { + notNull: true, + }, + }, + ], +``` + +The display set selector selecting the specific study to display is included +in the studyMatchingRules. Note that this rule will cause ONLY the second study +to be matched, so it won't attempt to match anything in other studies. +Additional series level criteria, such as modality rules must be included at the +`seriesMatchingRules`. + +```javascript + studyMatchingRules: [ + { + // The priorInstance is a study counter that indicates what position this study is in + // and the value comes from the options parameter. + attribute: 'studyInstanceUIDsIndex', + from: 'options', + required: true, + constraint: { + equals: { value: 1 }, + }, + }, + ], +``` + + +## Callbacks + + +Hanging protocols in `OHIF-v3` provide the flexibility to define various callbacks that allow you to customize the behavior of your viewer when specific events occur during protocol execution. These callbacks are defined in the `ProtocolNotifications` type and can be added to your hanging protocol configuration. + +Each callback is an array of commands or actions that are executed when the event occurs. + +```js +[ + { + commandName: 'showDownloadViewportModal', + commandOptions: {} + } +] +``` + + +Here, we'll explain the available callbacks and their purposes: + +### `onProtocolExit` + +The `onProtocolExit` callback is executed after the protocol is exited and the new one is applied. This callback is useful for performing actions or executing commands when switching between hanging protocols. + +### `onProtocolEnter` + +The `onProtocolEnter` callback is executed after the protocol is entered and applied. You can use this callback to define actions or commands that should run when entering a specific hanging protocol. + +### `onLayoutChange` + +The `onLayoutChange` callback is executed before the layout change is started. You can use it to apply a specific hanging protocol based on the current layout or other criteria. + +### `onViewportDataInitialized` + +The `onViewportDataInitialized` callback is executed after the initial viewport grid data is set and all viewport data includes a designated display set. This callback runs during the initial layout setup for each stage. You can use it to perform actions or apply settings to the viewports at the start. + +Here is an example of how you can add these callbacks to your hanging protocol configuration: + +```javascript +const protocol = { + id: 'myProtocol', + name: 'My Protocol', + // rest of the protocol configuration + callbacks: { + onProtocolExit: [ + // Array of commands or actions to execute on protocol exit + ], + onProtocolEnter: [ + // Array of commands or actions to execute on protocol enter + ], + onLayoutChange: [ + // Array of commands or actions to execute on layout change + ], + onViewportDataInitialized: [ + // Array of commands or actions to execute on viewport data initialization + ], + }, + // protocolMatchingRules + // the rest +}; diff --git a/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/layout-template.md b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/layout-template.md new file mode 100644 index 0000000..8214449 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/layout-template.md @@ -0,0 +1,139 @@ +--- +sidebar_position: 7 +sidebar_label: Layout Template +--- + +# Module: Layout Template + +## Overview + +`LayoutTemplates` are a new concept in v3 that modes use to control the layout +of a route. A layout template is a React component that is given a set of +managers that define apis to access toolbar state, commands, and hotkeys, as +well as props defined by the layout template. + +For instance the default LayoutTemplate takes in leftPanels, rightPanels and +viewports as props, which it uses to build its view. + +In addition, `layout template` has complete control over the structure of the +application. You could have tools down the left side, or a strict guided +workflow with tools set programmatically, the choice is yours for your use case. + +```jsx +const getLayoutTemplateModule = (/* ... */) => [ + { + id: 'exampleLayout', + name: 'exampleLayout', + component: ExampleLayoutComponent, + }, +]; +``` + +The `props` that are passed to `layoutTemplate` are managers and service, along +with the defined mode left/right panels, mode's defined viewports and OHIF +`ViewportGridComp`. LayoutTemplate leverages extensionManager to grab typed +extension module entries: `*.getModuleEntry(id)` + +A simplified code for `Default extension`'s layout template is: + +```jsx title="extensions/default/src/ViewerLayout/index.jsx" +import React from 'react'; +import { SidePanel } from '@ohif/ui'; + +function Toolbar({ servicesManager }) { + const { ToolBarService } = servicesManager.services; + + return ( + <> + // ToolBarService.getButtonSection('primary') to get toolbarButtons + {toolbarButtons.map((toolDef, index) => { + const { id, Component, componentProps } = toolDef; + return ( + ToolBarService.recordInteraction(args)} + /> + ); + })} + + ); +} + +function ViewerLayout({ + // From Extension Module Params + extensionManager, + servicesManager, + hotkeysManager, + commandsManager, + // From Modes + leftPanels, + rightPanels, + viewports, + ViewportGridComp, +}) { + const getPanelData = id => { + const entry = extensionManager.getModuleEntry(id); + const content = entry.component; + + return { + iconName: entry.iconName, + iconLabel: entry.iconLabel, + label: entry.label, + name: entry.name, + content, + }; + }; + + const getViewportComponentData = viewportComponent => { + const entry = extensionManager.getModuleEntry(viewportComponent.namespace); + + return { + component: entry.component, + displaySetsToDisplay: viewportComponent.displaySetsToDisplay, + }; + }; + + const leftPanelComponents = leftPanels.map(getPanelData); + const rightPanelComponents = rightPanels.map(getPanelData); + const viewportComponents = viewports.map(getViewportComponentData); + + return ( +
+ + +
+ {/* LEFT SIDEPANELS */} + + + {/* TOOLBAR + GRID */} + + + {/* Right SIDEPANELS */} + +
+
+ ); +} +``` + +## Overview Video + +
+ +
diff --git a/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/panel.md b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/panel.md new file mode 100644 index 0000000..9a9235c --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/panel.md @@ -0,0 +1,163 @@ +--- +sidebar_position: 6 +sidebar_label: Panel +--- + +# Module: Panel + +## Overview + +The default LayoutTemplate has panels on the left and right sides, however one +could make a template with panels at the top or bottom and make extensions with +panels intended for such slots. + +An extension can register a Panel Module by defining a `getPanelModule` method. +The panel module provides the ability to define `menuOptions` and `components` +that can be used by the consuming application. `components` are React Components +that can be displayed in the consuming application's "Panel" Component. + +![panel-module-v3](../../../assets/img/panel-module-v3.png) + +The `menuOptions`'s `target` key, points to a registered `components`'s `id`. A +`defaultContext` is applied to all `menuOption`s; however, each `menuOption` can +optionally provide its own `context` value. + +The `getPanelModule` receives an object containing the `ExtensionManager`'s +associated `ServicesManager` and `CommandsManager`. + +An extension can also trigger to activate/open a panel via the `PanelService` - +either by explicitly calling `PanelService.activatePanel` or triggering panel +activation when some other event fires. + +```jsx +import PanelMeasurementTable from './PanelMeasurementTable.js'; + +function getPanelModule({ + commandsManager, + extensionManager, + servicesManager, +}) { + const wrappedMeasurementPanel = () => { + return ( + + ); + }; + + return [ + { + name: 'measure', + iconName: 'list-bullets', + iconLabel: 'Measure', + label: 'Measurements', + isDisabled: studies => {}, // optional + component: wrappedMeasurementPanel, + }, + ]; +} +``` + +## Consuming Panels Inside Modes + +As explained earlier, extensions make the functionalities and components +available and `modes` utilize them to build an app. So, as seen above, we are +not actually defining which side the panel should be opened. Our extension is +providing the component with its. + +New: You can easily add multiple panels to the left/right side of the viewer +using the mode configuration. As seen below, the `leftPanels` and `rightPanels` +accept an `Array` of the `IDs`. The mode configuration also allows for either (or +both) side panels to be closed by default. In the code below, the right panel +is closed by default. The mode can optionally add event triggers to +the `PanelService` that when fired will cause a side panel that was defaulted +closed to open. In the code below, the right side panel, that contains the +`trackedMeasurements` panel, is triggered to open when a measurement is added. +Note that once a default closed side panel has been opened once, +only a `PanelService.EVENTS.ACTIVATE_PANEL` event with `forceActive === true` +will cause it open (again). + +```js + +const extensionDependencies = { + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', + '@ohif/extension-measurement-tracking': '^3.0.0', + '@ohif/extension-cornerstone-dicom-sr': '^3.0.0', +}; + +const id = 'viewer' +const version = '3.0.0 + +function modeFactory({ modeConfiguration }) { + let _activatePanelTriggersSubscriptions = []; + return { + id, + routes: [ + { + path: 'longitudinal', + layoutTemplate: ({ location, servicesManager }) => { + return { + id, + props: { + leftPanels: [ + '@ohif/extension-measurement-tracking.panelModule.seriesList', + ], + rightPanels: [ + '@ohif/extension-measurement-tracking.panelModule.trackedMeasurements', + ], + rightPanelClosed: true, + viewports, + }, + }; + }, + }, + ], + onModeEnter: ({ servicesManager }) => { + const { + measurementService, + panelService, + } = servicesManager.services; + + _activatePanelTriggersSubscriptions = [ + ...panelService.addActivatePanelTriggers('@ohif/extension-measurement-tracking.panelModule.trackedMeasurements', [ + { + sourcePubSubService: measurementService, + sourceEvents: [ + measurementService.EVENTS.MEASUREMENT_ADDED, + measurementService.EVENTS.RAW_MEASUREMENT_ADDED, + ], + }, + ]), + ]; + }, + onModeExit: () => { + _activatePanelTriggersSubscriptions.forEach(sub => sub.unsubscribe()); + _activatePanelTriggersSubscriptions = []; + }, + extensions: extensionDependencies + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; +``` + +:::note +You can stack multiple panel components on top of each other by providing an array of panel components in the `rightPanels` or `leftPanels` properties. + +For instance we can use + +``` +rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.measurements]] +``` + +This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other. + +::: diff --git a/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/sop-class-handler.md b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/sop-class-handler.md new file mode 100644 index 0000000..cf70fb4 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/sop-class-handler.md @@ -0,0 +1,120 @@ +--- +sidebar_position: 4 +sidebar_label: SOP Class Handler +--- +# Module: SOP Class Handler + +## Overview +This module defines how a specific DICOM SOP class should be processed to make a list of `DisplaySet`, things which can be hung in a viewport. An extension can register a [SOP Class][sop-class-link] Handler Module by defining a `getSopClassHandlerModule` method. The [SOP Class][sop-class-link]. + +The mode chooses what SOPClassHandlers to use, so you could process a series in a different way depending on mode within the same application. + +SOPClassHandler is a bit different from the other modules, as it doesn't provide a `1:1` +schema for UI or provide its own components. It instead defines: + +- `sopClassUIDs`: an array of string SOP Class UIDs that the + `getDisplaySetFromSeries` method should be applied to. +- `getDisplaySetFromSeries`: a method that maps series and study metadata to a + display set + +A `displaySet` has the following shape: + +```js +return { + Modality: 'MR', + displaySetInstanceUIDD + SeriesDate, + SeriesTime, + SeriesInstanceUID, + StudyInstanceUID, + SeriesNumber, + FrameRate, + SeriesDescription, + isMultiFrame, + numImageFrames, + SOPClassHandlerId, + madeInClient, +} +``` + +## Example SOP Class Handler Module + +```js +import ImageSet from '@ohif/core/src/classes/ImageSet'; + + +const sopClassDictionary = { + CTImageStorage: "1.2.840.10008.5.1.4.1.1.2", + MRImageStorage: "1.2.840.10008.5.1.4.1.1.4", +}; + + +// It is important to note that the used SOPClassUIDs in the modes are in the order that is specified in the array. +const sopClassUids = [ + sopClassDictionary.CTImageStorage, + sopClassDictionary.MRImageStorage, +]; + +function addInstances(instances) { + // Add instances to this display set, and return the display set updated. +} + +const makeDisplaySet = (instances) => { + const instance = instances[0]; + const imageSet = new ImageSet(instances); + + imageSet.setAttributes({ + displaySetInstanceUID: imageSet.uid, + SeriesDate: instance.SeriesDate, + SeriesTime: instance.SeriesTime, + SeriesInstanceUID: instance.SeriesInstanceUID, + StudyInstanceUID: instance.StudyInstanceUID, + SeriesNumber: instance.SeriesNumber, + FrameRate: instance.FrameTime, + SeriesDescription: instance.SeriesDescription, + Modality: instance.Modality, + isMultiFrame: isMultiFrame(instance), + numImageFrames: instances.length, + SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`, + addInstances, + }); + + // Note returns an array now + return [imageSet]; +}; + +getSopClassHandlerModule = () => { + return [ + { + name: 'stack, + sopClassUids, + getDisplaySetsFromSeries: makeDisplaySet, + }, + ]; +}; + +``` + +### addInstances +In order to allow new SOP instances to be received and added to an existing display +set, the addInstances method can be added to a display set. It is called +on the display set to be updated, and returns it when it has added at least one +of the instances to the display set. + +### More examples : +You can find another example for this mapping between raw metadata and displaySet for +`DICOM-SR` extension. + +## `@ohif/app` usage + +We use the `sopClassHandlerModule`s in `DisplaySetService` where we +transform instances from the raw metadata format to a OHIF displaySet format. +You can read more about DisplaySetService here. + + +[sop-class-link]: http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_B.5.html +[dicom-html-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-html/src/OHIFDicomHtmlSopClassHandler.js#L4-L12 +[dicom-pdf-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-pdf/src/OHIFDicomPDFSopClassHandler.js#L4-L6 +[dicom-micro-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-microscopy/src/DicomMicroscopySopClassHandler.js#L5-L7 +[dicom-seg-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-segmentation/src/OHIFDicomSegSopClassHandler.js#L5-L7 + diff --git a/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/toolbar.md b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/toolbar.md new file mode 100644 index 0000000..b353a03 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/toolbar.md @@ -0,0 +1,528 @@ +--- +sidebar_position: 1 +sidebar_label: Toolbar +--- + +# Module: Toolbar + +An extension can register a Toolbar Module by defining a `getToolbarModule` +method. `OHIF-v3`'s `default` extension (`"@ohif/extension-default"`) provides the +following toolbar button `uiTypes`: + +- `ohif.radioGroup`: which is a simple button that can be clicked +- `ohif.splitButton`: which is a button with a dropdown menu +- `ohif.divider`: which is a simple divider + +## Example Toolbar Module + +The Toolbar Module should return an array of `objects`. There are currently a +few different variations of definitions, each one is detailed further down. +There are two things that the toolbar module can provide, first +a component, and second evaluators. + +### Components +```js +export default function getToolbarModule({ commandsManager, servicesManager }) { + return [ + { + name: 'ohif.radioGroup', + defaultComponent: ToolbarButton, + clickHandler: () => {}, + }, + { + name: 'ohif.splitButton', + defaultComponent: ToolbarSplitButton, + clickHandler: () => {}, + }, + { + name: 'ohif.layoutSelector', + defaultComponent: ToolbarLayoutSelector, + clickHandler: (evt, clickedBtn, btnSectionName) => {}, + }, + { + name: 'ohif.toggle', + defaultComponent: ToolbarButton, + clickHandler: () => {}, + }, + ]; +} +``` + +### Custom Components + +You can also create your own extension, and add your new custom tool appearance +(e.g., split horizontally instead of vertically for split tool). Simply add +`getToolbarModule` to your extension, and pass your tool react component to its +`defaultComponent` property in the returned object. You can use `@ohif/ui` +components such as `IconButton, Icon, Tooltip, ToolbarButton` to build your own +component. + +```js +import myToolComponent from './myToolComponent'; + +export default function getToolbarModule({ commandsManager, servicesManager }) { + return [ + { + name: 'new-tool-type', + defaultComponent: myToolComponent, + clickHandler: () => {}, + }, + ]; +} +``` + +Check out how to assemble the toolbar in the [modes](../../modes/index.md) section. + + +### Evaluators +Buttons may be equipped with evaluators, which are functions invoked by the toolbarService to assess the button's status. These evaluators are expected to return an object of `{className}` and may include additional details, as elaborated in the subsequent section. + +Evaluators play a crucial role in determining the button's status based on the viewport. For example, users should be restricted from clicking on the mpr if the displaySet is not reconstructable. Additionally, certain buttons within the toolbar may be associated with specific toolGroups and should remain inactive for certain viewports. + +Let's look at one of the evaluators (for `evaluate.cornerstoneTool`) + +```js + { + name: 'evaluate.cornerstoneTool', + evaluate: ({ viewportId, button }) => { + const toolGroup = toolGroupService.getToolGroupForViewport(viewportId); + + if (!toolGroup) { + return; + } + + const toolName = toolbarService.getToolNameForButton(button); + + if (!toolGroup || !toolGroup.hasTool(toolName)) { + return { + disabled: true, + className: '!text-common-bright ohif-disabled', + disabledText: 'Tool not available', + }; + } + + const isPrimaryActive = toolGroup.getActivePrimaryMouseButtonTool() === toolName; + + return { + disabled: false, + className: isPrimaryActive + ? '!text-black bg-primary-light' + : '!text-common-bright hover:!bg-primary-dark hover:!text-primary-light', + }; + }, +}, +``` + +as you can see the job of this evaluator is to determine if the button should be disabled or not. It does so by checking the `toolGroup` and the `toolName` and then returns an object with `disabled` and `className` properties. + +The following evaluators are provided by us: + +- `evaluate.cornerstoneTool`: If assigned to a button (see next), it will make the button react to the active viewport state based on its toolGroup. +- `evaluate.cornerstoneTool.toggle`: It is designed to consider tools with toggle behavior, such as reference lines and image overlay (either on or off). +- `evaluate.cornerstone.synchronizer`: This is designed to consider the synchronizer state of the viewport, whether it is synced or not. +- `evaluate.viewportProperties.toggle`: Some properties of the viewport are toggleable, such as invert, flip, rotate, etc. By assigning this evaluator to those buttons, they will react to the active viewport state based on its properties. This allows for dynamic buttons that change their appearance based on the active viewport state. +- `evaluate.mpr`: special evaluator for MPR since it needs to check if the displaySet is reconstructable or not. + + +Sometime you want to use the same `evaluator` for different purposes, in that case you can use an object +with `name` and other properties. For example, in `'evaluate.cornerstone.segmentation'` we use +this pattern, where multiple toolbar buttons are using the same evaluator but with different options ( + in this case `toolNames` +) + +```js +{ + name: 'evaluate.cornerstone.segmentation', + toolNames: ['CircleBrush' , 'SphereBrush'] +}, +``` + +#### Composing evaluators + +You can choose to set up multiple evaluators for a single button. This comes in handy when you need to assess the button according to various conditions. For example, we aim to prevent the Cine player from showing up on the 3D viewport, so we have: + +```js +evaluate: [ + 'evaluate.cine', + { + name: 'evaluate.viewport.supported', + unsupportedViewportTypes: ['volume3d'], + }, +], +``` + +You can even come up with advanced evaluators such as: + +```js +evaluate: [ + 'evaluate.cornerstone.segmentation', + // need to put the disabled text last, since each evaluator will + // merge the result text into the final result + { + name: 'evaluate.cornerstoneTool', + disabledText: 'Select the PT Axial to enable this tool', + }, +], +``` + +that we use for our RectangleROIStartEndThreshold tool in tmtv mode. + +As you see this evaluator is composed of two evaluators, one is `evaluate.cornerstone.segmentation` which makes sure (in the implementation), that +there is a segmentation created, and the second one is `evaluate.cornerstoneTool` which makes sure that the tool is available in the viewport. + +Since we are using multiple evaluators, the `disabledText` of each evaluator will be merged into the final result, so you need to +put the `disabledText` in the last evaluator. + +#### Group evaluators +Split buttons (see in [ToolbarService](../../services/data/ToolbarService.md) on how to define one) may feature a group evaluator, we provide two of them and you can write your own. + + +- `evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList`: determine the outcome of user interactions with the split buttons on what button should be promoted to the primary section. In the example above, the cornerstone tool's status is checked, and if it is not active in the list of buttons, the button is promoted to the primary section. +- `evaluate.group.promoteToPrimary`: disregarding the cornerstone tool's status and promoting the button to the primary section regardless. + +Failure to specify a group evaluator will result in no action, leaving the button in the secondary section. + +:::note +As you have learned so far, the extension modules only 'provides' the functionality +and it is the mode's job to consume it. You can next learn how to consume these components +and evaluators to build a toolbar in the +::: + + +#### Custom Evaluators +You can create your own evaluators. For instance, you have the option to design tri-state buttons, which are buttons with three states such as Show All, Show Some, or Show None of the Viewport Overlays. + + +## Toolbar buttons consumed in modes +Providing just the components is not enough. You need to add the buttons to the toolbar service and decide which ones are used for each section. + +Below we can see a simplified version of the `longitudinal` (basic viewer) mode that shows how +a mode can add buttons to the toolbar by calling +`ToolBarService.addButtons(toolbarButtons)`. `toolbarButtons` is an array of +`toolDefinitions` which we will learn next. + +```js +function modeFactory({ modeConfiguration }) { + return { + id: 'viewer', + displayName: 'Basic Viewer', + + onModeEnter: ({ servicesManager, extensionManager }) => { + const { toolBarService } = servicesManager.services; + + toolbarService.addButtons([...toolbarButtons, ...moreTools]); + toolbarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'info', + 'WindowLevel', + 'Pan', + 'Capture', + 'Layout', + 'Crosshairs', + 'MoreTools', + ]); + }, + routes: [ + { + path: 'longitudinal', + layoutTemplate: ({ location, servicesManager }) => { + return { + /* */ + }; + }, + }, + ], + }; +} +``` + + +:::note +By default OHIF's default layout (`extensions/default/src/ViewerLayout/index.tsx`) which is used in all modes use a Toolbar component that creates a +`primary` section for tools. That is why we are creating a `primary` section in the example above. + +Layouts are also customizable, and you can create your own layout in your extensions and provide it to your modes view `getLayoutTemplateModule` module. + +By default we use `@ohif/extension-default.layoutTemplateModule.viewerLayout` to use the default layout which provides a + +- Header (with logo on left, toolbar in the middle and user menu on the right) +- Left panel +- Main viewport grid area +- Right panel +::: + + +## Alternative Toolbar sections + +In your UI component, such as panels, you have the option to include a toolbar section template. +This allows you to easily add buttons to it later on. To ensure that the buttons are added properly +to the toolbar, respond to interactions correctly, and evaluate states accurately, simply utilize the `useToolbar` hook. +This hook grants you access to the `onInteraction` function and the `toolbarButtons` array, which you can customize within your UI as needed. + +```js + +function myCustomPanel({servicesManager}){ + const { onInteraction, toolbarButtons } = useToolbar({ + servicesManager, + buttonSection: 'myCustomSectionName' + }); + + // map the buttons to the UI + return ( +
+ {toolbarButtons.map((button, index) => { + return ( + + ); + })} +
+ ); +} + +``` + +We have provided a common component for toolbar buttons called `Toolbox`. +The Toolbox component serves as a versatile and configurable container for toolbar tools within your application. +It is designed to work in conjunction with the useToolbar hook to manage tool states, handle user interactions, and memorize options +using context API. + + +The `Toolbox` can be easily integrated into your application UI, requiring only the necessary services (servicesManager, commandsManager) and configuration parameters (buttonSectionId, title). Here's a simple usage scenario: + + + +```js +function MyApplication({ servicesManager, commandsManager }) { + // Configuration for the toolbox container + const config = { + servicesManager, + commandsManager, + buttonSectionId: 'customButtonSection', + title: 'My Toolbox', + }; + + return ; +} +``` + +Then in your modes you can edit the tools in that button section. + +```js + +onModeEnter: ({ servicesManager, extensionManager }) => { + const { toolBarService } = servicesManager.services; + + toolbarService.addButtons([...toolbarButtons, ...moreTools]); + toolbarService.createButtonSection('customButtonSection', [ + 'MeasurementTools', + 'Zoom', + 'info', + ]); +}, +``` + +Another example might be you want to open a modal to show some tool options when a button is clicked. +You can use this pattern + + +```js +// ToolbarButton in mode + { + id: 'Others', + uiType: 'ohif.radioGroup', + props: { + icon: 'info-action', + label: 'Others', + commands: 'showOthersModal', + }, + }, +``` + +and inside your mode factory + +```js +// adding the 'Others' button to the primary section +toolbarService.createButtonSection('primary', [ + 'Others', // --------> this one +]); + +// adding the shapes button to the 'Other' section +toolbarService.createButtonSection('other', ['Shapes']); +``` + +here as you see we are using a command `showOthersModal` which is defined in the commands module. + +```js +// inside commandsModule of your extension + showOthersModal: () => { + const { uiModalService } = servicesManager.services; + uiModalService.show({ + content: OthersModal, + title: 'Others', + customClassName: 'w-8', + movable: true, + contentProps: { + onClose: uiModalService.hide, + servicesManager, + commandsManager, + }, + containerDimensions: 'h-[125px] w-[300px]', + contentDimensions: 'h-[125px] w-[300px]', + }); + }, +``` + +as you see it is opening a modal with `OthersModal` component (below) which contains the +`Toolbox` component. + +```js +// Others modal +import { Toolbox } from '@ohif/ui'; + +function OthersModal({ servicesManager, commandsManager }) { + return ( +
+ +
+ ); +} +``` + +The result would be a modal with a toolbox inside it when the `Others` button is clicked, and the +state will get synchronized with the toolbar service automatically. + + +![alt text](../../../assets/img/toolbox-modal.png) + +## Toolbox With Options + +Your toolbox toolbar buttons can have options, this is really useful +for advanced tools that require to change some parameters. For example, the brush tool that requires the brush size to change or the mode (2D or 3D). + +:::note +Toolbox with options will run the options commands +on the mount of the toolbox component. This is useful for setting the initial state of the toolbox. +::: + +Currently we support three types of options. + +### Radio option + +We use this in segmentation shapes to let the user choose between +three different modes + +```js +{ + id: 'Shapes', + uiType: 'ohif.radioGroup', + props: { + label: 'Shapes', + evaluate: { + name: 'evaluate.cornerstone.segmentation', + toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'], + }, + icon: 'icon-tool-shape', + options: [ + { + name: 'Shape', + type: 'radio', + value: 'CircleScissor', + id: 'shape-mode', + values: [ + { value: 'CircleScissor', label: 'Circle' }, + { value: 'SphereScissor', label: 'Sphere' }, + { value: 'RectangleScissor', label: 'Rectangle' }, + ], + commands: 'setToolActiveToolbar', + }, + ], + }, +}, +``` + +### Range option + +We use this for brush radius change + +```js +{ + id: 'Brush', + icon: 'icon-tool-brush', + label: 'Brush', + evaluate: { + name: 'evaluate.cornerstone.segmentation', + toolNames: ['CircularBrush', 'SphereBrush'], + disabledText: 'Create new segmentation to enable this tool.', + }, + options: [ + { + name: 'Radius (mm)', + id: 'brush-radius', + type: 'range', + min: 0.5, + max: 99.5, + step: 0.5, + value: 25, + commands: { + commandName: 'setBrushSize', + commandOptions: { toolNames: ['CircularBrush', 'SphereBrush'] }, + }, + }, + ], +}, +``` + +### Custom option + +We use this pattern inside `tmtv` mode for `RectangleROIThreshold` + +```js +{ + id: 'RectangleROIStartEndThreshold', + uiType: 'ohif.radioGroup', + props: { + icon: 'tool-create-threshold', + label: 'Rectangle ROI Threshold', + commands: setToolActiveToolbar, + evaluate: { + name: 'evaluate.cornerstoneTool', + disabledText: 'Select the PT Axial to enable this tool', + }, + options: 'tmtv.RectangleROIThresholdOptions', + }, +}, +``` + +Note that it is your job to provide the `tmvt.RectangleROIThresholdOptions` in the getToolbarModule of your extension + + + +## Change Toolbar with hanging protocols + +If you want to change the toolbar based on the hanging protocol, you can do a pattern like this. + +```js + + const { unsubscribe } = hangingProtocolService.subscribe( + hangingProtocolService.EVENTS.PROTOCOL_CHANGED, + () => { + toolbarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'WindowLevel', + ]); + } +); +``` diff --git a/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/utility.md b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/utility.md new file mode 100644 index 0000000..fbb975d --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/utility.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 9 +sidebar_label: Utility +--- + +# Module: Utility + +## Overview + +Often, an extension will need to expose some useful functionality to the other +extensions, or modes that consume the extension. For example, the `cornerstone` +extension, uses its `utility` module to expose methods via + +```js +getUtilityModule({ servicesManager }) { + return [ + { + name: 'common', + exports: { + getCornerstoneLibraries: () => { + return { cornerstone, cornerstoneTools }; + }, + getEnabledElement, + CornerstoneViewportService, + dicomLoaderService, + }, + }, + { + name: 'core', + exports: { + Enums: cs3DEnums, + }, + }, + { + name: 'tools', + exports: { + toolNames, + Enums: cs3DToolsEnums, + }, + }, + ]; + }, +}; +``` + +Then a consuming extension can use `getModuleEntry` to access the methods +Below, which is a code from `TrackedCornerstoneViewport` use the `getUtilityModule` method to get the internal `CornerstoneViewportService` which handles the `Cornerstone` viewport. + +```js title="extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx" +const utilityModule = extensionManager.getModuleEntry( + '@ohif/extension-cornerstone.utilityModule.common' +); + +const { CornerstoneViewportService } = utilityModule.exports; +``` diff --git a/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/viewport.md b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/viewport.md new file mode 100644 index 0000000..b64a13e --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/extensions/modules/viewport.md @@ -0,0 +1,134 @@ +--- +sidebar_position: 5 +sidebar_label: Viewport +--- + +# Module: Viewport + +## Overview + +Viewports consume a displaySet and display/allow the user to interact with data. +An extension can register a Viewport Module by defining a `getViewportModule` +method that returns a React component. Currently, we use viewport components to +add support for: + +- 2D Medical Image Viewing (cornerstone ext.) +- Structured Reports as SR (DICOM SR ext.) +- Encapsulated PDFs as PDFs (DICOM pdf ext.) + +The general pattern is that a mode can define which `Viewport` to use for which +specific `SOPClassHandlerUID`, so if you want to fork just a single Viewport +component for a specialized mode, this is possible. + +```jsx +// displaySet, dataSource +const getViewportModule = () => { + const wrappedViewport = props => { + return ( + { + commandsManager.runCommand('commandName', data); + }} + /> + ); + }; + + return [{ name: 'example', component: wrappedViewport }]; +}; +``` + +## Example Viewport Component + +A simplified version of the tracked `OHIFCornerstoneViewport` is shown below, which +creates a cornerstone viewport: + + +```jsx +function TrackedCornerstoneViewport({ + children, + dataSource, + displaySets, + viewportId, + servicesManager, + extensionManager, + commandsManager, +}) { + + return ( +
+ /** Resize Detector */ + + /** Div For displaying image */ +
e.preventDefault()} + onMouseDown={e => e.preventDefault()} + ref={elementRef} + >
+
+ ); +} +``` + +### Viewport re-rendering optimizations + +We make use of the React memoization pattern to prevent unnecessary re-renders +for the viewport unless certain aspects of the Viewport props change. You can take +a look into the `areEqual` function in the `OHIFCornerstoneViewport` component to +see how this is done. + +```js +function areEqual(prevProps, nextProps) { + if (prevProps.displaySets.length !== nextProps.displaySets.length) { + return false; + } + + if ( + prevProps.viewportOptions.orientation !== + nextProps.viewportOptions.orientation + ) { + return false; + } + + // rest of the code +``` + +as you see, we check if the `needsRerendering` prop is true, and if so, we will +re-render the viewport if the `displaySets` prop changes or the orientation +changes. + + +We use viewportId to identify a viewport and we use it as a key in React +rendering. This is important because it allows us to keep track of the viewport +and its state, and also let React optimize and move the viewport around in the +grid without re-rendering it. However, there are some cases where we need to +force re-render the viewport, for example, when the viewport is hydrated +with a new Segmentation. For these cases, we use the `needsRerendering` prop +to force re-render the viewport. You can add it to the `viewportOptions` + + + + + +### `@ohif/app` + +Viewport components are managed by the `ViewportGrid` Component. Which Viewport +component is used depends on: + +- Hanging Protocols +- The Layout Configuration +- Registered SopClassHandlers + +![viewportModule-layout](../../../assets/img/viewportModule-layout.png) + +
An example of three cornerstone Viewports
diff --git a/platform/docs/versioned_docs/version-3.9/platform/internationalization.md b/platform/docs/versioned_docs/version-3.9/platform/internationalization.md new file mode 100644 index 0000000..8caf9a7 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/internationalization.md @@ -0,0 +1,396 @@ +--- +sidebar_position: 4 +sidebar_label: Internationalization +--- + +# Viewer: Internationalization + +OHIF supports internationalization using [i18next](https://www.i18next.com/) +through the npm package [@ohif/i18n](https://www.npmjs.com/package/@ohif/i18n), +where is the main instance of i18n containing several languages and tools. + +
+

Our translation management is powered by + Locize + through their generous support of open source.

+ + Locize Translation Management Logo + +
+ +## How to change language for the viewer? + +You can take a look into user manuals to see how to change the viewer's +language. In summary, you can change the language: + +- In the preference modals +- Using the language query in the URL: `lng=Test-LNG` + +## Installing + +```bash +yarn add @ohif/i18n + +# OR + +npm install --save @ohif/i18n +``` + +## How it works + +After installing `@ohif/i18n` npm package, the translation function +[t](https://www.i18next.com/overview/api#t) can be used [with](#with-react) or +[without](#without-react) React. + +A translation will occur every time a text match happens in a +[t](https://www.i18next.com/overview/api#t) function. + +The [t](https://www.i18next.com/overview/api#t) function is responsible for +getting translations using all the power of i18next. + +E.g. + +Before: + +```html +
my translated text
+``` + +After: + +```html +
{t('my translated text')}
+``` + +If the translation.json file contains a key that matches the HTML content e.g. +`my translated text`, it will be replaced automatically by the +[t](https://www.i18next.com/overview/api#t) function. + +--- + +### With React + +This section will introduce you to [react-i18next](https://react.i18next.com/) +basics and show how to implement the [t](https://www.i18next.com/overview/api#t) +function easily. + +#### Using Hooks + +You can use `useTranslation` hooks that is provided by `react-i18next` + +You can read more about this +[here](https://react.i18next.com/latest/usetranslation-hook). + +```js +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +function MyComponent() { + const { t } = useTranslation(); + + return

{t('my translated text')}

; +} +``` + +### Using outside of OHIF viewer + +OHIF Viewer already sets a main +[I18nextProvider](https://react.i18next.com/latest/i18nextprovider) connected to +the shared i18n instance from `@ohif/i18n`, all extensions inside OHIF Viewer +will share this same provider at the end, you don't need to set new providers at +all. + +But, if you need to use it completely outside of OHIF viewer, you can set the +I18nextProvider this way: + +```jsx +import i18n from '@ohif/i18n'; +import { I18nextProvider } from 'react-i18next'; +import App from './App'; + + + +; +``` + +After setting `I18nextProvider` in your React App, all translations from +`@ohif/i18n` should be available following the basic [With React](#with-react) +usage. + +--- + +### Without React + +When needed, you can also use available translations _without React_. + +E.g. + +```js +import { T } from '@ohif/i18n'; +console.log(T('my translated text')); +console.log(T('$t(Common:Play) my translated text')); +``` + +--- + +## Main Concepts While Translating + +## Namespaces + +Namespaces are being used to organize translations in smaller portions, combined +semantically or by use. Each `.json` file inside `@ohif/i18n` npm package +becomes a new namespace automatically. + +- Buttons: All buttons translations +- CineDialog: Translations for the tool tips inside the Cine Player Dialog +- Common: all common jargons that can be reused like `t('$t(common:image)')` +- Header: translations related to OHIF's Header Top Bar +- MeasurementTable - Translations for the `@ohif/ui` Measurement Table +- UserPreferencesModal - Translations for the `@ohif/ui` Preferences Modal +- Modals - Translations available for other modals +- PatientInfo - Translations for patients info hover +- SidePanel - Translations for side panels +- ToolTip - Translations for tool tips + +### How to use another NameSpace inside the current NameSpace? + +i18next provides a parsing feature able to get translations strings from any +NameSpace, like this following example getting data from `Common` NameSpace: + +``` +$t('Common:Reset') +``` + +## Extending Languages in @ohif/i18n + +Sometimes, even using the same language, some nouns or jargons can change +according to the country, states or even from Hospital to Hospital. + +In this cases, you don't need to set an entire language again, you can extend +languages creating a new folder inside a pre existent language folder and +@ohif/i18n will do the hard work. + +This new folder must to be called with a double character name, like the `UK` in +the following file tree: + +```bash + |-- src + |-- locales + index.js + |-- en + |-- Buttons.json + index.js + | UK + |-- Buttons.js + index.js + | US + |-- Buttons.js + index.js + ... +``` + +All properties inside a Namespace will be merged in the new sub language, e.g +`en-US` and `en-UK` will merge the props with `en`, using i18next's fallback +languages tool. + +You will need to export all Json files in your `index.js` file, mounting an +object like this: + +```js + { + en: { + NameSpace: { + keyWord1: 'keyWord1Translation', + keyWord2: 'keyWord2Translation', + keyWord3: 'keyWord3Translation', + } + }, + 'en-UK': { + NameSpace: { + keyWord1: 'keyWord1DifferentTranslation', + } + } + } +``` + +Please check the `index.js` files inside locales folder for an example of this +exporting structure. + +### Extending languages dynamically + +You have access to the i18next instance, so you can use the +[addResourceBundle](https://www.i18next.com/how-to/add-or-load-translations#add-after-init) +method to add and change language resources as needed. + +E.g. + +```js +import { i18n } from '@ohif/i18n'; +i18next.addResourceBundle('pt-BR', 'Buttons', { + Angle: 'ร‚ngulo', +}); +``` + +--- + +### How to set a whole new language + +To set a brand new language you can do it in two different ways: + +- Opening a pull request for `@ohif/i18n` and sharing the translation with the + community. ๐Ÿ˜ Please see [Contributing](#contributing-with-new-languages) + section for further information. + +- Setting it only in your project or extension: + +You'll need a final object like the following, what is setting French as +language, and send it to `addLocales` method. + +```js +const newLanguage = + { + fr: { + Commons: { + "Reset": "Rรฉinitialiser", + "Previous": "Prรฉcรฉdent", + }, + Buttons: { + "Rectangle": "Rectangle", + "Circle": "Cercle", + } + } +``` + +To make it easier to translate, you can copy the .json files in the /locales +folder and theirs index.js exporters, keeping same keys and NameSpaces. +Importing the main index.js file, will provide you an Object as expected by the +method `addlocales`; + +E.g. of `addLocales` usage + +```js +import { addLocales } from '@ohif/i18n'; +import locales from './locales/index.js'; +addLocales(locales); +``` + +You can also set them manually, one by one, using this +[method](#extending-languages-dynamically). + +--- + +## Test Language + +We have created a test language that its translations can be seen in the locales +folder. You can copy paste the folder and its `.json` namespaces and add your +custom language translations. + +> If you apply the test-LNG you can see all the elements get appended with 'Test +> {}'. For instance `Study list` becomes `Test Study list`. + +## Language Detections + +@ohif/i18n uses +[i18next-browser-languageDetector](https://github.com/i18next/i18next-browser-languageDetector) +to manage detections, also exports a method called initI18n that accepts a new +detector config as parameter. + +### Changing the language + +OHIF Viewer accepts a query param called `lng` in the url to change the +language. + +E.g. + +``` +https://docs.ohif.org/demo/?lng=es-MX +``` + +### Language Persistence + +The user's language preference is kept automatically by the detector and stored +at a cookie called 'i18next', and in a localstorage key called 'i18nextLng'. +These names can be changed with a new +[Detector Config](https://github.com/i18next/i18next-browser-languageDetector). + +## Debugging translations + +There is an environment variable responsible for debugging the translations, +called `REACT_APP_I18N_DEBUG`. + +Run the project as following to get full debug information: + +```bash +REACT_APP_I18N_DEBUG=true yarn run dev +``` + +## Contributing with new languages + +We have integrated `i18next` into the OHIF Viewer and hooked it up with Locize +for translation management. Now we need your help to get the app translated into +as many languages as possible, and ensure that we haven't missed pieces of the +app that need translation. Locize has graciously offered to provide us with free +usage of their product. + +Once each crowd-sourcing project is completed, we can approve it and merge the +changes into the main project. At that point, the language will be immediately +available on https://viewer.ohif.org/ for testing, and can be used in any OHIF +project. We will support usage through both the Locize CDN and by copying the +language directly into the `@ohif/i18n` package, so that end users can serve the +content from their own domains. + +Here are a couple examples: + +Spanish: +https://viewer.ohif.org/viewer/1.2.840.113619.2.5.1762583153.215519.978957063.78?lng=es + +Chinese: +https://viewer.ohif.org/viewer/1.2.840.113619.2.5.1762583153.215519.978957063.78?lng=zh + +Portuguese: +https://viewer.ohif.org/viewer/1.2.840.113619.2.5.1762583153.215519.978957063.78?lng=pt-BR + +Here are some links you can use to sign up to help translate. All you have to do +is sign up, translate the strings, and click Save. On our side, we have a +dashboard to see how many strings are translated and by whom. + +This is a pretty random set of languages, so please post below if you'd like a +new language link to be added: + +Languages: + +[French](https://www.locize.io/register?invitation=Nj8jRPaFKYwtIfNZ6Y5GVOJOpeiXNAdVuSiOg9ceaiveP6uF6y1wVXM9lgfKoYZX) + +[German](https://www.locize.io/register?invitation=gChNiVi66YINTPpbKESVAVYPapwg3DkpvMSSomLTvVqBJTXrdmPvxi0WZYHER11q) + +[Dutch](https://www.locize.io/register?invitation=2PGe7I184aN0cazM4GXMhzeLtGTf9Zen5uyOEFhHQ8vYkfKHkgR0mJ8dwbNlIeCG) + +[Turkish](https://www.locize.io/register?invitation=NOMIXsfneqPbFDqjce5wI7Z6p2swXSjc0rHOH4KLcM6qXSNA4LGyJaLxS7nqWAe3) + +[Chinese](https://www.locize.io/register?invitation=lrcUbt7DvV4aJmQeEA4SMAj5xNWr3rltOcaZW1cFc6eod0nvzSPFU4V383tDHGGn) + +[Japanese](https://www.locize.io/register?invitation=AaRq2S22o5FsxArwgVuw1gZcQjoe2ffyxarqlAXOpN7JnR2sf2mfamc5qV6LG1Mn) + +[Arabic](https://www.locize.io/register?invitation=BiqI6fOm1sC84N3YJLbImXmaOCk8Hc3TMGpXg7NH2R0b0OKuPCp9wlCHLoqMRpfQ) + +[Hindi](https://www.locize.io/register?invitation=ph7JmOGTV95DF3EFaI1kvK5Hx98dV9w2wj9h9UhUCWnkBNAwWEdWMcyjnF94zkWb) + +[Malay](https://www.locize.io/register?invitation=HsV9F5mKZyeUZYrC3XFRzNI2l0EsIh6hK0MUIKP8IYZA3GxuzfgkvWBLCFwCpDik) + +[Russian](https://www.locize.io/register?invitation=da4V9Q8DVO3M1FIlvfT50ZiS8NDNgvC0dE5hHUEAp47FXy6pLXmf1cp2lgLBfLmb) + +[Swedish](https://www.locize.io/register?invitation=uR4kzBZC1vhJe6jyMwYXgGPj84QDMulQRlt2s6rONU6ljUh5dgwuUyhJEtZ4REA3) + +[Italian](https://www.locize.io/register?invitation=viAS1NC5q342OxtuIv3JFX9DJ3KoR4SmGoElkBlRMphsDKt4hy9bW8JfBjHlfnd7) + +[Spanish](https://www.locize.io/register?invitation=ZikXW3KI4w4eo5Cf6L1aQMWaR69XAQ0a9Va3NGorH7mAPvEPXp8w8NLkPNLs5nG8) + +[Ukrainian](https://www.locize.io/register?invitation=TY0s6onqH3Asl05Bh1qB44SNSABL2pTYoturwxAmcNKRnzBZFK7bGfn7kVi23Vpg) + +[Vietnamese](https://www.locize.io/register?invitation=eqfHDm0vaqxGfQ5TGt6SeV0dx9b2dCp1RrMRdIRavqzOCOAfD3IElzUsyIT689cK) + +[Portugese-Brazil](https://www.locize.io/register?invitation=Qc5Dq449xbblQqLTpWeMfsyFiu3gACcgpj0EIucQjjs9Ph9pzPLpq3MnZupF9t6N) + +Don't see your language in the above list? Add a request +[here](https://github.com/OHIF/Viewers/issues/618) so that we can create the +language for your translation contribution. diff --git a/platform/docs/versioned_docs/version-3.9/platform/managers/_category_.json b/platform/docs/versioned_docs/version-3.9/platform/managers/_category_.json new file mode 100644 index 0000000..f2a2e07 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/managers/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Managers", + "position": 10 +} diff --git a/platform/docs/versioned_docs/version-3.9/platform/managers/commands.md b/platform/docs/versioned_docs/version-3.9/platform/managers/commands.md new file mode 100644 index 0000000..1f07f31 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/managers/commands.md @@ -0,0 +1,180 @@ +--- +sidebar_position: 4 +sidebar_label: Commands Manager +--- +# Commands Manager + +## Overview + + +The `CommandsManager` is a class defined in the `@ohif/core` project. The Commands Manager tracks named commands (or functions) that are scoped to +a context. When we attempt to run a command with a given name, we look for it +in our active contexts. If found, we run the command, passing in any application +or call specific data specified in the command's definition. + +> Note: A single instance of `CommandsManager` should be defined in the consuming application, and it is used when constructing the `ExtensionManager`. + +A `simplified skeleton` of the `CommandsManager` is shown below: + +```js +export class CommandsManager { + constructor({ getActiveContexts } = {}) { + this.contexts = {}; + this._getActiveContexts = getActiveContexts; + } + + getContext(contextName) { + const context = this.contexts[contextName]; + return context; + } + + /**...**/ + + createContext(contextName) { + /** ... **/ + this.contexts[contextName] = {}; + } + + + registerCommand(contextName, commandName, definition) { + /**...**/ + const context = this.getContext(contextName); + /**...**/ + context[commandName] = definition; + } + + runCommand(commandName, options = {}, contextName) { + const definition = this.getCommand(commandName, contextName); + /**...**/ + const { commandFn } = definition; + const commandParams = Object.assign( + {}, + definition.options, // "Command configuration" + options // "Time of call" info + ); + /**...**/ + return commandFn(commandParams); + } + /**...**/ +} +``` + + + + +### Instantiating + +When we instantiate the `CommandsManager`, we are passing two methods: + +- `getAppState` - Should return the application's state when called (Not implemented in `v3`) +- `getActiveContexts` - Should return the application's active contexts when + called + +These methods are used internally to help determine which commands are currently +valid, and how to provide them with any state they may need at the time they are +called. + +```js title="platform/app/src/appInit.js" +const commandsManagerConfig = { + getAppState: () => {}, + /** Used by commands to determine active context */ + getActiveContexts: () => [ + 'VIEWER', + 'DEFAULT', + 'ACTIVE_VIEWPORT::CORNERSTONE', + ], +}; + +const commandsManager = new CommandsManager(commandsManagerConfig); +``` + + +## Commands/Context Registration +The `ExtensionManager` handles registering commands and creating contexts, so you don't need to register all your commands manually. Simply, create a `commandsModule` in your extension, and it will get automatically registered in the `context` provided. + +A *simplified version* of this registration is shown below to give an idea about the process. + + +```js +export default class ExtensionManager { + constructor({ commandsManager }) { + this._commandsManager = commandsManager + } + /** ... **/ + registerExtension = (extension, configuration = {}, dataSources = []) => { + let extensionId = extension.id + /** ... **/ + + // Register Modules provided by the extension + moduleTypeNames.forEach((moduleType) => { + const extensionModule = this._getExtensionModule( + moduleType, + extension, + extensionId, + configuration + ) + + if (moduleType === 'commandsModule') { + this._initCommandsModule(extensionModule) + } + /** registering other modules **/ + }) + } + + _initCommandsModule = (extensionModule) => { + let { definitions, defaultContext } = extensionModule + defaultContext = defaultContext || 'VIEWER' + + if (!this._commandsManager.getContext(defaultContext)) { + this._commandsManager.createContext(defaultContext) + } + + Object.keys(definitions).forEach((commandName) => { + const commandDefinition = definitions[commandName] + const commandHasContextThatDoesNotExist = + commandDefinition.context && + !this._commandsManager.getContext(commandDefinition.context) + + if (commandHasContextThatDoesNotExist) { + this._commandsManager.createContext(commandDefinition.context) + } + + this._commandsManager.registerCommand( + commandDefinition.context || defaultContext, + commandName, + commandDefinition + ) + }) + } +} + +``` + + +If you find yourself in a situation where you want to register a command/context manually, ask +yourself "why can't I register these commands via an extension?", but if you insist, you can use the `CommandsManager` API to do so: + +```js +// Command Registration +commandsManager.registerCommand('context', 'name', commandDefinition); + +// Context Creation +commandsManager.createContext('string'); +``` + +## `CommandsManager` Public API + +If you would like to run a command in the consuming app or an extension, you can +use `runCommand(commandName, options = {}, contextName)`. + + +```js +// Run a command, it will run all the `speak` commands in all contexts +commandsManager.runCommand('speak', { command: 'hello' }); + +// Run command, from Default context +commandsManager.runCommand('speak', { command: 'hello' }, ['DEFAULT']); + +// Returns all commands for a given context +commandsManager.getContext('string'); +``` diff --git a/platform/docs/versioned_docs/version-3.9/platform/managers/extension.md b/platform/docs/versioned_docs/version-3.9/platform/managers/extension.md new file mode 100644 index 0000000..a31f224 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/managers/extension.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 2 +sidebar_label: Extension Manager +--- + +# Extension Manager + +## Overview + +The `ExtensionManager` is a class made available to us via the `@ohif/core` +project (platform/core). Our application instantiates a single instance of it, +and provides a `ServicesManager` and `CommandsManager` along with the +application's configuration through the appConfig key (optional). + +```js +const commandsManager = new CommandsManager(); +const servicesManager = new ServicesManager(); +const extensionManager = new ExtensionManager({ + commandsManager, + servicesManager, + appConfig, +}); +``` +## Events +The following events get published by the `ExtensionManager`: + +| Event | Description | +| ---------------------------- | ------------------------------------------------------ | +| ACTIVE_DATA_SOURCE_CHANGED | Fired when the active data source is changed - either replaced with an entirely different one or the existing active data source gets its definition changed via `updateDataSourceConfiguration`. | + +## API +The `ExtensionManager` only has the following public API: + +- `setActiveDataSource` - Sets the active data source for the application +- `getDataSources` - Returns the registered data sources +- `getActiveDataSource` - Returns the currently active data source +- `getModuleEntry` - Returns the module entry by the give id. +- `addDataSource` - Dynamically adds a data source and optionally sets it as the active data source +- `updateDataSourceConfiguration` - Updates the configuration of a specified data source (name). +- `getDataSourceDef` - Gets the data source definition for a particular data source name. + +## Accessing Modules + +We use `getModuleEntry` in our `ViewerLayout` logic to find the panels based on +the provided IDs in the mode's configuration. + +For instance: +`extensionManager.getModuleEntry("@ohif/extension-measurement-tracking.panelModule.seriesList")` +accesses the `seriesList` panel from `panelModule` of the +`@ohif/extension-measurement-tracking` extension. + +```js +const getPanelData = id => { + const entry = extensionManager.getModuleEntry(id); + const content = entry.component; + + return { + iconName: entry.iconName, + iconLabel: entry.iconLabel, + label: entry.label, + name: entry.name, + content, + }; +}; +``` diff --git a/platform/docs/versioned_docs/version-3.9/platform/managers/hotkeys.md b/platform/docs/versioned_docs/version-3.9/platform/managers/hotkeys.md new file mode 100644 index 0000000..ebdd2bd --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/managers/hotkeys.md @@ -0,0 +1,79 @@ +--- +sidebar_position: 5 +sidebar_label: Hotkeys Manager +--- +# Hotkeys Managers + +## Overview +`HotkeysManager` handles all the logics for adding, setting and enabling/disabling +the hotkeys. + + + +## Instantiation +`HotkeysManager` is instantiated in the `appInit` similar to the other managers. + +```js +const commandsManager = new CommandsManager(commandsManagerConfig); +const servicesManager = new ServicesManager(commandsManager); +const hotkeysManager = new HotkeysManager(commandsManager, servicesManager); +const extensionManager = new ExtensionManager({ + commandsManager, + servicesManager, + hotkeysManager, + appConfig, +}); +``` + + + + +## Hotkeys Manager API + +- `setHotkeys`: The most important method in the `HotkeysManager` which binds the keys with commands. +- `setDefaultHotKeys`: set the defaultHotkeys **property**. Note that, this method **does not** bind the provided hotkeys; however, when `restoreDefaultBindings` +is called, the provided defaultHotkeys will get bound. +- `destroy`: reset the HotkeysManager, and remove the set hotkeys and empty out the `defaultHotkeys` + + + +## Structure of a Hotkey Definition +A hotkey definition should have the following properties: + +- `commandName`: name of the registered command +- `commandOptions`: extra arguments to the commands +- `keys`: an array defining the key to get bound to the command +- `label`: label to be shown in the hotkeys preference panel +- `isEditable`: whether the key can be edited by the user in the hotkey panel + + +### Default hotkeysBindings +The default key bindings can be find in `hotkeyBindings.js` + +```js +// platform/core/src/defaults/hotkeyBindings.js + +export default [ + /**..**/ + { + commandName: 'setToolActive', + commandOptions: { toolName: 'Zoom' }, + label: 'Zoom', + keys: ['z'], + isEditable: true, + }, + + { + commandName: 'flipViewportHorizontal', + label: 'Flip Vertically', + keys: ['v'], + isEditable: true, + }, + /**..**/ +] +``` + + +## Behind the Scene +When you `setHotkeys`, the `commandName` gets registered with the `commandsManager` and +get run after the key is pressed. diff --git a/platform/docs/versioned_docs/version-3.9/platform/managers/index.md b/platform/docs/versioned_docs/version-3.9/platform/managers/index.md new file mode 100644 index 0000000..ee6d6b2 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/managers/index.md @@ -0,0 +1,76 @@ +--- +sidebar_position: 1 +sidebar_label: Introduction +--- + +# Managers + +## Overview + +`OHIF` uses `Managers` to accomplish various purposes such as registering new +services, dependency injection, and aggregating and exposing `extension` +features. + +`OHIF-v3` provides the following managers which we will discuss in depth. + + + + + + + + + + + + + + + + + + + + + + + + + + +
ManagerDescription
+ + Extension Manager + + + Aggregating and exposing modules and features through out the app +
+ + Services Manager + + + Single point of registration for all internal and external services +
+ + Commands Manager + + + Register commands with specific context and run commands in the app +
+ + Hotkeys Manager + + + For keyboard keys assignment to commands +
+ + + + + +[core-services]: https://github.com/OHIF/Viewers/tree/master/platform/core/src/services +[services-manager]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/ServicesManager.js +[cross-cutting-concerns]: https://en.wikipedia.org/wiki/Cross-cutting_concern + diff --git a/platform/docs/versioned_docs/version-3.9/platform/managers/service.md b/platform/docs/versioned_docs/version-3.9/platform/managers/service.md new file mode 100644 index 0000000..d9269e4 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/managers/service.md @@ -0,0 +1,204 @@ +--- +sidebar_position: 3 +sidebar_label: Service Manager +--- + +# Services Manager + +## Overview + +Services manager is the single point of service registration. Each service needs +to implement a `create` method which gets called inside `ServicesManager` to +instantiate the service. In the app, you can get access to a registered service +via the `services` property of the `ServicesManager`. + +## Skeleton + +_Simplified_ skeleton of `ServicesManager` is shown below. There are two public +methods: + +- `registerService`: registering a new service with/without a configuration +- `registerServices`: registering batch of services + +```js +export default class ServicesManager { + constructor(commandsManager) { + this._commandsManager = commandsManager; + this.services = {}; + this.registeredServiceNames = []; + } + + registerService(service, configuration = {}) { + /** validation checks **/ + this.services[service.name] = service.create({ + configuration, + commandsManager: this._commandsManager, + }); + + /* Track service registration */ + this.registeredServiceNames.push(service.name); + } + + registerServices(services) { + /** ... **/ + } +} +``` + +## Default Registered Services + +By default, `OHIF-v3` registers the following services in the `appInit`. + +```js title="platform/app/src/appInit.js" +servicesManager.registerServices([ + CustomizationService, + UINotificationService, + UIModalService, + UIDialogService, + UIViewportDialogService, + MeasurementService, + DisplaySetService, + ToolBarService, + ViewportGridService, + HangingProtocolService, + CineService, +]); +``` + +## Service Architecture + +If you take a look at the folder of each service implementation above, you will +find out that services need to be exported as an object with `name` and `create` +method. + +For instance, `ToolBarService` is exported as: + +```js title="platform/core/src/services/ToolBarService/index.js" +import ToolBarService from './ToolBarService'; + +export default { + name: 'ToolBarService', + create: ({ configuration = {}, commandsManager }) => { + return new ToolBarService(commandsManager); + }, +}; +``` + +and the implementation of `ToolBarService` lies in the same folder at +`./ToolbarSerivce.js`. + +> Note: The create method is critical for any custom service that you write and +> want to add to the list of services + +> Note: For typescript definitions, the service type should be exported +> as part of the Types export on the module. This is recommended going forward +> and existing services will be migrated. As well, the capitalization of the +> name should be lower camel case, with the type being upper camel case. In +> the above example, the service instance should be `toolBarService` with the +> class being `ToolBarService`. + +## Accessing Services + +Throughout the app you can use `services` property of the service manager to +access the desired service. + +For instance in the `PanelMeasurementTableTracking` which is the right panel in +the `longitudinal` mode, we have the _simplified code below_ for downloading the +drawn measurements. + +```js +function PanelMeasurementTableTracking({ servicesManager }) { + const { MeasurementService } = servicesManager.services; + /** ... **/ + + async function exportReport() { + const measurements = MeasurementService.getMeasurements(); + /** ... **/ + downloadCSVReport(measurements, MeasurementService); + } + + /** ... **/ + return <> /** ... **/ ; +} +``` + +## Registering Custom Services + +You might need to write you own custom service in an extension. +`preRegistration` hook inside your extension is the place for registering your +custom service. + +```js title="extensions/customExtension/src/index.js" +import WrappedBackEndService from './services/backEndService'; + +export default { + // ID of the extension + id: 'myExtension', + preRegistration({ servicesManager }) { + servicesManager.registerService(WrappedBackEndService(servicesManager)); + }, +}; +``` + +and the logic for your service shall be + +```js title="extensions/customExtension/src/services/backEndService/index.js" +// Canonical name of upper camel case BackEndService for the class +import BackEndService from './BackEndService'; + +export default function WrappedBackEndService(servicesManager) { + return { + // Note the canonical name of lower camel case backEndService + name: 'backEndService', + create: ({ configuration = {} }) => { + return new BackEndService(servicesManager); + }, + }; +} +``` + +with implementation of + +```ts +export default class BackEndService { + constructor(servicesManager) { + this.servicesManager = servicesManager; + } + + putAnnotations() { + return post(/*...*/); + } +} +``` + +with a registration of + +```ts title="types/index.ts" +import BackEndService from "../services/BackEndService/BackEndService"; + +export { BackEndService }; +``` + +# Service Mode Lifecycle +Services may implement initialization and cleanup for mode specific data. +In order to prevent defects where there are differences between initial +and subsequent displays of a study, the contract of the service is that the +state the service is in on mode entry shall be the same whether the mode was +entered or was exited and entered again. + +To implement storage/recovery of state, the mode must store the data on +exiting the mode, and restore the data in it's onModeEnter. For example, +the mode may decide to preserve measurement data in the onModeExit, and +to restore it in the onModeEnter. This does not violate the contract since +it is the mode's decision to apply the stored state, and to cache it. + +## onModeEnter +A service may implement an onModeEnter call to initialize the service to +be ready for entering a mode. +This is called before the mode `onModeEnter` is called. + +## onModeExit +When entering a mode, the service contract states that the service needs to +be in the same state whether it is a fresh load or has previously entered the mode. +The onModeExit allows a service to clean itself up after the mode 'onModeExit' +has stored any persistent data. diff --git a/platform/docs/versioned_docs/version-3.9/platform/modes/_category_.json b/platform/docs/versioned_docs/version-3.9/platform/modes/_category_.json new file mode 100644 index 0000000..889d845 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/modes/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Modes", + "position": 12 +} diff --git a/platform/docs/versioned_docs/version-3.9/platform/modes/index.md b/platform/docs/versioned_docs/version-3.9/platform/modes/index.md new file mode 100644 index 0000000..aded22b --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/modes/index.md @@ -0,0 +1,413 @@ +--- +sidebar_position: 1 +sidebar_label: Introduction +--- + +# Modes + +## Overview + +A mode can be thought of as a viewer app configured to perform a specific task, +such as tracking measurements over time, 3D segmentation, a guided radiological +workflow, etc. Addition of modes enables _application_ with many _applications_ +as each mode become a mini _app configuration_ behind the scene. + +Upon initialization the viewer will consume extensions and modes and build up +the route desired, these can then be accessed via the study list, or directly +via url parameters. + + + +OHIF-v3 architecture can be seen in the following: + +![mode-archs](../../assets/img/mode-archs.png) + +> Note: Templates are now a part of โ€œextensionsโ€ Routes are configured by modes +> and/or app + +As mentioned, modes are tied to a specific route in the viewer, and multiple +modes/routes can be present within a single application. This allows for +tremendously more flexibility than before you can now: + +- Simultaneously host multiple viewers with for different use cases from within + the same app deploy. +- Make radiological viewers for specific purposes/workflows, e.g.: + - Tracking the size of lesions over time. + - PET/CT fusion workflows. + - Guided review workflows optimized for a specific clinical trial. +- Still host one single feature-rich viewer if you desire. + +## Anatomy + +A mode configuration has a `route` name which is dynamically transformed into a +viewer route on initialization of the application. Modes that are available to a +study will appear in the study list. + +![user-study-summary](../../assets/img/user-study-summary.png) + +The mode configuration specifies which `extensions` the mode requires, which +`LayoutTemplate` to use, and what props to pass to the template. For the default +template this defines which `side panels` will be available, as well as what +`viewports` and which `displaySets` they may hang. + +Mode's config is composed of three elements: +- `id`: the mode `id` +- `modeFactory`: the function that returns the mode specific configuration +- `extensionDependencies`: the list of extensions that the mode requires + + +that return a config object with certain +properties, the high-level view of this config object is: + +```js title="modes/example/src/index.js" +function modeFactory() { + return { + id: '', + version: '', + displayName: '', + onModeEnter: () => {}, + onModeExit: () => {}, + validationTags: {}, + isValidMode: () => {}, + routes: [ + { + path: '', + init: () => {}, + layoutTemplate: () => {}, + }, + ], + extensions: extensionDependencies, + hangingProtocol: [], + sopClassHandlers: [], + hotkeys: [] + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +}; + +export default mode; +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyDescription
+ id + unique mode id used to refer to the mode
+ displayName + actual name of the mode being displayed for each study in the study summary panel
+ + onModeEnter + + hook is called when the mode is entered by the specified route
+ + onModeExit + + hook is called when the mode exited
+ + validationTags + + validationTags
+ + isValidMode + + Checks if the mode is valid for a study
+ + routes + + route config which defines the route address, and the layout for it
+ + extensionDependencies + + extensions needed by the mode
+ + hanging protocol + + list of hanging protocols that the mode should have access to
+ + sopClassHandlers + + list of SOPClass modules needed by the mode
+ + hotkeys + + hotkeys
+ +### Consuming Extensions + +As mentioned in the [Extensions](../extensions/index.md) section, in `OHIF-v3` +developers write their extensions to create reusable functionalities that later +can be used by `modes`. Now, it is time to describe how the registered +extensions will get utilized for a workflow mode via its `id`. + +Each `mode` has a list of its `extensions dependencies` which are the +the `extension` name and version number. In addition, to use a module element you can use the +`${extensionId}.${moduleType}.${element.name}` schema. For instance, if a mode +requires the left panel with name of `AIPanel` that is added by the +`myAIExtension` via the following `getPanelModule` code, it should address it as +`myAIExtension.panelModule.AIPanel` inside the mode configuration file. In the +background `OHIF` will handle grabbing the correct panel via `ExtensionManager`. + +```js title="extensions/myAIExtension/getPanelModule.js" +import PanelAI from './PanelAI.js'; + +function getPanelModule({ + commandsManager, + extensionManager, + servicesManager, +}) { + const wrappedAIPanel = () => { + return ( + + ); + }; + + return [ + { + name: 'AIPanel', + iconName: 'list-bullets', + iconLabel: '', + label: 'AI Panel', + isDisabled: studies => {}, // optional + component: wrappedAIPanel, + }, + ]; +} +``` + +Now, let's look at a simplified code of the `basic viewer` mode which consumes various functionalities +from different extensions. + +```js + +const extensionDependencies = { + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', + '@ohif/extension-measurement-tracking': '^3.0.0', +}; + +const id = 'viewer'; +const version = '3.0.0'; + +function modeFactory({ modeConfiguration }) { + return { + id, + // ... + routes: [ + { + // ... + layoutTemplate: ({ location, servicesManager }) => { + return { + id: ohif.layout, + props: { + leftPanels: ['@ohif/extension-measurement-tracking.panelModule.seriesList'], + rightPanels: ['@ohif/extension-measurement-tracking.panelModule.trackedMeasurements'], + viewports: [ + { + namespace: '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked', + displaySetsToDisplay: ['@ohif/extension-default.sopClassHandlerModule.stack'], + }, + ], + }, + }; + }, + }, + ], + extensions: extensionDependencies, + hangingProtocol: ['@ohif/extension-default.hangingProtocolModule.petCT'], + sopClassHandlers: ['@ohif/extension-default.sopClassHandlerModule.stack'], + // ... + }; +} + +const mode = { + id, + modeFactory, + extensionDependencies, +} + +export default mode +``` + +### Routes + +routes config is an array of route settings, and the overall look and behavior +of the viewer at the designated route is defined by the `layoutTemplate` and +`init` functions for the route. We will learn more about each of the above +properties inside the [route documentation](./routes.md) + + +### HangingProtocols + +Currently, you can pass your defined hanging protocols inside the +`hangingProtocols` property of the mode's config. If you specify the hanging protocol +explicitly by its name (only string and not array), it will be THE hanging protocol +that the mode runs with. However, if you specify an array of hanging protocols, +they will get ranked based on the displaySetSelector requirements and the winner +will be the hanging protocol that the mode runs with. + + +### SopClassHandlers + +Mode's configuration also accepts the `sopClassHandler` modules that have been +added by the extensions. This information will get used to initialize `DisplaySetService` with the provided SOPClass modules which +handles creation of the displaySets. + + +### Hotkeys + +`hotkeys` is another property in the configuration of a mode that can be defined +to add the specific hotkeys to the viewer on the mode route. Additionally, the +name under which the hotkeys are stored can be configured as `hotkeyName`. +This allows user customization of the mode specific hotkeys. + +```js +// default hotkeys +import { utils } from '@ohif/ui'; + +const { hotkeys } = utils; + +const myHotkeys = [ + { + commandName: 'setToolActive', + commandOptions: { toolName: 'Zoom' }, + label: 'Zoom', + keys: ['z'], + isEditable: true, + }, + { + commandName: 'scaleUpViewport', + label: 'Zoom In', + keys: ['+'], + isEditable: true, + }, +] + +function modeFactory() { + return { + id: '', + id: '', + displayName: '', + /* + ... + */ + hotkeys: { + // The name in preferences to use for this set of hotkeys + // Allows defining different sets for different modes + name: 'custom-hotkey-name', + // And the actual custom values here. + hotkeys:[..hotkeys.defaults.hotkeyBindings, ...myHotkeys] + }, + } +} + +// exports +``` + + + + + +## Registration + +Similar to extension registration, `viewer` will look inside the `pluginConfig.json` to +find the `modes` to register. + + +```js title=platform/app/pluginConfig.json +// Simplified version of the `pluginConfig.json` file +{ + "extensions": [ + { + "packageName": "@ohif/extension-cornerstone", + "version": "3.4.0" + }, + // ... + ], + "modes": [ + { + "packageName": "@ohif/mode-longitudinal", + "version": "3.4.0" + } + ] +} +``` + +:::note Important +You SHOULD NOT directly register modes in the `pluginConfig.json` file. +Use the provided `cli` to add/remove/install/uninstall modes. Read more [here](../../development/ohif-cli.md) +::: + +The final registration and import of the modes happen inside a non-tracked file `pluginImport.js` (this file is also for internal use only). + + +:::note +You can stack multiple panel components on top of each other by providing an array of panel components in the `rightPanels` or `leftPanels` properties. + +For instance we can use + +``` +rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.measurements]] +``` + +This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other. + +::: diff --git a/platform/docs/versioned_docs/version-3.9/platform/modes/installation.md b/platform/docs/versioned_docs/version-3.9/platform/modes/installation.md new file mode 100644 index 0000000..08c456d --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/modes/installation.md @@ -0,0 +1,12 @@ +--- +sidebar_position: 5 +sidebar_label: Installation +--- + +# Modes: Installation + +OHIF-v3 provides the ability to utilize external modes. + + +You can use ohif `cli` tool to install both local and publicly published +modes on NPM. You can read more [here](../../development/ohif-cli.md) diff --git a/platform/docs/versioned_docs/version-3.9/platform/modes/lifecycle.md b/platform/docs/versioned_docs/version-3.9/platform/modes/lifecycle.md new file mode 100644 index 0000000..8c0ec41 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/modes/lifecycle.md @@ -0,0 +1,105 @@ +--- +sidebar_position: 2 +sidebar_label: Lifecycle Hooks +--- + +# Modes: Lifecycle Hooks + +## Overview + +Currently, there are two hooks that are called for modes: + +- onModeInit +- onModeEnter +- onModeExit + +## onModeInit + +This hook gets run before the defined route has been entered by the mode. This +hook can be used for initialization before the first render. + +This is called before `onModeEnter` calls. This allows modes to add or activate their own +data sources and configuration before entering the mode (pre registrations). + +## onModeEnter + +This hook gets run after the defined route has been entered by the mode. This +hook can be used to initialize the data, services and appearance of the viewer +upon the first render, in any way that is custom to the mode. + +This is called after service `onModeEnter` calls so that the entry into a mode +is done in a predefined/fixed state. That allows any restoring of existing state +to be performed. + +For instance, in `longitudinal` mode we are using this hook to initialize the +`ToolBarService` and set the window level/width tool to be active and add +buttons to the toolbar. + +:::note Tip + +In OHIF Version 3.1, there is a new service `ToolGroupService` that is used to +define and manage tools for the group of viewports. This is a new concept +borrowed from the Cornerstone ToolGroup, and you can read more +[here](https://www.cornerstonejs.org/docs/concepts/cornerstone-tools/toolgroups/) + +::: + +```js +function modeFactory() { + return { + id: '', + version: '', + displayName: '', + onModeEnter: ({ servicesManager, extensionManager }) => { + const { ToolBarService, ToolGroupService } = servicesManager.services; + + // Init Default and SR ToolGroups + initToolGroups(extensionManager, ToolGroupService); + + ToolBarService.addButtons(toolbarButtons); + ToolBarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'WindowLevel', + 'Pan', + 'Capture', + 'Layout', + 'MoreTools', + ]); + }, + /* + ... + */ + }; +} +``` + +## onModeExit + +This hook is called when the viewer navigates away from the route in the url. +It is called BEFORE the service specific onModeExit calls are performed, and +thus still has access to stateful data which can be cached or stored before +the services clean themselves up. +This is the place for cleaning up NON-service specific data, and services +by unsubscribing to the events. The cleanup of the service itself is intended +to occur in the service `onModeEnter`. + +For instance, it can be used to reset the `ToolBarService` which reset the +toggled buttons. + +```js +function modeFactory() { + return { + id: '', + displayName: '', + onModeExit: ({ servicesManager, extensionManager }) => { + // Turn of the toggled states on exit + const { ToolBarService } = servicesManager.services; + ToolBarService.reset(); + }, + /* + ... + */ + }; +} +``` diff --git a/platform/docs/versioned_docs/version-3.9/platform/modes/routes.md b/platform/docs/versioned_docs/version-3.9/platform/modes/routes.md new file mode 100644 index 0000000..5a3bdc6 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/modes/routes.md @@ -0,0 +1,359 @@ +--- +sidebar_position: 3 +sidebar_label: Routes +--- + +# Mode: Routes + +## Overview + +Modes are tied to a specific route in the viewer, and multiple modes/routes can +be present within a single application. This makes `routes` config, THE most +important part of the mode configuration. + +## Route + +`@ohif/app` **compose** extensions to build applications on different routes +for the platform. + +Below, you can see a simplified version of the `longitudinal` mode and the +`routes` section which has defined one `route`. Each route has three different +configuration: + +- **route path**: defines the route path to access the built application for + that route +- **route init**: hook that runs when application enters the defined route path, + if not defined the default init function will run for the mode. +- **route layout**: defines the layout of the application for the specified + route (panels, viewports) + +```js +function modeFactory() { + return { + id: 'viewer', + version: '3.0.0', + displayName: '', + routes: [ + { + path: 'longitudinal', + /*init: ({ servicesManager, extensionManager }) => { + //defaultViewerRouteInit + },*/ + layoutTemplate: ({ location, servicesManager }) => { + return { + id: ohif.layout, + props: { + leftPanels: [ + '@ohif/extension-measurement-tracking.panelModule.seriesList', + ], + rightPanels: [ + '@ohif/extension-measurement-tracking.panelModule.trackedMeasurements', + ], + viewports: [ + { + namespace: + '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked', + displaySetsToDisplay: [ + '@ohif/extension-default.sopClassHandlerModule.stack', + ], + }, + { + namespace: '@ohif/extension-cornerstone-dicom-sr.viewportModule.dicom-sr', + displaySetsToDisplay: [ + '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr', + ], + }, + ], + }, + }; + }, + }, + ], + /* + ... + */ + }; +} +``` + +### Route: path + +Upon initialization the viewer will consume extensions and modes and build up +the route desired, these can then be accessed via the study list, or directly +via url parameters. + +> Note: Currently, only one route is built for each mode, but we will enhance +> route creation to create separate routes based on the `path` config for each +> `route` object. + +There are two types of `routes` that are created by the mode. + +- Routes with dataSourceName `/${mode.id}/${dataSourceName}` +- Routes without dataSourceName `/${mode.id}` + +Therefore, navigating to +`http://localhost:3000/viewer/?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1` +will run the app with the layout and functionalities of the `viewer` mode using +the `defaultDataSourceName` which is defined in the +[App Config](../../configuration/configurationFiles.md) + +You can use the same exact mode using a different registered data source (e.g., +`dicomjson`) by navigating to +`http://localhost:3000/viewer/dicomjson/?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1` + +### Route: init + +The mode also has an init hook, which initializes the mode. If you don't define +an `init` function the `default init` function will get run (logic is located +inside `Mode.jsx`). However, you can define you own init function following +certain steps which we will discuss next. + +#### Default init + +Default init function will: + +- `retriveSeriesMetaData` for the `studyInstanceUIDs` that are defined in the + URL. +- Subscribe to `instanceAdded` event, to make display sets after a series have + finished retrieving its instances' metadata. +- Subscribe to `seriesAdded` event, to run the `HangingProtocolService` on the + retrieves series from the study. + +A _simplified_ "pseudocode" for the `defaultRouteInit` is: + +```jsx +async function defaultRouteInit({ + servicesManager, + studyInstanceUIDs, + dataSource, +}) { + const { + DisplaySetService, + HangingProtocolService, + } = servicesManager.services; + + // subscribe to run the function after the event happens + DicomMetadataStore.subscribe( + 'instancesAdded', + ({ StudyInstanceUID, SeriesInstanceUID }) => { + const seriesMetadata = DicomMetadataStore.getSeries( + StudyInstanceUID, + SeriesInstanceUID + ); + DisplaySetService.makeDisplaySets(seriesMetadata.instances); + } + ); + + studyInstanceUIDs.forEach(StudyInstanceUID => { + dataSource.retrieve.series.metadata({ StudyInstanceUID }); + }); + + DicomMetadataStore.subscribe('seriesAdded', ({ StudyInstanceUID }) => { + const studyMetadata = // get study metadata and displaySets + HangingProtocolService.run({studies, displaySets, activeStudy}); + }); + + return unsubscriptions; +} +``` + +#### Writing a custom init + +You can add your custom init function to enhance the default initialization for: + +- Fetching annotations from a server for the current study +- Changing the initial image index of the series to be displayed at first +- Caching the next study in the work list +- Adding a custom sort for the series to be displayed on the study browser panel + +and lots of other modifications. + +You just need to make sure, the mode `dataSource.retrieve.series.metadata`, +`makeDisplaySets` and `run` the HangingProtocols at some point. There are +various `events` that you can subscribe to and add your custom logic. **point to +events** + +For instance for jumping to the slice where a measurement is located at the +initial render, you need to follow a pattern similar to the following: + +```jsx +init: async ({ + servicesManager, + extensionManager, + hotkeysManager, + dataSource, + studyInstanceUIDs, +}) => { + const { DisplaySetService } = servicesManager.services; + + /** + ... + **/ + + const onDisplaySetsAdded = ({ displaySetsAdded, options }) => { + const displaySet = displaySetsAdded[0]; + const { SeriesInstanceUID } = displaySet; + + const toolData = myServer.fetchMeasurements(SeriesInstanceUID); + + if (!toolData.length) { + return; + } + + toolData.forEach(tool => { + const instance = displaySet.images.find( + image => image.SOPInstanceUID === tool.SOPInstanceUID + ); + }); + + MeasurementService.addMeasurement(/**...**/); + }; + + // subscription to the DISPLAY_SETS_ADDED + const { unsubscribe } = DisplaySetService.subscribe( + DisplaySetService.EVENTS.DISPLAY_SETS_ADDED, + onDisplaySetsAdded + ); + + /** + ... + **/ + + return unsubscriptions; +}; +``` + +### Route: layoutTemplate + +`layoutTemplate` is the last configuration for a certain route in a `mode`. +`layoutTemplate` is a function that returns an object that configures the +overall layout of the application. The returned object has two properties: + +- `id`: the id of the `layoutTemplate` being used (it should have been + registered via an extension) +- `props`: the required properties to be passed to the `layoutTemplate`. + +For instance `default extension` provides a layoutTemplate that builds the app +using left/right panels and viewports. Therefore, the `props` include +`leftPanels`, `rightPanels` and `viewports` sections. Note that the +`layoutTemplate` defines the properties it is expecting. So, if you write a +`layoutTemplate-2` that accepts a footer section, its logic should be written in +the extension, and any mode that is interested in using `layoutTemplate-2` +**should** provide the `id` for the footer component. + +**What module should the footer be registered?** + +```js +/* +... +*/ +layoutTemplate: ({ location, servicesManager }) => { + return { + id: '@ohif/extension-default.layoutTemplateModule.viewerLayout', + props: { + leftPanels: [ + 'myExtension.panelModule.leftPanel1', + 'myExtension.panelModule.leftPanel2', + ], + rightPanels: ['myExtension.panelModule.rightPanel'], + viewports: [ + { + namespace: 'myExtension.viewportModule.viewport1', + displaySetsToDisplay: ['myExtension.sopClassHandlerModule.sop1'], + }, + { + namespace: 'myExtension.viewportModule.viewport2', + displaySetsToDisplay: ['myExtension.sopClassHandlerModule.sop2'], + }, + ], + }, + }; +}; +/* +... +*/ +``` + +:::note +You can stack multiple panel components on top of each other by providing an array of panel components in the `rightPanels` or `leftPanels` properties. + +For instance we can use + +``` +rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.measurements]] +``` + +This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other. + +::: + +## FAQ + +> What is the difference between `onModeEnter` and `route.init` + +`onModeEnter` gets run first than `route.init`; however, each route can have +their own `init`, but they share the `onModeEnter`. + +> How can I change the `workList` appearance or add a new login page? + +This is where `OHIF-v3` shines! Since the default `layoutTemplate` is written +for the viewer part, you can simply add a new `layoutTemplate` and use the +component you have written for that route. `Mode` handle showing the correct +component for the specified route. + +```js +function modeFactory() { + return { + id: 'viewer', + displayName: '', + routes: [ + { + path: 'worklist', + init, + layoutTemplate: ({ location, servicesManager }) => { + return { + id: 'worklistLayout', + props: { + component: 'myNewWorkList', + }, + }; + }, + }, + ], + /* + ... + */ + }; +} +``` + +> How can I navigate to (or show) a different study via the browser history/URL? + +There is a command that does this: `navigateHistory`. It takes an object +argument with the `NavigateHistory` type: + +``` +export type NavigateHistory = { + to: string; // the URL to navigate to + options?: { + replace?: boolean; // replace or add/push to history? + }; +}; +``` + +For instance one could bind a hot key to this command to show a specific study +like this... + +``` + { + commandName: 'navigateHistory', + commandOptions: { + to: + '/viewer?StudyInstanceUIDs=1.2.3', + }, + context: 'DEFAULT', + label: 'Nav Study', + keys: ['n'], + isEditable: true, + }, +``` diff --git a/platform/docs/versioned_docs/version-3.9/platform/modes/validity.md b/platform/docs/versioned_docs/version-3.9/platform/modes/validity.md new file mode 100644 index 0000000..e3f70af --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/modes/validity.md @@ -0,0 +1,37 @@ +--- +sidebar_position: 4 +sidebar_label: Validity +--- +# Mode: Validity + + +## Overview +There are two mechanism for checking the validity of a mode for a study. + +- `isValidMode`: which is called on a selected study in the workList. +- `validTags` + + + +## isValidMode +This hook can be used to define a function that return a `boolean` which decided the +validity of the mode based on `StudyInstanceUID` and `modalities` that are in the study. + +For instance, for pet-ct mode, both `PT` and 'CT' modalities should be available inside the study. + +```js +function modeFactory() { + return { + id: '', + displayName: '', + isValidMode: ({ modalities, StudyInstanceUID }) => { + const modalities_list = modalities.split('\\'); + const validMode = ['CT', 'PT'].every(modality => modalities_list.includes(modality)); + return validMode; + }, + /* + ... + */ + } +} +``` diff --git a/platform/docs/versioned_docs/version-3.9/platform/pwa-vs-packaged.md b/platform/docs/versioned_docs/version-3.9/platform/pwa-vs-packaged.md new file mode 100644 index 0000000..bdc411e --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/pwa-vs-packaged.md @@ -0,0 +1,34 @@ +--- +sidebar_position: 3 +--- + +# PWA vs Packaged + +It's important to know that the OHIF Viewer project provides two different build +processes: + +```bash +# Static Asset output: For deploying PWAs +yarn run build +``` + +## Progressive Web Application (PWA) + +> [Progressive Web Apps][pwa] are a new breed of web applications that meet the +> [following requirements][pwa-checklist]. Notably, targeting a PWA allows us +> provide a reliable, fast, and engaging experience across different devices and +> network conditions. + +The OHIF Viewer is maintained as a [monorepo][monorepo]. We use WebPack to build +the many small static assets that comprise our application. Also generated is an +`index.html` that will serve as an entry point for loading configuration and the +application, as well as a `service-worker` that can intelligently cache files so +that subsequent requests are from the local file system instead of over the +network. + +You can read more about this particular strategy in our +[Build for Production Deployment Guide](./../deployment/build-for-production.md) + +## Commonjs Bundle (Packaged Script) + +We are not supporting `Commonjs` bundling inside `OHIF-v3`. diff --git a/platform/docs/versioned_docs/version-3.9/platform/scope-of-project.md b/platform/docs/versioned_docs/version-3.9/platform/scope-of-project.md new file mode 100644 index 0000000..95460e4 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/scope-of-project.md @@ -0,0 +1,69 @@ +--- +sidebar_position: 1 +--- +# Scope of Project + +The OHIF Viewer is a web based medical imaging viewer. This allows it to be used +on almost any device, anywhere. The OHIF Viewer is what is commonly referred to +as a ["Dumb Client"][simplicable] + +> A dumb client is software that fully depends on a connection to a server or +> cloud service for its functionality. Without a network connection, the +> software offers nothing useful. - [simplicable.com][simplicable] + +While the Viewer persists some data, it's scope is limited to caching things +like user preferences and previous query parameters. Because of this, the Viewer +has been built to be highly configurable to work with almost any web accessible +data source. + +![scope-of-project diagram](./../assets/img/scope-of-project.png) + +To be more specific, the OHIF Viewer is a collection of HTML, JS, and CSS files. +These can be delivered to your end users however you would like: + +- From the local network +- From a remote web server +- From a CDN (content delivery network) +- From a service-worker's cache +- etc. + +These "static asset" files are referred to collectively as a "Progressive Web +Application" (PWA), and have the same capabilities and limitations that all PWAs +have. + +All studies, series, images, imageframes, metadata, and the images themselves +must come from an external source. There are many, many ways to provide this +information, the OHIF Viewer's scope **DOES NOT** encompass providing _any_ +data; only the configuration necessary to interface with one or more of these +many data sources. The OHIF Viewer's scope **DOES** include configuration and +support for services that are protected with OpenID-Connect. + +In an effort to aid our users and contributors, we attempt to provide several +[deployment and hosting recipes](../deployment/index.md) as potential starting +points. These are not meant to be rock solid, production ready, solutions; like +most recipes, they should be augmented to best fit you and your organization's +taste, preferences, etc. + +## FAQ + +_Am I able to cache studies for offline viewing?_ + +Not currently. A web page's offline cache capabilities are limited and somewhat +volatile (mostly imposed at the browser vendor level). For more robust offline +caching, you may want to consider a server on the local network, or packaging +the OHIF Viewer as a desktop application. + +_Does the OHIF Viewer work with the local filesystem?_ + +It is possible to accomplish this through extensions; however, for a user +experience that accommodates a large number of studies, you would likely need to +package the OHIF Viewer as an [Electron app][electron]. + + + + +[simplicable]: https://simplicable.com/new/dumb-client +[electron]: https://electronjs.org/ + diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/_category_.json b/platform/docs/versioned_docs/version-3.9/platform/services/_category_.json new file mode 100644 index 0000000..9a6c3ae --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Services", + "position": 11 +} diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/data/DicomMetadataStore.md b/platform/docs/versioned_docs/version-3.9/platform/services/data/DicomMetadataStore.md new file mode 100644 index 0000000..927b8b8 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/data/DicomMetadataStore.md @@ -0,0 +1,112 @@ +--- +sidebar_position: 2 +sidebar_label: DICOM Metadata Store +--- +# DICOM Metadata Store + + +## Overview +`DicomMetaDataStore` is the central location that stores the metadata in `OHIF-v3`. There +are several APIs to add study/series/instance metadata and also for getting from the store. +DataSource utilize the `DicomMetaDataStore` to add the retrieved metadata to `OHIF Viewer`. + +> In `OHIF-v3` we have significantly changed the architecture of the metadata storage to +> provide a much cleaner way of handling metadata-related tasks and services. Classes such as +> `OHIFInstanceMetadata`, `OHIFSeriesMetadata` and `OHIFStudyMetadata` has been removed and +> replaced with `DicomMetaDataStore`. +> + + +## Events +There are two events that get publish in `DicomMetaDataStore`: + + +| Event | Description | +|-----------------|------------------------------------------------------------------------------------------------| +| SERIES_ADDED | Fires when all series of one study have added their summary metadata to the `DicomMetaDataStore` | +| INSTANCES_ADDED | Fires when all instances of one series have added their metadata to the `DicomMetaDataStore` | + + +## API +Let's find out about the public API for `DicomMetaDataStore` service. + +- `EVENTS`: Object including the events mentioned above. You can subscribe to these events + by calling DicomMetaDataStore.subscribe(EVENTS.SERIES_ADDED, myFunction). [Read more about pub/sub pattern here](../pubsub.md) + +- `addInstances(instances, madeInClient = false)`: adds the instances' metadata to the store. madeInClient is explained in detail below. After + adding to the store it fires the EVENTS.INSTANCES_ADDED. + +- `addSeriesMetadata(seriesSummaryMetadata, madeInClient = false)`: adds series summary metadata. After adding it fires EVENTS.SERIES_ADDED + +- `addStudy(study)`: creates and add study-level metadata to the store. + +- `getStudy(StudyInstanceUID)`: returns the study metadata from the store. It includes all the series and instance metadata in the requested study + +- `getSeries(StudyInstanceUID, SeriesInstanceUID`: returns the series-level metadata for the requested study and series UIDs. + +- `getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID)`: returns the instance metadata based on the study, series and sop instanceUIDs. + +- `geteInstanceFromImageId`: returns the instance metadata based on the requested imageId. It searches the store for the instance that has the same imageId. + + + +### madeInClient + +Since upon adding the metadata to the store, the relevant events are fired, and there are +other services that are subscribed to these events (`HangingProtocolService` or `DisplaySetService`), sometimes +we want to add instance metadata but don't want the events to get fired. For instance, when +you are caching the data for the next study in advance, you probably don't want to trigger hanging protocol +logic, so you set `madeInClient=true` to not fire events. + + +## Storage +As discussed before, there are several API that enables getting metadata from the store and adding to the store. +However, it is good to have an understanding of where they get +stored and in what format and hierarchy. `_model` is a private variable in the store +which holds all the metadata for all studies, series, and instances, and it looks like: + + +```js title="platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js" +const _model = { + studies: [ + { + /** Study Metadata **/ + seriesLists: [ + { + // Series in study from dicom web server 1 (or different backend 1) + series: [ + { + // Series 1 Metadata + instances: [ + { + // Instance 1 metadata of Series 1 + }, + { + // Instance 2 metadata of Series 1 + }, + /** Other instances metadata **/ + ], + }, + { + // Series 2 Metadata + instances: [ + { + // Instance 1 metadata of Series 2 + }, + { + // Instance 2 metadata of Series 1 + }, + /** Other instances metadata **/ + ], + }, + ], + }, + { + // Series in study from dicom web server 2 (or different backend 2) + /** ... **/ + }, + ], + }, + ], +} +``` diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/data/DisplaySetService.md b/platform/docs/versioned_docs/version-3.9/platform/services/data/DisplaySetService.md new file mode 100644 index 0000000..8b8bcae --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/data/DisplaySetService.md @@ -0,0 +1,73 @@ +--- +sidebar_position: 3 +sidebar_label: DisplaySet Service +--- +# DisplaySet Service + + +## Overview +`DisplaySetService` handles converting the `instanceMetadata` into `DisplaySet` that `OHIF` uses for the visualization. `DisplaySetService` gets initialized at service startup time, but is then cleared in the `Mode.jsx`. During the initialization `SOPClassHandlerIds` of the `modes` gets registered with the `DisplaySetService`. + +:::tip + +DisplaySet is a general set of entities and contains links to bunch of displayable objects (images, etc.) Some series might get split up into different displaySets e.g., MG might have mixed views in a single series, but users might want to have separate LCC, RCC, etc. for hanging protocol usage. A viewport renders a display set into a displayable object. + +imageSet is a particular implementation of image displays. +::: + + +> Based on the instanceMetadata's `SOPClassHandlerId`, the correct module from the registered extensions is found by `OHIF` and its `getDisplaySetsFromSeries` runs to create a DisplaySet for the Series. Note +that this is an ordered operation, and consumes the instances as it proceeds, with the first registered +handlers being able to consume instances first. + +DisplaySets are created synchronously when the instances metadata is retrieved and added to the [DicomMetaDataStore](../data//DicomMetadataStore.md). They are ALSO updated when +the DicommetaDataStore receives new data. This update first checks the addInstances +of existing `DisplaySet` values to see if the new instance belongs in an existing set. +Then, the same process is used as was originally done to create new display sets. + +NOTE: Any instances not matched are NOT added to any display set and will not be displayed. + +## Adding `madeInClient` display sets +It is possible to filter or combine display sets from different series by +performing the filter operation desired, and then calling the `addActiveDisplaySets` +on the new `DisplaySet` instances. This allows operations like combining +two series or sub-selecting a series. + +## Events +There are three events that get broadcasted in `DisplaySetService`: + +| Event | Description | +| -------------------- | ---------------------------------------------------- | +| DISPLAY_SETS_ADDED | Fires a displayset is added to the displaysets cache | +| DISPLAY_SETS_CHANGED | Fires when a displayset is changed | +| DISPLAY_SETS_REMOVED | Fires when a displayset is removed | +| DISPLAY_SET_SERIES_METADATA_INVALIDATED | Fires when a displayset's series metadata has been altered. An object payload for the event is sent with properties: `displaySetInstanceUID` - the UID of the display set affected; `invalidateData` - boolean indicating if data should be invalidated + + +## API +Let's find out about the public API for `DisplaySetService`. + +- `EVENTS`: Object including the events mentioned above. You can subscribe to these events + by calling DisplaySetService.subscribe(EVENTS.DISPLAY_SETS_CHANGED, myFunction). [Read more about pub/sub pattern here](../pubsub.md) + +- `makeDisplaySets(input, { batch, madeInClient, settings } = {}`): Creates displaySet for the provided + array of instances metadata. Each display set gets a random UID assigned. + + - `input`: Array of instances Metadata + - `batch = false`: If you need to pass array of arrays of instances metadata to have a batch creation + - `madeInClient = false`: Disables the events firing + - `settings = {}`: Hanging protocol viewport or rendering settings. For instance, setting the initial `voi`, or activating a tool upon + displaySet rendering. [Read more about hanging protocols settings here](./HangingProtocolService.md#Settings) + + +- `getDisplaySetByUID`: Returns the displaySet based on the DisplaySetUID. + +- `getDisplaySetForSOPInstanceUID`: Returns the displaySet that includes an image with the provided SOPInstanceUID + +- `getActiveDisplaySets`: Returns the active displaySets + +- `deleteDisplaySet`: Deletes the displaySets from the displaySets cache + +- `addActiveDisplaySets`: Adds a new display set independently of the make operation. + +- `setDisplaySetMetadataInvalidated`: Fires the `DISPLAY_SET_SERIES_METADATA_INVALIDATED` event. diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/data/HangingProtocolService.md b/platform/docs/versioned_docs/version-3.9/platform/services/data/HangingProtocolService.md new file mode 100644 index 0000000..de33e9b --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/data/HangingProtocolService.md @@ -0,0 +1,367 @@ +--- +sidebar_position: 4 +sidebar_label: Hanging Protocol Service +--- + +# Hanging Protocol Service + +## Overview + + +This service handles the arrangement of the images in the viewport. In +short, the registered protocols will get matched with the DisplaySets that are +available. Each protocol gets a score, and they are ranked. The +winning protocol (highest score) gets applied and its settings run for the viewports +to be arranged. + +You can read more about a HangingProtocol Structure and its properties in the +[HangingProtocol Module](../../extensions/modules/hpModule.md). + +The rest of this page is dedicated on how the Hanging Protocol Service works and +what you can do with it. + +## Protocols + +Protocols are provided by each extension's HangingProtocolModule and are +registered automatically to the HangingProtocolService. + +All protocols are stored in the `HangingProtocolService` using their `id` as the key, and the protocol itself as the value. + +## Protocol Definition +Protocols are defined in a getHangingProtocolModule inside an extension. As such, +they are defined with a module structure that starts with an id, and has field protocol +that is the actual protocol definition. This setup allows defining more than +one protocol within a module, each one needing it's own definition file. + +```javascript +import MyProtocol from './MyProtocol'; +export default function getHangingProtocolModule() { + return [ + { + id: MyProtocol.id, + protocol: MyProtocol, + }, + ]; +} +``` + +Within the protocol itself, the structure is laid out as described in the HangingProtocol.ts +type definition, starting with `Protocol`. See the type definition for more details. + +## Events + +There are two events that get publish in `HangingProtocolService`: + +| Event | Description | +| ------------ | -------------------------------------------------------------------- | +| NEW_LAYOUT | Fires when a new layout is requested by the `HangingProtocolService` | +| PROTOCOL_CHANGED | Fires when the the protocol is changed in the hanging protocols, or when the applied stage is changed. | +| RESTORE_PROTOCOL | Fires when the protocol or stage is restored, for example, after turning off MPR mode | +| STAGE_ACTIVATION | Fires when the stages are known to have stage.status set. | + +## Stage Activation and Status +Sometimes a hanging protocol can be applicable generally, but not all stages +should be shown by default, or should be shown at all. This can be handled by +using the stage activation to control whether the stage is shown by default (`enabled`), +whether it can be navigated to (`passive`) or whether it should not be shown +at all (`disabled`). + +The `stage.status` is used to control this, and the status is controlled by +the stage activate. The status values are: + +* enabled - meaning that the stage is fully applicable +* passive - meaning that the stage can be applied, but might be missing details +* disabled - meaning that the study has insufficient information for this stage + +The default values for no `stageActivation` are to assume that `enabled` has `minViewports` of 1, +and `passive` has `minViewports=0`. That is, enable the stage if at least one +viewport is filled, and make it passive if no viewports are filled. + +The setting for these are controlled by the stageActivation property, for example +the following: + +```javascript +stageActivation: { + // The enabled activation specifies requirements to enable the stage, that is, + // make it preferred. + enabled: { + // The default value here is 1, and indicates how many non-blank viewports + // are required. + minViewportsMatched: 3, + // This enables specifying cross cutting concerns, such as having a stage + // only apply to males or females, and is a list of display set selector ids + displaySetSelectorsMatched: ['dsMale'], + }, + // The passive check is performed first. If it fails, the enabled is NOT + // checked, but the status set to disabled. The default passive check + // should always be passed, so it is fine to just define enabled if desired. + passive: { + // The default is 0, which means allow the stage even if no viewports are + // filled. This allows dragging and dropping into the viewports to + // make matches manually, which can then be re-used for other stages. + minViewportsMatched: 0, + displaySetSelectorsMatched: [...], + }, +} +``` + +## API + +- `destroy`: Destroys the HP service + +- `reset` and `onModeEnter`: Resets the HP service to not have any active + hanging protocols + +- `getActiveProtocol`: Returns an object of the internal state of the HP service, + useful for storing said state, as well as for getting direct access to the + protocol and stage objects. Users of this should count on it being not completely + stable as to exactly what this returns, as internal details can change. + +- `getState`: Returns the currently applied protocol ID, stage index and active study UID. + This information is storable/usable as state information to be used elsewhere. + +- `getDefaultProtocol`: Returns the default protocol to apply. + +- `getMatchDetails`: returns an object which contains the details of the + matching for the viewports, displaySets and whether the protocol is + applied to the viewport or not yet. This is deprecated as it is expected + to be communicated by events instead. + +- `getProtocols`: Returns a list of the currently active protocols. + +- `getProtocolById`: Gets the protocol with the given id. + +- `addProtocol`: adds provided protocol to the list of registered protocols + for matching. Will replacing any protocol with the same id, allowing, for example, + to replace the default protocol. + +- `setActiveProtocols`: Choose the protocols which are active. Can take a +single protocol id or a list. When a single one is provided, that one will be +applied whether or not the required rules match. Called automatically on mode +init. + +- `setActiveStudyUID`: Sets the given study UID as active, which has significance + in terms of the matching rules being able to match against the active study. + +- `run({studies, activeStudy, displaySets }, protocolId)`: runs the HPService with the provided + studyMetaData and optional protocolId. If protocol is not given, HP Matching + engine will search all the registered protocols for the best matching one + based on the constraints. + +- `registerImageLoadStrategy`: Adds a custom image load strategy. + +- `addCustomAttribute`: adding a custom attribute for matching. (see below) + +- `setProtocol`: applies a protocol to the current studies, it can be used for instance to apply a + hanging protocol when pressing a button in the toolbar. We use this for applying 'mpr' + hanging protocol when pressing the MPR button in the toolbar. `setProtocol` will + accept a set of options that can be used to define the displaySets that will be + used for the protocol. If no options are provided, all displaySets will + be used to match the protocol. + +- `getStageIndex`: Finds the stage index for a given set of match keys. Currently + only works on the currently active protocol, but is supposed to be able to work + with other protocols as well. + +- `getMissingViewport`: Returns a viewport object to be used as the missing + viewport instance. This is used to fill out new viewports. + +Default initialization of the modes handles running the `HangingProtocolService` + +## Hanging Protocol Instance Definition +A hanging protocol has an id provided in the module which is used to identify +the protocol. Mostly these should include the module name so that they +do not overlap, with the suggested id being `${moduleId}.${simpleName}`. The +'default' name is used as the hanging protocol id when no other protocol applies, +and can be set as the last module listed containing 'default'. + +A hanging protocol can also be defined with a generator. +A generator is a function we can write this way: + +```ts +function protocolGenerator({ servicesManager, commandsManager }) { + // Some computations using services and commands ... + + return { + protocol: generatedProtocol + } +} +``` + +See the typescript definitions for more details on the structure of protocols. + +## Additional viewports for layout - `defaultViewport` +Sometimes the user manually selects a layout of a given size, say `2x3`. The +hanging protocol can define what viewport options to use for this viewport by +defining an extra viewport option in `defaultViewport`. For example: + +```javascript + defaultViewport: { + viewportOptions: { + viewportType: 'stack', + toolGroupId: 'default', + allowUnmatchedView: true, + }, + displaySets: [ + { + id: 'defaultDisplaySetId', + matchedDisplaySetsIndex: -1, + }, + ], + }, +``` + +This allows defining the type of additional viewports, what tool group etc they +are allowed in, and which display set is used to fill them. In the above case, +the display set is the same as the other viewports, but the +`matchedDisplaySetsIndex=-1`, so that means find the next matching display set +from the display set selector which isn't already filling a view. + +## Custom Attribute +In some situations, you might want to match based on a custom +attribute and not the DICOM tags. For instance, +if you have assigned a `timepointId` to each study, and you want to match based on it. +Good news is that, in `OHIF-v3` you can define you custom attribute and use it for matching. + +There are various ways that you can let `HangingProtocolService` know of you +custom attribute. We will show how to add it inside the mode configuration. + +```js +const defaultProtocol = { + id: 'defaultProtocol', + /** ... **/ + protocolMatchingRules: [ + { + weight: 3, + attribute: 'timepoint', + constraint: { + equals: 'first', + }, + required: false, + }, + ], + displaySetSelectors: { + /** ... */ + } + stages: [ + /** ... **/ + ], + numberOfPriorsReferenced: -1, +}; + +// Custom function for custom attribute +const getTimePointUID = metaData => { + // requesting the timePoint Id + return myBackEndAPI(metaData); +}; + +function modeFactory() { + return { + id: 'myMode', + /** .. **/ + routes: [ + { + path: 'myModeRoute', + init: async ({}) => { + const { + DicomMetadataStore, + HangingProtocolService, + } = servicesManager.services; + + const onSeriesAdded = ({ + StudyInstanceUID, + madeInClient = false, + }) => { + const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID); + + // Adding custom attribute to the hangingprotocol + HangingProtocolService.addCustomAttribute( + 'timepoint', + 'timepoint', + metaData => getFirstMeasurementSeriesInstanceUID(metaData) + ); + + HangingProtocolService.run(studyMetadata); + }; + + DicomMetadataStore.subscribe( + DicomMetadataStore.EVENTS.SERIES_ADDED, + onSeriesAdded + ); + }, + }, + ], + /** ... **/ + }; +} +``` + +### Custom Attributes for Viewport Options + +The custom attributes can also be used for viewport options. This example, +from the default hanging protocol navigates the image to the image +specified in the URL: + +```javascript +viewportOptions: { + initialImageOptions: { + // custom attribute name is selected by 'custom' + custom: 'sopInstanceLocation', + // This is the value returned if the above doesn't return anything + defaultValue: { index: 5 }, + } +} +``` + +### Included Custom Attributes + +A few custom attributes are included under @ohif/extension-test, these are namely: +*sameAs +*maxNumImageFrames +*numberOfDisplaySets + +To use these included custom attributes, the extension will need to be enabled under platform/app/pluginConfig.json: + +```javascript +{ + "extensions": [ + ... + { + "packageName": "@ohif/extension-test", + "version": "3.4.0" + }, + ... + ] +} + ``` + +Furthermore, the extension will also need to be included under extensionDependencies in the desired mode (e.g. modes/tmtv/src/index.js): + +```javascript +const extensionDependencies = { + '@ohif/extension-default': '^3.0.0', + '@ohif/extension-cornerstone': '^3.0.0', + '@ohif/extension-tmtv': '^3.0.0', + '@ohif/extension-test': '^0.0.1', + }; + ``` + +The below example modifies the included hanging protocol (extensions/tmtv/src/getHangingProtocolModule.js) and uses the sameAs attribute included in the @ohif/extension-test extension to check that the selected PT has the same frame of reference as the CT: + +```javascript +ptDisplaySet: { + ... + seriesMatchingRules: [ + { + attribute: 'sameAs', + sameAttribute: 'FrameOfReferenceUID', + sameDisplaySetId: 'ctDisplaySet', + constraint: { + equals: { + value: true, + }, + }, + required: true, + }, + ... +``` diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/data/MeasurementService.md b/platform/docs/versioned_docs/version-3.9/platform/services/data/MeasurementService.md new file mode 100644 index 0000000..f5ede2f --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/data/MeasurementService.md @@ -0,0 +1,182 @@ +--- +sidebar_position: 6 +sidebar_label: Measurement Service +--- + +# Measurement Service + +## Overview + +`MeasurementService` handles the internal measurement representation inside +`OHIF` platform. Developers can add their custom `sources` with `mappers` to +enable adding measurements inside OHIF. Currently, we are maintaining +`CornerstoneTools` annotations and corresponding mappers can be found inside the +`cornerstone` extension. However, `MeasurementService` can be configured to work +with any custom tools given that its `mappers` is added to the +`MeasurementService`. We can see the overall architecture of the +`MeasurementService` below: + +![services-measurements](../../../assets/img/services-measurements.png) + +## Events + +There are seven events that get publish in `MeasurementService`: + +| Event | Description | +| --------------------- | ------------------------------------------------------ | +| MEASUREMENT_UPDATED | Fires when a measurement is updated | +| MEASUREMENT_ADDED | Fires when a new measurement is added | +| RAW_MEASUREMENT_ADDED | Fires when a raw measurement is added (e.g., dicom-sr) | +| MEASUREMENT_REMOVED | Fires when a measurement is removed | +| MEASUREMENTS_CLEARED | Fires when all measurements are deleted | +| JUMP_TO_MEASUREMENT_VIEWPORT | Fires when a measurement is requested to be jumped to, applying to individual viewports. | +| JUMP_TO_MEASUREMENT_LAYOUT | Fires when a measurement is requested to be jumped to, applying to the overall layout. | + +## API + +- `getMeasurements`: returns array of measurements + +- `getMeasurement(id)`: returns the corresponding measurement based on the + provided Id. + +- `remove(id, source)`: removes a measurement and broadcasts the + `MEASUREMENT_REMOVED` event. + +- `clearMeasurements`: removes all measurements and broadcasts + `MEASUREMENTS_CLEARED` event. + +- `createSource(name, version)`: creates a new measurement source, generates a + uid and adds it to the `sources` property of the service. + +- `addMapping(source, definition, matchingCriteria, toSourceSchema, toMeasurementSchema)`: + adds a new measurement matching criteria along with mapping functions. We will + learn more about [source/mappers below](#source--mappers) + +- `update`: updates the measurement details and fires `MEASUREMENT_UPDATED` + +- `addRawMeasurement(source,definition,data,toMeasurementSchema,dataSource = {}` + : adds a raw measurement into a source so that it may be converted to/from + annotation in the same way. E.g. import serialized data of the same form as + the measurement source. Fires `MEASUREMENT_UPDATED` or `MEASUREMENT_ADDED`. + Note that, `MeasurementService` handles finding the correct mapper upon new + measurements; however, `addRawMeasurement` provides more flexibility. You can + take a look into its usage in `dicom-sr` extension. + + - `source`: The measurement source instance. + - `definition`: The source definition you want to add the measurement to. + - `data`: The data you wish to add to the source. + - `toMeasurementSchema`: A function to get the `data` into the same shape as + the source definition. + +- `jumpToMeasurement(viewportId, id)`: calls the listeners who have + subscribed to `JUMP_TO_MEASUREMENT`. + +## Source / Mappers + +To create a custom measurement source and relevant mappers for each tool, you +can take a look at the `init.js` inside the `cornerstone` extension. In which we +are registering our `CornerstoneTools-v4` measurement source to +MeasurementService. Let's take a peek at the _simplified_ implementation +together. To achieve this, for each tool, we need to provide three mappers: + +- `matchingCriteria`: criteria used for finding the correct mapper for the drawn + tool. +- `toAnnotation`: tbd +- `toMeasurement`: a function that converts the tool data to OHIF internal + representation of measurement data. + +```js title="extensions/cornerstone/src/utils/measurementServiceMappings/Length.js" +function toMeasurement( + csToolsAnnotation, + DisplaySetService, + getValueTypeFromToolType +) { + const { element, measurementData } = csToolsAnnotation; + + /** ... **/ + + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = getSOPInstanceAttributes(element); + + const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID( + SOPInstanceUID, + SeriesInstanceUID + ); + + /** ... **/ + return { + id: measurementData.id, + SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + label: measurementData.label, + description: measurementData.description, + unit: measurementData.unit, + length: measurementData.length, + type: getValueTypeFromToolType(tool), + points: getPointsFromHandles(measurementData.handles), + }; +} + +////////////////////////////////////////// + +// extensions/cornerstone/src/init.js + +const Length = { + toAnnotation, + toMeasurement, + matchingCriteria: [ + { + valueType: MeasurementService.VALUE_TYPES.POLYLINE, + points: 2, + }, + ], +}; + +const _initMeasurementService = (MeasurementService, DisplaySetService) => { + /** ... **/ + + const csToolsVer4MeasurementSource = MeasurementService.createSource( + 'CornerstoneTools', + '4' + ); + + /* Mappings */ + MeasurementService.addMapping( + csToolsVer4MeasurementSource, + 'Length', + Length.matchingCriteria, + toAnnotation, + toMeasurement + ); + + /** Other tools **/ + return csToolsVer4MeasurementSource; +}; +``` + + +## Auto complete +Use a customization service to add more customizations for measurement labels. Later, when adding a measurement, the user will be prompted to choose from a list of labels. + +```js +customizationService.addModeCustomizations([ + { + id: 'measurementLabels', + labelOnMeasure: true, + exclusive: true, + items: [ + { value: 'Head', label: 'Head' }, + { value: 'Neck', label: 'Neck' }, + { value: 'Knee', label: 'Knee' }, + { value: 'Toe', label: 'Toe' }, + ], + }, +]); +``` diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/data/PanelService.md b/platform/docs/versioned_docs/version-3.9/platform/services/data/PanelService.md new file mode 100644 index 0000000..b0a530b --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/data/PanelService.md @@ -0,0 +1,49 @@ +--- +sidebar_position: 8 +sidebar_label: Panel Service +--- + +# Panel Service + +## Overview + +The Panel Service provides for activating/showing a panel that was registered +via the `getPanelModule` extension method. Such panels can be either explicitly +activated or implicitly triggered to activate when some other event occurs. + +## Events + +The following events are published in `PanelService`. + +| Event | Description | +| --------------------- | ------------------------------------------------------ | +| ACTIVATE__PANEL | Fires a `ActivatePanelEvent` when a particular panel should be activated (i.e. shown). | + + +## API + +### Panel Activation + +- `activatePanel`: Fires the `ACTIVATE_PANEL` event for a particular panel (id). +An optional `forceActive` flag can be passed that when `true` "forces" a +panel to show. Ultimately, it is up to a panel's container whether it +is appropriate to activate/show the panel. For instance, if the user opened and then +closed a side panel that contains the panel to activate, that side panel +may decide that the user knows best and will not open the panel (again). + +- `addActivatePanelTriggers`: Creates and returns event subscriptions that when +fired will activate the specified panel with an optional `forceActive` flag +(see `activatePanel`). This allows for panel activation to be directly triggered +by some other event(s). When the triggers are no longer needed, simply +unsubscribe to the returned subscriptions. For example, a panel +for tracking measurements might get activated every time the +`MeasurementService` fires a `MEASUREMENT_ADDED` event like this: + ```js + panelService.addActivatePanelTriggers('measurement-tracking-panel-id', [ + sourcePubSubService: measurementService, + sourceEvents: [ + measurementService.EVENTS.MEASUREMENT_ADDED, + measurementService.EVENTS.RAW_MEASUREMENT_ADDED, + ], + ]); + ``` diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/data/SegmentationService.md b/platform/docs/versioned_docs/version-3.9/platform/services/data/SegmentationService.md new file mode 100644 index 0000000..31e4b67 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/data/SegmentationService.md @@ -0,0 +1,54 @@ +--- +sidebar_position: 7 +sidebar_label: Segmentation Service +--- + +# Segmentation Service + +## Overview + +Using Segmentation Service you can create, edit and delete segmentation data, and +change appearance of the segmentation including color, opacity and visibility. + +Segmentations in OHIF are based on the Segmentations in Cornerstone3D. You can +read more about it in the [Cornerstone Segmentation](https://www.cornerstonejs.org/docs/concepts/cornerstone-tools/segmentation/). OHIF currently only supports +one representation of the segmentation data. + +## Events + +There are seven events that get publish in `MeasurementService`: + +| Event | Description | +| --------------------- | ------------------------------------------------------ | +| SEGMENTATION_MODIFIED | Fires when a segmentation is updated e.g., segment added, removed etc.| +| SEGMENTATION_DATA_MODIFIED | Fires when the segmentation data changes | +| SEGMENTATION_ADDED | Fires when a new segmentation is added to OHIF | +| SEGMENTATION_REMOVED | Fires when a segmentation is removed from OHIF | +| SEGMENT_LOADING_COMPLETE | Fires when a segment group adds its pixel data to the volume | +| SEGMENTATION_LOADING_COMPLETE | Fires when the full segmentation volume is filled with its segments | + + +## API + +### Segmentation Creation + +- `createEmptyLabelmapForDisplaySetUID`: based on a reference displaySet, create a new segmentation. E.g., create a new segmentation based on a CT series +- `createSegmentationForSEGDisplaySet`: given a segDisplaySet loaded by a sopClassHandler, create a new segmentation +- `addSegmentationRepresentationToToolGroup`: given the toolGroupId, add the given segmentationId to the toolGroup. + + +### Segmentation Behavior + +- setActiveSegmentationForToolGroup, getSegmentations, getSegmentation, jumpToSegmentCenter, highlightSegment + + +### Segment Behavior + +- setSegmentLocked, removeSegment, addSegment, setSegmentLocked, setSegmentLabel, setActiveSegment, +setSegmentRGBAColor + +### Segmentation Configuration + +Setters + +- setSegmentVisibility, setSegmentColor, setSegmentRGBA, setSegmentOpacity, toggleSegmentationVisibility, diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/data/SyncGroupService.md b/platform/docs/versioned_docs/version-3.9/platform/services/data/SyncGroupService.md new file mode 100644 index 0000000..835fd56 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/data/SyncGroupService.md @@ -0,0 +1,147 @@ +--- +sidebar_position: 8 +sidebar_label: SyncGroup Service +--- + +# Sync Group Service + +## Overview + +The `SyncGroupService` is responsible for managing synchronization groups in the OHIF Viewer. Synchronization groups allow multiple viewports to be synchronized based on various criteria, such as camera position, window level, zoom/pan, and image slice position. This service provides a centralized way to create, update, and manage synchronization groups. + +Right now, synchronization groups can be defined in the hanging protocols or manually assigning buttons. + + + + +## API + +- `getSyncCreatorForType(type)`: Returns the synchronizer creator function for the specified type. +- `addSynchronizerType(type, creator)`: Adds a new synchronizer type with a custom creator function. +- `getSynchronizer(id)`: Retrieves a synchronizer by its ID. +- `getSynchronizersOfType(type)`: Retrieves an array of synchronizers of the specified type. +- `addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups)`: Adds a viewport to one or more synchronization groups. +- `destroy()`: Destroys all synchronizers. +- `getSynchronizersForViewport(viewportId)`: Retrieves an array of synchronizers associated with the specified viewport. +- `removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroupId?)`: Removes a viewport from a specific synchronization group or all synchronization groups if no group ID is provided. + +## Usage +### Via hanging protocols +You can set up different types of synchronization groups for your viewports. For example, in the TMTV hanging protocol (`extensions/tmtv/src/getHangingProtocolModule.js`), we can see how different synchronization groups are defined for various viewports: + +```javascript +const ptAXIAL = { + viewportOptions: { + // ... + syncGroups: [ + { + type: 'cameraPosition', + id: 'axialSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ptWLSync', + source: true, + target: true, + }, + { + type: 'voi', + id: 'ptFusionWLSync', + source: true, + target: false, + options: { + syncInvertState: false, + }, + }, + ], + }, + // ... +}; +``` + + + +In this example, the `ptAXIAL` viewport is part of three synchronization groups: + +1. `cameraPosition` group with the ID `'axialSync'`: This group synchronizes the camera position across viewports that are both source and target. +2. `voi` (Window Level) group with the ID `'ptWLSync'`: This group synchronizes the window level settings across viewports that are both source and target. +3. `voi` group with the ID `'ptFusionWLSync'`: This group synchronizes the window level settings, but the `ptAXIAL` viewport is only a source, not a target. + + +:::tip +You can control the state of the synchronizer via a toolbar button after you define the synchronization group in the hanging protocol. + +```js +{ + id: 'SyncToggle', + uiType: 'ohif.radioGroup', + props: { + icon: 'tool-info', + label: 'toggle', + commands: { + commandName: 'toggleSynchronizer', + commandOptions: { + syncId: 'axialSync' + } + } + }, +}, +``` + +as you can see by using the `toggleSynchronizer` command you can toggle the state of the synchronizer for the specified syncId. + +::: + +### Manually through a button +You can create a button on the toolbar that you provice the synchronization group type, +and it applys it to all viewports. + +:::note +Currently we don't have a proper way to select viewports to apply the synchronization group to. It is applied to all applicable viewports +::: + +For instance look at `imageSliceSync` button in the longitudinal mode (`modes/longitudinal/src/moreTools.ts`) and how it runs a command + +```js +ToolbarService.createButton({ + id: 'ImageSliceSync', + icon: 'link', + label: 'Image Slice Sync', + tooltip: 'Enable position synchronization on stack viewports', + commands: [ + { + commandName: 'toggleSynchronizer', + commandOptions: { + type: 'imageSlice', + }, + }, + ], +}) +``` + +You can create another button to toggle 'voi' synchronization. Currently we group +viewports by modality and apply the voi synchronization to all viewports of the same modality. + +```js +ToolbarService.createButton({ + id: 'VoiSync', + icon: 'link', + label: 'VOI Sync', + tooltip: 'Enable VOI synchronization on viewports', + commands: [ + { + commandName: 'toggleSynchronizer', + commandOptions: { + type: 'voi', + }, + }, + ], +}) +``` + +:::tip +For your custom synchronization groups, you can create a new synchronizer type and follow the +same pattern as the existing synchronizers. +::: diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/data/ToolGroupService.md b/platform/docs/versioned_docs/version-3.9/platform/services/data/ToolGroupService.md new file mode 100644 index 0000000..0d7a04c --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/data/ToolGroupService.md @@ -0,0 +1,89 @@ +--- +sidebar_position: 7 +sidebar_label: ToolGroup Service +--- + +# Tool Group Service + +## Overview + +The `ToolGroupService` is responsible for managing tool groups in the OHIF Viewer. + +:::tip +Read more about toolGroups [here](https://www.cornerstonejs.org/docs/concepts/cornerstone-tools/toolGroups) +::: + +It allows you to create, update, and manage tool groups and the tools associated with them. Tool groups are used to organize and control the behavior of various tools in the viewer, such as window level, pan, zoom, measurements, and annotations. + +## Events + +The `ToolGroupService` emits the following events: + +| Event | Description | +| ---------------------------------- | ----------------------------------------------- | +| `VIEWPORT_ADDED` | Fires when a viewport is added to a tool group | +| `TOOLGROUP_CREATED` | Fires when a new tool group is created | + +## API + +- `getToolGroup(toolGroupId?)`: Retrieves a tool group by its ID. If no ID is provided, it returns the tool group for the active viewport. +- `getToolGroupIds()`: Returns an array of all tool group IDs. +- `getToolGroupForViewport(viewportId)`: Returns the tool group associated with the specified viewport. +- `getActiveToolForViewport(viewportId)`: Returns the active tool for the specified viewport. +- `destroy()`: Destroys all tool groups. +- `destroyToolGroup(toolGroupId)`: Destroys the specified tool group. +- `removeViewportFromToolGroup(viewportId, renderingEngineId, deleteToolGroupIfEmpty?)`: Removes a viewport from a tool group. If `deleteToolGroupIfEmpty` is true and the tool group becomes empty after removing the viewport, it will be destroyed. +- `addViewportToToolGroup(viewportId, renderingEngineId, toolGroupId?)`: Adds a viewport to a tool group. If `toolGroupId` is not provided, the viewport will be added to all tool groups. +- `createToolGroup(toolGroupId)`: Creates a new tool group with the specified ID. +- `addToolsToToolGroup(toolGroupId, tools, configs?)`: Adds tools to the specified tool group with optional configurations. +- `createToolGroupAndAddTools(toolGroupId, tools)`: Creates a new tool group and adds the specified tools to it. +- `getToolConfiguration(toolGroupId, toolName)`: Retrieves the configuration for the specified tool in the given tool group. +- `setToolConfiguration(toolGroupId, toolName, config)`: Sets the configuration for the specified tool in the given tool group. + +## Usage + +Here's an example of how to create a new tool group and add tools to it in our basic viewer mode (modes/longitudinal/src/initToolGroups.js) + +```js +import { initToolGroups } from '@ohif/extension-cornerstone'; +import { ToolGroupService } from '@ohif/core'; + +const toolGroupService = new ToolGroupService(); + +// Create a new tool group +const defaultToolGroup = toolGroupService.createToolGroup('default'); + +// Define tools for the tool group +const tools = { + active: [ + { toolName: 'WindowLevel', bindings: [{ mouseButton: 1 }] }, + { toolName: 'Pan', bindings: [{ mouseButton: 2 }] }, + { toolName: 'Zoom', bindings: [{ mouseButton: 3 }] }, + ], + passive: [ + { toolName: 'Length' }, + { toolName: 'ArrowAnnotate' }, + { toolName: 'Bidirectional' }, + ], +}; + +// Add tools to the tool group +toolGroupService.addToolsToToolGroup('default', tools); +``` + +In this example, we create a new `ToolGroupService` instance and use it to create a new tool group with the ID `'default'`. We then define an object `tools` that contains the active and passive tools we want to add to the tool group. Finally, we call the `addToolsToToolGroup` method to add the tools to the newly created tool group. + +:::tip +You can begin the viewer with certain toggle tools already active. For example, if you have your 'referencelines' tool enabled, it will be active when the viewer starts, and the icon state will be correctly set to active as well. +::: + +```js +const tools = { + // the reset + // enabled + enabled: [{ toolName: toolNames.ImageOverlayViewer }, { toolName: toolNames.ReferenceLines }], + }; +``` + + +![alt text](../../../assets/img/reference-lines-from-start.png) diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/data/ToolbarService.md b/platform/docs/versioned_docs/version-3.9/platform/services/data/ToolbarService.md new file mode 100644 index 0000000..3c8aff7 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/data/ToolbarService.md @@ -0,0 +1,266 @@ +--- +sidebar_position: 5 +sidebar_label: Toolbar Service +--- + +# Toolbar **Service** + +## Overview + +The `ToolBarService` is a straightforward service designed to handle the toolbar. Its main tasks include adding buttons, configuring them, and organizing button sections. When a button is clicked, it executes the designated commands. In the past, this service was more intricate, managing button states and logic. However, all that functionality has now been transferred to the `ToolBarModule` and evaluators. + + + +## Events + +| Event | Description | +| ----------------------- | ---------------------------------------------------------------------- | +| TOOL_BAR_MODIFIED | Fires when a button is added/removed to the toolbar | +| TOOL_BAR_STATE_MODIFIED | Fires when an interaction happens and ToolBarService state is modified | + +## API + +- `createButtonSection(key, buttons)` : creates a section of buttons in the toolbar with the given key and button Ids + +- `addButtons`: add the button definition to the service. + [See below for button definition](#button-definitions). + +- `removeButton(key)` : remove a button from the toolbar. + +- `setButtons`: sets the buttons defined in the service. It overrides all the + previous buttons + + + + +## Button Definitions + + +### Basic + +The simplest toolbarButtons definition has the following properties: + +![toolbarModule-zoom](../../../assets/img/toolbarModule-zoom.png) + + +```js +{ + id: 'Zoom', + uiType: 'ohif.radioGroup', + props: { + icon: 'tool-zoom', + label: 'Zoom', + "commands": [ + { + "commandName": "setToolActive", + "commandOptions": { + "toolName": "Zoom" + }, + "context": "CORNERSTONE" + } + ] + evaluate: 'evaluate.cornerstoneTool', + }, +}, +``` + +| property | description | values | +| ---------------- | ----------------------------------------------------------------- | ------------------------------------------- | +| `id` | Unique string identifier for the definition | \* | +| `icon` | A string name for an icon supported by the consuming application. | \* | +| `label` | User/display friendly to show in UI | \* | +| `commands` | (optional) The commands to run when the button is used. It include a commandName, commandOptions, and/or a context | Any command registered by a `CommandModule` | + + +### Nested (dropdown) + +You can use the `ohif.splitButton` type to build a button with extra tools in +the dropdown. + +- First you need to give your `primary` tool definition to the split button +- the `secondary` properties can be a simple arrow down (`chevron-down` icon) +- For adding the extra tools add them to the `items` list. + +You can see below how `longitudinal` mode is using the available toolbarModule +to create `MeasurementTools` nested button + +![toolbarModule-nested-buttons](../../../assets/img/toolbarModule-nested-buttons.png) + +```js title="modes/longitudinal/src/toolbarButtons.js" +{ + id: 'MeasurementTools', + uiType: 'ohif.splitButton', + props: { + groupId: 'MeasurementToolsGroupId', + // group evaluate to determine which item should move to the top + evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList', + primary: ToolbarService.createButton({ + id: 'Length', + icon: 'tool-length', + label: 'Length', + tooltip: 'Length Tool', + commands: _createSetToolActiveCommands('Length'), + evaluate: 'evaluate.cornerstoneTool', + }), + secondary: { + icon: 'chevron-down', + tooltip: 'More Measure Tools', + }, + items: [ + ToolbarService.createButton({ + id: 'Length', + icon: 'tool-length', + label: 'Length', + tooltip: 'Length Tool', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'Length', + }, + context: 'CORNERSTONE', + }, + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'SRLength', + toolGroupId: 'SRToolGroup', + }, + // we can use the setToolActive command for this from Cornerstone commandsModule + context: 'CORNERSTONE', + }, + ], + evaluate: 'evaluate.cornerstoneTool', + }), + ToolbarService.createButton({ + id: 'Bidirectional', + icon: 'tool-bidirectional', + label: 'Bidirectional', + tooltip: 'Bidirectional Tool', + commands: [ + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'Bidirectional', + }, + context: 'CORNERSTONE', + }, + { + commandName: 'setToolActive', + commandOptions: { + toolName: 'SRBidirectional', + toolGroupId: 'SRToolGroup', + }, + context: 'CORNERSTONE', + }, + ], + evaluate: 'evaluate.cornerstoneTool', + }), + ], + }, + }, +``` + +:::tip +split buttons can have a group evaluator (in the above example `evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList`) which can decide what happens +when the user interacts with the buttons. In the above example, we are promoting the button to the primary section if the cornerstone tool is not active in the list of buttons. + +There are other evaluators for instance `evaluate.group.promoteToPrimary` +which does not care about the cornerstone tool and promotes the button to the primary section anyway +::: + +:::tip +If you don't provide a group evaluator nothing would happen and the button will stay in the secondary section. +::: + +## Listeners +Sometimes you need a tool to listen to specific events in order to react properly. +You can add `listeners` for this purpose. We use this pattern for referencelineTools +which should set its source of reference upon active viewport change + +Currently you can subscribe to the following events: +- `ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED`: when the active viewport changes +- `ViewportGridService.EVENTS.VIEWPORTS_READY`: when the viewports are ready in the grid + + +```js + +const ReferenceLinesListeners: RunCommand = [ + { + commandName: 'setSourceViewportForReferenceLinesTool', + context: 'CORNERSTONE', + }, +]; + +ToolbarService.createButton({ + id: 'ReferenceLines', + icon: 'tool-referenceLines', + label: 'Reference Lines', + tooltip: 'Show Reference Lines', + commands: [ + { + commandName: 'setToolEnabled', + commandOptions: { + toolName: 'ReferenceLines', + toggle: true, + }, + context: 'CORNERSTONE', + }, + ], + listeners: { + [ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners, + [ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners, + }, + evaluate: 'evaluate.cornerstoneTool.toggle', +}), + +``` + + + +## Button Sections +In order to organize the buttons, you can create button sections in the toolbar. And +assign buttons to each section separately. + +OHIF provides a `primary` section by default. You can add more sections as needed in your UI +and use toolbarService to create and manage them. (You can look at the toolBox implementation +which take advantage of having a dedicated section for the tools with advanced options, +we use that in the segmentation mode). + + +## Example + +For instance in `longitudinal` mode we are using the `onModeEnter` hook to +add the buttons to the toolbarService and assign them to the primary section. + +```js title="modes/longitudinal/src/index.js" +toolbarService.addButtons([...toolbarButtons, ...moreTools]); +toolbarService.createButtonSection('primary', [ + 'MeasurementTools', + 'Zoom', + 'info', + 'WindowLevel', + 'Pan', + 'Capture', + 'Layout', + 'Crosshairs', + 'MoreTools', +]); +``` + +as you see we creating the button section and assigning buttons based on their Ids. + +:::tip +You can even duplicate the same button in different sections and the button will be +in sync in all sections (thanks to the evaluation system). +::: + +:::tip +we will add more section in the toolbar (other than primary) in the future. +::: + +:::note +Don't forget to set up your toolGroups to ensure that your buttons function correctly. Buttons serve as a visual interface. When you interact with them, they execute their commands, and evaluators determine their state post-interaction. +::: diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/data/WorkflowStepService.md b/platform/docs/versioned_docs/version-3.9/platform/services/data/WorkflowStepService.md new file mode 100644 index 0000000..7070cad --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/data/WorkflowStepService.md @@ -0,0 +1,174 @@ +--- +sidebar_position: 9 +sidebar_label: WorkflowStep Service +--- + +# Workflow Step Service + +This service allows you to manage your workflow in smaller steps. It provides a structured way to define and navigate through different stages +or phases of a larger process or workflow. Each step can have its own configuration, layout, toolbar buttons, and other settings tailored to the specific requirements of that stage. + +## Anatomy of a Workflow Step + +The anatomy of a workflow step refers to the different components or properties that define and configure each individual step within the workflow. Each step can be customized with various settings to tailor the user interface, available tools, and behavior of the application for that specific stage of the workflow. Here are the key components that make up a workflow step: + +- `id`: A unique identifier for the step +- `name`: A human-readable name or title for the step, which can be displayed in the user interface to help users understand the current stage of the workflow. +- `hangingProtocol`: The hanging protocol configuration specifies the protocol and stage ID to be used for displaying the images. This ensures that the appropriate data viewports and presentation are used for the current workflow step. +- `layout`: The layout configuration defines the arrangement and visibility of various panels or viewports within the application's user interface for the specific step. This can include specifying which panels should be visible on the left or right side of the screen, as well as any options for panel visibility or behavior. +- `toolbarButtons`: Each step can define a set of toolbar buttons that should be available and displayed in the application's toolbar during that step. Remember the button definitions should already be registered to toolbarService beforehand, here we are just referencing the buttons id in each section. +- `info` : An optional description or additional information about the current workflow step can be provided. which +will be displayed as tooltip in the UI. + +- Step Callbacks or Commands: Some workflow steps may require specific actions or commands to be executed when the step is entered or exited. These callbacks or commands can be defined within the step configuration and can be used to update the application's state, perform data processing, or trigger other relevant actions. For instance you have access to `onEnter` hook to run a command right after the step is entered. + +For instance, a simplified example of our pre-clinical 4D workflow steps configuration might look like this: + +```js +const dynamicVolume = { + sopClassHandler: + "@ohif/extension-cornerstone-dynamic-volume.sopClassHandlerModule.dynamic-volume", + leftPanel: + "@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-volume", + toolBox: + "@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-toolbox", + export: + "@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-export", +} + +const cs3d = { + segmentation: + "@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation", +} + + + +const steps = [ + { + id: "dataPreparation", + name: "Data Preparation", + layout: { + panels: { + left: [dynamicVolume.leftPanel], + }, + }, + toolbarButtons: { + buttonSection: "primary", + buttons: ["MeasurementTools", "Zoom", "WindowLevel", "Crosshairs", "Pan"], + }, + hangingProtocol: { + protocolId: "default4D", + stageId: "dataPreparation", + }, + info: "In the Data Preparation step...", + }, + { + id: "roiQuantification", + name: "ROI Quantification", + layout: { + panels: { + left: [dynamicVolume.leftPanel], + right: [ + [dynamicVolume.toolBox, cs3d.segmentation, dynamicVolume.export], + ], + }, + options: { + leftPanelClosed: false, + rightPanelClosed: false, + }, + }, + toolbarButtons: [ + { + buttonSection: "primary", + buttons: [ + "MeasurementTools", + "Zoom", + "WindowLevel", + "Crosshairs", + "Pan", + ], + }, + { + buttonSection: "dynamic-toolbox", + buttons: ["BrushTools", "RectangleROIStartEndThreshold"], + }, + ], + hangingProtocol: { + protocolId: "default4D", + stageId: "roiQuantification", + }, + info: "The ROI quantification step ...", + }, + { + id: "kineticAnalysis", + name: "Kinetic Analysis", + layout: { + panels: { + left: [dynamicVolume.leftPanel], + right: [], + }, + }, + toolbarButtons: { + buttonSection: "primary", + buttons: ["MeasurementTools", "Zoom", "WindowLevel", "Crosshairs", "Pan"], + }, + hangingProtocol: { + protocolId: "default4D", + stageId: "kineticAnalysis", + }, + onEnter: [ + { + commandName: "updateSegmentationsChartDisplaySet", + options: { servicesManager }, + }, + ], + info: "The Kinetic Analysis step ...", + }, +] + +``` + +## Integration + +After you have defined your workflow steps, you can integrate them into your application by using the `workflowStepsService`. + +These steps should be called on `onSetupRouteComplete` in your mode factory. + + +Note: onModeEnter is too soon to call these steps as the mode is not yet fully initialized. + + +```js +onSetupRouteComplete: ({ servicesManager }) => { + workflowStepsService.addWorkflowSteps(workflowSettings.steps); + workflowStepsService.setActiveWorkflowStep(workflowSettings.steps[0].id); +}, +``` + +check out the `modes/preclinical-4d/src/index.tsx` for a complete example. + + +## User Interface + +We have developed a simple dropdown UI element that you can use to navigate between the different steps of your workflow. This dropdown can be added to the toolbar like below: + +```js +toolbarService.addButtons([ + { + id: 'ProgressDropdown', + uiType: 'ohif.progressDropdown', + }, +]) +toolbarService.createButtonSection('secondary', ['ProgressDropdown']); +``` + +It will appear in the `secondary` location in the toolbar. + +![alt text](../../../assets/img/progressDropdown.png) + +:::note +if you like to place the progressbar in a different location, you can use the Toolbox component +to create a button section and place the progress bar there. + +Read more in the [Toolbar module](../../extensions//modules/toolbar.md) +::: diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/data/_category_.json b/platform/docs/versioned_docs/version-3.9/platform/services/data/_category_.json new file mode 100644 index 0000000..984ac9a --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/data/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Data Services", + "position": 2 +} diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/data/index.md b/platform/docs/versioned_docs/version-3.9/platform/services/data/index.md new file mode 100644 index 0000000..65f50b9 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/data/index.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 1 +sidebar_label: Overview +--- + +# Overview + +Data services are the first category of services which deal with handling non-ui +related state Each service have their own internal state which they handle. + +> We have replaced the _redux_ store. Instead, we have introduced various +> services and a pub/sub pattern to subscribe and run, which makes the `OHIF-v3` +> architecture nice and clean. + +We maintain the following non-ui Services: + +- [DicomMetadata Store](./../data/DicomMetadataStore.md) +- [DisplaySet Service](./../data/DisplaySetService.md) +- [Hanging Protocol Service](../data/HangingProtocolService.md) +- [Toolbar Service](./ToolbarService.md) +- [Measurement Service](../data/MeasurementService.md) +- [Customization Service](./../ui/customization-service.md) +- [State Sync Service](../../../migration-guide/3p8-to-3p9/5-StateSyncService.md) +- [Panel Service](../data/PanelService.md) + +## Service Architecture + +![services-data](../../../assets/img/services-data.png) + +> We have explained services and how to create a custom service in the +> [`ServicesManager`](../../managers/service.md) section of the docs + +To recap: The simplest service return a new object that has a `name` property, +and `Create` method which instantiate the service class. The "Factory Function" +that creates the service is provided with the implementation (this is slightly +different for UI Services). + +```js +// extensions/customExtension/src/services/backEndService/index.js +import backEndService from './backEndService'; + +export default function WrappedBackEndService(servicesManager) { + return { + name: 'myService', + create: ({ configuration = {} }) => { + return new backEndService(servicesManager); + }, + }; +} +``` + +A service, once created, can be registered with the `ServicesManager` to make it +accessible to extensions. Similarly, the application code can access named +services from the `ServicesManager`. + +[Read more of how to design a new custom service and register it](../../managers/service.md) diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/index.md b/platform/docs/versioned_docs/version-3.9/platform/services/index.md new file mode 100644 index 0000000..4fecc0e --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/index.md @@ -0,0 +1,195 @@ +--- +sidebar_position: 1 +sidebar_label: Introduction +--- + +# Services + +## Overview + +Services are "concern-specific" code modules that can be consumed across layers. +Services provide a set of operations, often tied to some shared state, and are +made available to through out the app via the `ServicesManager`. Services are +particularly well suited to address [cross-cutting +concerns][cross-cutting-concerns]. + +Each service should be: + +- self-contained +- able to fail and/or be removed without breaking the application +- completely interchangeable with another module implementing the same interface + +> In `OHIF-v3` we have added multiple non-UI services and have introduced +> **pub/sub** pattern to reduce coupling between layers. +> +> [Read more about Pub/Sub](./pubsub.md) + +## Services + +The following services is available in the `OHIF-v3`. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ServiceTypeDescription
+ + DicomMetadataStore + + Data Service + DicomMetadataStore +
+ + DisplaySetService + + Data Service + DisplaySetService +
+ + segmentationService + + Segmentation Service + segmentationService +
+ + HangingProtocolService + + Data Service + HangingProtocolService +
+ + MeasurementService (MODIFIED) + + Data Service + MeasurementService +
+ + ToolBarService + + Data Service + ToolBarService +
+ + ViewportGridService + + UI Service + ViewportGridService +
+ + Cine Service + + UI Service + cine +
+ + CustomizationService + + UI Service + customizationService +
+ + UIDialogService + + UI Service + UIDialogService +
+ + UIModalService + + UI Service + UIModalService +
+ + UINotificationService + + UI Service + UINotificationService +
+ + UIViewportDialogService + + UI Service + UIViewportDialogService +
+ + + + + +[core-services]: https://github.com/OHIF/Viewers/tree/master/platform/core/src/services +[services-manager]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/ServicesManager.js +[cross-cutting-concerns]: https://en.wikipedia.org/wiki/Cross-cutting_concern + diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/pubsub.md b/platform/docs/versioned_docs/version-3.9/platform/services/pubsub.md new file mode 100644 index 0000000..036e592 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/pubsub.md @@ -0,0 +1,114 @@ +--- +sidebar_position: 4 +sidebar_label: Pub Sub +--- + +# Pub sub + +## Overview + +Publishโ€“subscribe pattern is a messaging pattern that is one of the fundamentals +patterns used in reusable software components. + +In short, services that implement this pattern, can have listeners subscribed +to their broadcasted events. After the event is fired, the corresponding +listener will execute the function that is registered. + +You can read more about this design pattern +[here](https://cloud.google.com/pubsub/docs/overview). + +## Example: Default Initialization + +In `Mode.jsx` we have a default initialization that demonstrates a series of +subscriptions to various events. + +```js +async function defaultRouteInit({ + servicesManager, + studyInstanceUIDs, + dataSource, +}) { + const { + DisplaySetService, + HangingProtocolService, + } = servicesManager.services; + + const unsubscriptions = []; + + const { + unsubscribe: instanceAddedUnsubscribe, + } = DicomMetadataStore.subscribe( + DicomMetadataStore.EVENTS.INSTANCES_ADDED, + ({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) => { + const seriesMetadata = DicomMetadataStore.getSeries( + StudyInstanceUID, + SeriesInstanceUID + ); + + DisplaySetService.makeDisplaySets(seriesMetadata.instances, madeInClient); + } + ); + + unsubscriptions.push(instanceAddedUnsubscribe); + + studyInstanceUIDs.forEach(StudyInstanceUID => { + dataSource.retrieve.series.metadata({ StudyInstanceUID }); + }); + + const { unsubscribe: seriesAddedUnsubscribe } = DicomMetadataStore.subscribe( + DicomMetadataStore.EVENTS.SERIES_ADDED, + ({ StudyInstanceUID }) => { + HangingProtocolService.run({studies, displaySets, activeStudy}); + } + ); + unsubscriptions.push(seriesAddedUnsubscribe); + + return unsubscriptions; +} +``` + +## Unsubscription + +You need to be careful if you are adding custom subscriptions to the app. Each +subscription will return an unsubscription function that needs to be executed on +component destruction to avoid adding multiple subscriptions to the same +observer. + +Below, we can see `simplified` `Mode.jsx` and the corresponding `useEffect` +where the unsubscription functions are executed upon destruction. + +```js title="platform/app/src/routes/Mode/Mode.jsx" +export default function ModeRoute(/**..**/) { + /**...**/ + useEffect(() => { + /**...**/ + + DisplaySetService.init(extensionManager, sopClassHandlers); + + extensionManager.onModeEnter(); + mode?.onModeEnter({ servicesManager, extensionManager }); + + const setupRouteInit = async () => { + if (route.init) { + return await route.init(/**...**/); + } + + return await defaultRouteInit(/**...**/); + }; + + let unsubscriptions; + setupRouteInit().then(unsubs => { + unsubscriptions = unsubs; + }); + + return () => { + extensionManager.onModeExit(); + mode?.onModeExit({ servicesManager, extensionManager }); + unsubscriptions.forEach(unsub => { + unsub(); + }); + }; + }); + return <> /**...**/ ; +} +``` diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/ui/_category_.json b/platform/docs/versioned_docs/version-3.9/platform/services/ui/_category_.json new file mode 100644 index 0000000..9c01213 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/ui/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "UI Services", + "position": 3 +} diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/ui/cine-service.md b/platform/docs/versioned_docs/version-3.9/platform/services/ui/cine-service.md new file mode 100644 index 0000000..707b5d9 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/ui/cine-service.md @@ -0,0 +1,8 @@ +--- +sidebar_position: 7 +sidebar_label: CINE Service +--- + +# CINE Service + +TODO... diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md b/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md new file mode 100644 index 0000000..a6baf90 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/ui/customization-service.md @@ -0,0 +1,504 @@ +--- +sidebar_position: 7 +sidebar_label: Customization Service +--- +# Customization Service + +There are a lot of places where users may want to configure certain elements +differently between different modes or for different deployments. A mode +example might be the use of a custom overlay showing mode related DICOM header +information such as radiation dose or patient age. + +The use of this service enables these to be defined in a typed fashion by +providing an easy way to set default values for this, but to allow a +non-default value to be specified by the configuration or mode. + +This service is a UI service in that part of the registration allows for registering +UI components and types to deal with, but it does not directly provide an UI +displayable elements unless customized to do so. + +Note: Customization Service itself doesn't implement the actual customization, +but rather just provide mechanism to register reusable prototypes, to configure +those prototypes with actual configurations, and to use the configured objects +(components, data, whatever). +Actual implementation of the customization is totally up to the component that +supports customization. (for example, `CustomizableViewportOverlay` component uses +`CustomizationService` to implement viewport overlay that is easily customizable +from configuration.) + +## Global, Default and Mode customizations +There are various customization sets that define the lifetime/setup of the +customization. The global customizations are those used for overriding +customizations defined elsewhere, and allow replacing a customization. + +Mode customizations are only registered for the lifetime of the mode, allowing +the mode definition to update/modify the underlying behaviour. This is related +to default customizations, which provide a fallback if the mode or global customization +isn't defined. Default customizations may only be defined once, otherwise throwing +an exception. + +## Append and Merge Customizations +In addition to the replace a customization, there is the ability to merge or append +a customization. The merge customization simply applies the lodash merge functionality +to the existing customization, with the new one, while the append customization +modifies the customization by appending to the value. + +### Append Behaviour +When a list is found in the destination object, the append source object is +examined to see how to handle the change. If the source is simply a list, +then the list object is appended, and no additional changes are performed. +However, if the source is an object other than a list, then the iterable +attributes of the object are examined to match child objects to the destination list, +according to the following table: + +* Natural or zero number value - match the given index location and merge at the point +* Fractional number value - insert at a new point in the list, starting from the end or beginning +* keyword - match a value having the same id as the keyword, inserting at the end, or at _priority as defined in the keywords above. + +#### Example Append + +```javascript +const destination = [ + 1, + {id: 'two', value: 2}, + {id: 'three', value: 3} +] + +const source = { + two: { value: 'updated2' }, + 1: { extraValue: 2 }, + 1.0001: { id: 'inserted', value: 1.0001 }, + -1: { value: -3 }, +} +``` + +Results in two updates to `destination[1]`, the first using an id match on 'two', while the second one +does a positional match on `1`, resulting in the value `{id: 'two', value: 'updated2', extraValue: 2 }` + +Then, it inserts the id 'inserted' after position 1. + +Finally, position -1 (the end position) is updated from value 3 to value -3. + +The ordering is not specified on any of these insertions, so can happen out of order. Use multiple updates to perform order specific inserts. + +## Registering customizable modules (or defining customization prototypes) + +Extensions and Modes can register customization templates they support. +It is done by adding `getCustomizationModule()` in the extension or mode definition. + +Below is the protocol of the `getCustomizationModule()`, if defined in Typescript. + +```typescript + getCustomizationModule() : { name: string, value: any }[] +``` + +If the name is 'default', it is the a default customization, while if it +is 'global', then it is a priority/over-riding customization. + +In the `value` of each customizations, you will define customization prototype(s). +These customization prototype(s) can be considered like "Prototype" in Javascript. +These can be used to extend the customization definitions from configurations. +Default customizations will be often used to define all the customization prototypes, +Default customizations will be often used to define all the customization prototypes, +as they will be loaded automatically along with the defining extension or mode. + + +For example, the `@ohif/extension-default` extension defines, + +```js + getCustomizationModule: () => [ + //... + + { + name: 'default', + value: [ + { + id: 'ohif.overlayItem', + content: function (props) { + if (this.condition && !this.condition(props)) return null; + + const { instance } = props; + const value = + instance && this.attribute + ? instance[this.attribute] + : this.contentF && typeof this.contentF === 'function' + ? this.contentF(props) + : null; + if (!value) return null; + + return ( + + {this.label && ( + {this.label} + )} + {value} + + ); + }, + }, + ], + }, + + //... + ], +``` + +And this `ohif.overlayItem` object will be used as a prototype (and template) to define items +to be displayed on `CustomizableViewportOverlay`. See how we use the `ohif.overlayItem` in +the example below. + +## Configuring customizations + +There are several ways to register customizations. The +`APP_CONFIG.customizationService` +field is used as a per-configuration entry. This object can list single +configurations by id, or it can list sets of customizations by referring to +the `customizationModule` in an extension. + +NOTE that these definitions from APP_CONFIG will be loaded by default, just like +extension/modes default customization. + +Below is the example configuration for `CustomizableViewportOverlay` component +customization, using the customization prototype `ohif.overlayItem` defined in +`ohif/extension-defaul` extension.: + +```js +window.config = { + //... + + // in the APP_CONFIG file set the top right area to show the patient name + // using PN: as a prefix when the study has a non-empty patient name. + customizationService: { + cornerstoneOverlayTopRight: { + id: 'cornerstoneOverlayTopRight', + items: [ + { + id: 'PatientNameOverlay', + // Note below that here we are using the customization prototype of + // `ohif.overlayItem` which was registered to the customization module in + // `ohif/extension-default` extension. + customizationType: 'ohif.overlayItem', + // the following props are passed to the `ohif.overlayItem` prototype + // which is used to render the overlay item based on the label, color, + // conditions, etc. + attribute: 'PatientName', + label: 'PN:', + title: 'Patient Name', + color: 'yellow', + condition: ({ instance }) => + instance && + instance.PatientName && + instance.PatientName.Alphabetic, + contentF: ({ instance, formatters: { formatPN } }) => + formatPN(instance.PatientName.Alphabetic) + + ' ' + + (instance.PatientSex ? '(' + instance.PatientSex + ')' : ''), + }, + ], + }, + }, + + //... +} +``` + +In the customization configuration, you can use `customizationType` fields to +define the prototype that customization object should inherit from. +The `customizationType` field is simply the id of another customization object. + + +## Implementing customization using CustomizationService + +### Mode Customizations + +Mode-specific customizations are no different from the global ones, +except that the mode customizations are specific to one mode and +are not globally applied. Mode-specific customizations are also cleared +before the mode `onModeEnter` is called, and they can have new values registered in the `onModeEnter` + +Following on our example above to customize the overlay, we can now add a mode customization +with a bottom-right overlay. + +```js +// Import the type from the extension itself +import OverlayUICustomization from "@ohif/cornerstone-extension"; + +// In the mode itself, customizations can be registered: +onModeEnter: { + // Note how the object can be strongly typed + const bottomRight: OverlayUICustomization = { + id: 'cornerstoneOverlayBottomRight', + // Note the type is the previously registered ohif.cornerstoneOverlay + customizationType: 'ohif.cornerstoneOverlay', + // The cornerstoneOverlay definition requires an items list here. + items: [ + // Custom definitions for the context menu here. + ], + }; + customizationService.addModeCustomizations(bottomRight); +} +``` + +The mode customizations are retrieved via the `getModeCustomization` function, +providing an id, and optionally a default value. The retrieval will return, +in order: + +1. Global customization with the given id. +2. Mode customization with the id. +3. The default value specified. + +The return value then inherits the `customizationType` instance, so that the +value can be typed and have default values and functionality provided. The object +can then be used in a way defined by the extension provided that customization +point. + +```ts +const cornerstoneOverlay = customizationService.getModeCustomization( + "cornerstoneOverlay", + { customizationType: "ohif.cornerstoneOverlay" }, +); + +const { component: overlayComponent, props } = + customizationService.getComponent(cornerstoneOverlay); + +return ( + +); +``` + +This example shows fetching the default component to render this object. The +returned object would be a sub-type of ohif.cornerstoneOverlay if defined. This +object can be a React component or other object such as a commands list, for +example (this example comes from the context menu customizations as that one +uses commands lists): + +```ts +cornerstoneContextMenu = customizationService.get( + "cornerstoneContextMenu", + defaultMenu, +); +commandsManager.run(cornerstoneContextMenu, extraProps); +``` + +### Global Customizations + +Global customizations are retrieved in the same was as mode customizations, except +that the `getGlobalCustomization` is called instead of the mode call. + +### Types + +Some types for the customization service are provided by the `@ohif/ui` types +export. Additionally, extensions can provide a Types export with custom +typing, allowing for better typing for the extension specific capabilities. +This allows for having strong typing when declaring customizations, for example: + +```ts +import { Types } from '@ohif/ui'; + +const customContextMenu: Types.ContextMenu.Menu = + { + id: 'cornerstoneContextMenu', + customizationType: 'ohif.contextMenu', + // items will be type checked to be in accordance with UIContextMenu.items + items: [ ... ] + }, +``` + +### Inheritance + +JavaScript property inheritance can be supplied by defining customizations +with id corresponding to the customizationType value. For example: + +```js +getCustomizationModule = () => ([ + { + name: 'default', + value: [ + { + id: 'ohif.overlayItem', + content: function (props) { + return (

{this.label} {props.instance[this.attribute]}

) + }, + }, + ], + } +]) +``` + +defines an overlay item which has a React content object as the render value. +This can then be used by specifying a `customizationType` of `ohif.overlayItem`, for example: + +```js +const overlayItem: Types.UIOverlayItem = { + id: 'anOverlayItem', + customizationType: 'ohif.overlayItem', + attribute: 'PatientName', + label: 'PN:', +}; +``` + +# Customizations + +This section can be used to specify various customization capabilities. + +## Text color for StudyBrowser tabs + +This is the recommended pattern for deep customization of class attributes, +making it fine grained, and have it apply a set of attributes, mostly from +tailwind. In this case it is a double indirection, as the buttons class +uses it's own internal class names. + +* Name: 'class:StudyBrowser' +* Attributes: +** `true` for the is active true text color +** `false` for the is active false text color. +** Values are button colors, from the Button class, eg default, white, black + +## customRoutes + +* Name: `customRoutes` global +* Attributes: +** `routes` of type List of route objects (see `route/index.tsx`) is a set of route objects to add. +** Should any element of routes match an existing baked in element, the baked in one will be replaced. +** `notFoundRoute` is the route to display when nothing is found (this has to be at the end of the overall list, so can't be added to routes) + +### Example + +```js +{ + id: 'customRoutes', + routes: [ + { + path: '/myroute', + children: MyRouteReactFunction, + } + ], +} +``` + +There is a usage of this example commented out in config/default.js that +looks like the code below. This example is provided by the default extension, +again with commented out code. Uncomment the getCustomizationModule customRoutes +code in the default module to activate this, and then go to: `http://localhost:3000/custom` +to see the custom route. + +Note the name of this is the customization module name, which usually won't match +the id, and in fact there can be multiple customization objects defined for a single +customization module, to allow for customizing sets of related values. + +```js +customizationService: [ + // Shows a custom route -access via http://localhost:3000/custom + '@ohif/extension-default.customizationModule.helloPage', +], +``` + +## Customizable Viewport Overlay + +Below is the full example configuration of the customizable viewport overlay and the screenshot of the result overlay. + +There are working examples that can be run with: +``` +set APP_CONFIG=config/customization.js +yarn dev +``` + +```javascript +// this is part of customization.js, an example customization dataset +window.config = { + + // This shows how to append to the customization data + customizationService: [ + { + id: '@ohif/cornerstoneOverlay', + // Append recursively, rather than replacing + merge: 'Append', + topRightItems: { + id: 'cornerstoneOverlayTopRight', + items: [ + { + id: 'PatientNameOverlay', + // Note below that here we are using the customization prototype of + // `ohif.overlayItem` which was registered to the customization module in + // `ohif/extension-default` extension. + customizationType: 'ohif.overlayItem', + // the following props are passed to the `ohif.overlayItem` prototype + // which is used to render the overlay item based on the label, color, + // conditions, etc. + attribute: 'PatientName', + label: 'PN:', + title: 'Patient Name', + color: 'yellow', + condition: ({ instance }) => instance?.PatientName, + contentF: ({ instance, formatters: { formatPN } }) => + formatPN(instance.PatientName) + + (instance.PatientSex ? ' (' + instance.PatientSex + ')' : ''), + }, + ], + }, + + topLeftItems: { + items: { + // Note the -10000 means -10000 + length of existing list, which is + // much before the start of hte list, so put the new value at the start. + '-10000': + { + id: 'Species', + customizationType: 'ohif.overlayItem', + label: 'Species:', + color: 'red', + background: 'green', + condition: ({ instance }) => + instance?.PatientSpeciesDescription, + contentF: ({ instance }) => + instance.PatientSpeciesDescription + + '/' + + instance.PatientBreedDescription, + }, + }, + }, + }, +... +``` + + + +## Context Menus + +Context menus can be created by defining the menu structure and click +interaction, as defined in the `ContextMenu/types`. There are examples +below specific to the cornerstone context, because the actual click +handler and attributes used to decide when and how to display the menu +are specific to the context used for where the menu is displayed. + +## Cornerstone Context Menu + +The default cornerstone context menu can be customized by setting the +`cornerstoneContextMenu`. For a full example, see `findingsContextMenu`. + +## Customizable Cornerstone Viewport Click Behaviour + +The behaviour on clicking on the cornerstone viewport can be customized +by setting the `cornerstoneViewportClickCommands`. This is intended to +support both the cornerstone 3D internal commands as well as things like +context menus. Currently it supports buttons 1-3, as well as modifier keys +by associating a commands list with the button to click. See `initContextMenu` +for more details. + +## Please add additional customizations above this section +> 3rd Party implementers may be added to this table via pull requests. + + + + +[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIModalService/index.js +[modal-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/ModalProvider.js +[modal-consumer]: https://github.com/OHIF/Viewers/tree/master/platform/ui/src/components/ohifModal +[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c + diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/ui/index.md b/platform/docs/versioned_docs/version-3.9/platform/services/ui/index.md new file mode 100644 index 0000000..2a0203a --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/ui/index.md @@ -0,0 +1,304 @@ +--- +sidebar_position: 1 +sidebar_label: Overview +--- + +# Overview + + + +A typical web application will have components and state for common UI like +modals, notifications, dialogs, etc. A UI service makes it possible to leverage +these components from an extension. + +We maintain the following UI Services: + +- [UI Notification Service](ui-notification-service.md) +- [UI Modal Service](ui-modal-service.md) +- [UI Dialog Service](ui-dialog-service.md) +- [UI Viewport Dialog Service](ui-viewport-dialog-service.md) +- [CINE Service](cine-service.md) +- [Viewport Grid Service](viewport-grid-service.md) + + + +![UIService](../../../assets/img/ui-services.png) + + + +## Providers for UI services + +**There are several context providers that wraps the application routes. This +makes the context values exposed in the app, and service's `setImplementation` +can get run to override the implementation of the service.** + +```js title="platform/app/src/App.jsx" +function App({ config, defaultExtensions }) { + /**...**/ + /**...**/ + return ( + /**...**/ + + + + + + + {appRoutes} + + + + + + + /**...**/ + ); +} +``` + +## Example + +For instance `UIModalService` has the following Public API: + +```js title="platform/core/src/services/UIModalService/index.js" +const publicAPI = { + name, + hide: _hide, + show: _show, + setServiceImplementation, +}; + +function setServiceImplementation({ + hide: hideImplementation, + show: showImplementation, +}) { + /** ... **/ + serviceImplementation._hide = hideImplementation; + serviceImplementation._show = showImplementation; + /** ... **/ +} + +export default { + name: 'UIModalService', + create: ({ configuration = {} }) => { + return publicAPI; + }, +}; +``` + +`UIModalService` implementation can be set (override) in its context provider. +For instance in `ModalProvider` we have: + +```js title="platform/ui/src/contextProviders/ModalProvider.jsx" +import { Modal } from '@ohif/ui'; + +const ModalContext = createContext(null); +const { Provider } = ModalContext; + +export const useModal = () => useContext(ModalContext); + +const ModalProvider = ({ children, modal: Modal, service }) => { + const DEFAULT_OPTIONS = { + content: null, + contentProps: null, + shouldCloseOnEsc: true, + isOpen: true, + closeButton: true, + title: null, + customClassName: '', + }; + + const show = useCallback(props => setOptions({ ...options, ...props }), [ + options, + ]); + + const hide = useCallback(() => setOptions(DEFAULT_OPTIONS), [ + DEFAULT_OPTIONS, + ]); + + useEffect(() => { + if (service) { + service.setServiceImplementation({ hide, show }); + } + }, [hide, service, show]); + + const { + content: ModalContent, + contentProps, + isOpen, + title, + customClassName, + shouldCloseOnEsc, + closeButton, + } = options; + + return ( + + {ModalContent && ( + + + + )} + {children} + + ); +}; + +export default ModalProvider; + +export const ModalConsumer = ModalContext.Consumer; +``` + +Therefore, anywhere in the app that we have access to react context we can use +it by calling the `useModal` from `@ohif/ui`. As a matter of fact, we are +utilizing the modal for the preference window which shows the hotkeys after +clicking on the gear button on the right side of the header. + +A `simplified` code for our worklist is: + +```js title="platform/app/src/routes/WorkList/WorkList.jsx" +import { useModal, Header } from '@ohif/ui'; + +function WorkList({ + history, + data: studies, + dataTotal: studiesTotal, + isLoadingData, + dataSource, + hotkeysManager, +}) { + const { show, hide } = useModal(); + + /** ... **/ + + const menuOptions = [ + { + title: t('Header:About'), + icon: 'info', + onClick: () => show({ content: AboutModal, title: 'About OHIF Viewer' }), + }, + { + title: t('Header:Preferences'), + icon: 'settings', + onClick: () => + show({ + title: t('UserPreferencesModal:User Preferences'), + content: UserPreferences, + contentProps: { + hotkeyDefaults: hotkeysManager.getValidHotkeyDefinitions( + hotkeyDefaults + ), + hotkeyDefinitions, + onCancel: hide, + currentLanguage: currentLanguage(), + availableLanguages, + defaultLanguage, + onSubmit: state => { + i18n.changeLanguage(state.language.value); + hotkeysManager.setHotkeys(state.hotkeyDefinitions); + hide(); + }, + onReset: () => hotkeysManager.restoreDefaultBindings(), + }, + }), + }, + ]; + /** ... **/ + return ( +
+ /** ... **/ +
+ /** ... **/ +
+ ); +} +``` + + + + + + + +## Tips & Tricks + +It's important to remember that all we're doing is making it possible to control +bits of the application's UI from an extension. Here are a few non-obvious +takeaways worth mentioning: + +- Your application code should continue to use React context + (consumers/providers) as it normally would +- You can substitute our "out of the box" UI implementations with your own +- You can create and register your own UI services +- You can choose not to register a service or provide a service implementation +- In extensions, you can provide fallback/alternative behavior if an expected + service is not registered + - No `UIModalService`? Use the `UINotificationService` to notify users. +- You can technically register a service in an extension and expose it to the + core application + +> Note: These are recommended patterns, not hard and fast rules. Following them +> will help reduce confusion and interoperability with the larger OHIF +> community, but they're not silver bullets. Please speak up, create an issue, +> if you would like to discuss new services or improvements to this pattern. diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/ui/ui-dialog-service.md b/platform/docs/versioned_docs/version-3.9/platform/services/ui/ui-dialog-service.md new file mode 100644 index 0000000..152841a --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/ui/ui-dialog-service.md @@ -0,0 +1,48 @@ +--- +sidebar_position: 4 +sidebar_label: UI Dialog Service +--- +# UI Dialog Service + +Dialogs have similar characteristics to that of Modals, but often with a +streamlined focus. They can be helpful when: + +- We need to grab the user's attention +- We need user input +- We need to show additional information + +If you're curious about the DOs and DON'Ts of dialogs and modals, check out this +article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article] + + + +## Interface + +For a more detailed look on the options and return values each of these methods +is expected to support, [check out it's interface in `@ohif/core`][interface] + +| API Member | Description | +| -------------- | ------------------------------------------------------ | +| `create()` | Creates a new Dialog that is displayed until dismissed | +| `dismiss()` | Dismisses the specified dialog | +| `dismissAll()` | Dismisses all dialogs | + +## Implementations + +| Implementation | Consumer | +| ------------------------------------ | -------------------------- | +| [Dialog Provider][dialog-provider]\* | Baked into Dialog Provider | + +`*` - Denotes maintained by OHIF + +> 3rd Party implementers may be added to this table via pull requests. + + + + +[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIDialogService/index.js +[dialog-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/DialogProvider.js +[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c + diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/ui/ui-modal-service.md b/platform/docs/versioned_docs/version-3.9/platform/services/ui/ui-modal-service.md new file mode 100644 index 0000000..e78bc9c --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/ui/ui-modal-service.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 3 +sidebar_label: UI Modal Service +--- +# UI Modal Service + +Modals have similar characteristics to that of Dialogs, but are often larger, +and only allow for a single instance to be viewable at once. They also tend to +be centered, and not draggable. They're commonly used when: + +- We need to grab the user's attention +- We need user input +- We need to show additional information + +If you're curious about the DOs and DON'Ts of dialogs and modals, check out this +article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article] + + +
+ +
+ +## Interface + +For a more detailed look on the options and return values each of these methods +is expected to support, [check out it's interface in `@ohif/core`][interface] + +| API Member | Description | +| ---------- | ------------------------------------- | +| `hide()` | Hides the open modal | +| `show()` | Shows the provided content in a modal | + +## Implementations + +| Implementation | Consumer | +| ---------------------------------- | --------- | +| [Modal Provider][modal-provider]\* | Modal.jsx | + +`*` - Denotes maintained by OHIF + + + + + +> 3rd Party implementers may be added to this table via pull requests. + + + + +[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIModalService/index.js +[modal-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/ModalProvider.js +[modal-consumer]: https://github.com/OHIF/Viewers/tree/master/platform/ui/src/components/ohifModal +[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c + diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/ui/ui-notification-service.md b/platform/docs/versioned_docs/version-3.9/platform/services/ui/ui-notification-service.md new file mode 100644 index 0000000..815ac6d --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/ui/ui-notification-service.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 2 +sidebar_label: UI Notification Service +--- +# UI Notification Service + +Notifications can be annoying and disruptive. They can also deliver timely +helpful information, or expedite the user's workflow. Here is some high level +guidance on when and how to use them: + +- Notifications should be non-interfering (timely, relevant, important) +- We should only show small/brief notifications +- Notifications should be contextual to current behavior/actions +- Notifications can serve warnings (acting as a confirmation) + +If you're curious about the DOs and DON'Ts of notifications, check out this +article: ["How To Design Notifications For Better UX"][ux-article] + + + +
+ +
+ + +## Interface + +For a more detailed look on the options and return values each of these methods +is expected to support, [check out it's interface in `@ohif/core`][interface] + +| API Member | Description | +| ---------- | --------------------------------------- | +| `hide()` | Hides the specified notification | +| `show()` | Creates and displays a new notification | + +## Implementations + +| Implementation | Consumer | +| ---------------------------------------- | ----------------------------------------- | +| [Snackbar Provider][snackbar-provider]\* | [SnackbarContainer][snackbar-container]\* | + +`*` - Denotes maintained by OHIF + +> 3rd Party implementers may be added to this table via pull requests. + + + + +[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UINotificationService/index.js +[snackbar-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/SnackbarProvider.js +[snackbar-container]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/components/snackbar/SnackbarContainer.js +[ux-article]: https://uxplanet.org/how-to-design-notifications-for-better-ux-6fb0711be54d + diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/ui/ui-viewport-dialog-service.md b/platform/docs/versioned_docs/version-3.9/platform/services/ui/ui-viewport-dialog-service.md new file mode 100644 index 0000000..b50e3e7 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/ui/ui-viewport-dialog-service.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 5 +sidebar_label: UI Viewport Dialog Service +--- + +# UI Viewport Dialog Service + +## Overview +This is a new UI service, that creates a modal inside the viewport. + +Dialogs have similar characteristics to that of Modals, but often with a +streamlined focus. They can be helpful when: + +- We need to grab the user's attention +- We need user input +- We need to show additional information + +If you're curious about the DOs and DON'Ts of dialogs and modals, check out this +article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article] + + + +
+ +
+ +## Interface + +For a more detailed look on the options and return values each of these methods +is expected to support, [check out it's interface in `@ohif/core`][interface] + +| API Member | Description | +| -------------- | ------------------------------------------------------ | +| `create()` | Creates a new Dialog that is displayed until dismissed | +| `dismiss()` | Dismisses the specified dialog | +| `dismissAll()` | Dismisses all dialogs | + +## Implementations + +| Implementation | Consumer | +| ------------------------ | -------------------------- | +| [ViewportDialogProvider] | Baked into Dialog Provider | + +`*` - Denotes maintained by OHIF + + +## State + +```js +const DEFAULT_STATE = { + viewportId: null, + message: undefined, + type: 'info', // "error" | "warning" | "info" | "success" + actions: undefined, // array of { type, text, value } + onSubmit: () => { + console.log('btn value?'); + }, + onOutsideClick: () => { + console.warn('default: onOutsideClick') + }, + onDismiss: () => { + console.log('dismiss? -1'); + }, +}; +``` diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/ui/viewport-action-menu.md b/platform/docs/versioned_docs/version-3.9/platform/services/ui/viewport-action-menu.md new file mode 100644 index 0000000..5a05925 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/ui/viewport-action-menu.md @@ -0,0 +1,30 @@ +--- +sidebar_position: 8 +sidebar_label: Viewport Action Corners +--- + +# Viewport Action Corners Service + +The Viewport Action Corners Service is a powerful tool for managing interactive components in the corners of viewports within the OHIF viewer. This service allows developers to dynamically add, remove, and organize various UI elements such as menus, buttons, or custom components in specific locations around the viewport. + +## Overview + +The Viewport Action Corners Service extends the PubSubService and provides methods to: + +- Add single or multiple components to viewport corners +- Clear components from a specific viewport +- Manage the state of viewport corner components + +## Key Features + +- **Flexible Positioning**: Components can be placed in top-left, top-right, bottom-left, or bottom-right corners of the viewport. +- **Priority Ordering**: Components can be assigned priority indices for ordering within a corner. +- **Viewport-Specific**: Actions are associated with specific viewports, allowing for individualized control. +- **Dynamic Updates**: Components can be added or removed at runtime, enabling context-sensitive UI elements. + +## Usage + +To use the Viewport Action Corners Service, you typically interact with it through the `servicesManager`. Here's a basic example of how to add a component: + + +Take a look at how we add window level menu to the top right corner of the viewport in the `OHIFCornerstoneViewport` component. diff --git a/platform/docs/versioned_docs/version-3.9/platform/services/ui/viewport-grid-service.md b/platform/docs/versioned_docs/version-3.9/platform/services/ui/viewport-grid-service.md new file mode 100644 index 0000000..186afc4 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/services/ui/viewport-grid-service.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 6 +sidebar_label: Viewport Grid Service +--- + +# Viewport Grid Service + +## Overview + +This is a new UI service, that handles the grid layout of the viewer. + +## Events + +There are seven events that get publish in `ViewportGridService `: + +| Event | Description | +| ----------------------------- | --------------------------------------------------| +| ACTIVE_VIEWPORT_ID_CHANGED | Fires the Id of the active viewport is changed | +| LAYOUT_CHANGED | Fires the layout is changed | +| GRID_STATE_CHANGED | Fires when the entire grid state is changed | +| VIEWPORTS_READY | Fires when the viewports are ready in the grid | + +## Interface + +For a more detailed look on the options and return values each of these methods +is expected to support, [check out it's interface in `@ohif/core`][interface] + +| API Member | Description | +| --------------------------------------------------------------------- | --------------------------------------------------- | +| `setActiveViewportId(viewportId)` | Sets the active viewport Id in the app | +| `getState()` | Gets the states of the viewport (see below) | +| `setDisplaySetsForViewport({ viewportId, displaySetInstanceUID })` | Sets displaySet for viewport based on displaySet Id | +| `setLayout({numCols, numRows, keepExtraViewports})` | Sets rows and columns. When the total number of viewports decreases, optionally keep the extra/offscreen viewports. | +| `reset()` | Resets the default states | +| `getNumViewportPanes()` | Gets the number of visible viewport panes | +| `getLayoutOptionsFromState(gridState)` | Utility method that produces a `ViewportLayoutOptions` based on the passed in state| +| `getActiveViewportId()` | Returns the viewport Id of the active viewport in the grid| +| `getActiveViewportOptionByKey(key)` | Gets the specified viewport option field (key) for the active viewport | + +## Implementations + +| Implementation | Consumer | +| ---------------------- | -------------------------- | +| [ViewportGridProvider] | Baked into Dialog Provider | + +`*` - Denotes maintained by OHIF + +## State + +```js +const DEFAULT_STATE = { + // starting from null, hanging + // protocol will defined number of rows and cols + numRows: null, + numCols: null, + viewports: [ + /* + * { + * displaySetInstanceUID: string, + * } + */ + ], + activeViewportId: null, +}; +``` diff --git a/platform/docs/versioned_docs/version-3.9/platform/themeing.md b/platform/docs/versioned_docs/version-3.9/platform/themeing.md new file mode 100644 index 0000000..48a881c --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/platform/themeing.md @@ -0,0 +1,166 @@ +--- +sidebar_position: 2 +sidebar_label: Theming +--- + +# Viewer: Theming + +`OHIF-v3` has introduced the +[`LayoutTemplateModule`](./extensions/modules/layout-template.md) which enables +addition of custom layouts. You can easily design your custom components inside +an extension and consume it via the layoutTemplate module you write. + +## Tailwind CSS + +[Tailwind CSS](https://tailwindcss.com/) is a utility-first CSS framework for +creating custom user interfaces. + +Below you can see a compiled version of the tailwind configs. Each section can +be edited accordingly. For instance screen size break points, primary and +secondary colors, etc. + +```js +module.exports = { + prefix: '', + important: false, + separator: ':', + theme: { + screens: { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + }, + colors: { + overlay: 'rgba(0, 0, 0, 0.8)', + transparent: 'transparent', + black: '#000', + white: '#fff', + initial: 'initial', + inherit: 'inherit', + + indigo: { + dark: '#0b1a42', + }, + aqua: { + pale: '#7bb2ce', + }, + + primary: { + light: '#5acce6', + main: '#0944b3', + dark: '#090c29', + active: '#348cfd', + }, + + secondary: { + light: '#3a3f99', + main: '#2b166b', + dark: '#041c4a', + active: '#1f1f27', + }, + + common: { + bright: '#e1e1e1', + light: '#a19fad', + main: '#fff', + dark: '#726f7e', + active: '#2c3074', + }, + + customgreen: { + 100: '#05D97C', + }, + + customblue: { + 100: '#c4fdff', + 200: '#38daff', + }, + }, + }, +}; +``` + +You can also use the color variable like before. For instance: + +```js +primary: { + default: โ€˜var(--default-color)โ€˜, + light: โ€˜#5ACCE6โ€™, + main: โ€˜#0944B3โ€™, + dark: โ€˜#090C29โ€™, + active: โ€˜#348CFDโ€™, +} +``` + +## White Labeling + +A white-label product is a product or service produced by one company (the +producer) that other companies (the marketers) rebrand to make it appear as if +they had made it - +[Wikipedia: White-Label Product](https://en.wikipedia.org/wiki/White-label_product) + +Current white-labeling options are limited. We expose the ability to replace the +"Logo" section of the application with a custom "Logo" component. You can do +this by adding a whiteLabeling key to your configuration file. + +```js +window.config = { + /** .. **/ + whiteLabeling: { + createLogoComponentFn: function(React) { + return React.createElement( + 'a', + { + target: '_blank', + rel: 'noopener noreferrer', + className: 'text-white underline', + href: 'http://radicalimaging.com', + }, + React.createElement('h5', {}, 'RADICAL IMAGING') + ); + }, + }, + /** .. **/ +}; +``` + +> You can simply use the stylings from tailwind CSS in the whiteLabeling + +In addition to text, you can also add your custom logo + +```js +window.config = { + /** .. **/ + whiteLabeling: { + createLogoComponentFn: function(React) { + return React.createElement( + 'a', + { + target: '_self', + rel: 'noopener noreferrer', + className: 'text-purple-600 line-through', + href: '/', + }, + React.createElement('img', { + src: './customLogo.svg', + // className: 'w-8 h-8', + }) + ); + }, + }, + /** .. **/ +}; +``` + +The output will look like + +![custom-logo](../assets/img/custom-logo.png) + + + + +[wikipedia]: https://en.wikipedia.org/wiki/White-label_product + diff --git a/platform/docs/versioned_docs/version-3.9/release-notes.md b/platform/docs/versioned_docs/version-3.9/release-notes.md new file mode 100644 index 0000000..48491be --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/release-notes.md @@ -0,0 +1,10 @@ +--- +sidebar_position: 2 +sidebar_label: Release Notes +--- + +# Release Notes + + + +You can find the detailed release notes on the OHIF website. Please visit [https://ohif.org/release-notes](https://ohif.org/release-notes) diff --git a/platform/docs/versioned_docs/version-3.9/resources.md b/platform/docs/versioned_docs/version-3.9/resources.md new file mode 100644 index 0000000..4c60e18 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/resources.md @@ -0,0 +1,156 @@ +--- +sidebar_position: 13 +sidebar_label: Resources +--- + +# Resources + +Throughout the development of the OHIF Viewer, we have participated in various +conferences and "hackathons". In this page, we will provide the presentations +and other resources that we have provided to the community in the past: + +## 2024 + +### IMNO 2024 - March 19-20, 2024 + +We participated in the Imaging Network Ontario (ImNO) 2024 symposium, presenting three posters. One of our presentations received the best talk award during the session. + + +- Advancing Medical Imaging on the Web: Implementation of Hanging Protocols for Automated Image Display Configuration in OHIF V3 [Poster](https://www.dropbox.com/scl/fi/z4h86bmsxi0c62e1n6h9l/P7-9-Alireza-Sedghi-Final.pdf?rlkey=v5pm0p5ygkbq41x9bz3hr5yi8&dl=0) +- Advancing Medical Imaging on the Web: Optimizing the Dicomweb Server Architecture with Static Dicomweb [Poster](https://www.dropbox.com/scl/fi/ep0lxjp90kbxhjoffe4kh/P7-10-Bill-Wallace-Final.pdf?rlkey=xl2u6tdnh9j9hgvkajxv3b02o&dl=0) +- (**๐Ÿ†๐Ÿ† BEST PRESENTATION AWARD in the Session 7 Pitches: Devices, HW, SW Development ๐Ÿ†๐Ÿ†**) Advancing Medical Imaging on the Web: Integrating High Throughput JPEG 2000 (HTJ2K) in Cornerstone3D for Streamlined Progressive Loading and Visualization [Poster](https://www.dropbox.com/scl/fi/srs2rxgtv2r69ver9ub1j/P7-8-Bill-Wallace-Final.pdf?rlkey=k9mmraw76r9q2s3b9w9s0793w&dl=0) + +## 2023 + +### ITCR 2023 Conference | September 11-13, 2023 + +Dr. Gordon Harris presented an update on OHIF in [NCI Informatics Technology for Cancer Research Annual Meeting](https://www.itcr2023.org/). You can find the slides and poster here: +[[Slides]](https://docs.google.com/presentation/d/1R38s95db_yZj0WoYdlUbaWGZsWVb3H-3u_hXBZXiTaE/edit?usp=sharing)[[Poster]](https://ohif-assets.s3.us-east-2.amazonaws.com/presentations/OHIF-ITCR-2023-FINAL-PRINT.pdf) + + + + +### SIIM 2023 Tech Tools Webinar | April 12th, 2023 + +Free, Open Source Tools for Research: MONAI and OHIF Viewer +[[Slides](https://docs.google.com/presentation/d/1afJ5Y9Tzukgn7eAbaO1oiCtN7XvIimFdmZP-HcOUofA/edit?usp=sharing)][[Video](https://www.youtube.com/watch?v=lo8J5w5jUJI)] + + +### NA-MIC Project Week 38th 2023 - Remote + +We participated in the 38th Project Week with three projects around OHIF. [[Website](https://projectweek.na-mic.org/PW38_2023_GranCanaria/)] + +- PolySeg representations for OHIF Viewer ([link](https://projectweek.na-mic.org/PW38_2023_GranCanaria/Projects/OHIF_PolySeg/)) +- Cross study synchronizer for OHIF Crosshair ([link](https://projectweek.na-mic.org/PW38_2023_GranCanaria/Projects/OHIF_SyncCrosshair/)) +- DATSCAN Viewer implementation in OHIF ([link](https://projectweek.na-mic.org/PW38_2023_GranCanaria/Projects/OHIF_DATSCAN/)) + + + +## 2022 + +### OHIF Demo to Interns +[[Slides]](https://docs.google.com/presentation/d/1a2PkUnqkVMaXaBsuFn7-PPlBJULU3dBwzI_44gKFeYI/edit?usp=sharing) + +### SIIM 2022 - Updates from the Imaging Informatics Community +We participated in the SIIM 2022 conference to give update for the imaging +informatics community. +[[Slides]](https://docs.google.com/presentation/d/1EUGaUzQtGhZbZWpGLe6ONqChpVMw9Qr9l3KHODevMow/edit?usp=sharing) +[[Video]](https://vimeo.com/734463662/dbd5a88371) + +### The Imaging Network Ontario - Remote + +The Imaging Network Ontario (ImNO) is an annual symposium that brings together +medical imaging researchers and scientists from across Canada to share +knowledge, ideas, and experiences. +[[Slides]](https://docs.google.com/presentation/d/18XZDon4-Sitc2a70V5sFyhyUVZI_mIgfXHGtdxhZMjE/edit?usp=sharing) +[[Video]](https://vimeo.com/843234581/ad7d308a44) + + +### [NA-MIC Project Week 36th 2022 - Remote](https://github.com/NA-MIC/ProjectWeek/blob/master/PW36_2022_Virtual/README.md) + +The Project Week is a week-long hackathon of hands-on activity in which medical +image computing researchers. OHIF team participated and gave a talk on OHIF and +Cornerstone in the 36th Project Week: +[[Slides]](https://docs.google.com/presentation/d/1-GtOKmr2cQi-r3OFyseSmgLeurtB3KXUkGMx2pVLh1I/edit?usp=sharing) +[[Video]](https://vimeo.com/668339696/63a2c48de8) + +## 2021 + +### [NA-MIC Project Week 35th 2021 - Remote](https://github.com/NA-MIC/ProjectWeek/tree/master/PW35_2021_Virtual) + +The Project Week is a week-long hackathon of hands-on activity in which medical +image computing researchers. OHIF team participated in the 35th Project Week +in 2021. +[[Slides]](https://docs.google.com/presentation/d/1KYNjuiI8lT1foQ4P9TGNV0lBhM6H-5KBs0wkYj4JJbk/edit?usp=sharing) + +### Chan Zuckerberg Initiative (CZI) + +Project presentations and demonstrations of Essential Open Source Software for +Science (EOSS) grantees +[[Slides]](https://docs.google.com/presentation/d/1_CLtG2hsL3ZxOtV2olVnzBOzq-TMLrHLomOy3FiU4NE/edit?usp=sharing) +[[Video]](https://youtu.be/0FjKkTJO0Rc?t=3737) + +### Google Cloud Tech + +Healthcare Imaging with Cloud Healthcare API +[[Video]](https://www.youtube.com/watch?v=2MiX9ScHFhY) + +## 2020 + +### OHIF ITCR Pitch + +OHIF pitch for Informatics Technology for Cancer Research (ITCR) +[[Slides]](https://docs.google.com/presentation/d/1MZXnZrVAnjmhVIWqC-aRSvJOoMMRLhLddACdCa1TybM/edit?usp=sharing) +[[Video]](https://vimeo.com/843234613/625bdb8793) + +## 2019 + +### OHIF and VTK.js Training Course + +OHIF and Kitware collaboration to create a training course for OHIF and VTK.js +developers. Funding for this work was provided by Kitware (NIH NINDS +R44NS081792, NIH NINDS R42NS086295, NIH NIBIB and NIGMS R01EB021396, NIH NIBIB +R01EB014955), Isomics (NIH P41 EB015902), and Massachusetts General Hospital +(NIH U24 CA199460). + +1. Introduction to VTK.js and OHIF + [[Slides]](https://docs.google.com/presentation/d/1NCJxpfx_qUGJI_2DhbECzaOg0k-Z6b65QlUptCofN-A/edit#slide=id.p) + [[Video]](https://vimeo.com/375520781) +2. Developing with VTK.js + [[Slides]](https://docs.google.com/presentation/d/17TCS6EhFi6SWFIrcAJ-DFdFzFFL-WD9BBTv-owmMdDU/edit#slide=id.p) + [[Video]](https://vimeo.com/375521036) +3. VTK.js Architecture and Tooling + [[Slides]](https://docs.google.com/presentation/d/1Sr1OGxMSw0oCt46koKQbmwSIE11Kqq8MGtyW3W0ASpk/edit?usp=gmail_thread) + [[Video]](https://vimeo.com/375521810) +4. OHIF + VTK.js Integration + [[Slides]](https://docs.google.com/presentation/d/1Iwg-u01HGVf1CgC6NbcBD3gm3uHN9WhjU59FSz55TN8/edit?ts=5d9c9ce4#slide=id.g59aa99cda4_0_131) + [[Video]](https://vimeo.com/375521206) + +## 2017 + +### Lesion Tracker + +LesionTracker: Extensible Open-Source Zero-Footprint Web Viewer for Cancer +Imaging Research and Clinical Trials. This project was supported in part by +grant U24 CA199460 from the National Cancer Institute (NCI) Informatics +Technology for Cancer Research (ITCR) Program. +[[Video]](https://www.youtube.com/watch?v=gUIPtoSBL-Q) + +### OHIF Community Meeting - June + +[[Slides]](https://docs.google.com/presentation/d/1K9Y6eP5DYTXoDlfwCZE6GkCUp83AK4_40YQS0dlzVBo/edit?usp=sharing) + +## 2016 + +### Imaging Community Call + +Open Source Oncology Web Viewer; Presentation by Gordon J. Harris +[[Slides]](https://www.slideshare.net/imgcommcall/lesiontracker) + +### OHIF Community Meeting - June + +[[Slides]](https://docs.google.com/presentation/d/1Ai25mBG0ZWUPhaadp3VnbCVmkYs9K51sQ8osMixrvJ0/edit?usp=sharing) + +### OHIF Community Meeting - September + +[[Slides]](https://docs.google.com/presentation/d/1iYZoU7v7KHSLHiKwH1_9_wweAkG7RGnyxrWeeHva4zQ/edit?usp=sharing) diff --git a/platform/docs/versioned_docs/version-3.9/user-guide/_category_.json b/platform/docs/versioned_docs/version-3.9/user-guide/_category_.json new file mode 100644 index 0000000..68ea78e --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/user-guide/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "User Guide", + "position": 2 +} diff --git a/platform/docs/versioned_docs/version-3.9/user-guide/index.md b/platform/docs/versioned_docs/version-3.9/user-guide/index.md new file mode 100644 index 0000000..52f44ba --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/user-guide/index.md @@ -0,0 +1,83 @@ +--- +sidebar_position: 2 +sidebar_label: Study List +--- + +# Study List + +## Overview + +The first page you will see when the viewer is loaded is the `Study List`. In +this page you can explore all the studies that are stored on the configured +server for the `OHIF Viewer`. + +![user-study-list](../assets/img/user-study-list.png) + +## Sorting + +When the Study List is opened, the application queries the PACS for 101 studies +by default. If there are greater than 100 studies returned, the default sort for +the study list is dictated by the image archive that hosts these studies for the +viewer and study list sorting will be disabled. If there are less than or equal +to 100 studies returned, they will be sorted by study date (most recent to +oldest) and study list sorting will be enabled. Whenever a query returns greater +than 100 studies, use filters to narrow results below 100 studies to enable +Study List sorting. + +## Filters + +There are certain filters that can be used to limit the study list to the +desired criteria. + +- Patient Name: Searches between patients names +- MRN: Searches between patients Medical Record Number +- Study Date: Filters the date of the acquisition +- Description: Searches between study descriptions +- Modality: Filters the modalities +- Accession: Searches between patients accession number + +An example of using study list filter is shown below: + +![user-study-filter](../assets/img/user-study-filter.png) + +Below the study list are pagination options for 25, 50, or 100 studies per page. + +![user-study-next](../assets/img/user-study-next.png) + +## Study Summary + +Click on a study to expand the study summary panel. + +![user-study-summary](../assets/img/user-study-summary.png) + +A summary of series available in the study is shown, which contains the series +description, series number, modality of the series, instances in the series, and +buttons to launch viewer modes to display the study. + +## Study Specific Modes + +All available modes are seen in the study expanded view. Modes can be enabled or +disabled for a study based on the modalities contained within the study. + +In the screenshot below, there are two modes shown for the selected study + +- Basic Viewer: Default mode that enables rendering and measurement tracking + +- PET/CT Fusion: Mode for visualizing the PET CT study in a 3x3 format. + +Based on the mode configurations (e.g., available modalities), PET/CT mode is +disabled for studies that do not contain PET AND CT images. + + + +![user-studyist-modespecific](../assets/img/user-studyist-modespecific.png) + +The previous screenshot shows a study containing PET and CT images and both +Basic Viewer and PET/CT Mode are available. + +## View Study + +The `Basic Viewer` mode is available for all studies by default. Click on the +mode button to launch the viewer. + +![user-open-viewer](../assets/img/user-open-viewer.png) diff --git a/platform/docs/versioned_docs/version-3.9/user-guide/viewer/Language.md b/platform/docs/versioned_docs/version-3.9/user-guide/viewer/Language.md new file mode 100644 index 0000000..de06487 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/user-guide/viewer/Language.md @@ -0,0 +1,22 @@ +--- +sidebar_position: 8 +--- + +# Language + +OHIF supports internationalization capabilities and setting the general language +of the Viewer. + +It should be noted that we don't have complete translations for all the components +and all the languages; however, you can easily add the key value translation pairs +following developer guides. + +Summary of language changing usage can be seen below: + + + +## Overview Video + +
+ +
diff --git a/platform/docs/versioned_docs/version-3.9/user-guide/viewer/_category_.json b/platform/docs/versioned_docs/version-3.9/user-guide/viewer/_category_.json new file mode 100644 index 0000000..417861d --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/user-guide/viewer/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Basic Viewer", + "position": 2 +} diff --git a/platform/docs/versioned_docs/version-3.9/user-guide/viewer/hotkeys.md b/platform/docs/versioned_docs/version-3.9/user-guide/viewer/hotkeys.md new file mode 100644 index 0000000..5d8aac1 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/user-guide/viewer/hotkeys.md @@ -0,0 +1,15 @@ +--- +sidebar_position: 7 +--- + +# Hotkeys + +To open the hotkey assignment panel, you can click on the Preferences gear on the +top right side of the viewer. + + +Below, you can see the default hotkeys key bindings: + +![user-hotkeys-default](../../assets/img/user-hotkeys-default.png) + +Hotkeys can be assigned to custom bindings that persist for the duration of the browser session. diff --git a/platform/docs/versioned_docs/version-3.9/user-guide/viewer/index.md b/platform/docs/versioned_docs/version-3.9/user-guide/viewer/index.md new file mode 100644 index 0000000..6918ce3 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/user-guide/viewer/index.md @@ -0,0 +1,29 @@ +--- +sidebar_position: 1 +sidebar_label: Overview +--- + + +# Overview +When you open a mode, viewport, toolbar and panels of the mode get shown. +It is important to note that each mode has a different UI, which serves its purpose. +Here we explain various components of `Basic Viewer` mode which includes measurement +tracking functionalities. + +Basic viewer mode (longitudinal): + +![user-viewer](../../assets/img/user-viewer.png) + +Let's break different aspects of the viewer to the main components: + +- Left Panel (study panel): displays series thumbnails with series details +- Viewport: renders the image and displays annotations +- Right Panel (measurements): displays annotations details +- Toolbar: displays tools and logo + +![user-viewer-components](../../assets/img/overview.png) + + + + +Now, we explain each component and its sub-elements in detail. diff --git a/platform/docs/versioned_docs/version-3.9/user-guide/viewer/measurement-panel.md b/platform/docs/versioned_docs/version-3.9/user-guide/viewer/measurement-panel.md new file mode 100644 index 0000000..b4a728d --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/user-guide/viewer/measurement-panel.md @@ -0,0 +1,74 @@ +--- +sidebar_position: 3 +--- + +# Measurement Panel + +## Introduction +In `Basic Viewer` mode, the right panel is the `Measurement Panel`. The Measurement Panel can be expanded or hidden by clicking on the arrow to the left of `Measurements`. + +Select a measurement tool and mark an image to initiate measurement tracking. A pop-up will ask if you want to track measurements for the series on which the annotation was drawn. + +![user-measurement-panel-modal](../../assets/img/measurement-panel-prompt.png) + + + + + +If you select `Yes`, the series becomes a `tracked series`, and the current drawn measurement and next measurements are shown on the measurement panel on the right. + +![user-measurement-panel-tracked](../../assets/img/measurement-panel-tracked.png) + +If you select `No`, the measurement becomes temporary. The next annotation made will repeat the measurement tracking prompt. + +If you select `No, do not ask again`, all annotations made on the study will be temporary. + +![measurement-temporary](../../assets/img/measurement-temporary.png) + + +## Labeling Measurements +You can edit the measurement name by hovering over the measurement and selecting the edit icon. You can also label or relabel a measurement by right-clicking on it in the viewport. + +![user-measurement-edit](../../assets/img/measurement-panel-1.png) + + + +## Deleting a Measurement +A measurement can be deleting by dragging it outside the image in the viewport or by right-clicking on the measurement in the viewport and selecting 'Delete'. + + +## Jumping to a Measurement +Measurement navigation inside the top viewport can be used to move to previous and next measurement. + + +![measurements-prevNext](../../assets/img/measurements-prevNext.png) + +If a series containing a measurement is currently being displayed in a viewport, you can jump to display the measurement in the viewport by clicking on it in the Measurement Panel. + +## Export Measurements + +You can export the measurements by clicking on the `Export`. A CSV file will get downloaded to your local computer containing the drawn measurements. + + +![user-measurement-export](../../assets/img/user-measurement-export.png) + + +If you have set up your DICOM server to be able to store instances from the viewer, then you are able to create a report by clicking on the `Create Report`. +This will create a DICOM Structured Report (SR) from the measurements and push it +to the server. + +For instance, running the Viewer on a local DCM4CHEE: + + + +
+ +
+ +## Overview Video +An overview of measurement drawing and exporting can be seen below: + + +
+ +
diff --git a/platform/docs/versioned_docs/version-3.9/user-guide/viewer/measurement-tracking.md b/platform/docs/versioned_docs/version-3.9/user-guide/viewer/measurement-tracking.md new file mode 100644 index 0000000..528ec78 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/user-guide/viewer/measurement-tracking.md @@ -0,0 +1,124 @@ +--- +sidebar_position: 4 +--- + +# Measurement Tracking + +## Introduction +OHIF-V3's `Basic Viewer` implements a `Measurement Tracking` workflow. Measurement +tracking allows you to: + +- Draw annotations and have them shown in the measurement panel +- Create a report from the tracked measurement and export them as DICOM SR +- Use already exported DICOM SR to re-hydrate the measurements in the viewer + + +## Status Icon +Each viewport has a left icon indicating whether the series within the viewport +contains: + +- tracked measurement OR +- untracked measurement OR +- Structured Report OR +- Locked (uneditable) Structured Report + +In the following, we will discuss each category. + +### Tracked vs Untracked Measurements + +`OHIF-v3` implements a workflow for measurement tracking that can be seen below. + +![user-measurement-panel-modal](../../assets/img/tracking-workflow1.png) + +In summary, when you create an annotation, a prompt will be shown whether to start tracking or not. If you start the tracking, the annotation style will change to a solid line, and annotation details get displayed on the measurement panel. +On the other hand, if you decline the tracking prompt, the measurement will be considered "temporary," and annotation style remains as a dashed line and not shown on the right panel, and cannot be exported. + + +Below, you can see different icons that appear for a tracked vs. untracked series in +`OHIF-v3`. + +![tracked-not-tracked](../../assets/img/tracked-not-tracked.png) + + + +#### Overview video for starting the tracking for measurements: + + +
+ +
+ + +

+ +#### Overview video for not starting tracking for measurements: + + +
+ +
+ + +### Reading and Writing DICOM SR + +`OHIF-v3` provides full support for reading, writing and mapping the DICOM Structured +Report (SR) to interactable `Cornerstone Tools`. When you load an already exported +DICOM SR into the viewer, you will be prompted whether to track the measurements +for the series or not. + +![SR-exported](../../assets/img/SR-exported.png) + +If you click Yes, DICOM SR measurements gets re-hydrated into the viewer and +the series become a tracked series. However, If you say no and later decide to say track the measurements, you can always click on the SR button that will prompt you +with the same message again. + +![restore-exported-sr](../../assets/img/restore-exported-sr.png) + +The full workflow for saving measurements to SR and loading SR into the viewer is shown below. + +![user-measurement-panel-modal](../../assets/img/tracking-workflow2.png) +![user-measurement-panel-modal](../../assets/img/tracking-workflow3.png) + + +#### Overview video for loading DICOM SR and making a tracked series: + + +
+ +
+ +

+ +#### Overview video for loading DICOM SR and not making a tracked series: + + +
+ +
+ +

+ +
+ +
+ +### Loading DICOM SR into an Already Tracked Series + +If you have an already tracked series and try to load a DICOM SR measurements, +you will be shown the following lock icon. This means that, you can review the +DICOM SR measurement, manipulate image and draw "temporary" measurements; however, +you cannot edit the DICOM SR measurement. + + +![locked-sr](../../assets/img/locked-sr.png) + +

+ + +#### Overview video for loading DICOM SR inside an already tracked series: + + + +
+ +
diff --git a/platform/docs/versioned_docs/version-3.9/user-guide/viewer/study-panel.md b/platform/docs/versioned_docs/version-3.9/user-guide/viewer/study-panel.md new file mode 100644 index 0000000..1b5190f --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/user-guide/viewer/study-panel.md @@ -0,0 +1,32 @@ +--- +sidebar_position: 2 +--- + +# Study Panel + +In `Basic Viewer` mode, the left panel includes Studies related to the current +patient. You can see three main type of studies below + +- Primary: The opened study from the study list. This study is always expanded + by default. +- Recent: All studies for the patient that contain study dates within 1 year of + the primary study +- All: All studies available for the patient contained within the source + repository + +The `Study Panel` displays the measurement tracking status of each series within +a study. As you can see in the first picture, the dashed circle on the left side +of each series demonstrates whether the series is being tracked for measurement +or not. + + + +![user-study-panel](../../assets/img/user-study-panel.png) + +Studies can be expanded or collapsed by clicking on the study information in the +Study Panel. If a series is being tracking within a study, the Measurement Panel +will display this information while the study is collapsed. + + + + diff --git a/platform/docs/versioned_docs/version-3.9/user-guide/viewer/toolbar.md b/platform/docs/versioned_docs/version-3.9/user-guide/viewer/toolbar.md new file mode 100644 index 0000000..1fb5527 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/user-guide/viewer/toolbar.md @@ -0,0 +1,85 @@ +--- +sidebar_position: 6 +--- + + +# Toolbar + +The four main components of the toolbar are: + +- Navigation back to the [Study List](../index.md) +- Logo and white labelling +- [Tools](#tools) +- [Preferences](#preferences) + +![user-viewer-toolbar](../../assets/img/user-viewer-toolbar.png) + + +## Tools +This section displays all the available tools inside the mode. +## Measurement tools +The basic viewer comes with the following default measurement tools: + +- Length Tool: Calculates the linear distance between two points in *mm* +- Bidirectional Tool: Creates a measurement of the longest diameter (LD) and longest perpendicular diameter (LPD) in *mm* +- Annotation: Used to create a qualitative marker with a freetext label +- Ellipse: Measures an elliptical area in *mm2* and Hounsfield Units (HU) +- Calibration Tool: Calibrate (or override) the Pixel Spacing Attribute (Physical distance in the patient between the center of each pixel, specified by a numeric pair - adjacent row spacing (delimiter) adjacent column spacing in mm) + +When a measurement tool is selected from the toolbar, it becomes the `active` tool. Use the caret to expand the measurement tools and select another tool. + + +![user-viewer-toolbar-measurements](../../assets/img/user-viewer-toolbar-measurements.png) + + +## Window/Level +The `Window/Level` tool enables manipulating the window level and window width of the rendered image. Click on the tool to enable freeform adjustment, then click and drag on the viewport to freely adjust the window/level. + +Click on the caret to expand the tool and choose from predefined W/L settings for common imaging scenarios. + + +![user-toolbar-preset](../../assets/img/user-toolbar-preset.png) + + +## Pan and Zoom +With the Zoom tool selected, click and drag the cursor on an image to adjust the zoom. The magnification level is displayed in the viewport. + +With the Pan tool selected, click and drag the cursor on an image to adjust the image position. + +## Image Capture +Click on the Camera icon to download a high quality image capture using common image formats (png, jpg) + +![user-toolbar-download-icon](../../assets/img/user-toolbar-download-icon.png) + +In the opened modal, the filename, image's width and height, and filetype and can be configured before downloading the image to your local computer. + +![user-toolbarDownload](../../assets/img/user-toolbarDownload.png) + + + +## Layout Selector +Please see the `Viewport` section for details. + + +## More Tools Menu +- Reset View: Resets all image manipulation such as position, zoom, and W/L +- Rotate Right: Flips the image 90 degrees clockwise +- Flip Horizontally: Flips the image 180 degrees horizontally +- Stack Scroll: Links all viewports containing images to scroll together +- Magnify: Click on an image to magnify a particular area of interest +- Invert: Inverts the color scale +- Cine: Toggles the Cine player control in the currently selected viewport. Click the `x` on the Cine player or click the tool again to toggle off. +- Angle: Measures an adjustable angle on an image +- Probe: Drag the probe to see pixel values +- Rectangle: Measures a rectangular area in mm^2 and HU + +When a tool is selected from the `More Tools` menu, it becomes the active tool until it is replaced by clicking on a different tool in the More Tools menu or main toolbar. + + +## Overview Video +An overview of tool usage can been seen below: + + +
+ +
diff --git a/platform/docs/versioned_docs/version-3.9/user-guide/viewer/viewport.md b/platform/docs/versioned_docs/version-3.9/user-guide/viewer/viewport.md new file mode 100644 index 0000000..1bfc693 --- /dev/null +++ b/platform/docs/versioned_docs/version-3.9/user-guide/viewer/viewport.md @@ -0,0 +1,39 @@ +--- +sidebar_position: 5 +--- + +# Viewport + +Image visualization happens at the viewport which contains canvas or canvases that +renders series. + +![user-viewer-main](../../assets/img/user-viewer-main.png) + + +By default, you can modify: + +- Zoom: right click dragging up or down +- Contrast/brightness: left click dragging up/down to change contrast, and left/right for changing brightness +- Pan: middle click dragging + + +## Changing Series for display +To change the displayed series, you can drag and drop the desired series from the left panel. Start, by dragging the thumbnail of the series, and drop it on the viewport. + +## Changing Layout +If you click on the layout icon on the toolbar, you can use the layout selector UI. After changing the layout, you can select studies for each new viewport by dragging and dropping in to the viewport. + +After changing the layout from 1x1, you will see each viewport gets tagged by a letter, +which matches its series section in the study list. + + +![user-viewer-layout](../../assets/img/user-viewer-layout.png) + + +## Overview Video +An overview of viewport layout change, and manipulation can be seen below: + + +
+ +
diff --git a/platform/docs/versioned_sidebars/version-3.8-sidebars.json b/platform/docs/versioned_sidebars/version-3.8-sidebars.json new file mode 100644 index 0000000..caea0c0 --- /dev/null +++ b/platform/docs/versioned_sidebars/version-3.8-sidebars.json @@ -0,0 +1,8 @@ +{ + "tutorialSidebar": [ + { + "type": "autogenerated", + "dirName": "." + } + ] +} diff --git a/platform/docs/versioned_sidebars/version-3.9-sidebars.json b/platform/docs/versioned_sidebars/version-3.9-sidebars.json new file mode 100644 index 0000000..caea0c0 --- /dev/null +++ b/platform/docs/versioned_sidebars/version-3.9-sidebars.json @@ -0,0 +1,8 @@ +{ + "tutorialSidebar": [ + { + "type": "autogenerated", + "dirName": "." + } + ] +} diff --git a/platform/docs/versions.json b/platform/docs/versions.json new file mode 100644 index 0000000..2c682a5 --- /dev/null +++ b/platform/docs/versions.json @@ -0,0 +1 @@ +["3.9"] diff --git a/platform/docs/yarn.lock b/platform/docs/yarn.lock new file mode 100644 index 0000000..0ace9db --- /dev/null +++ b/platform/docs/yarn.lock @@ -0,0 +1,12722 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@algolia/autocomplete-core@1.17.9": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.17.9.tgz#83374c47dc72482aa45d6b953e89377047f0dcdc" + integrity sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ== + dependencies: + "@algolia/autocomplete-plugin-algolia-insights" "1.17.9" + "@algolia/autocomplete-shared" "1.17.9" + +"@algolia/autocomplete-plugin-algolia-insights@1.17.9": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.9.tgz#74c86024d09d09e8bfa3dd90b844b77d9f9947b6" + integrity sha512-u1fEHkCbWF92DBeB/KHeMacsjsoI0wFhjZtlCq2ddZbAehshbZST6Hs0Avkc0s+4UyBGbMDnSuXHLuvRWK5iDQ== + dependencies: + "@algolia/autocomplete-shared" "1.17.9" + +"@algolia/autocomplete-preset-algolia@1.17.9": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.9.tgz#911f3250544eb8ea4096fcfb268f156b085321b5" + integrity sha512-Na1OuceSJeg8j7ZWn5ssMu/Ax3amtOwk76u4h5J4eK2Nx2KB5qt0Z4cOapCsxot9VcEN11ADV5aUSlQF4RhGjQ== + dependencies: + "@algolia/autocomplete-shared" "1.17.9" + +"@algolia/autocomplete-shared@1.17.9": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.9.tgz#5f38868f7cb1d54b014b17a10fc4f7e79d427fa8" + integrity sha512-iDf05JDQ7I0b7JEA/9IektxN/80a2MZ1ToohfmNS3rfeuQnIKI3IJlIafD0xu4StbtQTghx9T3Maa97ytkXenQ== + +"@algolia/client-abtesting@5.19.0": + version "5.19.0" + resolved "https://registry.yarnpkg.com/@algolia/client-abtesting/-/client-abtesting-5.19.0.tgz#0a6e73da05decc8f1bbcd7e5b9a82a8d876e7bf5" + integrity sha512-dMHwy2+nBL0SnIsC1iHvkBao64h4z+roGelOz11cxrDBrAdASxLxmfVMop8gmodQ2yZSacX0Rzevtxa+9SqxCw== + dependencies: + "@algolia/client-common" "5.19.0" + "@algolia/requester-browser-xhr" "5.19.0" + "@algolia/requester-fetch" "5.19.0" + "@algolia/requester-node-http" "5.19.0" + +"@algolia/client-abtesting@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@algolia/client-abtesting/-/client-abtesting-5.20.0.tgz#984472e4ae911285a8e3be2b81c121108f87a179" + integrity sha512-YaEoNc1Xf2Yk6oCfXXkZ4+dIPLulCx8Ivqj0OsdkHWnsI3aOJChY5qsfyHhDBNSOhqn2ilgHWxSfyZrjxBcAww== + dependencies: + "@algolia/client-common" "5.20.0" + "@algolia/requester-browser-xhr" "5.20.0" + "@algolia/requester-fetch" "5.20.0" + "@algolia/requester-node-http" "5.20.0" + +"@algolia/client-analytics@5.19.0": + version "5.19.0" + resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-5.19.0.tgz#45e33343fd4517e05a340a97bb37bebb4466000e" + integrity sha512-CDW4RwnCHzU10upPJqS6N6YwDpDHno7w6/qXT9KPbPbt8szIIzCHrva4O9KIfx1OhdsHzfGSI5hMAiOOYl4DEQ== + dependencies: + "@algolia/client-common" "5.19.0" + "@algolia/requester-browser-xhr" "5.19.0" + "@algolia/requester-fetch" "5.19.0" + "@algolia/requester-node-http" "5.19.0" + +"@algolia/client-analytics@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-5.20.0.tgz#25944c8c7bcc06a16ae3b26ddf86d0d18f984349" + integrity sha512-CIT9ni0+5sYwqehw+t5cesjho3ugKQjPVy/iPiJvtJX4g8Cdb6je6SPt2uX72cf2ISiXCAX9U3cY0nN0efnRDw== + dependencies: + "@algolia/client-common" "5.20.0" + "@algolia/requester-browser-xhr" "5.20.0" + "@algolia/requester-fetch" "5.20.0" + "@algolia/requester-node-http" "5.20.0" + +"@algolia/client-common@5.19.0": + version "5.19.0" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.19.0.tgz#efddaaf28f0f478117c2aab22d19c99b06f99761" + integrity sha512-2ERRbICHXvtj5kfFpY5r8qu9pJII/NAHsdgUXnUitQFwPdPL7wXiupcvZJC7DSntOnE8AE0lM7oDsPhrJfj5nQ== + +"@algolia/client-common@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.20.0.tgz#0b6b96c779d30afada68cf36f20f0c280e3f1273" + integrity sha512-iSTFT3IU8KNpbAHcBUJw2HUrPnMXeXLyGajmCL7gIzWOsYM4GabZDHXOFx93WGiXMti1dymz8k8R+bfHv1YZmA== + +"@algolia/client-insights@5.19.0": + version "5.19.0" + resolved "https://registry.yarnpkg.com/@algolia/client-insights/-/client-insights-5.19.0.tgz#81ff8eb3df724f6dd8ea3f423966b9ef7d36f903" + integrity sha512-xPOiGjo6I9mfjdJO7Y+p035aWePcbsItizIp+qVyfkfZiGgD+TbNxM12g7QhFAHIkx/mlYaocxPY/TmwPzTe+A== + dependencies: + "@algolia/client-common" "5.19.0" + "@algolia/requester-browser-xhr" "5.19.0" + "@algolia/requester-fetch" "5.19.0" + "@algolia/requester-node-http" "5.19.0" + +"@algolia/client-insights@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@algolia/client-insights/-/client-insights-5.20.0.tgz#37b59043a86423dd283d05909faea06e4eff026b" + integrity sha512-w9RIojD45z1csvW1vZmAko82fqE/Dm+Ovsy2ElTsjFDB0HMAiLh2FO86hMHbEXDPz6GhHKgGNmBRiRP8dDPgJg== + dependencies: + "@algolia/client-common" "5.20.0" + "@algolia/requester-browser-xhr" "5.20.0" + "@algolia/requester-fetch" "5.20.0" + "@algolia/requester-node-http" "5.20.0" + +"@algolia/client-personalization@5.19.0": + version "5.19.0" + resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-5.19.0.tgz#9a75230b9dec490a1e0851539a40a9371c8cd987" + integrity sha512-B9eoce/fk8NLboGje+pMr72pw+PV7c5Z01On477heTZ7jkxoZ4X92dobeGuEQop61cJ93Gaevd1of4mBr4hu2A== + dependencies: + "@algolia/client-common" "5.19.0" + "@algolia/requester-browser-xhr" "5.19.0" + "@algolia/requester-fetch" "5.19.0" + "@algolia/requester-node-http" "5.19.0" + +"@algolia/client-personalization@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-5.20.0.tgz#d10da6d798f9a5f6cf239c57b9a850deb29e5683" + integrity sha512-p/hftHhrbiHaEcxubYOzqVV4gUqYWLpTwK+nl2xN3eTrSW9SNuFlAvUBFqPXSVBqc6J5XL9dNKn3y8OA1KElSQ== + dependencies: + "@algolia/client-common" "5.20.0" + "@algolia/requester-browser-xhr" "5.20.0" + "@algolia/requester-fetch" "5.20.0" + "@algolia/requester-node-http" "5.20.0" + +"@algolia/client-query-suggestions@5.19.0": + version "5.19.0" + resolved "https://registry.yarnpkg.com/@algolia/client-query-suggestions/-/client-query-suggestions-5.19.0.tgz#007d1b09818d6a225fbfdf93bbcb2edf8ab17da0" + integrity sha512-6fcP8d4S8XRDtVogrDvmSM6g5g6DndLc0pEm1GCKe9/ZkAzCmM3ZmW1wFYYPxdjMeifWy1vVEDMJK7sbE4W7MA== + dependencies: + "@algolia/client-common" "5.19.0" + "@algolia/requester-browser-xhr" "5.19.0" + "@algolia/requester-fetch" "5.19.0" + "@algolia/requester-node-http" "5.19.0" + +"@algolia/client-query-suggestions@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@algolia/client-query-suggestions/-/client-query-suggestions-5.20.0.tgz#1d4f1d638f857fad202cee7feecd3ffc270d9c60" + integrity sha512-m4aAuis5vZi7P4gTfiEs6YPrk/9hNTESj3gEmGFgfJw3hO2ubdS4jSId1URd6dGdt0ax2QuapXufcrN58hPUcw== + dependencies: + "@algolia/client-common" "5.20.0" + "@algolia/requester-browser-xhr" "5.20.0" + "@algolia/requester-fetch" "5.20.0" + "@algolia/requester-node-http" "5.20.0" + +"@algolia/client-search@5.19.0": + version "5.19.0" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-5.19.0.tgz#04fc5d7e26d41c99144eb33eedb0ea6f9b1c0056" + integrity sha512-Ctg3xXD/1VtcwmkulR5+cKGOMj4r0wC49Y/KZdGQcqpydKn+e86F6l3tb3utLJQVq4lpEJud6kdRykFgcNsp8Q== + dependencies: + "@algolia/client-common" "5.19.0" + "@algolia/requester-browser-xhr" "5.19.0" + "@algolia/requester-fetch" "5.19.0" + "@algolia/requester-node-http" "5.19.0" + +"@algolia/client-search@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-5.20.0.tgz#4b847bda4bef2eee8ba72ef3ce59be612319e8d0" + integrity sha512-KL1zWTzrlN4MSiaK1ea560iCA/UewMbS4ZsLQRPoDTWyrbDKVbztkPwwv764LAqgXk0fvkNZvJ3IelcK7DqhjQ== + dependencies: + "@algolia/client-common" "5.20.0" + "@algolia/requester-browser-xhr" "5.20.0" + "@algolia/requester-fetch" "5.20.0" + "@algolia/requester-node-http" "5.20.0" + +"@algolia/events@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@algolia/events/-/events-4.0.1.tgz#fd39e7477e7bc703d7f893b556f676c032af3950" + integrity sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ== + +"@algolia/ingestion@1.19.0": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@algolia/ingestion/-/ingestion-1.19.0.tgz#b481bd2283866a1df18af9babba0ecb3f1d1d675" + integrity sha512-LO7w1MDV+ZLESwfPmXkp+KLeYeFrYEgtbCZG6buWjddhYraPQ9MuQWLhLLiaMlKxZ/sZvFTcZYuyI6Jx4WBhcg== + dependencies: + "@algolia/client-common" "5.19.0" + "@algolia/requester-browser-xhr" "5.19.0" + "@algolia/requester-fetch" "5.19.0" + "@algolia/requester-node-http" "5.19.0" + +"@algolia/ingestion@1.20.0": + version "1.20.0" + resolved "https://registry.yarnpkg.com/@algolia/ingestion/-/ingestion-1.20.0.tgz#b91849fe4a8efed21c048a0a69ad77934d2fc3fd" + integrity sha512-shj2lTdzl9un4XJblrgqg54DoK6JeKFO8K8qInMu4XhE2JuB8De6PUuXAQwiRigZupbI0xq8aM0LKdc9+qiLQA== + dependencies: + "@algolia/client-common" "5.20.0" + "@algolia/requester-browser-xhr" "5.20.0" + "@algolia/requester-fetch" "5.20.0" + "@algolia/requester-node-http" "5.20.0" + +"@algolia/monitoring@1.19.0": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@algolia/monitoring/-/monitoring-1.19.0.tgz#abc85ac073c25233c7f8dae3000cc0821d582514" + integrity sha512-Mg4uoS0aIKeTpu6iv6O0Hj81s8UHagi5TLm9k2mLIib4vmMtX7WgIAHAcFIaqIZp5D6s5EVy1BaDOoZ7buuJHA== + dependencies: + "@algolia/client-common" "5.19.0" + "@algolia/requester-browser-xhr" "5.19.0" + "@algolia/requester-fetch" "5.19.0" + "@algolia/requester-node-http" "5.19.0" + +"@algolia/monitoring@1.20.0": + version "1.20.0" + resolved "https://registry.yarnpkg.com/@algolia/monitoring/-/monitoring-1.20.0.tgz#5b3a7964b08a91b1c71466bf5adb8a1597e3134b" + integrity sha512-aF9blPwOhKtWvkjyyXh9P5peqmhCA1XxLBRgItT+K6pbT0q4hBDQrCid+pQZJYy4HFUKjB/NDDwyzFhj/rwKhw== + dependencies: + "@algolia/client-common" "5.20.0" + "@algolia/requester-browser-xhr" "5.20.0" + "@algolia/requester-fetch" "5.20.0" + "@algolia/requester-node-http" "5.20.0" + +"@algolia/recommend@5.19.0": + version "5.19.0" + resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-5.19.0.tgz#5898219e9457853c563eb527f0d1cbfcb8998c87" + integrity sha512-PbgrMTbUPlmwfJsxjFhal4XqZO2kpBNRjemLVTkUiti4w/+kzcYO4Hg5zaBgVqPwvFDNQ8JS4SS3TBBem88u+g== + dependencies: + "@algolia/client-common" "5.19.0" + "@algolia/requester-browser-xhr" "5.19.0" + "@algolia/requester-fetch" "5.19.0" + "@algolia/requester-node-http" "5.19.0" + +"@algolia/recommend@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-5.20.0.tgz#49f8f8d31f815b107c8ebd1c35220d90b22fd876" + integrity sha512-T6B/WPdZR3b89/F9Vvk6QCbt/wrLAtrGoL8z4qPXDFApQ8MuTFWbleN/4rHn6APWO3ps+BUePIEbue2rY5MlRw== + dependencies: + "@algolia/client-common" "5.20.0" + "@algolia/requester-browser-xhr" "5.20.0" + "@algolia/requester-fetch" "5.20.0" + "@algolia/requester-node-http" "5.20.0" + +"@algolia/requester-browser-xhr@5.19.0": + version "5.19.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.19.0.tgz#979a340a81a381214c0dbdd235b51204098e3b4a" + integrity sha512-GfnhnQBT23mW/VMNs7m1qyEyZzhZz093aY2x8p0era96MMyNv8+FxGek5pjVX0b57tmSCZPf4EqNCpkGcGsmbw== + dependencies: + "@algolia/client-common" "5.19.0" + +"@algolia/requester-browser-xhr@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.20.0.tgz#998fd5c1123fbc49b664c484c6b0cd7cefc6a1fa" + integrity sha512-t6//lXsq8E85JMenHrI6mhViipUT5riNhEfCcvtRsTV+KIBpC6Od18eK864dmBhoc5MubM0f+sGpKOqJIlBSCg== + dependencies: + "@algolia/client-common" "5.20.0" + +"@algolia/requester-fetch@5.19.0": + version "5.19.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-fetch/-/requester-fetch-5.19.0.tgz#59fe52733a718fc23bde548b377b52baf7228993" + integrity sha512-oyTt8ZJ4T4fYvW5avAnuEc6Laedcme9fAFryMD9ndUTIUe/P0kn3BuGcCLFjN3FDmdrETHSFkgPPf1hGy3sLCw== + dependencies: + "@algolia/client-common" "5.19.0" + +"@algolia/requester-fetch@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-fetch/-/requester-fetch-5.20.0.tgz#fed4f135f22c246ce40cf23c9d6518884be43e5e" + integrity sha512-FHxYGqRY+6bgjKsK4aUsTAg6xMs2S21elPe4Y50GB0Y041ihvw41Vlwy2QS6K9ldoftX4JvXodbKTcmuQxywdQ== + dependencies: + "@algolia/client-common" "5.20.0" + +"@algolia/requester-node-http@5.19.0": + version "5.19.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-5.19.0.tgz#edbd58158d9dec774d608fbf2b2196d0ca4b257c" + integrity sha512-p6t8ue0XZNjcRiqNkb5QAM0qQRAKsCiebZ6n9JjWA+p8fWf8BvnhO55y2fO28g3GW0Imj7PrAuyBuxq8aDVQwQ== + dependencies: + "@algolia/client-common" "5.19.0" + +"@algolia/requester-node-http@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-5.20.0.tgz#920a9488be07c0521951da92f36be61f47c4d0e0" + integrity sha512-kmtQClq/w3vtPteDSPvaW9SPZL/xrIgMrxZyAgsFwrJk0vJxqyC5/hwHmrCraDnStnGSADnLpBf4SpZnwnkwWw== + dependencies: + "@algolia/client-common" "5.20.0" + +"@alloc/quick-lru@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" + integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== + +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@apideck/better-ajv-errors@^0.3.1": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz#957d4c28e886a64a8141f7522783be65733ff097" + integrity sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA== + dependencies: + json-schema "^0.4.0" + jsonpointer "^5.0.0" + leven "^3.1.0" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2", "@babel/code-frame@^7.8.3": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.26.0", "@babel/compat-data@^7.26.5": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.5.tgz#df93ac37f4417854130e21d72c66ff3d4b897fc7" + integrity sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg== + +"@babel/core@^7.12.3", "@babel/core@^7.21.3", "@babel/core@^7.24.4", "@babel/core@^7.25.9": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40" + integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.26.0" + "@babel/generator" "^7.26.0" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-module-transforms" "^7.26.0" + "@babel/helpers" "^7.26.0" + "@babel/parser" "^7.26.0" + "@babel/template" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.26.0" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.25.9", "@babel/generator@^7.26.0", "@babel/generator@^7.26.5": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.5.tgz#e44d4ab3176bbcaf78a5725da5f1dc28802a9458" + integrity sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw== + dependencies: + "@babel/parser" "^7.26.5" + "@babel/types" "^7.26.5" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + +"@babel/helper-annotate-as-pure@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz#d8eac4d2dc0d7b6e11fa6e535332e0d3184f06b4" + integrity sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g== + dependencies: + "@babel/types" "^7.25.9" + +"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.25.9": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz#75d92bb8d8d51301c0d49e52a65c9a7fe94514d8" + integrity sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA== + dependencies: + "@babel/compat-data" "^7.26.5" + "@babel/helper-validator-option" "^7.25.9" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz#7644147706bb90ff613297d49ed5266bde729f83" + integrity sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-member-expression-to-functions" "^7.25.9" + "@babel/helper-optimise-call-expression" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/traverse" "^7.25.9" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.25.9": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz#5169756ecbe1d95f7866b90bb555b022595302a0" + integrity sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + regexpu-core "^6.2.0" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.6.2", "@babel/helper-define-polyfill-provider@^0.6.3": + version "0.6.3" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz#f4f2792fae2ef382074bc2d713522cf24e6ddb21" + integrity sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + +"@babel/helper-member-expression-to-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz#9dfffe46f727005a5ea29051ac835fb735e4c1a3" + integrity sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" + integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-module-transforms@^7.25.9", "@babel/helper-module-transforms@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" + integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== + dependencies: + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/helper-optimise-call-expression@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz#3324ae50bae7e2ab3c33f60c9a877b6a0146b54e" + integrity sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ== + dependencies: + "@babel/types" "^7.25.9" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.25.9", "@babel/helper-plugin-utils@^7.26.5", "@babel/helper-plugin-utils@^7.8.0": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz#18580d00c9934117ad719392c4f6585c9333cc35" + integrity sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg== + +"@babel/helper-remap-async-to-generator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz#e53956ab3d5b9fb88be04b3e2f31b523afd34b92" + integrity sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-wrap-function" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/helper-replace-supers@^7.25.9": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz#6cb04e82ae291dae8e72335dfe438b0725f14c8d" + integrity sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.25.9" + "@babel/helper-optimise-call-expression" "^7.25.9" + "@babel/traverse" "^7.26.5" + +"@babel/helper-skip-transparent-expression-wrappers@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz#0b2e1b62d560d6b1954893fd2b705dc17c91f0c9" + integrity sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/helper-validator-option@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" + integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== + +"@babel/helper-wrap-function@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz#d99dfd595312e6c894bd7d237470025c85eea9d0" + integrity sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g== + dependencies: + "@babel/template" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helpers@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.0.tgz#30e621f1eba5aa45fe6f4868d2e9154d884119a4" + integrity sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw== + dependencies: + "@babel/template" "^7.25.9" + "@babel/types" "^7.26.0" + +"@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.5": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.5.tgz#6fec9aebddef25ca57a935c86dbb915ae2da3e1f" + integrity sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw== + dependencies: + "@babel/types" "^7.26.5" + +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz#cc2e53ebf0a0340777fff5ed521943e253b4d8fe" + integrity sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz#af9e4fb63ccb8abcb92375b2fcfe36b60c774d30" + integrity sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz#e8dc26fcd616e6c5bf2bd0d5a2c151d4f92a9137" + integrity sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz#807a667f9158acac6f6164b4beb85ad9ebc9e1d1" + integrity sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/plugin-transform-optional-chaining" "^7.25.9" + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz#de7093f1e7deaf68eadd7cc6b07f2ab82543269e" + integrity sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-import-assertions@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz#620412405058efa56e4a564903b79355020f445f" + integrity sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-syntax-import-attributes@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz#3b1412847699eea739b4f2602c74ce36f6b0b0f7" + integrity sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-syntax-jsx@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz#a34313a178ea56f1951599b929c1ceacee719290" + integrity sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-syntax-typescript@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz#67dda2b74da43727cf21d46cf9afef23f4365399" + integrity sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-arrow-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz#7821d4410bee5daaadbb4cdd9a6649704e176845" + integrity sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-async-generator-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz#1b18530b077d18a407c494eb3d1d72da505283a2" + integrity sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-remap-async-to-generator" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/plugin-transform-async-to-generator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz#c80008dacae51482793e5a9c08b39a5be7e12d71" + integrity sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ== + dependencies: + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-remap-async-to-generator" "^7.25.9" + +"@babel/plugin-transform-block-scoped-functions@^7.25.9": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz#3dc4405d31ad1cbe45293aa57205a6e3b009d53e" + integrity sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ== + dependencies: + "@babel/helper-plugin-utils" "^7.26.5" + +"@babel/plugin-transform-block-scoping@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz#c33665e46b06759c93687ca0f84395b80c0473a1" + integrity sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-class-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz#a8ce84fedb9ad512549984101fa84080a9f5f51f" + integrity sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-class-static-block@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz#6c8da219f4eb15cae9834ec4348ff8e9e09664a0" + integrity sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-classes@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz#7152457f7880b593a63ade8a861e6e26a4469f52" + integrity sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" + "@babel/traverse" "^7.25.9" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz#db36492c78460e534b8852b1d5befe3c923ef10b" + integrity sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/template" "^7.25.9" + +"@babel/plugin-transform-destructuring@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz#966ea2595c498224340883602d3cfd7a0c79cea1" + integrity sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-dotall-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz#bad7945dd07734ca52fe3ad4e872b40ed09bb09a" + integrity sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-duplicate-keys@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz#8850ddf57dce2aebb4394bb434a7598031059e6d" + integrity sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz#6f7259b4de127721a08f1e5165b852fcaa696d31" + integrity sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-dynamic-import@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz#23e917de63ed23c6600c5dd06d94669dce79f7b8" + integrity sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-exponentiation-operator@^7.25.9": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz#e29f01b6de302c7c2c794277a48f04a9ca7f03bc" + integrity sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-export-namespace-from@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz#90745fe55053394f554e40584cda81f2c8a402a2" + integrity sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-for-of@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz#4bdc7d42a213397905d89f02350c5267866d5755" + integrity sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + +"@babel/plugin-transform-function-name@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz#939d956e68a606661005bfd550c4fc2ef95f7b97" + integrity sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA== + dependencies: + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/plugin-transform-json-strings@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz#c86db407cb827cded902a90c707d2781aaa89660" + integrity sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz#1a1c6b4d4aa59bc4cad5b6b3a223a0abd685c9de" + integrity sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-logical-assignment-operators@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz#b19441a8c39a2fda0902900b306ea05ae1055db7" + integrity sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-member-expression-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz#63dff19763ea64a31f5e6c20957e6a25e41ed5de" + integrity sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-modules-amd@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz#49ba478f2295101544abd794486cd3088dddb6c5" + integrity sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw== + dependencies: + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-modules-commonjs@^7.25.9": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz#8f011d44b20d02c3de44d8850d971d8497f981fb" + integrity sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ== + dependencies: + "@babel/helper-module-transforms" "^7.26.0" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-modules-systemjs@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz#8bd1b43836269e3d33307151a114bcf3ba6793f8" + integrity sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA== + dependencies: + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/plugin-transform-modules-umd@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz#6710079cdd7c694db36529a1e8411e49fcbf14c9" + integrity sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw== + dependencies: + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz#454990ae6cc22fd2a0fa60b3a2c6f63a38064e6a" + integrity sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-new-target@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz#42e61711294b105c248336dcb04b77054ea8becd" + integrity sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.25.9": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.5.tgz#b0e8943a8a4689c55e91eac573b1fe6bc105026a" + integrity sha512-OHqczNm4NTQlW1ghrVY43FPoiRzbmzNVbcgVnMKZN/RQYezHUSdjACjaX50CD3B7UIAjv39+MlsrVDb3v741FA== + dependencies: + "@babel/helper-plugin-utils" "^7.26.5" + +"@babel/plugin-transform-numeric-separator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz#bfed75866261a8b643468b0ccfd275f2033214a1" + integrity sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-object-rest-spread@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz#0203725025074164808bcf1a2cfa90c652c99f18" + integrity sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg== + dependencies: + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-transform-parameters" "^7.25.9" + +"@babel/plugin-transform-object-super@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz#385d5de135162933beb4a3d227a2b7e52bb4cf03" + integrity sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" + +"@babel/plugin-transform-optional-catch-binding@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz#10e70d96d52bb1f10c5caaac59ac545ea2ba7ff3" + integrity sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-optional-chaining@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz#e142eb899d26ef715435f201ab6e139541eee7dd" + integrity sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + +"@babel/plugin-transform-parameters@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz#b856842205b3e77e18b7a7a1b94958069c7ba257" + integrity sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-private-methods@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz#847f4139263577526455d7d3223cd8bda51e3b57" + integrity sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-private-property-in-object@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz#9c8b73e64e6cc3cbb2743633885a7dd2c385fe33" + integrity sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-property-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz#d72d588bd88b0dec8b62e36f6fda91cedfe28e3f" + integrity sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-react-constant-elements@^7.12.1", "@babel/plugin-transform-react-constant-elements@^7.21.3": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.25.9.tgz#08a1de35a301929b60fdf2788a54b46cd8ecd0ef" + integrity sha512-Ncw2JFsJVuvfRsa2lSHiC55kETQVLSnsYGQ1JDDwkUeWGTL/8Tom8aLTnlqgoeuopWrbbGndrc9AlLYrIosrow== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-react-display-name@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.9.tgz#4b79746b59efa1f38c8695065a92a9f5afb24f7d" + integrity sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-react-jsx-development@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.9.tgz#8fd220a77dd139c07e25225a903b8be8c829e0d7" + integrity sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.25.9" + +"@babel/plugin-transform-react-jsx@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.9.tgz#06367940d8325b36edff5e2b9cbe782947ca4166" + integrity sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-syntax-jsx" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/plugin-transform-react-pure-annotations@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.9.tgz#ea1c11b2f9dbb8e2d97025f43a3b5bc47e18ae62" + integrity sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-regenerator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz#03a8a4670d6cebae95305ac6defac81ece77740b" + integrity sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + regenerator-transform "^0.15.2" + +"@babel/plugin-transform-regexp-modifiers@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz#2f5837a5b5cd3842a919d8147e9903cc7455b850" + integrity sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-reserved-words@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz#0398aed2f1f10ba3f78a93db219b27ef417fb9ce" + integrity sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-runtime@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.9.tgz#62723ea3f5b31ffbe676da9d6dae17138ae580ea" + integrity sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ== + dependencies: + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.10.6" + babel-plugin-polyfill-regenerator "^0.6.1" + semver "^6.3.1" + +"@babel/plugin-transform-shorthand-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz#bb785e6091f99f826a95f9894fc16fde61c163f2" + integrity sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-spread@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz#24a35153931b4ba3d13cec4a7748c21ab5514ef9" + integrity sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + +"@babel/plugin-transform-sticky-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz#c7f02b944e986a417817b20ba2c504dfc1453d32" + integrity sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-template-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz#6dbd4a24e8fad024df76d1fac6a03cf413f60fe1" + integrity sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-typeof-symbol@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz#224ba48a92869ddbf81f9b4a5f1204bbf5a2bc4b" + integrity sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-typescript@^7.25.9": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.5.tgz#6d9b48e8ee40a45a3ed12ebc013449fdf261714c" + integrity sha512-GJhPO0y8SD5EYVCy2Zr+9dSZcEgaSmq5BLR0Oc25TOEhC+ba49vUAGZFjy8v79z9E1mdldq4x9d1xgh4L1d5dQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.26.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/plugin-syntax-typescript" "^7.25.9" + +"@babel/plugin-transform-unicode-escapes@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz#a75ef3947ce15363fccaa38e2dd9bc70b2788b82" + integrity sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-unicode-property-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz#a901e96f2c1d071b0d1bb5dc0d3c880ce8f53dd3" + integrity sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-unicode-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz#5eae747fe39eacf13a8bd006a4fb0b5d1fa5e9b1" + integrity sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-unicode-sets-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz#65114c17b4ffc20fa5b163c63c70c0d25621fabe" + integrity sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.12.1", "@babel/preset-env@^7.20.2", "@babel/preset-env@^7.25.9": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.26.0.tgz#30e5c6bc1bcc54865bff0c5a30f6d4ccdc7fa8b1" + integrity sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw== + dependencies: + "@babel/compat-data" "^7.26.0" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.9" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.9" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.9" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.25.9" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.9" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-import-assertions" "^7.26.0" + "@babel/plugin-syntax-import-attributes" "^7.26.0" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.25.9" + "@babel/plugin-transform-async-generator-functions" "^7.25.9" + "@babel/plugin-transform-async-to-generator" "^7.25.9" + "@babel/plugin-transform-block-scoped-functions" "^7.25.9" + "@babel/plugin-transform-block-scoping" "^7.25.9" + "@babel/plugin-transform-class-properties" "^7.25.9" + "@babel/plugin-transform-class-static-block" "^7.26.0" + "@babel/plugin-transform-classes" "^7.25.9" + "@babel/plugin-transform-computed-properties" "^7.25.9" + "@babel/plugin-transform-destructuring" "^7.25.9" + "@babel/plugin-transform-dotall-regex" "^7.25.9" + "@babel/plugin-transform-duplicate-keys" "^7.25.9" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.9" + "@babel/plugin-transform-dynamic-import" "^7.25.9" + "@babel/plugin-transform-exponentiation-operator" "^7.25.9" + "@babel/plugin-transform-export-namespace-from" "^7.25.9" + "@babel/plugin-transform-for-of" "^7.25.9" + "@babel/plugin-transform-function-name" "^7.25.9" + "@babel/plugin-transform-json-strings" "^7.25.9" + "@babel/plugin-transform-literals" "^7.25.9" + "@babel/plugin-transform-logical-assignment-operators" "^7.25.9" + "@babel/plugin-transform-member-expression-literals" "^7.25.9" + "@babel/plugin-transform-modules-amd" "^7.25.9" + "@babel/plugin-transform-modules-commonjs" "^7.25.9" + "@babel/plugin-transform-modules-systemjs" "^7.25.9" + "@babel/plugin-transform-modules-umd" "^7.25.9" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.25.9" + "@babel/plugin-transform-new-target" "^7.25.9" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.25.9" + "@babel/plugin-transform-numeric-separator" "^7.25.9" + "@babel/plugin-transform-object-rest-spread" "^7.25.9" + "@babel/plugin-transform-object-super" "^7.25.9" + "@babel/plugin-transform-optional-catch-binding" "^7.25.9" + "@babel/plugin-transform-optional-chaining" "^7.25.9" + "@babel/plugin-transform-parameters" "^7.25.9" + "@babel/plugin-transform-private-methods" "^7.25.9" + "@babel/plugin-transform-private-property-in-object" "^7.25.9" + "@babel/plugin-transform-property-literals" "^7.25.9" + "@babel/plugin-transform-regenerator" "^7.25.9" + "@babel/plugin-transform-regexp-modifiers" "^7.26.0" + "@babel/plugin-transform-reserved-words" "^7.25.9" + "@babel/plugin-transform-shorthand-properties" "^7.25.9" + "@babel/plugin-transform-spread" "^7.25.9" + "@babel/plugin-transform-sticky-regex" "^7.25.9" + "@babel/plugin-transform-template-literals" "^7.25.9" + "@babel/plugin-transform-typeof-symbol" "^7.25.9" + "@babel/plugin-transform-unicode-escapes" "^7.25.9" + "@babel/plugin-transform-unicode-property-regex" "^7.25.9" + "@babel/plugin-transform-unicode-regex" "^7.25.9" + "@babel/plugin-transform-unicode-sets-regex" "^7.25.9" + "@babel/preset-modules" "0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.10.6" + babel-plugin-polyfill-regenerator "^0.6.1" + core-js-compat "^3.38.1" + semver "^6.3.1" + +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/preset-react@^7.12.5", "@babel/preset-react@^7.18.6", "@babel/preset-react@^7.25.9": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.26.3.tgz#7c5e028d623b4683c1f83a0bd4713b9100560caa" + integrity sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + "@babel/plugin-transform-react-display-name" "^7.25.9" + "@babel/plugin-transform-react-jsx" "^7.25.9" + "@babel/plugin-transform-react-jsx-development" "^7.25.9" + "@babel/plugin-transform-react-pure-annotations" "^7.25.9" + +"@babel/preset-typescript@^7.21.0", "@babel/preset-typescript@^7.25.9": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz#4a570f1b8d104a242d923957ffa1eaff142a106d" + integrity sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + "@babel/plugin-syntax-jsx" "^7.25.9" + "@babel/plugin-transform-modules-commonjs" "^7.25.9" + "@babel/plugin-transform-typescript" "^7.25.9" + +"@babel/runtime-corejs3@^7.25.9": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.26.0.tgz#5af6bed16073eb4a0191233d61e158a5c768c430" + integrity sha512-YXHu5lN8kJCb1LOb9PgV6pvak43X2h4HvRApcN5SdWeaItQOzfn1hgP6jasD6KWQyJDBxrVmA9o9OivlnNJK/w== + dependencies: + core-js-pure "^3.30.2" + regenerator-runtime "^0.14.0" + +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.25.9", "@babel/runtime@^7.8.4": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/template@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" + integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== + dependencies: + "@babel/code-frame" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.5": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.5.tgz#6d0be3e772ff786456c1a37538208286f6e79021" + integrity sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.5" + "@babel/parser" "^7.26.5" + "@babel/template" "^7.25.9" + "@babel/types" "^7.26.5" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.12.6", "@babel/types@^7.21.3", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.5", "@babel/types@^7.4.4": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.5.tgz#7a1e1c01d28e26d1fe7f8ec9567b3b92b9d07747" + integrity sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@csstools/cascade-layer-name-parser@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.4.tgz#64d128529397aa1e1c986f685713363b262b81b1" + integrity sha512-7DFHlPuIxviKYZrOiwVU/PiHLm3lLUR23OMuEEtfEOQTOp9hzQ2JjdY6X5H18RVuUPJqSCI+qNnD5iOLMVE0bA== + +"@csstools/color-helpers@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-5.0.1.tgz#829f1c76f5800b79c51c709e2f36821b728e0e10" + integrity sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA== + +"@csstools/css-calc@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-2.1.1.tgz#a7dbc66627f5cf458d42aed14bda0d3860562383" + integrity sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag== + +"@csstools/css-color-parser@^3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-3.0.7.tgz#442d61d58e54ad258d52c309a787fceb33906484" + integrity sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA== + dependencies: + "@csstools/color-helpers" "^5.0.1" + "@csstools/css-calc" "^2.1.1" + +"@csstools/css-parser-algorithms@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz#74426e93bd1c4dcab3e441f5cc7ba4fb35d94356" + integrity sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A== + +"@csstools/css-tokenizer@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz#a5502c8539265fecbd873c1e395a890339f119c2" + integrity sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw== + +"@csstools/media-query-list-parser@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.2.tgz#e80e17eba1693fceafb8d6f2cfc68c0e7a9ab78a" + integrity sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A== + +"@csstools/postcss-cascade-layers@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz#8a997edf97d34071dd2e37ea6022447dd9e795ad" + integrity sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA== + dependencies: + "@csstools/selector-specificity" "^2.0.2" + postcss-selector-parser "^6.0.10" + +"@csstools/postcss-cascade-layers@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.1.tgz#9640313e64b5e39133de7e38a5aa7f40dc259597" + integrity sha512-XOfhI7GShVcKiKwmPAnWSqd2tBR0uxt+runAxttbSp/LY2U16yAVPmAf7e9q4JJ0d+xMNmpwNDLBXnmRCl3HMQ== + dependencies: + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + +"@csstools/postcss-color-function@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz#2bd36ab34f82d0497cfacdc9b18d34b5e6f64b6b" + integrity sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-color-function@^4.0.7": + version "4.0.7" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-4.0.7.tgz#d31d2044d8a4f8b3154ac54ac77014879eae9f56" + integrity sha512-aDHYmhNIHR6iLw4ElWhf+tRqqaXwKnMl0YsQ/X105Zc4dQwe6yJpMrTN6BwOoESrkDjOYMOfORviSSLeDTJkdQ== + dependencies: + "@csstools/css-color-parser" "^3.0.7" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-color-mix-function@^3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.7.tgz#39735bbc84dc173061e4c2842ec656bb9bc6ed2e" + integrity sha512-e68Nev4CxZYCLcrfWhHH4u/N1YocOfTmw67/kVX5Rb7rnguqqLyxPjhHWjSBX8o4bmyuukmNf3wrUSU3//kT7g== + dependencies: + "@csstools/css-color-parser" "^3.0.7" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-content-alt-text@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.4.tgz#76f4687fb15ed45bc1139bb71e5775779762897a" + integrity sha512-YItlZUOuZJCBlRaCf8Aucc1lgN41qYGALMly0qQllrxYJhiyzlI6RxOTMUvtWk+KhS8GphMDsDhKQ7KTPfEMSw== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-exponential-functions@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.6.tgz#dcee86d22102576b13d8bea059125fbcf98e83cc" + integrity sha512-IgJA5DQsQLu/upA3HcdvC6xEMR051ufebBTIXZ5E9/9iiaA7juXWz1ceYj814lnDYP/7eWjZnw0grRJlX4eI6g== + dependencies: + "@csstools/css-calc" "^2.1.1" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + +"@csstools/postcss-font-format-keywords@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz#677b34e9e88ae997a67283311657973150e8b16a" + integrity sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-font-format-keywords@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz#6730836eb0153ff4f3840416cc2322f129c086e6" + integrity sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-gamut-mapping@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.7.tgz#8aaa4b6ffb6e2187379a83d253607f988533be25" + integrity sha512-gzFEZPoOkY0HqGdyeBXR3JP218Owr683u7KOZazTK7tQZBE8s2yhg06W1tshOqk7R7SWvw9gkw2TQogKpIW8Xw== + dependencies: + "@csstools/css-color-parser" "^3.0.7" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + +"@csstools/postcss-gradients-interpolation-method@^5.0.7": + version "5.0.7" + resolved "https://registry.yarnpkg.com/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.7.tgz#57e19d25e98aa028b98e22ef392ea24c3e61c568" + integrity sha512-WgEyBeg6glUeTdS2XT7qeTFBthTJuXlS9GFro/DVomj7W7WMTamAwpoP4oQCq/0Ki2gvfRYFi/uZtmRE14/DFA== + dependencies: + "@csstools/css-color-parser" "^3.0.7" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-hwb-function@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz#ab54a9fce0ac102c754854769962f2422ae8aa8b" + integrity sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-hwb-function@^4.0.7": + version "4.0.7" + resolved "https://registry.yarnpkg.com/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.7.tgz#d09528098c4b99c49c76de686a4ae35585acc691" + integrity sha512-LKYqjO+wGwDCfNIEllessCBWfR4MS/sS1WXO+j00KKyOjm7jDW2L6jzUmqASEiv/kkJO39GcoIOvTTfB3yeBUA== + dependencies: + "@csstools/css-color-parser" "^3.0.7" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-ic-unit@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz#28237d812a124d1a16a5acc5c3832b040b303e58" + integrity sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-ic-unit@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.0.tgz#b60ec06500717c337447c39ae7fe7952eeb9d48f" + integrity sha512-9QT5TDGgx7wD3EEMN3BSUG6ckb6Eh5gSPT5kZoVtUuAonfPmLDJyPhqR4ntPpMYhUKAMVKAg3I/AgzqHMSeLhA== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-initial@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-initial/-/postcss-initial-2.0.0.tgz#a86f5fc59ab9f16f1422dade4c58bd941af5df22" + integrity sha512-dv2lNUKR+JV+OOhZm9paWzYBXOCi+rJPqJ2cJuhh9xd8USVrd0cBEPczla81HNOyThMQWeCcdln3gZkQV2kYxA== + +"@csstools/postcss-is-pseudo-class@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz#846ae6c0d5a1eaa878fce352c544f9c295509cd1" + integrity sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA== + dependencies: + "@csstools/selector-specificity" "^2.0.0" + postcss-selector-parser "^6.0.10" + +"@csstools/postcss-is-pseudo-class@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.1.tgz#12041448fedf01090dd4626022c28b7f7623f58e" + integrity sha512-JLp3POui4S1auhDR0n8wHd/zTOWmMsmK3nQd3hhL6FhWPaox5W7j1se6zXOG/aP07wV2ww0lxbKYGwbBszOtfQ== + dependencies: + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + +"@csstools/postcss-light-dark-function@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.7.tgz#807c170cd28eebb0c00e64dfc6ab0bf418f19209" + integrity sha512-ZZ0rwlanYKOHekyIPaU+sVm3BEHCe+Ha0/px+bmHe62n0Uc1lL34vbwrLYn6ote8PHlsqzKeTQdIejQCJ05tfw== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-logical-float-and-clear@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz#62617564182cf86ab5d4e7485433ad91e4c58571" + integrity sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ== + +"@csstools/postcss-logical-overflow@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz#c6de7c5f04e3d4233731a847f6c62819bcbcfa1d" + integrity sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA== + +"@csstools/postcss-logical-overscroll-behavior@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz#43c03eaecdf34055ef53bfab691db6dc97a53d37" + integrity sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w== + +"@csstools/postcss-logical-resize@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz#4df0eeb1a61d7bd85395e56a5cce350b5dbfdca6" + integrity sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-logical-viewport-units@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.3.tgz#f6cc63520ca2a6eb76b9cd946070c38dda66d733" + integrity sha512-OC1IlG/yoGJdi0Y+7duz/kU/beCwO+Gua01sD6GtOtLi7ByQUpcIqs7UE/xuRPay4cHgOMatWdnDdsIDjnWpPw== + dependencies: + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-media-minmax@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.6.tgz#427921c0f08033203810af16dfed0baedc538eab" + integrity sha512-J1+4Fr2W3pLZsfxkFazK+9kr96LhEYqoeBszLmFjb6AjYs+g9oDAw3J5oQignLKk3rC9XHW+ebPTZ9FaW5u5pg== + dependencies: + "@csstools/css-calc" "^2.1.1" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/media-query-list-parser" "^4.0.2" + +"@csstools/postcss-media-queries-aspect-ratio-number-values@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.4.tgz#d71102172c74baf3f892fac88cf1ea46a961600d" + integrity sha512-AnGjVslHMm5xw9keusQYvjVWvuS7KWK+OJagaG0+m9QnIjZsrysD2kJP/tr/UJIyYtMCtu8OkUd+Rajb4DqtIQ== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/media-query-list-parser" "^4.0.2" + +"@csstools/postcss-nested-calc@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz#d7e9d1d0d3d15cf5ac891b16028af2a1044d0c26" + integrity sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-nested-calc@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz#754e10edc6958d664c11cde917f44ba144141c62" + integrity sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-normalize-display-values@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz#15da54a36e867b3ac5163ee12c1d7f82d4d612c3" + integrity sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-normalize-display-values@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.0.tgz#ecdde2daf4e192e5da0c6fd933b6d8aff32f2a36" + integrity sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-oklab-function@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz#88cee0fbc8d6df27079ebd2fa016ee261eecf844" + integrity sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-oklab-function@^4.0.7": + version "4.0.7" + resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.7.tgz#33b3322dfb27b0b5eb83a7ad36e67f08bc4e66cd" + integrity sha512-I6WFQIbEKG2IO3vhaMGZDkucbCaUSXMxvHNzDdnfsTCF5tc0UlV3Oe2AhamatQoKFjBi75dSEMrgWq3+RegsOQ== + dependencies: + "@csstools/css-color-parser" "^3.0.7" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-progressive-custom-properties@^1.1.0", "@csstools/postcss-progressive-custom-properties@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz#542292558384361776b45c85226b9a3a34f276fa" + integrity sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-progressive-custom-properties@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.0.0.tgz#ecdb85bcdb1852d73970a214a376684a91f82bdc" + integrity sha512-XQPtROaQjomnvLUSy/bALTR5VCtTVUFwYs1SblvYgLSeTo2a/bMNwUwo2piXw5rTv/FEYiy5yPSXBqg9OKUx7Q== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-random-function@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@csstools/postcss-random-function/-/postcss-random-function-1.0.2.tgz#699702820f19bb6b9632966ff44d8957db6889d2" + integrity sha512-vBCT6JvgdEkvRc91NFoNrLjgGtkLWt47GKT6E2UDn3nd8ZkMBiziQ1Md1OiKoSsgzxsSnGKG3RVdhlbdZEkHjA== + dependencies: + "@csstools/css-calc" "^2.1.1" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + +"@csstools/postcss-relative-color-syntax@^3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.7.tgz#862f8c6a2bbbab1a46aff8265b6a095fd267a3a6" + integrity sha512-apbT31vsJVd18MabfPOnE977xgct5B1I+Jpf+Munw3n6kKb1MMuUmGGH+PT9Hm/fFs6fe61Q/EWnkrb4bNoNQw== + dependencies: + "@csstools/css-color-parser" "^3.0.7" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-scope-pseudo-class@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz#9fe60e9d6d91d58fb5fc6c768a40f6e47e89a235" + integrity sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q== + dependencies: + postcss-selector-parser "^7.0.0" + +"@csstools/postcss-sign-functions@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.1.tgz#eb8e4a5ac637982aeb9264cb99f85817612ad3e8" + integrity sha512-MslYkZCeMQDxetNkfmmQYgKCy4c+w9pPDfgOBCJOo/RI1RveEUdZQYtOfrC6cIZB7sD7/PHr2VGOcMXlZawrnA== + dependencies: + "@csstools/css-calc" "^2.1.1" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + +"@csstools/postcss-stepped-value-functions@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz#f8772c3681cc2befed695e2b0b1d68e22f08c4f4" + integrity sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-stepped-value-functions@^4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.6.tgz#ee88c6122daf58a1b8641f462e8e33427c60b1f1" + integrity sha512-/dwlO9w8vfKgiADxpxUbZOWlL5zKoRIsCymYoh1IPuBsXODKanKnfuZRr32DEqT0//3Av1VjfNZU9yhxtEfIeA== + dependencies: + "@csstools/css-calc" "^2.1.1" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + +"@csstools/postcss-text-decoration-shorthand@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz#ea96cfbc87d921eca914d3ad29340d9bcc4c953f" + integrity sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-text-decoration-shorthand@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.1.tgz#251fab0939d50c6fd73bb2b830b2574188efa087" + integrity sha512-xPZIikbx6jyzWvhms27uugIc0I4ykH4keRvoa3rxX5K7lEhkbd54rjj/dv60qOCTisoS+3bmwJTeyV1VNBrXaw== + dependencies: + "@csstools/color-helpers" "^5.0.1" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-trigonometric-functions@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz#94d3e4774c36d35dcdc88ce091336cb770d32756" + integrity sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-trigonometric-functions@^4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.6.tgz#fc5c5f4c9bd0fd796b58b9a14d5d663be76d19fa" + integrity sha512-c4Y1D2Why/PeccaSouXnTt6WcNHJkoJRidV2VW9s5gJ97cNxnLgQ4Qj8qOqkIR9VmTQKJyNcbF4hy79ZQnWD7A== + dependencies: + "@csstools/css-calc" "^2.1.1" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + +"@csstools/postcss-unset-value@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz#c99bb70e2cdc7312948d1eb41df2412330b81f77" + integrity sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g== + +"@csstools/postcss-unset-value@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz#7caa981a34196d06a737754864baf77d64de4bba" + integrity sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA== + +"@csstools/selector-resolve-nested@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.0.0.tgz#704a9b637975680e025e069a4c58b3beb3e2752a" + integrity sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ== + +"@csstools/selector-specificity@^2.0.0", "@csstools/selector-specificity@^2.0.2": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz#2cbcf822bf3764c9658c4d2e568bd0c0cb748016" + integrity sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw== + +"@csstools/selector-specificity@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz#037817b574262134cabd68fc4ec1a454f168407b" + integrity sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw== + +"@csstools/utilities@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/utilities/-/utilities-2.0.0.tgz#f7ff0fee38c9ffb5646d47b6906e0bc8868bde60" + integrity sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ== + +"@discoveryjs/json-ext@0.5.7": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@docsearch/css@3.8.3": + version "3.8.3" + resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.8.3.tgz#12f377cf8c14b687042273f920efdfdb794e9fcf" + integrity sha512-1nELpMV40JDLJ6rpVVFX48R1jsBFIQ6RnEQDsLFGmzOjPWTOMlZqUcXcvRx8VmYV/TqnS1l784Ofz+ZEb+wEOQ== + +"@docsearch/react@^3.8.1": + version "3.8.3" + resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.8.3.tgz#72f6bcbbda6cd07f23398af641e483c27d16e00a" + integrity sha512-6UNrg88K7lJWmuS6zFPL/xgL+n326qXqZ7Ybyy4E8P/6Rcblk3GE8RXxeol4Pd5pFpKMhOhBhzABKKwHtbJCIg== + dependencies: + "@algolia/autocomplete-core" "1.17.9" + "@algolia/autocomplete-preset-algolia" "1.17.9" + "@docsearch/css" "3.8.3" + algoliasearch "^5.14.2" + +"@docusaurus/babel@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/babel/-/babel-3.7.0.tgz#770dd5da525a9d6a2fee7d3212ec62040327f776" + integrity sha512-0H5uoJLm14S/oKV3Keihxvh8RV+vrid+6Gv+2qhuzbqHanawga8tYnsdpjEyt36ucJjqlby2/Md2ObWjA02UXQ== + dependencies: + "@babel/core" "^7.25.9" + "@babel/generator" "^7.25.9" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-transform-runtime" "^7.25.9" + "@babel/preset-env" "^7.25.9" + "@babel/preset-react" "^7.25.9" + "@babel/preset-typescript" "^7.25.9" + "@babel/runtime" "^7.25.9" + "@babel/runtime-corejs3" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@docusaurus/logger" "3.7.0" + "@docusaurus/utils" "3.7.0" + babel-plugin-dynamic-import-node "^2.3.3" + fs-extra "^11.1.1" + tslib "^2.6.0" + +"@docusaurus/bundler@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/bundler/-/bundler-3.7.0.tgz#d8e7867b3b2c43a1e320ed429f8dfe873c38506d" + integrity sha512-CUUT9VlSGukrCU5ctZucykvgCISivct+cby28wJwCC/fkQFgAHRp/GKv2tx38ZmXb7nacrKzFTcp++f9txUYGg== + dependencies: + "@babel/core" "^7.25.9" + "@docusaurus/babel" "3.7.0" + "@docusaurus/cssnano-preset" "3.7.0" + "@docusaurus/logger" "3.7.0" + "@docusaurus/types" "3.7.0" + "@docusaurus/utils" "3.7.0" + babel-loader "^9.2.1" + clean-css "^5.3.2" + copy-webpack-plugin "^11.0.0" + css-loader "^6.8.1" + css-minimizer-webpack-plugin "^5.0.1" + cssnano "^6.1.2" + file-loader "^6.2.0" + html-minifier-terser "^7.2.0" + mini-css-extract-plugin "^2.9.1" + null-loader "^4.0.1" + postcss "^8.4.26" + postcss-loader "^7.3.3" + postcss-preset-env "^10.1.0" + react-dev-utils "^12.0.1" + terser-webpack-plugin "^5.3.9" + tslib "^2.6.0" + url-loader "^4.1.1" + webpack "^5.95.0" + webpackbar "^6.0.1" + +"@docusaurus/core@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.7.0.tgz#e871586d099093723dfe6de81c1ce610aeb20292" + integrity sha512-b0fUmaL+JbzDIQaamzpAFpTviiaU4cX3Qz8cuo14+HGBCwa0evEK0UYCBFY3n4cLzL8Op1BueeroUD2LYAIHbQ== + dependencies: + "@docusaurus/babel" "3.7.0" + "@docusaurus/bundler" "3.7.0" + "@docusaurus/logger" "3.7.0" + "@docusaurus/mdx-loader" "3.7.0" + "@docusaurus/utils" "3.7.0" + "@docusaurus/utils-common" "3.7.0" + "@docusaurus/utils-validation" "3.7.0" + boxen "^6.2.1" + chalk "^4.1.2" + chokidar "^3.5.3" + cli-table3 "^0.6.3" + combine-promises "^1.1.0" + commander "^5.1.0" + core-js "^3.31.1" + del "^6.1.1" + detect-port "^1.5.1" + escape-html "^1.0.3" + eta "^2.2.0" + eval "^0.1.8" + fs-extra "^11.1.1" + html-tags "^3.3.1" + html-webpack-plugin "^5.6.0" + leven "^3.1.0" + lodash "^4.17.21" + p-map "^4.0.0" + prompts "^2.4.2" + react-dev-utils "^12.0.1" + react-helmet-async "npm:@slorber/react-helmet-async@1.3.0" + react-loadable "npm:@docusaurus/react-loadable@6.0.0" + react-loadable-ssr-addon-v5-slorber "^1.0.1" + react-router "^5.3.4" + react-router-config "^5.1.1" + react-router-dom "^5.3.4" + semver "^7.5.4" + serve-handler "^6.1.6" + shelljs "^0.8.5" + tslib "^2.6.0" + update-notifier "^6.0.2" + webpack "^5.95.0" + webpack-bundle-analyzer "^4.10.2" + webpack-dev-server "^4.15.2" + webpack-merge "^6.0.1" + +"@docusaurus/cssnano-preset@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.7.0.tgz#8fe8f2c3acbd32384b69e14983b9a63c98cae34e" + integrity sha512-X9GYgruZBSOozg4w4dzv9uOz8oK/EpPVQXkp0MM6Tsgp/nRIU9hJzJ0Pxg1aRa3xCeEQTOimZHcocQFlLwYajQ== + dependencies: + cssnano-preset-advanced "^6.1.2" + postcss "^8.4.38" + postcss-sort-media-queries "^5.2.0" + tslib "^2.6.0" + +"@docusaurus/faster@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/faster/-/faster-3.7.0.tgz#8062e05a3d044c0dd470b4812796a113b10b835c" + integrity sha512-d+7uyOEs3SBk38i2TL79N6mFaP7J4knc5lPX/W9od+jplXZhnDdl5ZMh2u2Lg7JxGV/l33Bd7h/xwv4mr21zag== + dependencies: + "@docusaurus/types" "3.7.0" + "@rspack/core" "1.2.0-alpha.0" + "@swc/core" "^1.7.39" + "@swc/html" "^1.7.39" + browserslist "^4.24.2" + lightningcss "^1.27.0" + swc-loader "^0.2.6" + tslib "^2.6.0" + webpack "^5.95.0" + +"@docusaurus/logger@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.7.0.tgz#07ecc2f460c4d2382df4991f9ce4e348e90af04c" + integrity sha512-z7g62X7bYxCYmeNNuO9jmzxLQG95q9QxINCwpboVcNff3SJiHJbGrarxxOVMVmAh1MsrSfxWkVGv4P41ktnFsA== + dependencies: + chalk "^4.1.2" + tslib "^2.6.0" + +"@docusaurus/lqip-loader@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/lqip-loader/-/lqip-loader-3.7.0.tgz#af1e9ef7d11076a8a4b06bdac860cc2a682b301d" + integrity sha512-bEQ/6o9VSzpqV6OYbyoZUtrKAFJOPKdo8tBmvZCee3M+Hl4V1XAg4TY/KmlAlw6HfMdr42FuqGIy9CsFNxL3CQ== + dependencies: + "@docusaurus/logger" "3.7.0" + file-loader "^6.2.0" + lodash "^4.17.21" + sharp "^0.32.3" + tslib "^2.6.0" + +"@docusaurus/mdx-loader@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.7.0.tgz#5890c6e7a5b68cb1d066264ac5290cdcd59d4ecc" + integrity sha512-OFBG6oMjZzc78/U3WNPSHs2W9ZJ723ewAcvVJaqS0VgyeUfmzUV8f1sv+iUHA0DtwiR5T5FjOxj6nzEE8LY6VA== + dependencies: + "@docusaurus/logger" "3.7.0" + "@docusaurus/utils" "3.7.0" + "@docusaurus/utils-validation" "3.7.0" + "@mdx-js/mdx" "^3.0.0" + "@slorber/remark-comment" "^1.0.0" + escape-html "^1.0.3" + estree-util-value-to-estree "^3.0.1" + file-loader "^6.2.0" + fs-extra "^11.1.1" + image-size "^1.0.2" + mdast-util-mdx "^3.0.0" + mdast-util-to-string "^4.0.0" + rehype-raw "^7.0.0" + remark-directive "^3.0.0" + remark-emoji "^4.0.0" + remark-frontmatter "^5.0.0" + remark-gfm "^4.0.0" + stringify-object "^3.3.0" + tslib "^2.6.0" + unified "^11.0.3" + unist-util-visit "^5.0.0" + url-loader "^4.1.1" + vfile "^6.0.1" + webpack "^5.88.1" + +"@docusaurus/module-type-aliases@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.7.0.tgz#15c0745b829c6966c5b3b2c2527c72b54830b0e5" + integrity sha512-g7WdPqDNaqA60CmBrr0cORTrsOit77hbsTj7xE2l71YhBn79sxdm7WMK7wfhcaafkbpIh7jv5ef5TOpf1Xv9Lg== + dependencies: + "@docusaurus/types" "3.7.0" + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-router-config" "*" + "@types/react-router-dom" "*" + react-helmet-async "npm:@slorber/react-helmet-async@*" + react-loadable "npm:@docusaurus/react-loadable@6.0.0" + +"@docusaurus/plugin-client-redirects@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-3.7.0.tgz#b5cf92529768c457c01ad350bfc50862c6149463" + integrity sha512-6B4XAtE5ZVKOyhPgpgMkb7LwCkN+Hgd4vOnlbwR8nCdTQhLjz8MHbGlwwvZ/cay2SPNRX5KssqKAlcHVZP2m8g== + dependencies: + "@docusaurus/core" "3.7.0" + "@docusaurus/logger" "3.7.0" + "@docusaurus/utils" "3.7.0" + "@docusaurus/utils-common" "3.7.0" + "@docusaurus/utils-validation" "3.7.0" + eta "^2.2.0" + fs-extra "^11.1.1" + lodash "^4.17.21" + tslib "^2.6.0" + +"@docusaurus/plugin-content-blog@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.7.0.tgz#7bd69de87a1f3adb652e1473ef5b7ccc9468f47e" + integrity sha512-EFLgEz6tGHYWdPU0rK8tSscZwx+AsyuBW/r+tNig2kbccHYGUJmZtYN38GjAa3Fda4NU+6wqUO5kTXQSRBQD3g== + dependencies: + "@docusaurus/core" "3.7.0" + "@docusaurus/logger" "3.7.0" + "@docusaurus/mdx-loader" "3.7.0" + "@docusaurus/theme-common" "3.7.0" + "@docusaurus/types" "3.7.0" + "@docusaurus/utils" "3.7.0" + "@docusaurus/utils-common" "3.7.0" + "@docusaurus/utils-validation" "3.7.0" + cheerio "1.0.0-rc.12" + feed "^4.2.2" + fs-extra "^11.1.1" + lodash "^4.17.21" + reading-time "^1.5.0" + srcset "^4.0.0" + tslib "^2.6.0" + unist-util-visit "^5.0.0" + utility-types "^3.10.0" + webpack "^5.88.1" + +"@docusaurus/plugin-content-docs@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.7.0.tgz#297a549e926ee2b1147b5242af6f21532c7b107c" + integrity sha512-GXg5V7kC9FZE4FkUZA8oo/NrlRb06UwuICzI6tcbzj0+TVgjq/mpUXXzSgKzMS82YByi4dY2Q808njcBCyy6tQ== + dependencies: + "@docusaurus/core" "3.7.0" + "@docusaurus/logger" "3.7.0" + "@docusaurus/mdx-loader" "3.7.0" + "@docusaurus/module-type-aliases" "3.7.0" + "@docusaurus/theme-common" "3.7.0" + "@docusaurus/types" "3.7.0" + "@docusaurus/utils" "3.7.0" + "@docusaurus/utils-common" "3.7.0" + "@docusaurus/utils-validation" "3.7.0" + "@types/react-router-config" "^5.0.7" + combine-promises "^1.1.0" + fs-extra "^11.1.1" + js-yaml "^4.1.0" + lodash "^4.17.21" + tslib "^2.6.0" + utility-types "^3.10.0" + webpack "^5.88.1" + +"@docusaurus/plugin-content-pages@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.7.0.tgz#c4a8f7237872236aacb77665822c474c0a00e91a" + integrity sha512-YJSU3tjIJf032/Aeao8SZjFOrXJbz/FACMveSMjLyMH4itQyZ2XgUIzt4y+1ISvvk5zrW4DABVT2awTCqBkx0Q== + dependencies: + "@docusaurus/core" "3.7.0" + "@docusaurus/mdx-loader" "3.7.0" + "@docusaurus/types" "3.7.0" + "@docusaurus/utils" "3.7.0" + "@docusaurus/utils-validation" "3.7.0" + fs-extra "^11.1.1" + tslib "^2.6.0" + webpack "^5.88.1" + +"@docusaurus/plugin-debug@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.7.0.tgz#a4fd45132e40cffe96bb51f48e89982a1cb8e194" + integrity sha512-Qgg+IjG/z4svtbCNyTocjIwvNTNEwgRjSXXSJkKVG0oWoH0eX/HAPiu+TS1HBwRPQV+tTYPWLrUypYFepfujZA== + dependencies: + "@docusaurus/core" "3.7.0" + "@docusaurus/types" "3.7.0" + "@docusaurus/utils" "3.7.0" + fs-extra "^11.1.1" + react-json-view-lite "^1.2.0" + tslib "^2.6.0" + +"@docusaurus/plugin-google-analytics@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.7.0.tgz#d20f665e810fb2295d1c1bbfe13398c5ff42eb24" + integrity sha512-otIqiRV/jka6Snjf+AqB360XCeSv7lQC+DKYW+EUZf6XbuE8utz5PeUQ8VuOcD8Bk5zvT1MC4JKcd5zPfDuMWA== + dependencies: + "@docusaurus/core" "3.7.0" + "@docusaurus/types" "3.7.0" + "@docusaurus/utils-validation" "3.7.0" + tslib "^2.6.0" + +"@docusaurus/plugin-google-gtag@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.7.0.tgz#a48638dfd132858060458b875a440b6cbda6bf8f" + integrity sha512-M3vrMct1tY65ModbyeDaMoA+fNJTSPe5qmchhAbtqhDD/iALri0g9LrEpIOwNaoLmm6lO88sfBUADQrSRSGSWA== + dependencies: + "@docusaurus/core" "3.7.0" + "@docusaurus/types" "3.7.0" + "@docusaurus/utils-validation" "3.7.0" + "@types/gtag.js" "^0.0.12" + tslib "^2.6.0" + +"@docusaurus/plugin-google-tag-manager@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.7.0.tgz#0a4390f4b0e760d073bdb1905436bfa7bd71356b" + integrity sha512-X8U78nb8eiMiPNg3jb9zDIVuuo/rE1LjGDGu+5m5CX4UBZzjMy+klOY2fNya6x8ACyE/L3K2erO1ErheP55W/w== + dependencies: + "@docusaurus/core" "3.7.0" + "@docusaurus/types" "3.7.0" + "@docusaurus/utils-validation" "3.7.0" + tslib "^2.6.0" + +"@docusaurus/plugin-ideal-image@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-ideal-image/-/plugin-ideal-image-3.7.0.tgz#85d44db4fda8a07ad8d6882f1ef5e478dd0c715c" + integrity sha512-1IKmXJ6I7WKxfESdCMroechuoQEo1IZzIOhQlga8m7ioHzu+sb+Egnyrau2buCYh0QJ8gZoXtscSt5TBFlzMOQ== + dependencies: + "@docusaurus/core" "3.7.0" + "@docusaurus/lqip-loader" "3.7.0" + "@docusaurus/responsive-loader" "^1.7.0" + "@docusaurus/theme-translations" "3.7.0" + "@docusaurus/types" "3.7.0" + "@docusaurus/utils-validation" "3.7.0" + "@slorber/react-ideal-image" "^0.0.14" + react-waypoint "^10.3.0" + sharp "^0.32.3" + tslib "^2.6.0" + webpack "^5.88.1" + +"@docusaurus/plugin-pwa@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-pwa/-/plugin-pwa-3.7.0.tgz#9ade08513bcf63bcb35e863bed8555c6bf84263b" + integrity sha512-I/4C2Uuc/+96fDJ3enMBlPJRR2gAzdVRXMKgR/W3U7gAJEl13pKjT8Tn5BTX52+nVMVR23eUmCZsfaipUhiiTA== + dependencies: + "@babel/core" "^7.25.9" + "@babel/preset-env" "^7.25.9" + "@docusaurus/bundler" "3.7.0" + "@docusaurus/core" "3.7.0" + "@docusaurus/logger" "3.7.0" + "@docusaurus/theme-common" "3.7.0" + "@docusaurus/theme-translations" "3.7.0" + "@docusaurus/types" "3.7.0" + "@docusaurus/utils" "3.7.0" + "@docusaurus/utils-validation" "3.7.0" + babel-loader "^9.2.1" + clsx "^2.0.0" + core-js "^3.31.1" + tslib "^2.6.0" + webpack "^5.95.0" + webpack-merge "^5.9.0" + workbox-build "^7.0.0" + workbox-precaching "^7.0.0" + workbox-window "^7.0.0" + +"@docusaurus/plugin-sitemap@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.7.0.tgz#2c1bf9de26aeda455df6f77748e5887ace39b2d7" + integrity sha512-bTRT9YLZ/8I/wYWKMQke18+PF9MV8Qub34Sku6aw/vlZ/U+kuEuRpQ8bTcNOjaTSfYsWkK4tTwDMHK2p5S86cA== + dependencies: + "@docusaurus/core" "3.7.0" + "@docusaurus/logger" "3.7.0" + "@docusaurus/types" "3.7.0" + "@docusaurus/utils" "3.7.0" + "@docusaurus/utils-common" "3.7.0" + "@docusaurus/utils-validation" "3.7.0" + fs-extra "^11.1.1" + sitemap "^7.1.1" + tslib "^2.6.0" + +"@docusaurus/plugin-svgr@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-svgr/-/plugin-svgr-3.7.0.tgz#018e89efd615d5fde77b891a8c2aadf203013f5d" + integrity sha512-HByXIZTbc4GV5VAUkZ2DXtXv1Qdlnpk3IpuImwSnEzCDBkUMYcec5282hPjn6skZqB25M1TYCmWS91UbhBGxQg== + dependencies: + "@docusaurus/core" "3.7.0" + "@docusaurus/types" "3.7.0" + "@docusaurus/utils" "3.7.0" + "@docusaurus/utils-validation" "3.7.0" + "@svgr/core" "8.1.0" + "@svgr/webpack" "^8.1.0" + tslib "^2.6.0" + webpack "^5.88.1" + +"@docusaurus/preset-classic@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.7.0.tgz#f6656a04ae6a4877523dbd04f7c491632e4003b9" + integrity sha512-nPHj8AxDLAaQXs+O6+BwILFuhiWbjfQWrdw2tifOClQoNfuXDjfjogee6zfx6NGHWqshR23LrcN115DmkHC91Q== + dependencies: + "@docusaurus/core" "3.7.0" + "@docusaurus/plugin-content-blog" "3.7.0" + "@docusaurus/plugin-content-docs" "3.7.0" + "@docusaurus/plugin-content-pages" "3.7.0" + "@docusaurus/plugin-debug" "3.7.0" + "@docusaurus/plugin-google-analytics" "3.7.0" + "@docusaurus/plugin-google-gtag" "3.7.0" + "@docusaurus/plugin-google-tag-manager" "3.7.0" + "@docusaurus/plugin-sitemap" "3.7.0" + "@docusaurus/plugin-svgr" "3.7.0" + "@docusaurus/theme-classic" "3.7.0" + "@docusaurus/theme-common" "3.7.0" + "@docusaurus/theme-search-algolia" "3.7.0" + "@docusaurus/types" "3.7.0" + +"@docusaurus/remark-plugin-npm2yarn@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/remark-plugin-npm2yarn/-/remark-plugin-npm2yarn-3.7.0.tgz#ccbd81c0990dce2862240211897e45a1d63c6859" + integrity sha512-2QkZh75vZzPefW5Ljt8gwc1i0ERuS0MRZTEwHsSXSi6vc2NpLVbcmfIuHhwR8o0PcGVTxmBEhQRP0NN1vHdOAA== + dependencies: + mdast-util-mdx "^3.0.0" + npm-to-yarn "^3.0.0" + tslib "^2.6.0" + unified "^11.0.3" + unist-util-visit "^5.0.0" + +"@docusaurus/responsive-loader@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/responsive-loader/-/responsive-loader-1.7.0.tgz#508df2779e04311aa2a38efb67cf743109afd681" + integrity sha512-N0cWuVqTRXRvkBxeMQcy/OF2l7GN8rmni5EzR3HpwR+iU2ckYPnziceojcxvvxQ5NqZg1QfEW0tycQgHp+e+Nw== + dependencies: + loader-utils "^2.0.0" + +"@docusaurus/theme-classic@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.7.0.tgz#b483bd8e2923b6994b5f47238884b9f8984222c5" + integrity sha512-MnLxG39WcvLCl4eUzHr0gNcpHQfWoGqzADCly54aqCofQX6UozOS9Th4RK3ARbM9m7zIRv3qbhggI53dQtx/hQ== + dependencies: + "@docusaurus/core" "3.7.0" + "@docusaurus/logger" "3.7.0" + "@docusaurus/mdx-loader" "3.7.0" + "@docusaurus/module-type-aliases" "3.7.0" + "@docusaurus/plugin-content-blog" "3.7.0" + "@docusaurus/plugin-content-docs" "3.7.0" + "@docusaurus/plugin-content-pages" "3.7.0" + "@docusaurus/theme-common" "3.7.0" + "@docusaurus/theme-translations" "3.7.0" + "@docusaurus/types" "3.7.0" + "@docusaurus/utils" "3.7.0" + "@docusaurus/utils-common" "3.7.0" + "@docusaurus/utils-validation" "3.7.0" + "@mdx-js/react" "^3.0.0" + clsx "^2.0.0" + copy-text-to-clipboard "^3.2.0" + infima "0.2.0-alpha.45" + lodash "^4.17.21" + nprogress "^0.2.0" + postcss "^8.4.26" + prism-react-renderer "^2.3.0" + prismjs "^1.29.0" + react-router-dom "^5.3.4" + rtlcss "^4.1.0" + tslib "^2.6.0" + utility-types "^3.10.0" + +"@docusaurus/theme-common@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.7.0.tgz#18bf5c6b149a701f4bd865715ee8b595aa40b354" + integrity sha512-8eJ5X0y+gWDsURZnBfH0WabdNm8XMCXHv8ENy/3Z/oQKwaB/EHt5lP9VsTDTf36lKEp0V6DjzjFyFIB+CetL0A== + dependencies: + "@docusaurus/mdx-loader" "3.7.0" + "@docusaurus/module-type-aliases" "3.7.0" + "@docusaurus/utils" "3.7.0" + "@docusaurus/utils-common" "3.7.0" + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-router-config" "*" + clsx "^2.0.0" + parse-numeric-range "^1.3.0" + prism-react-renderer "^2.3.0" + tslib "^2.6.0" + utility-types "^3.10.0" + +"@docusaurus/theme-live-codeblock@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-live-codeblock/-/theme-live-codeblock-3.7.0.tgz#efdd69d73ea38a2a6e5b83907c41847b689a7b0a" + integrity sha512-peLs77sk+TuHjAnhyhT8IH3Qsr/zewpwHg5A4EOe/8K4Lj2T8fhro1/Dj66FS8784wwAoxhy5A9Ux9Rsp8h87w== + dependencies: + "@docusaurus/core" "3.7.0" + "@docusaurus/theme-common" "3.7.0" + "@docusaurus/theme-translations" "3.7.0" + "@docusaurus/utils-validation" "3.7.0" + "@philpl/buble" "^0.19.7" + clsx "^2.0.0" + fs-extra "^11.1.1" + react-live "^4.1.6" + tslib "^2.6.0" + +"@docusaurus/theme-search-algolia@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.7.0.tgz#2108ddf0b300b82de7c2b9ff9fcf62121b66ea37" + integrity sha512-Al/j5OdzwRU1m3falm+sYy9AaB93S1XF1Lgk9Yc6amp80dNxJVplQdQTR4cYdzkGtuQqbzUA8+kaoYYO0RbK6g== + dependencies: + "@docsearch/react" "^3.8.1" + "@docusaurus/core" "3.7.0" + "@docusaurus/logger" "3.7.0" + "@docusaurus/plugin-content-docs" "3.7.0" + "@docusaurus/theme-common" "3.7.0" + "@docusaurus/theme-translations" "3.7.0" + "@docusaurus/utils" "3.7.0" + "@docusaurus/utils-validation" "3.7.0" + algoliasearch "^5.17.1" + algoliasearch-helper "^3.22.6" + clsx "^2.0.0" + eta "^2.2.0" + fs-extra "^11.1.1" + lodash "^4.17.21" + tslib "^2.6.0" + utility-types "^3.10.0" + +"@docusaurus/theme-translations@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.7.0.tgz#0891aedc7c7040afcb3a1b34051d3a69096d0d25" + integrity sha512-Ewq3bEraWDmienM6eaNK7fx+/lHMtGDHQyd1O+4+3EsDxxUmrzPkV7Ct3nBWTuE0MsoZr3yNwQVKjllzCMuU3g== + dependencies: + fs-extra "^11.1.1" + tslib "^2.6.0" + +"@docusaurus/tsconfig@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/tsconfig/-/tsconfig-3.0.0.tgz#89ce292cff8debaa03d93d651ffd6375561e7dab" + integrity sha512-yR9sng4izFudS+v1xV5yboNfc1hATMDpYp9iYfWggbBDwKSm0J1IdIgkygRnqC/AWs1ARUQUpG0gFotPCE/4Ew== + +"@docusaurus/types@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.0.0.tgz#3edabe43f70b45f81a48f3470d6a73a2eba41945" + integrity sha512-Qb+l/hmCOVemReuzvvcFdk84bUmUFyD0Zi81y651ie3VwMrXqC7C0E7yZLKMOsLj/vkqsxHbtkAuYMI89YzNzg== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + commander "^5.1.0" + joi "^17.9.2" + react-helmet-async "^1.3.0" + utility-types "^3.10.0" + webpack "^5.88.1" + webpack-merge "^5.9.0" + +"@docusaurus/types@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.7.0.tgz#3f5a68a60f80ecdcb085666da1d68f019afda943" + integrity sha512-kOmZg5RRqJfH31m+6ZpnwVbkqMJrPOG5t0IOl4i/+3ruXyNfWzZ0lVtVrD0u4ONc/0NOsS9sWYaxxWNkH1LdLQ== + dependencies: + "@mdx-js/mdx" "^3.0.0" + "@types/history" "^4.7.11" + "@types/react" "*" + commander "^5.1.0" + joi "^17.9.2" + react-helmet-async "npm:@slorber/react-helmet-async@1.3.0" + utility-types "^3.10.0" + webpack "^5.95.0" + webpack-merge "^5.9.0" + +"@docusaurus/utils-common@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.7.0.tgz#1bef52837d321db5dd2361fc07f3416193b5d029" + integrity sha512-IZeyIfCfXy0Mevj6bWNg7DG7B8G+S6o6JVpddikZtWyxJguiQ7JYr0SIZ0qWd8pGNuMyVwriWmbWqMnK7Y5PwA== + dependencies: + "@docusaurus/types" "3.7.0" + tslib "^2.6.0" + +"@docusaurus/utils-validation@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.7.0.tgz#dc0786fb633ae5cef8e93337bf21c2a826c7ecbd" + integrity sha512-w8eiKk8mRdN+bNfeZqC4nyFoxNyI1/VExMKAzD9tqpJfLLbsa46Wfn5wcKH761g9WkKh36RtFV49iL9lh1DYBA== + dependencies: + "@docusaurus/logger" "3.7.0" + "@docusaurus/utils" "3.7.0" + "@docusaurus/utils-common" "3.7.0" + fs-extra "^11.2.0" + joi "^17.9.2" + js-yaml "^4.1.0" + lodash "^4.17.21" + tslib "^2.6.0" + +"@docusaurus/utils@3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.7.0.tgz#dfdebd63524c52b498f36b2907a3b2261930b9bb" + integrity sha512-e7zcB6TPnVzyUaHMJyLSArKa2AG3h9+4CfvKXKKWNx6hRs+p0a+u7HHTJBgo6KW2m+vqDnuIHK4X+bhmoghAFA== + dependencies: + "@docusaurus/logger" "3.7.0" + "@docusaurus/types" "3.7.0" + "@docusaurus/utils-common" "3.7.0" + escape-string-regexp "^4.0.0" + file-loader "^6.2.0" + fs-extra "^11.1.1" + github-slugger "^1.5.0" + globby "^11.1.0" + gray-matter "^4.0.3" + jiti "^1.20.0" + js-yaml "^4.1.0" + lodash "^4.17.21" + micromatch "^4.0.5" + prompts "^2.4.2" + resolve-pathname "^3.0.0" + shelljs "^0.8.5" + tslib "^2.6.0" + url-loader "^4.1.1" + utility-types "^3.10.0" + webpack "^5.88.1" + +"@emotion/is-prop-valid@^0.8.2": + version "0.8.8" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" + integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA== + dependencies: + "@emotion/memoize" "0.7.4" + +"@emotion/memoize@0.7.4": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" + integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== + +"@floating-ui/core@^1.6.0": + version "1.6.9" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.9.tgz#64d1da251433019dafa091de9b2886ff35ec14e6" + integrity sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw== + dependencies: + "@floating-ui/utils" "^0.2.9" + +"@floating-ui/dom@^1.0.0", "@floating-ui/dom@^1.6.5": + version "1.6.13" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.13.tgz#a8a938532aea27a95121ec16e667a7cbe8c59e34" + integrity sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w== + dependencies: + "@floating-ui/core" "^1.6.0" + "@floating-ui/utils" "^0.2.9" + +"@floating-ui/react-dom@^2.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.2.tgz#a1349bbf6a0e5cb5ded55d023766f20a4d439a31" + integrity sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A== + dependencies: + "@floating-ui/dom" "^1.0.0" + +"@floating-ui/utils@^0.2.9": + version "0.2.9" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.9.tgz#50dea3616bc8191fb8e112283b49eaff03e78429" + integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg== + +"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" + integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== + +"@hapi/topo@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" + integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.8" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142" + integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" + integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== + +"@mdx-js/mdx@^3.0.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-3.1.0.tgz#10235cab8ad7d356c262e8c21c68df5850a97dc3" + integrity sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw== + dependencies: + "@types/estree" "^1.0.0" + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdx" "^2.0.0" + collapse-white-space "^2.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + estree-util-scope "^1.0.0" + estree-walker "^3.0.0" + hast-util-to-jsx-runtime "^2.0.0" + markdown-extensions "^2.0.0" + recma-build-jsx "^1.0.0" + recma-jsx "^1.0.0" + recma-stringify "^1.0.0" + rehype-recma "^1.0.0" + remark-mdx "^3.0.0" + remark-parse "^11.0.0" + remark-rehype "^11.0.0" + source-map "^0.7.0" + unified "^11.0.0" + unist-util-position-from-estree "^2.0.0" + unist-util-stringify-position "^4.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +"@mdx-js/react@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-3.0.1.tgz#997a19b3a5b783d936c75ae7c47cfe62f967f746" + integrity sha512-9ZrPIU4MGf6et1m1ov3zKf+q9+deetI51zprKB1D/z3NOb+rUxxtEl3mCjW5wTGh6VhRdwPueh1oRzi6ezkA8A== + dependencies: + "@types/mdx" "^2.0.0" + +"@mdx-js/react@^3.0.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-3.1.0.tgz#c4522e335b3897b9a845db1dbdd2f966ae8fb0ed" + integrity sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ== + dependencies: + "@types/mdx" "^2.0.0" + +"@module-federation/error-codes@0.8.4": + version "0.8.4" + resolved "https://registry.yarnpkg.com/@module-federation/error-codes/-/error-codes-0.8.4.tgz#c66ead0da86bc010fa53187462c704b3e0d5a256" + integrity sha512-55LYmrDdKb4jt+qr8qE8U3al62ZANp3FhfVaNPOaAmdTh0jHdD8M3yf5HKFlr5xVkVO4eV/F/J2NCfpbh+pEXQ== + +"@module-federation/runtime-tools@0.8.4": + version "0.8.4" + resolved "https://registry.yarnpkg.com/@module-federation/runtime-tools/-/runtime-tools-0.8.4.tgz#ddf8461fe9b5d5e962511f4e5b622008ee46bde8" + integrity sha512-fjVOsItJ1u5YY6E9FnS56UDwZgqEQUrWFnouRiPtK123LUuqUI9FH4redZoKWlE1PB0ir1Z3tnqy8eFYzPO38Q== + dependencies: + "@module-federation/runtime" "0.8.4" + "@module-federation/webpack-bundler-runtime" "0.8.4" + +"@module-federation/runtime@0.8.4": + version "0.8.4" + resolved "https://registry.yarnpkg.com/@module-federation/runtime/-/runtime-0.8.4.tgz#7fc63e1b7dda0506bb2a70c1a52aa73513c5b508" + integrity sha512-yZeZ7z2Rx4gv/0E97oLTF3V6N25vglmwXGgoeju/W2YjsFvWzVtCDI7zRRb0mJhU6+jmSM8jP1DeQGbea/AiZQ== + dependencies: + "@module-federation/error-codes" "0.8.4" + "@module-federation/sdk" "0.8.4" + +"@module-federation/sdk@0.8.4": + version "0.8.4" + resolved "https://registry.yarnpkg.com/@module-federation/sdk/-/sdk-0.8.4.tgz#956e178e104d640482e5afe93c7e3a095a589807" + integrity sha512-waABomIjg/5m1rPDBWYG4KUhS5r7OUUY7S+avpaVIY/tkPWB3ibRDKy2dNLLAMaLKq0u+B1qIdEp4NIWkqhqpg== + dependencies: + isomorphic-rslog "0.0.6" + +"@module-federation/webpack-bundler-runtime@0.8.4": + version "0.8.4" + resolved "https://registry.yarnpkg.com/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.8.4.tgz#c01f5a5c5d61664c21ac6c479ebe9d8bf09d22d6" + integrity sha512-HggROJhvHPUX7uqBD/XlajGygMNM1DG0+4OAkk8MBQe4a18QzrRNzZt6XQbRTSG4OaEoyRWhQHvYD3Yps405tQ== + dependencies: + "@module-federation/runtime" "0.8.4" + "@module-federation/sdk" "0.8.4" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@philpl/buble@^0.19.7": + version "0.19.7" + resolved "https://registry.yarnpkg.com/@philpl/buble/-/buble-0.19.7.tgz#27231e6391393793b64bc1c982fc7b593198b893" + integrity sha512-wKTA2DxAGEW+QffRQvOhRQ0VBiYU2h2p8Yc1oBNlqSKws48/8faxqKNIuub0q4iuyTuLwtB8EkwiKwhlfV1PBA== + dependencies: + acorn "^6.1.1" + acorn-class-fields "^0.2.1" + acorn-dynamic-import "^4.0.0" + acorn-jsx "^5.0.1" + chalk "^2.4.2" + magic-string "^0.25.2" + minimist "^1.2.0" + os-homedir "^1.0.1" + regexpu-core "^4.5.4" + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@pnpm/config.env-replace@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c" + integrity sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w== + +"@pnpm/network.ca-file@^1.0.1": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz#2ab05e09c1af0cdf2fcf5035bea1484e222f7983" + integrity sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA== + dependencies: + graceful-fs "4.2.10" + +"@pnpm/npm-conf@^2.1.0": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz#bb375a571a0bd63ab0a23bece33033c683e9b6b0" + integrity sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw== + dependencies: + "@pnpm/config.env-replace" "^1.1.0" + "@pnpm/network.ca-file" "^1.0.1" + config-chain "^1.1.11" + +"@polka/url@^1.0.0-next.24": + version "1.0.0-next.28" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.28.tgz#d45e01c4a56f143ee69c54dd6b12eade9e270a73" + integrity sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw== + +"@radix-ui/number@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.0.tgz#1e95610461a09cdf8bb05c152e76ca1278d5da46" + integrity sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ== + +"@radix-ui/primitive@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.1.tgz#fc169732d755c7fbad33ba8d0cd7fd10c90dc8e3" + integrity sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA== + +"@radix-ui/react-accordion@^1.2.0": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-1.2.2.tgz#96ac3de896189553219e342d5e773589eb119dce" + integrity sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-collapsible" "1.1.2" + "@radix-ui/react-collection" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-controllable-state" "1.1.0" + +"@radix-ui/react-arrow@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz#2103721933a8bfc6e53bbfbdc1aaad5fc8ba0dd7" + integrity sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w== + dependencies: + "@radix-ui/react-primitive" "2.0.1" + +"@radix-ui/react-checkbox@^1.1.1": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.3.tgz#0e2ab913fddf3c88603625f7a9457d73882c8a32" + integrity sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-presence" "1.1.2" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-use-previous" "1.1.0" + "@radix-ui/react-use-size" "1.1.0" + +"@radix-ui/react-collapsible@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.2.tgz#42477c428bb0d2eec35b9b47601c5ff0a6210165" + integrity sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-presence" "1.1.2" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-collection@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.1.tgz#be2c7e01d3508e6d4b6d838f492e7d182f17d3b0" + integrity sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA== + dependencies: + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-slot" "1.1.1" + +"@radix-ui/react-compose-refs@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec" + integrity sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw== + +"@radix-ui/react-context@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.1.tgz#82074aa83a472353bb22e86f11bcbd1c61c4c71a" + integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q== + +"@radix-ui/react-dialog@^1.1.1", "@radix-ui/react-dialog@^1.1.2": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz#d68e977acfcc0d044b9dab47b6dd2c179d2b3191" + integrity sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-dismissable-layer" "1.1.3" + "@radix-ui/react-focus-guards" "1.1.1" + "@radix-ui/react-focus-scope" "1.1.1" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-portal" "1.1.3" + "@radix-ui/react-presence" "1.1.2" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-slot" "1.1.1" + "@radix-ui/react-use-controllable-state" "1.1.0" + aria-hidden "^1.1.1" + react-remove-scroll "^2.6.1" + +"@radix-ui/react-direction@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.0.tgz#a7d39855f4d077adc2a1922f9c353c5977a09cdc" + integrity sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg== + +"@radix-ui/react-dismissable-layer@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz#4ee0f0f82d53bf5bd9db21665799bb0d1bad5ed8" + integrity sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-escape-keydown" "1.1.0" + +"@radix-ui/react-dropdown-menu@^2.1.1": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.4.tgz#5e1f453296dd9ae99224a26c36851832d26cf507" + integrity sha512-iXU1Ab5ecM+yEepGAWK8ZhMyKX4ubFdCNtol4sT9D0OVErG9PNElfx3TQhjw7n7BC5nFVz68/5//clWy+8TXzA== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-menu" "2.1.4" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-controllable-state" "1.1.0" + +"@radix-ui/react-focus-guards@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz#8635edd346304f8b42cae86b05912b61aef27afe" + integrity sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg== + +"@radix-ui/react-focus-scope@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz#5c602115d1db1c4fcfa0fae4c3b09bb8919853cb" + integrity sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA== + dependencies: + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + +"@radix-ui/react-icons@^1.3.0": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.2.tgz#09be63d178262181aeca5fb7f7bc944b10a7f441" + integrity sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g== + +"@radix-ui/react-id@1.1.0", "@radix-ui/react-id@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed" + integrity sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-label@^2.1.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-2.1.1.tgz#f30bd577b26873c638006e4f65761d4c6b80566d" + integrity sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw== + dependencies: + "@radix-ui/react-primitive" "2.0.1" + +"@radix-ui/react-menu@2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.4.tgz#ac7aec296f29608206a7c6ef6335d8f102edaa95" + integrity sha512-BnOgVoL6YYdHAG6DtXONaR29Eq4nvbi8rutrV/xlr3RQCMMb3yqP85Qiw/3NReozrSW+4dfLkK+rc1hb4wPU/A== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-collection" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-dismissable-layer" "1.1.3" + "@radix-ui/react-focus-guards" "1.1.1" + "@radix-ui/react-focus-scope" "1.1.1" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-popper" "1.2.1" + "@radix-ui/react-portal" "1.1.3" + "@radix-ui/react-presence" "1.1.2" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-roving-focus" "1.1.1" + "@radix-ui/react-slot" "1.1.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + aria-hidden "^1.1.1" + react-remove-scroll "^2.6.1" + +"@radix-ui/react-popover@^1.0.7": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.4.tgz#d83104e5fb588870a673b55f3387da4844e5836e" + integrity sha512-aUACAkXx8LaFymDma+HQVji7WhvEhpFJ7+qPz17Nf4lLZqtreGOFRiNQWQmhzp7kEWg9cOyyQJpdIMUMPc/CPw== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-dismissable-layer" "1.1.3" + "@radix-ui/react-focus-guards" "1.1.1" + "@radix-ui/react-focus-scope" "1.1.1" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-popper" "1.2.1" + "@radix-ui/react-portal" "1.1.3" + "@radix-ui/react-presence" "1.1.2" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-slot" "1.1.1" + "@radix-ui/react-use-controllable-state" "1.1.0" + aria-hidden "^1.1.1" + react-remove-scroll "^2.6.1" + +"@radix-ui/react-popper@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.1.tgz#2fc66cfc34f95f00d858924e3bee54beae2dff0a" + integrity sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw== + dependencies: + "@floating-ui/react-dom" "^2.0.0" + "@radix-ui/react-arrow" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-use-rect" "1.1.0" + "@radix-ui/react-use-size" "1.1.0" + "@radix-ui/rect" "1.1.0" + +"@radix-ui/react-portal@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.3.tgz#b0ea5141103a1671b715481b13440763d2ac4440" + integrity sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw== + dependencies: + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-presence@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc" + integrity sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg== + dependencies: + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-primitive@2.0.1", "@radix-ui/react-primitive@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz#6d9efc550f7520135366f333d1e820cf225fad9e" + integrity sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg== + dependencies: + "@radix-ui/react-slot" "1.1.1" + +"@radix-ui/react-roving-focus@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz#3b3abb1e03646937f28d9ab25e96343667ca6520" + integrity sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-collection" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + +"@radix-ui/react-scroll-area@^1.1.0": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.2.tgz#28e34fd4d83e9de5d987c5e8914a7bd8be9546a5" + integrity sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g== + dependencies: + "@radix-ui/number" "1.1.0" + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-presence" "1.1.2" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-select@^2.1.1": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-2.1.4.tgz#8957050203640b668a883a225260c403514b3772" + integrity sha512-pOkb2u8KgO47j/h7AylCj7dJsm69BXcjkrvTqMptFqsE2i0p8lHkfgneXKjAgPzBMivnoMyt8o4KiV4wYzDdyQ== + dependencies: + "@radix-ui/number" "1.1.0" + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-collection" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-dismissable-layer" "1.1.3" + "@radix-ui/react-focus-guards" "1.1.1" + "@radix-ui/react-focus-scope" "1.1.1" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-popper" "1.2.1" + "@radix-ui/react-portal" "1.1.3" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-slot" "1.1.1" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-use-previous" "1.1.0" + "@radix-ui/react-visually-hidden" "1.1.1" + aria-hidden "^1.1.1" + react-remove-scroll "^2.6.1" + +"@radix-ui/react-separator@^1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.1.1.tgz#dd60621553c858238d876be9b0702287424866d2" + integrity sha512-RRiNRSrD8iUiXriq/Y5n4/3iE8HzqgLHsusUSg5jVpU2+3tqcUFPJXHDymwEypunc2sWxDUS3UC+rkZRlHedsw== + dependencies: + "@radix-ui/react-primitive" "2.0.1" + +"@radix-ui/react-slider@^1.2.0": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slider/-/react-slider-1.2.2.tgz#4ca883e3f0dea7b97d43c6cbc6c4305c64e75a86" + integrity sha512-sNlU06ii1/ZcbHf8I9En54ZPW0Vil/yPVg4vQMcFNjrIx51jsHbFl1HYHQvCIWJSr1q0ZmA+iIs/ZTv8h7HHSA== + dependencies: + "@radix-ui/number" "1.1.0" + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-collection" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-use-previous" "1.1.0" + "@radix-ui/react-use-size" "1.1.0" + +"@radix-ui/react-slot@1.1.1", "@radix-ui/react-slot@^1.0.2": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.1.tgz#ab9a0ffae4027db7dc2af503c223c978706affc3" + integrity sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g== + dependencies: + "@radix-ui/react-compose-refs" "1.1.1" + +"@radix-ui/react-switch@^1.1.0": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.2.tgz#61323f4cccf25bf56c95fceb3b56ce1407bc9aec" + integrity sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-use-previous" "1.1.0" + "@radix-ui/react-use-size" "1.1.0" + +"@radix-ui/react-tabs@^1.1.0": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz#a72da059593cba30fccb30a226d63af686b32854" + integrity sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-presence" "1.1.2" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-roving-focus" "1.1.1" + "@radix-ui/react-use-controllable-state" "1.1.0" + +"@radix-ui/react-toggle@^1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle/-/react-toggle-1.1.1.tgz#939162f87d2c6cfba912a9908ed5ee651bd1ce8f" + integrity sha512-i77tcgObYr743IonC1hrsnnPmszDRn8p+EGUsUt+5a/JFn28fxaM88Py6V2mc8J5kELMWishI0rLnuGLFD/nnQ== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-use-controllable-state" "1.1.0" + +"@radix-ui/react-tooltip@^1.1.2": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.1.6.tgz#eab98e9a5c876ef0abfae3cfeee229870528ed06" + integrity sha512-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA== + dependencies: + "@radix-ui/primitive" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-dismissable-layer" "1.1.3" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-popper" "1.2.1" + "@radix-ui/react-portal" "1.1.3" + "@radix-ui/react-presence" "1.1.2" + "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-slot" "1.1.1" + "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-visually-hidden" "1.1.1" + +"@radix-ui/react-use-callback-ref@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1" + integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw== + +"@radix-ui/react-use-controllable-state@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0" + integrity sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw== + dependencies: + "@radix-ui/react-use-callback-ref" "1.1.0" + +"@radix-ui/react-use-escape-keydown@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz#31a5b87c3b726504b74e05dac1edce7437b98754" + integrity sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw== + dependencies: + "@radix-ui/react-use-callback-ref" "1.1.0" + +"@radix-ui/react-use-layout-effect@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27" + integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w== + +"@radix-ui/react-use-previous@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz#d4dd37b05520f1d996a384eb469320c2ada8377c" + integrity sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og== + +"@radix-ui/react-use-rect@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz#13b25b913bd3e3987cc9b073a1a164bb1cf47b88" + integrity sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ== + dependencies: + "@radix-ui/rect" "1.1.0" + +"@radix-ui/react-use-size@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz#b4dba7fbd3882ee09e8d2a44a3eed3a7e555246b" + integrity sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-visually-hidden@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz#f7b48c1af50dfdc366e92726aee6d591996c5752" + integrity sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg== + dependencies: + "@radix-ui/react-primitive" "2.0.1" + +"@radix-ui/rect@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.0.tgz#f817d1d3265ac5415dadc67edab30ae196696438" + integrity sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg== + +"@rollup/plugin-babel@^5.2.0": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" + integrity sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@rollup/pluginutils" "^3.1.0" + +"@rollup/plugin-node-resolve@^15.2.3": + version "15.3.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz#66008953c2524be786aa319d49e32f2128296a78" + integrity sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA== + dependencies: + "@rollup/pluginutils" "^5.0.1" + "@types/resolve" "1.20.2" + deepmerge "^4.2.2" + is-module "^1.0.0" + resolve "^1.22.1" + +"@rollup/plugin-replace@^2.4.1": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz#a2d539314fbc77c244858faa523012825068510a" + integrity sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + magic-string "^0.25.7" + +"@rollup/plugin-terser@^0.4.3": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz#15dffdb3f73f121aa4fbb37e7ca6be9aeea91962" + integrity sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A== + dependencies: + serialize-javascript "^6.0.1" + smob "^1.0.0" + terser "^5.17.4" + +"@rollup/pluginutils@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" + integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== + dependencies: + "@types/estree" "0.0.39" + estree-walker "^1.0.1" + picomatch "^2.2.2" + +"@rollup/pluginutils@^5.0.1": + version "5.1.4" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.4.tgz#bb94f1f9eaaac944da237767cdfee6c5b2262d4a" + integrity sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^4.0.2" + +"@rspack/binding-darwin-arm64@1.2.0-alpha.0": + version "1.2.0-alpha.0" + resolved "https://registry.yarnpkg.com/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.2.0-alpha.0.tgz#234a0c42f6e89a2589f53ad8c44b2e85638bc77b" + integrity sha512-EPprIe6BrkJ9XuWL5HBXJFaH4vvt5C2kBTvyu+t5E3wacyH9A0gIDaMOEmH30Kt3zl4B07OCBC1nCiJ1sTtimw== + +"@rspack/binding-darwin-x64@1.2.0-alpha.0": + version "1.2.0-alpha.0" + resolved "https://registry.yarnpkg.com/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.2.0-alpha.0.tgz#b40778afa61292e543c812d9790e852c52145aef" + integrity sha512-ACwdgWg0V9j0o3gs1wvhqRJ4xui82L+Fii9Fa74az7P974iWO0ZHw4QIUaO5r434+v9OWMqpyBRN1M7cBrx3GA== + +"@rspack/binding-linux-arm64-gnu@1.2.0-alpha.0": + version "1.2.0-alpha.0" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.0-alpha.0.tgz#d9bdbc5835a7c69afc646c221a58ff7a0f0671fa" + integrity sha512-Ex9SviDikz9E36R4I5si/626FsYOJ35l1Lb+DCRUijjjsvoq4k8Shi8csyBfubR+JZ1M0uOXjJftu1Gm5z8Q0Q== + +"@rspack/binding-linux-arm64-musl@1.2.0-alpha.0": + version "1.2.0-alpha.0" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.0-alpha.0.tgz#e774394097e711a2791e29842d21a2e65730a335" + integrity sha512-U320xZmTcTwQ0BR8yIzE1L4olMCqzYkT3VFjXPR6iok/Mj0xjfk/SiKhLoZml473qQrHSGaFJ321cp02zgTFJg== + +"@rspack/binding-linux-x64-gnu@1.2.0-alpha.0": + version "1.2.0-alpha.0" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.0-alpha.0.tgz#8393f4c423403fe2d1958ed8a916f189676a6e47" + integrity sha512-GNur7VXJ29NtJhY8PYgv3Fv1Zxbx0XZhDUj/+7Wp40CAXRFsLgXScZIRh2U30TECYaihboZ7BD+xugv8MQPDoA== + +"@rspack/binding-linux-x64-musl@1.2.0-alpha.0": + version "1.2.0-alpha.0" + resolved "https://registry.yarnpkg.com/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.0-alpha.0.tgz#5685699b5679dcbba47722ed8b278896247da777" + integrity sha512-0IdswzpG9+sgxvGu7KTwSeqfV0hvciaHMoZvGklfZa2txpcUqAg4ASp7uxrNaUo+G2a1fTUMOtP9351Cnl8DBg== + +"@rspack/binding-win32-arm64-msvc@1.2.0-alpha.0": + version "1.2.0-alpha.0" + resolved "https://registry.yarnpkg.com/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.0-alpha.0.tgz#4af6243595e394ba6f5349c6ae7f7f6575256d55" + integrity sha512-FcFgoWGjSrCfJwDZY5bDA2aO02l5BP7qdyW6ehjwBiMxNZyeSbGvKz3jXl5TtTHR1IgdLzi9kEJkTPYLLMiE1A== + +"@rspack/binding-win32-ia32-msvc@1.2.0-alpha.0": + version "1.2.0-alpha.0" + resolved "https://registry.yarnpkg.com/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.2.0-alpha.0.tgz#7761f5aa4eb7a7ff4e0874a3481ba38dcd8b1759" + integrity sha512-cZYFJw6DKCaPPz9VDJPndZ9KSp+/eedgt11Mv8OTpq+MJTUjB2HjtcjqJh8xxVcp3IuwvSMndTkC69WWt/4feA== + +"@rspack/binding-win32-x64-msvc@1.2.0-alpha.0": + version "1.2.0-alpha.0" + resolved "https://registry.yarnpkg.com/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.0-alpha.0.tgz#c3ba42ed10bb6156b6cca59a6fb1512acad6f0fa" + integrity sha512-gfOqb/rq5716NV+Vbk5MteBhV4VhJeSoh2+dRQjdy4EN1wPZ+Uebs9ORVrT9uRjY3JrPn/5PkAHJXtgaOA9Uyg== + +"@rspack/binding@1.2.0-alpha.0": + version "1.2.0-alpha.0" + resolved "https://registry.yarnpkg.com/@rspack/binding/-/binding-1.2.0-alpha.0.tgz#24f2239b02cff6876edac382588d42ec2f980121" + integrity sha512-rtmDScjtGUxv1zA1m3jXecuX2LsgNp4aWaAjOowHasoO1YqfHK0fMyprCiPowTjoHtpZ7Xt/tnMhii0GlGIITQ== + optionalDependencies: + "@rspack/binding-darwin-arm64" "1.2.0-alpha.0" + "@rspack/binding-darwin-x64" "1.2.0-alpha.0" + "@rspack/binding-linux-arm64-gnu" "1.2.0-alpha.0" + "@rspack/binding-linux-arm64-musl" "1.2.0-alpha.0" + "@rspack/binding-linux-x64-gnu" "1.2.0-alpha.0" + "@rspack/binding-linux-x64-musl" "1.2.0-alpha.0" + "@rspack/binding-win32-arm64-msvc" "1.2.0-alpha.0" + "@rspack/binding-win32-ia32-msvc" "1.2.0-alpha.0" + "@rspack/binding-win32-x64-msvc" "1.2.0-alpha.0" + +"@rspack/core@1.2.0-alpha.0": + version "1.2.0-alpha.0" + resolved "https://registry.yarnpkg.com/@rspack/core/-/core-1.2.0-alpha.0.tgz#942fd797b923215c6b8826a1573c0db09504a0e3" + integrity sha512-YiD0vFDj+PfHs3ZqJwPNhTYyVTb4xR6FpOI5WJ4jJHV4lgdErS+RChTCPhf1xeqxfuTSSnFA7UeqosLhBuNSqQ== + dependencies: + "@module-federation/runtime-tools" "0.8.4" + "@rspack/binding" "1.2.0-alpha.0" + "@rspack/lite-tapable" "1.0.1" + caniuse-lite "^1.0.30001616" + +"@rspack/lite-tapable@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@rspack/lite-tapable/-/lite-tapable-1.0.1.tgz#d4540a5d28bd6177164bc0ba0bee4bdec0458591" + integrity sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w== + +"@scarf/scarf@^1.3.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scarf/scarf/-/scarf-1.4.0.tgz#3bbb984085dbd6d982494538b523be1ce6562972" + integrity sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ== + +"@sideway/address@^4.1.5": + version "4.1.5" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" + integrity sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + +"@sindresorhus/is@^4.6.0": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" + integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== + +"@sindresorhus/is@^5.2.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-5.6.0.tgz#41dd6093d34652cddb5d5bdeee04eafc33826668" + integrity sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g== + +"@slorber/react-ideal-image@^0.0.14": + version "0.0.14" + resolved "https://registry.yarnpkg.com/@slorber/react-ideal-image/-/react-ideal-image-0.0.14.tgz#35b0756c6f06ec60c4a2b5cae9dcf346500e1e8a" + integrity sha512-ULJ1VtNg+B5puJp4ZQzEnDqYyDT9erbABoQygmAovg35ltOymLMH8jXPuxJQBVskcmaG29bTZ+++hE/PAXRgxA== + +"@slorber/remark-comment@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@slorber/remark-comment/-/remark-comment-1.0.0.tgz#2a020b3f4579c89dec0361673206c28d67e08f5a" + integrity sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.1.0" + micromark-util-symbol "^1.0.1" + +"@surma/rollup-plugin-off-main-thread@^2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053" + integrity sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ== + dependencies: + ejs "^3.1.6" + json5 "^2.2.0" + magic-string "^0.25.0" + string.prototype.matchall "^4.0.6" + +"@svgr/babel-plugin-add-jsx-attribute@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz#4001f5d5dd87fa13303e36ee106e3ff3a7eb8b22" + integrity sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g== + +"@svgr/babel-plugin-add-jsx-attribute@^5.4.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz#81ef61947bb268eb9d50523446f9c638fb355906" + integrity sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg== + +"@svgr/babel-plugin-remove-jsx-attribute@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz#69177f7937233caca3a1afb051906698f2f59186" + integrity sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA== + +"@svgr/babel-plugin-remove-jsx-attribute@^5.4.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz#6b2c770c95c874654fd5e1d5ef475b78a0a962ef" + integrity sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg== + +"@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz#c2c48104cfd7dcd557f373b70a56e9e3bdae1d44" + integrity sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA== + +"@svgr/babel-plugin-remove-jsx-empty-expression@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz#25621a8915ed7ad70da6cea3d0a6dbc2ea933efd" + integrity sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA== + +"@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz#8fbb6b2e91fa26ac5d4aa25c6b6e4f20f9c0ae27" + integrity sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ== + +"@svgr/babel-plugin-replace-jsx-attribute-value@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz#0b221fc57f9fcd10e91fe219e2cd0dd03145a897" + integrity sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ== + +"@svgr/babel-plugin-svg-dynamic-title@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz#1d5ba1d281363fc0f2f29a60d6d936f9bbc657b0" + integrity sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og== + +"@svgr/babel-plugin-svg-dynamic-title@^5.4.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz#139b546dd0c3186b6e5db4fefc26cb0baea729d7" + integrity sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg== + +"@svgr/babel-plugin-svg-em-dimensions@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz#35e08df300ea8b1d41cb8f62309c241b0369e501" + integrity sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g== + +"@svgr/babel-plugin-svg-em-dimensions@^5.4.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz#6543f69526632a133ce5cabab965deeaea2234a0" + integrity sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw== + +"@svgr/babel-plugin-transform-react-native-svg@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz#90a8b63998b688b284f255c6a5248abd5b28d754" + integrity sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q== + +"@svgr/babel-plugin-transform-react-native-svg@^5.4.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz#00bf9a7a73f1cad3948cdab1f8dfb774750f8c80" + integrity sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q== + +"@svgr/babel-plugin-transform-svg-component@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz#013b4bfca88779711f0ed2739f3f7efcefcf4f7e" + integrity sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw== + +"@svgr/babel-plugin-transform-svg-component@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz#583a5e2a193e214da2f3afeb0b9e8d3250126b4a" + integrity sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ== + +"@svgr/babel-preset@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-8.1.0.tgz#0e87119aecdf1c424840b9d4565b7137cabf9ece" + integrity sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug== + dependencies: + "@svgr/babel-plugin-add-jsx-attribute" "8.0.0" + "@svgr/babel-plugin-remove-jsx-attribute" "8.0.0" + "@svgr/babel-plugin-remove-jsx-empty-expression" "8.0.0" + "@svgr/babel-plugin-replace-jsx-attribute-value" "8.0.0" + "@svgr/babel-plugin-svg-dynamic-title" "8.0.0" + "@svgr/babel-plugin-svg-em-dimensions" "8.0.0" + "@svgr/babel-plugin-transform-react-native-svg" "8.1.0" + "@svgr/babel-plugin-transform-svg-component" "8.0.0" + +"@svgr/babel-preset@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-5.5.0.tgz#8af54f3e0a8add7b1e2b0fcd5a882c55393df327" + integrity sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig== + dependencies: + "@svgr/babel-plugin-add-jsx-attribute" "^5.4.0" + "@svgr/babel-plugin-remove-jsx-attribute" "^5.4.0" + "@svgr/babel-plugin-remove-jsx-empty-expression" "^5.0.1" + "@svgr/babel-plugin-replace-jsx-attribute-value" "^5.0.1" + "@svgr/babel-plugin-svg-dynamic-title" "^5.4.0" + "@svgr/babel-plugin-svg-em-dimensions" "^5.4.0" + "@svgr/babel-plugin-transform-react-native-svg" "^5.4.0" + "@svgr/babel-plugin-transform-svg-component" "^5.5.0" + +"@svgr/core@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/core/-/core-8.1.0.tgz#41146f9b40b1a10beaf5cc4f361a16a3c1885e88" + integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA== + dependencies: + "@babel/core" "^7.21.3" + "@svgr/babel-preset" "8.1.0" + camelcase "^6.2.0" + cosmiconfig "^8.1.3" + snake-case "^3.0.4" + +"@svgr/core@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@svgr/core/-/core-5.5.0.tgz#82e826b8715d71083120fe8f2492ec7d7874a579" + integrity sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ== + dependencies: + "@svgr/plugin-jsx" "^5.5.0" + camelcase "^6.2.0" + cosmiconfig "^7.0.0" + +"@svgr/hast-util-to-babel-ast@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz#6952fd9ce0f470e1aded293b792a2705faf4ffd4" + integrity sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q== + dependencies: + "@babel/types" "^7.21.3" + entities "^4.4.0" + +"@svgr/hast-util-to-babel-ast@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz#5ee52a9c2533f73e63f8f22b779f93cd432a5461" + integrity sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ== + dependencies: + "@babel/types" "^7.12.6" + +"@svgr/plugin-jsx@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz#96969f04a24b58b174ee4cd974c60475acbd6928" + integrity sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA== + dependencies: + "@babel/core" "^7.21.3" + "@svgr/babel-preset" "8.1.0" + "@svgr/hast-util-to-babel-ast" "8.0.0" + svg-parser "^2.0.4" + +"@svgr/plugin-jsx@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz#1aa8cd798a1db7173ac043466d7b52236b369000" + integrity sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA== + dependencies: + "@babel/core" "^7.12.3" + "@svgr/babel-preset" "^5.5.0" + "@svgr/hast-util-to-babel-ast" "^5.5.0" + svg-parser "^2.0.2" + +"@svgr/plugin-svgo@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz#b115b7b967b564f89ac58feae89b88c3decd0f00" + integrity sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA== + dependencies: + cosmiconfig "^8.1.3" + deepmerge "^4.3.1" + svgo "^3.0.2" + +"@svgr/plugin-svgo@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz#02da55d85320549324e201c7b2e53bf431fcc246" + integrity sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ== + dependencies: + cosmiconfig "^7.0.0" + deepmerge "^4.2.2" + svgo "^1.2.2" + +"@svgr/webpack@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-5.5.0.tgz#aae858ee579f5fa8ce6c3166ef56c6a1b381b640" + integrity sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g== + dependencies: + "@babel/core" "^7.12.3" + "@babel/plugin-transform-react-constant-elements" "^7.12.1" + "@babel/preset-env" "^7.12.1" + "@babel/preset-react" "^7.12.5" + "@svgr/core" "^5.5.0" + "@svgr/plugin-jsx" "^5.5.0" + "@svgr/plugin-svgo" "^5.5.0" + loader-utils "^2.0.0" + +"@svgr/webpack@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-8.1.0.tgz#16f1b5346f102f89fda6ec7338b96a701d8be0c2" + integrity sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA== + dependencies: + "@babel/core" "^7.21.3" + "@babel/plugin-transform-react-constant-elements" "^7.21.3" + "@babel/preset-env" "^7.20.2" + "@babel/preset-react" "^7.18.6" + "@babel/preset-typescript" "^7.21.0" + "@svgr/core" "8.1.0" + "@svgr/plugin-jsx" "8.1.0" + "@svgr/plugin-svgo" "8.1.0" + +"@swc/core-darwin-arm64@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.7.tgz#ff727de61faabfbdfe062747e47305ee3472298e" + integrity sha512-SI0OFg987P6hcyT0Dbng3YRISPS9uhLX1dzW4qRrfqQdb0i75lPJ2YWe9CN47HBazrIA5COuTzrD2Dc0TcVsSQ== + +"@swc/core-darwin-x64@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.10.7.tgz#a276d5ee56e7c9fb03201c92c620143f8df6b52e" + integrity sha512-RFIAmWVicD/l3RzxgHW0R/G1ya/6nyMspE2cAeDcTbjHi0I5qgdhBWd6ieXOaqwEwiCd0Mot1g2VZrLGoBLsjQ== + +"@swc/core-linux-arm-gnueabihf@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.7.tgz#8f2041b818691e7535bc275d32659e77b5f2fecc" + integrity sha512-QP8vz7yELWfop5mM5foN6KkLylVO7ZUgWSF2cA0owwIaziactB2hCPZY5QU690coJouk9KmdFsPWDnaCFUP8tg== + +"@swc/core-linux-arm64-gnu@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.7.tgz#c185499f7db12ee95fdceb4c00fb503ed398cf1d" + integrity sha512-NgUDBGQcOeLNR+EOpmUvSDIP/F7i/OVOKxst4wOvT5FTxhnkWrW+StJGKj+DcUVSK5eWOYboSXr1y+Hlywwokw== + +"@swc/core-linux-arm64-musl@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.7.tgz#20732c402ba44fbd708e9871aaa10df5597a3d01" + integrity sha512-gp5Un3EbeSThBIh6oac5ZArV/CsSmTKj5jNuuUAuEsML3VF9vqPO+25VuxCvsRf/z3py+xOWRaN2HY/rjMeZog== + +"@swc/core-linux-x64-gnu@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.7.tgz#d6310152dd154c0796d1c0d99eb89fc26957c8f6" + integrity sha512-k/OxLLMl/edYqbZyUNg6/bqEHTXJT15l9WGqsl/2QaIGwWGvles8YjruQYQ9d4h/thSXLT9gd8bExU2D0N+bUA== + +"@swc/core-linux-x64-musl@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.7.tgz#e03d4ec66f4234323887774151d1034339d0d7af" + integrity sha512-XeDoURdWt/ybYmXLCEE8aSiTOzEn0o3Dx5l9hgt0IZEmTts7HgHHVeRgzGXbR4yDo0MfRuX5nE1dYpTmCz0uyA== + +"@swc/core-win32-arm64-msvc@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.7.tgz#f1a8c3149e2671d477af4ca39c761d6ade342d4c" + integrity sha512-nYAbi/uLS+CU0wFtBx8TquJw2uIMKBnl04LBmiVoFrsIhqSl+0MklaA9FVMGA35NcxSJfcm92Prl2W2LfSnTqQ== + +"@swc/core-win32-ia32-msvc@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.7.tgz#133f3168fee9910566a874eb1d422dc79eb17d54" + integrity sha512-+aGAbsDsIxeLxw0IzyQLtvtAcI1ctlXVvVcXZMNXIXtTURM876yNrufRo4ngoXB3jnb1MLjIIjgXfFs/eZTUSw== + +"@swc/core-win32-x64-msvc@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.7.tgz#84d6ed82b2f19bc00b868c9747f03ea6661d8023" + integrity sha512-TBf4clpDBjF/UUnkKrT0/th76/zwvudk5wwobiTFqDywMApHip5O0VpBgZ+4raY2TM8k5+ujoy7bfHb22zu17Q== + +"@swc/core@^1.7.39": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.10.7.tgz#736a5bbf0db7628cb2de3eac871e331f9a27e60b" + integrity sha512-py91kjI1jV5D5W/Q+PurBdGsdU5TFbrzamP7zSCqLdMcHkKi3rQEM5jkQcZr0MXXSJTaayLxS3MWYTBIkzPDrg== + dependencies: + "@swc/counter" "^0.1.3" + "@swc/types" "^0.1.17" + optionalDependencies: + "@swc/core-darwin-arm64" "1.10.7" + "@swc/core-darwin-x64" "1.10.7" + "@swc/core-linux-arm-gnueabihf" "1.10.7" + "@swc/core-linux-arm64-gnu" "1.10.7" + "@swc/core-linux-arm64-musl" "1.10.7" + "@swc/core-linux-x64-gnu" "1.10.7" + "@swc/core-linux-x64-musl" "1.10.7" + "@swc/core-win32-arm64-msvc" "1.10.7" + "@swc/core-win32-ia32-msvc" "1.10.7" + "@swc/core-win32-x64-msvc" "1.10.7" + +"@swc/counter@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" + integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== + +"@swc/html-darwin-arm64@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/html-darwin-arm64/-/html-darwin-arm64-1.10.7.tgz#b30af0dac8b9b2fb2ff7915e53dc6705b4fc8480" + integrity sha512-9ocyn/wi0YcEuNl/8S1Lz6vXBzRNnO/BsXdWk8M67/zATdZdYe7PikI8vqvNuRilGVECgbQfrl5mrNF2rbk0TQ== + +"@swc/html-darwin-x64@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/html-darwin-x64/-/html-darwin-x64-1.10.7.tgz#e81230464a2e8cb6a0a564c32a02fcfa46f70aad" + integrity sha512-nk5ye8LS5Jm+hAkFTrnrd+/1gVZ12rl5E3uBCYSxj6P9hOVLo9+QDdP7g7WdOYPMUK3S2Xnvo3d6S0uyeAKeqw== + +"@swc/html-linux-arm-gnueabihf@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/html-linux-arm-gnueabihf/-/html-linux-arm-gnueabihf-1.10.7.tgz#f1141b8ca1d33aa9519ff8aa84b247a76778ce45" + integrity sha512-YRrJzJxnQd9MPUys7orEPszFdWWxCjsvSIlCw3/TI8DAoNXDRJu1wM079UdhOkRa2mz0B0AGcK6T1CC95xvkSg== + +"@swc/html-linux-arm64-gnu@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.10.7.tgz#340013956e16a81c91385ca3b6ba74f1bd1ca16f" + integrity sha512-yW04lLeTv2TUXQ1bMudRsB+XfQCbj9DK71Wv6svIZyZnGcS2lWva5YwdaQtw8+IVHi8/pmVEYkXX93kN5wzmOg== + +"@swc/html-linux-arm64-musl@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/html-linux-arm64-musl/-/html-linux-arm64-musl-1.10.7.tgz#a5149219904b6f6b408e2c76f043a285f3372ce9" + integrity sha512-xBcWSNDIosq79s8dbURFwgZs4H0+3qqnCEhoe/CmAxP+Cd7DSYMF6kT+IcpvqoTYvnnY9oN3UIrl2QqJdOb91Q== + +"@swc/html-linux-x64-gnu@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.10.7.tgz#0546aa198544b67c07efc086bd6a2f992df51709" + integrity sha512-lo57pQIfX68NmaWvMUCO1mKl32V7rPRxzb1tLxcwiYXUvVwY4XNpwXnZ/d8+AK41VMqqWR/qkFpBifGmDjpOAw== + +"@swc/html-linux-x64-musl@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/html-linux-x64-musl/-/html-linux-x64-musl-1.10.7.tgz#72fbeec90b5e9f8812ce6806e37cb4bf7a8ad9f5" + integrity sha512-j2Egvkq3nczH6gDxmVXO53bZ0qW+3YkTHIOcM8+DKKXDT1Y68nQ3F8hMgpE0izAINFaQlAsgmblmVRm9x9E7Cg== + +"@swc/html-win32-arm64-msvc@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/html-win32-arm64-msvc/-/html-win32-arm64-msvc-1.10.7.tgz#10a6197b27ac2b51b3c69fae2ce608b069706b75" + integrity sha512-ytHBj7Qr/quYOaL2B+QhxF//ZduzbS5toriaB8ESy5cvlJEwZJ5TGQM9wa6f/WErfZjnsUssfxdyBfpPGCSGUg== + +"@swc/html-win32-ia32-msvc@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/html-win32-ia32-msvc/-/html-win32-ia32-msvc-1.10.7.tgz#1a3568db9afcf53034cb08220f56a80c848d3d6e" + integrity sha512-8NCoCK2OaedV/CznBiRbDDlcSf3RODWQXeq1lp6ozf3aVkmDaMJ4OD7M/hD94f5UwO4GVpp86Ow0ikLJ+5zhbg== + +"@swc/html-win32-x64-msvc@1.10.7": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/html-win32-x64-msvc/-/html-win32-x64-msvc-1.10.7.tgz#056c6226a6e0f14df7d1e7acd64980fbfbc064c4" + integrity sha512-t2jT2D+3ZC9rP/K8Tg0/MPkRnDE59ugUXMnNTE2HfiXttITn7PCcwUNU+HCbB2iqiLIZixhuayU/to4kkpjHZQ== + +"@swc/html@^1.7.39": + version "1.10.7" + resolved "https://registry.yarnpkg.com/@swc/html/-/html-1.10.7.tgz#47767b5ff390a047ce3e359e8fd97b4008a2113d" + integrity sha512-3+5qmP/CX8tOxYBqtw8uPWS0gIhKesu1UWXLRFJ8QgDDZFevtI4yPP0AcSGuLxlnjTMUGblbpcQLj3Jco5ks0g== + dependencies: + "@swc/counter" "^0.1.3" + optionalDependencies: + "@swc/html-darwin-arm64" "1.10.7" + "@swc/html-darwin-x64" "1.10.7" + "@swc/html-linux-arm-gnueabihf" "1.10.7" + "@swc/html-linux-arm64-gnu" "1.10.7" + "@swc/html-linux-arm64-musl" "1.10.7" + "@swc/html-linux-x64-gnu" "1.10.7" + "@swc/html-linux-x64-musl" "1.10.7" + "@swc/html-win32-arm64-msvc" "1.10.7" + "@swc/html-win32-ia32-msvc" "1.10.7" + "@swc/html-win32-x64-msvc" "1.10.7" + +"@swc/types@^0.1.17": + version "0.1.17" + resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.17.tgz#bd1d94e73497f27341bf141abdf4c85230d41e7c" + integrity sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ== + dependencies: + "@swc/counter" "^0.1.3" + +"@szmarczak/http-timer@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-5.0.1.tgz#c7c1bf1141cdd4751b0399c8fc7b8b664cd5be3a" + integrity sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw== + dependencies: + defer-to-connect "^2.0.1" + +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== + +"@types/acorn@^4.0.0": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.6.tgz#d61ca5480300ac41a7d973dd5b84d0a591154a22" + integrity sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ== + dependencies: + "@types/estree" "*" + +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.9": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" + integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== + dependencies: + "@types/node" "*" + +"@types/connect-history-api-fallback@^1.3.5": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" + integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/debug@^4.0.0": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + +"@types/eslint-scope@^3.7.7": + version "3.7.7" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "9.6.1" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584" + integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree-jsx@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz#858a88ea20f34fe65111f005a689fa1ebf70dc18" + integrity sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg== + dependencies: + "@types/estree" "*" + +"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + +"@types/estree@0.0.39": + version "0.0.39" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.4.tgz#88c29e3052cec3536d64b6ce5015a30dfcbefca7" + integrity sha512-5kz9ScmzBdzTgB/3susoCgfqNDzBjvLL4taparufgSvlwjdLy6UyUy9T/tCpYd2GIdIilCatC4iSQS0QSYHt0w== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express-serve-static-core@^4.17.33": + version "4.19.6" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" + integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.0.tgz#13a7d1f75295e90d19ed6e74cab3678488eaa96c" + integrity sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/express@^4.17.13": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/gtag.js@^0.0.12": + version "0.0.12" + resolved "https://registry.yarnpkg.com/@types/gtag.js/-/gtag.js-0.0.12.tgz#095122edca896689bdfcdd73b057e23064d23572" + integrity sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg== + +"@types/hast@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + dependencies: + "@types/unist" "*" + +"@types/history@^4.7.11": + version "4.7.11" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" + integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== + +"@types/html-minifier-terser@^6.0.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" + integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== + +"@types/http-cache-semantics@^4.0.2": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" + integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/http-proxy@^1.17.8": + version "1.17.15" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.15.tgz#12118141ce9775a6499ecb4c01d02f90fc839d36" + integrity sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ== + dependencies: + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== + +"@types/istanbul-lib-report@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" + integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" + integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/mdast@^4.0.0", "@types/mdast@^4.0.2": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" + integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== + dependencies: + "@types/unist" "*" + +"@types/mdx@^2.0.0": + version "2.0.13" + resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.13.tgz#68f6877043d377092890ff5b298152b0a21671bd" + integrity sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/ms@*": + version "0.7.34" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" + integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== + +"@types/node-forge@^1.3.0": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "22.10.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.5.tgz#95af89a3fb74a2bb41ef9927f206e6472026e48b" + integrity sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ== + dependencies: + undici-types "~6.20.0" + +"@types/node@^17.0.5": + version "17.0.45" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" + integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== + +"@types/parse-json@^4.0.0": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" + integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== + +"@types/prismjs@^1.26.0": + version "1.26.5" + resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.5.tgz#72499abbb4c4ec9982446509d2f14fb8483869d6" + integrity sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ== + +"@types/prop-types@*": + version "15.7.14" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2" + integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== + +"@types/q@^1.5.1": + version "1.5.8" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.8.tgz#95f6c6a08f2ad868ba230ead1d2d7f7be3db3837" + integrity sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw== + +"@types/qs@*": + version "6.9.17" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.17.tgz#fc560f60946d0aeff2f914eb41679659d3310e1a" + integrity sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/react-router-config@*", "@types/react-router-config@^5.0.7": + version "5.0.11" + resolved "https://registry.yarnpkg.com/@types/react-router-config/-/react-router-config-5.0.11.tgz#2761a23acc7905a66a94419ee40294a65aaa483a" + integrity sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-router" "^5.1.0" + +"@types/react-router-dom@*": + version "5.3.3" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" + integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*", "@types/react-router@^5.1.0": + version "5.1.20" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c" + integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + +"@types/react@*": + version "19.0.4" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.4.tgz#ad1270e090118ac3c5f0928a29fe0ddf164881df" + integrity sha512-3O4QisJDYr1uTUMZHA2YswiQZRq+Pd8D+GdVFYikTutYsTz+QZgWkAPnP7rx9txoI6EXKcPiluMqWPFV3tT9Wg== + dependencies: + csstype "^3.0.2" + +"@types/react@^18.2.29": + version "18.3.18" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.18.tgz#9b382c4cd32e13e463f97df07c2ee3bbcd26904b" + integrity sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + +"@types/resolve@1.20.2": + version "1.20.2" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" + integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== + +"@types/retry@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + +"@types/sax@^1.2.1": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/sax/-/sax-1.2.7.tgz#ba5fe7df9aa9c89b6dff7688a19023dd2963091d" + integrity sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A== + dependencies: + "@types/node" "*" + +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-index@^1.9.1": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" + integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== + dependencies: + "@types/express" "*" + +"@types/serve-static@*", "@types/serve-static@^1.13.10": + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/sockjs@^0.3.33": + version "0.3.36" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" + integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== + dependencies: + "@types/node" "*" + +"@types/trusted-types@^2.0.2": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + +"@types/unist@*", "@types/unist@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + +"@types/unist@^2.0.0": + version "2.0.11" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" + integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== + +"@types/ws@^8.5.5": + version "8.5.13" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.13.tgz#6414c280875e2691d0d1e080b05addbf5cb91e20" + integrity sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA== + dependencies: + "@types/node" "*" + +"@types/yargs-parser@*": + version "21.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" + integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== + +"@types/yargs@^17.0.8": + version "17.0.33" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d" + integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== + dependencies: + "@types/yargs-parser" "*" + +"@ungap/structured-clone@^1.0.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.1.tgz#28fa185f67daaf7b7a1a8c1d445132c5d979f8bd" + integrity sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA== + +"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" + integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== + dependencies: + "@webassemblyjs/helper-numbers" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + +"@webassemblyjs/floating-point-hex-parser@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb" + integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== + +"@webassemblyjs/helper-api-error@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7" + integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== + +"@webassemblyjs/helper-buffer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b" + integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== + +"@webassemblyjs/helper-numbers@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d" + integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.13.2" + "@webassemblyjs/helper-api-error" "1.13.2" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b" + integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== + +"@webassemblyjs/helper-wasm-section@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348" + integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/wasm-gen" "1.14.1" + +"@webassemblyjs/ieee754@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba" + integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0" + integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" + integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== + +"@webassemblyjs/wasm-edit@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" + integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/helper-wasm-section" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-opt" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + "@webassemblyjs/wast-printer" "1.14.1" + +"@webassemblyjs/wasm-gen@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570" + integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + +"@webassemblyjs/wasm-opt@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b" + integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + +"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" + integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-api-error" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + +"@webassemblyjs/wast-printer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07" + integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@xtuc/long" "4.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +accepts@~1.3.4, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-class-fields@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/acorn-class-fields/-/acorn-class-fields-0.2.1.tgz#748058bceeb0ef25164bbc671993984083f5a085" + integrity sha512-US/kqTe0H8M4LN9izoL+eykVAitE68YMuYZ3sHn3i1fjniqR7oQ3SPvuMK/VT1kjOQHrx5Q88b90TtOKgAv2hQ== + +acorn-dynamic-import@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" + integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw== + +acorn-jsx@^5.0.0, acorn-jsx@^5.0.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-walk@^8.0.0: + version "8.3.4" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" + integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== + dependencies: + acorn "^8.11.0" + +acorn@^6.1.1: + version "6.4.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" + integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== + +acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.8.2: + version "8.14.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" + integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== + +address@^1.0.1, address@^1.1.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" + integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.12.2, ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0, ajv@^8.6.0, ajv@^8.9.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +algoliasearch-helper@^3.22.6: + version "3.24.1" + resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.24.1.tgz#763115d81fc56518bff36b7c707967f70d8fdf45" + integrity sha512-knYRACqLH9UpeR+WRUrBzBFR2ulGuOjI2b525k4PNeqZxeFMHJE7YcL7s6Jh12Qza0rtHqZdgHMfeuaaAkf4wA== + dependencies: + "@algolia/events" "^4.0.1" + +algoliasearch@^5.14.2: + version "5.19.0" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-5.19.0.tgz#2a1490bb46a937515797fac30b2d1503fb028536" + integrity sha512-zrLtGhC63z3sVLDDKGW+SlCRN9eJHFTgdEmoAOpsVh6wgGL1GgTTDou7tpCBjevzgIvi3AIyDAQO3Xjbg5eqZg== + dependencies: + "@algolia/client-abtesting" "5.19.0" + "@algolia/client-analytics" "5.19.0" + "@algolia/client-common" "5.19.0" + "@algolia/client-insights" "5.19.0" + "@algolia/client-personalization" "5.19.0" + "@algolia/client-query-suggestions" "5.19.0" + "@algolia/client-search" "5.19.0" + "@algolia/ingestion" "1.19.0" + "@algolia/monitoring" "1.19.0" + "@algolia/recommend" "5.19.0" + "@algolia/requester-browser-xhr" "5.19.0" + "@algolia/requester-fetch" "5.19.0" + "@algolia/requester-node-http" "5.19.0" + +algoliasearch@^5.17.1: + version "5.20.0" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-5.20.0.tgz#15f4eb6428f258d083d1cbc47d04a8d66eecba5f" + integrity sha512-groO71Fvi5SWpxjI9Ia+chy0QBwT61mg6yxJV27f5YFf+Mw+STT75K6SHySpP8Co5LsCrtsbCH5dJZSRtkSKaQ== + dependencies: + "@algolia/client-abtesting" "5.20.0" + "@algolia/client-analytics" "5.20.0" + "@algolia/client-common" "5.20.0" + "@algolia/client-insights" "5.20.0" + "@algolia/client-personalization" "5.20.0" + "@algolia/client-query-suggestions" "5.20.0" + "@algolia/client-search" "5.20.0" + "@algolia/ingestion" "1.20.0" + "@algolia/monitoring" "1.20.0" + "@algolia/recommend" "5.20.0" + "@algolia/requester-browser-xhr" "5.20.0" + "@algolia/requester-fetch" "5.20.0" + "@algolia/requester-node-http" "5.20.0" + +ansi-align@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + +ansi-escapes@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^5.0.0, arg@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +aria-hidden@^1.1.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522" + integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A== + dependencies: + tslib "^2.0.0" + +array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" + integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== + dependencies: + call-bound "^1.0.3" + is-array-buffer "^3.0.5" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.reduce@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.7.tgz#6aadc2f995af29cb887eb866d981dc85ab6f7dc7" + integrity sha512-mzmiUCVwtiD4lgxYP8g7IYy8El8p2CSMePvIbTS7gchKir/L1fgJrk0yDKmAX6mnRQFKNADYIk8nNlTris5H1Q== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-array-method-boxes-properly "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + is-string "^1.0.7" + +arraybuffer.prototype.slice@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz#9d760d84dbdd06d0cbf92c8849615a1a7ab3183c" + integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + is-array-buffer "^3.0.4" + +astring@^1.8.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/astring/-/astring-1.9.0.tgz#cc73e6062a7eb03e7d19c22d8b0b3451fd9bfeef" + integrity sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg== + +async@^3.2.3: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +autoprefixer@^10.4.13, autoprefixer@^10.4.19, autoprefixer@^10.4.20: + version "10.4.20" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b" + integrity sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g== + dependencies: + browserslist "^4.23.3" + caniuse-lite "^1.0.30001646" + fraction.js "^4.3.7" + normalize-range "^0.1.2" + picocolors "^1.0.1" + postcss-value-parser "^4.2.0" + +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + +b4a@^1.6.4: + version "1.6.7" + resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.7.tgz#a99587d4ebbfbd5a6e3b21bdb5d5fa385767abe4" + integrity sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg== + +babel-loader@^9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.2.1.tgz#04c7835db16c246dd19ba0914418f3937797587b" + integrity sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA== + dependencies: + find-cache-dir "^4.0.0" + schema-utils "^4.0.0" + +babel-plugin-dynamic-import-node@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" + integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== + dependencies: + object.assign "^4.1.0" + +babel-plugin-polyfill-corejs2@^0.4.10: + version "0.4.12" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz#ca55bbec8ab0edeeef3d7b8ffd75322e210879a9" + integrity sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og== + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.6.3" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.10.6: + version "0.10.6" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz#2deda57caef50f59c525aeb4964d3b2f867710c7" + integrity sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.2" + core-js-compat "^3.38.0" + +babel-plugin-polyfill-regenerator@^0.6.1: + version "0.6.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz#abeb1f3f1c762eace37587f42548b08b57789bc8" + integrity sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.3" + +bail@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" + integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +bare-events@^2.0.0, bare-events@^2.2.0: + version "2.5.4" + resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.4.tgz#16143d435e1ed9eafd1ab85f12b89b3357a41745" + integrity sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA== + +bare-fs@^2.1.1: + version "2.3.5" + resolved "https://registry.yarnpkg.com/bare-fs/-/bare-fs-2.3.5.tgz#05daa8e8206aeb46d13c2fe25a2cd3797b0d284a" + integrity sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw== + dependencies: + bare-events "^2.0.0" + bare-path "^2.0.0" + bare-stream "^2.0.0" + +bare-os@^2.1.0: + version "2.4.4" + resolved "https://registry.yarnpkg.com/bare-os/-/bare-os-2.4.4.tgz#01243392eb0a6e947177bb7c8a45123d45c9b1a9" + integrity sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ== + +bare-path@^2.0.0, bare-path@^2.1.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/bare-path/-/bare-path-2.1.3.tgz#594104c829ef660e43b5589ec8daef7df6cedb3e" + integrity sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA== + dependencies: + bare-os "^2.1.0" + +bare-stream@^2.0.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.6.1.tgz#b3b9874fab05b662c9aea2706a12fb0698c46836" + integrity sha512-eVZbtKM+4uehzrsj49KtCy3Pbg7kO1pJ3SKZ1SFrIH/0pnj9scuGGgUlNDf/7qS8WKtGdiJY5Kyhs/ivYPTB/g== + dependencies: + streamx "^2.21.0" + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour-service@^1.0.11: + version "1.3.0" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.3.0.tgz#80d867430b5a0da64e82a8047fc1e355bdb71722" + integrity sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA== + dependencies: + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + +boolbase@^1.0.0, boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + +boxen@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-6.2.1.tgz#b098a2278b2cd2845deef2dff2efc38d329b434d" + integrity sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw== + dependencies: + ansi-align "^3.0.1" + camelcase "^6.2.0" + chalk "^4.1.2" + cli-boxes "^3.0.0" + string-width "^5.0.1" + type-fest "^2.5.0" + widest-line "^4.0.1" + wrap-ansi "^8.0.1" + +boxen@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-7.1.1.tgz#f9ba525413c2fec9cdb88987d835c4f7cad9c8f4" + integrity sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog== + dependencies: + ansi-align "^3.0.1" + camelcase "^7.0.1" + chalk "^5.2.0" + cli-boxes "^3.0.0" + string-width "^5.1.2" + type-fest "^2.13.0" + widest-line "^4.0.1" + wrap-ansi "^8.1.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.4, browserslist@^4.23.0, browserslist@^4.23.1, browserslist@^4.23.3, browserslist@^4.24.0, browserslist@^4.24.2, browserslist@^4.24.3: + version "4.24.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b" + integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A== + dependencies: + caniuse-lite "^1.0.30001688" + electron-to-chromium "^1.5.73" + node-releases "^2.0.19" + update-browserslist-db "^1.1.1" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cacheable-lookup@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27" + integrity sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w== + +cacheable-request@^10.2.8: + version "10.2.14" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-10.2.14.tgz#eb915b665fda41b79652782df3f553449c406b9d" + integrity sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ== + dependencies: + "@types/http-cache-semantics" "^4.0.2" + get-stream "^6.0.1" + http-cache-semantics "^4.1.1" + keyv "^4.5.3" + mimic-response "^4.0.0" + normalize-url "^8.0.0" + responselike "^3.0.0" + +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840" + integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bind@^1.0.7, call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-bound@^1.0.2, call-bound@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.3.tgz#41cfd032b593e39176a71533ab4f384aa04fd681" + integrity sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA== + dependencies: + call-bind-apply-helpers "^1.0.1" + get-intrinsic "^1.2.6" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camel-case@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" + integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== + dependencies: + pascal-case "^3.1.2" + tslib "^2.0.3" + +camelcase-css@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" + integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +camelcase@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-7.0.1.tgz#f02e50af9fd7782bc8b88a3558c32fd3a388f048" + integrity sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw== + +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001616, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001688: + version "1.0.30001692" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz#4585729d95e6b95be5b439da6ab55250cd125bf9" + integrity sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A== + +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + +chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^5.0.1, chalk@^5.2.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" + integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +character-entities-html4@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" + integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + +character-reference-invalid@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" + integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== + +cheerio-select@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" + integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== + dependencies: + boolbase "^1.0.0" + css-select "^5.1.0" + css-what "^6.1.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + +cheerio@1.0.0-rc.12: + version "1.0.0-rc.12" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" + integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.0.1" + htmlparser2 "^8.0.1" + parse5 "^7.0.0" + parse5-htmlparser2-tree-adapter "^7.0.0" + +chokidar@^3.4.2, chokidar@^3.5.3, chokidar@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +chrome-trace-event@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== + +ci-info@^3.2.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + +class-variance-authority@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz#4008a798a0e4553a781a57ac5177c9fb5d043787" + integrity sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg== + dependencies: + clsx "^2.1.1" + +classnames@^2.3.2: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + +clean-css@^5.2.2, clean-css@^5.3.2, clean-css@~5.3.2: + version "5.3.3" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" + integrity sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg== + dependencies: + source-map "~0.6.0" + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-boxes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-3.0.0.tgz#71a10c716feeba005e4504f36329ef0b17cf3145" + integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g== + +cli-table3@^0.6.3: + version "0.6.5" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f" + integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +clsx@^1.1.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + +clsx@^2.0.0, clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + +cmdk@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cmdk/-/cmdk-1.0.4.tgz#cbddef6f5ade2378f85c80a0b9ad9a8a712779b5" + integrity sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg== + dependencies: + "@radix-ui/react-dialog" "^1.1.2" + "@radix-ui/react-id" "^1.1.0" + "@radix-ui/react-primitive" "^2.0.0" + use-sync-external-store "^1.2.2" + +coa@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" + integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== + dependencies: + "@types/q" "^1.5.1" + chalk "^2.4.1" + q "^1.1.2" + +collapse-white-space@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-2.1.0.tgz#640257174f9f42c740b40f3b55ee752924feefca" + integrity sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + +colord@^2.9.3: + version "2.9.3" + resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" + integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== + +colorette@^2.0.10: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +combine-promises@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/combine-promises/-/combine-promises-1.2.0.tgz#5f2e68451862acf85761ded4d9e2af7769c2ca6a" + integrity sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ== + +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +commander@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +commander@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + +common-path-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0" + integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== + +common-tags@^1.8.0: + version "1.8.2" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" + integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== + +compressible@~2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.5" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.5.tgz#fdd256c0a642e39e314c478f6c2cd654edd74c93" + integrity sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q== + dependencies: + bytes "3.1.2" + compressible "~2.0.18" + debug "2.6.9" + negotiator "~0.6.4" + on-headers "~1.0.2" + safe-buffer "5.2.1" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +config-chain@^1.1.11: + version "1.1.13" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" + integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + +configstore@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-6.0.0.tgz#49eca2ebc80983f77e09394a1a56e0aca8235566" + integrity sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA== + dependencies: + dot-prop "^6.0.1" + graceful-fs "^4.2.6" + unique-string "^3.0.0" + write-file-atomic "^3.0.3" + xdg-basedir "^5.0.1" + +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + +consola@^3.2.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.3.3.tgz#0dd8a2314b0f7bf18a49064138ad685f3346543d" + integrity sha512-Qil5KwghMzlqd51UXM0b6fyaGHtOC22scxrwrz4A2882LyUMwQjnvaedN1HAeXzphspQ6CpHkzMAWxBTUruDLg== + +"consolidated-events@^1.1.0 || ^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-2.0.2.tgz#da8d8f8c2b232831413d9e190dc11669c79f4a91" + integrity sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ== + +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + integrity sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== + +copy-text-to-clipboard@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.0.tgz#0202b2d9bdae30a49a53f898626dcc3b49ad960b" + integrity sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q== + +copy-webpack-plugin@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a" + integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ== + dependencies: + fast-glob "^3.2.11" + glob-parent "^6.0.1" + globby "^13.1.1" + normalize-path "^3.0.0" + schema-utils "^4.0.0" + serialize-javascript "^6.0.0" + +core-js-compat@^3.38.0, core-js-compat@^3.38.1: + version "3.40.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.40.0.tgz#7485912a5a4a4315c2fdb2cbdc623e6881c88b38" + integrity sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ== + dependencies: + browserslist "^4.24.3" + +core-js-pure@^3.30.2: + version "3.40.0" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.40.0.tgz#d9a019e9160f9b042eeb6abb92242680089d486e" + integrity sha512-AtDzVIgRrmRKQai62yuSIN5vNiQjcJakJb4fbhVw3ehxx7Lohphvw9SGNWKhLFqSxC4ilD0g/L1huAYFQU3Q6A== + +core-js@^3.31.1: + version "3.40.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.40.0.tgz#2773f6b06877d8eda102fc42f828176437062476" + integrity sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cosmiconfig@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" + integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.7.2" + +cosmiconfig@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +cosmiconfig@^8.1.3, cosmiconfig@^8.3.5: + version "8.3.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== + dependencies: + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + path-type "^4.0.0" + +cross-spawn@^7.0.0, cross-spawn@^7.0.3: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + +crypto-random-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-4.0.0.tgz#5a3cc53d7dd86183df5da0312816ceeeb5bb1fc2" + integrity sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA== + dependencies: + type-fest "^1.0.1" + +css-blank-pseudo@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz#36523b01c12a25d812df343a32c322d2a2324561" + integrity sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ== + dependencies: + postcss-selector-parser "^6.0.9" + +css-blank-pseudo@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz#32020bff20a209a53ad71b8675852b49e8d57e46" + integrity sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag== + dependencies: + postcss-selector-parser "^7.0.0" + +css-declaration-sorter@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz#6dec1c9523bc4a643e088aab8f09e67a54961024" + integrity sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow== + +css-has-pseudo@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz#57f6be91ca242d5c9020ee3e51bbb5b89fc7af73" + integrity sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw== + dependencies: + postcss-selector-parser "^6.0.9" + +css-has-pseudo@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-7.0.2.tgz#fb42e8de7371f2896961e1f6308f13c2c7019b72" + integrity sha512-nzol/h+E0bId46Kn2dQH5VElaknX2Sr0hFuB/1EomdC7j+OISt2ZzK7EHX9DZDY53WbIVAR7FYKSO2XnSf07MQ== + dependencies: + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + postcss-value-parser "^4.2.0" + +css-loader@^6.8.1: + version "6.11.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.11.0.tgz#33bae3bf6363d0a7c2cf9031c96c744ff54d85ba" + integrity sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g== + dependencies: + icss-utils "^5.1.0" + postcss "^8.4.33" + postcss-modules-extract-imports "^3.1.0" + postcss-modules-local-by-default "^4.0.5" + postcss-modules-scope "^3.2.0" + postcss-modules-values "^4.0.0" + postcss-value-parser "^4.2.0" + semver "^7.5.4" + +css-minimizer-webpack-plugin@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-5.0.1.tgz#33effe662edb1a0bf08ad633c32fa75d0f7ec565" + integrity sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" + cssnano "^6.0.1" + jest-worker "^29.4.3" + postcss "^8.4.24" + schema-utils "^4.0.1" + serialize-javascript "^6.0.1" + +css-prefers-color-scheme@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz#ba001b99b8105b8896ca26fc38309ddb2278bd3c" + integrity sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ== + +css-prefers-color-scheme@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz#ca8a22e5992c10a5b9d315155e7caee625903349" + integrity sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA== + +css-select-base-adapter@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" + integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== + +css-select@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" + integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== + dependencies: + boolbase "^1.0.0" + css-what "^3.2.1" + domutils "^1.7.0" + nth-check "^1.0.2" + +css-select@^4.1.3: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== + dependencies: + boolbase "^1.0.0" + css-what "^6.0.1" + domhandler "^4.3.1" + domutils "^2.8.0" + nth-check "^2.0.1" + +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-tree@1.0.0-alpha.37: + version "1.0.0-alpha.37" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" + integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg== + dependencies: + mdn-data "2.0.4" + source-map "^0.6.1" + +css-tree@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-tree@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" + integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== + dependencies: + mdn-data "2.0.30" + source-map-js "^1.0.1" + +css-tree@~2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.2.1.tgz#36115d382d60afd271e377f9c5f67d02bd48c032" + integrity sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA== + dependencies: + mdn-data "2.0.28" + source-map-js "^1.0.1" + +css-what@^3.2.1: + version "3.4.2" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" + integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== + +css-what@^6.0.1, css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +cssdb@^7.1.0: + version "7.11.2" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.11.2.tgz#127a2f5b946ee653361a5af5333ea85a39df5ae5" + integrity sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A== + +cssdb@^8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.2.3.tgz#7e6980bb5a785a9b4eb2a21bd38d50624b56cb46" + integrity sha512-9BDG5XmJrJQQnJ51VFxXCAtpZ5ebDlAREmO8sxMOVU0aSxN/gocbctjIG5LMh3WBUq+xTlb/jw2LoljBEqraTA== + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +cssnano-preset-advanced@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz#82b090872b8f98c471f681d541c735acf8b94d3f" + integrity sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ== + dependencies: + autoprefixer "^10.4.19" + browserslist "^4.23.0" + cssnano-preset-default "^6.1.2" + postcss-discard-unused "^6.0.5" + postcss-merge-idents "^6.0.3" + postcss-reduce-idents "^6.0.3" + postcss-zindex "^6.0.2" + +cssnano-preset-default@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz#adf4b89b975aa775f2750c89dbaf199bbd9da35e" + integrity sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg== + dependencies: + browserslist "^4.23.0" + css-declaration-sorter "^7.2.0" + cssnano-utils "^4.0.2" + postcss-calc "^9.0.1" + postcss-colormin "^6.1.0" + postcss-convert-values "^6.1.0" + postcss-discard-comments "^6.0.2" + postcss-discard-duplicates "^6.0.3" + postcss-discard-empty "^6.0.3" + postcss-discard-overridden "^6.0.2" + postcss-merge-longhand "^6.0.5" + postcss-merge-rules "^6.1.1" + postcss-minify-font-values "^6.1.0" + postcss-minify-gradients "^6.0.3" + postcss-minify-params "^6.1.0" + postcss-minify-selectors "^6.0.4" + postcss-normalize-charset "^6.0.2" + postcss-normalize-display-values "^6.0.2" + postcss-normalize-positions "^6.0.2" + postcss-normalize-repeat-style "^6.0.2" + postcss-normalize-string "^6.0.2" + postcss-normalize-timing-functions "^6.0.2" + postcss-normalize-unicode "^6.1.0" + postcss-normalize-url "^6.0.2" + postcss-normalize-whitespace "^6.0.2" + postcss-ordered-values "^6.0.2" + postcss-reduce-initial "^6.1.0" + postcss-reduce-transforms "^6.0.2" + postcss-svgo "^6.0.3" + postcss-unique-selectors "^6.0.4" + +cssnano-utils@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-4.0.2.tgz#56f61c126cd0f11f2eef1596239d730d9fceff3c" + integrity sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ== + +cssnano@^6.0.1, cssnano@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-6.1.2.tgz#4bd19e505bd37ee7cf0dc902d3d869f6d79c66b8" + integrity sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA== + dependencies: + cssnano-preset-default "^6.1.2" + lilconfig "^3.1.1" + +csso@^4.0.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + +csso@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6" + integrity sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ== + dependencies: + css-tree "~2.2.0" + +csstype@^3.0.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +data-view-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" + integrity sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-data-view "^1.0.2" + +data-view-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz#9e80f7ca52453ce3e93d25a35318767ea7704735" + integrity sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-data-view "^1.0.2" + +data-view-byte-offset@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz#068307f9b71ab76dbbe10291389e020856606191" + integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +date-fns@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf" + integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== + +debounce@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" + integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== + +debug@2.6.9, debug@^2.6.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + +decode-named-character-reference@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" + integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== + dependencies: + character-entities "^2.0.0" + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +deepmerge-ts@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz#c55206cc4c7be2ded89b9c816cf3608884525d7a" + integrity sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw== + +deepmerge@^4.2.2, deepmerge@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +default-gateway@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" + integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== + dependencies: + execa "^5.0.0" + +defer-to-connect@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" + integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== + +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +define-properties@^1.1.3, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +del@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/del/-/del-6.1.1.tgz#3b70314f1ec0aa325c6b14eb36b95786671edb7a" + integrity sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg== + dependencies: + globby "^11.0.1" + graceful-fs "^4.2.4" + is-glob "^4.0.1" + is-path-cwd "^2.2.0" + is-path-inside "^3.0.2" + p-map "^4.0.0" + rimraf "^3.0.2" + slash "^3.0.0" + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +dequal@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== + +detect-libc@^2.0.0, detect-libc@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + +detect-node-es@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" + integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +detect-port-alt@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" + integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== + dependencies: + address "^1.0.1" + debug "^2.6.0" + +detect-port@^1.5.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.6.1.tgz#45e4073997c5f292b957cb678fb0bb8ed4250a67" + integrity sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q== + dependencies: + address "^1.0.1" + debug "4" + +devlop@^1.0.0, devlop@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + +didyoumean@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" + integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dlv@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" + integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== + +dns-packet@^5.2.2: + version "5.6.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" + integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + +docusaurus-plugin-image-zoom@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/docusaurus-plugin-image-zoom/-/docusaurus-plugin-image-zoom-1.0.1.tgz#17afec39f2e630cac50a4ed3a8bbdad8d0aa8b9d" + integrity sha512-96IpSKUx2RWy3db9aZ0s673OQo5DWgV9UVWouS+CPOSIVEdCWh6HKmWf6tB9rsoaiIF3oNn9keiyv6neEyKb1Q== + dependencies: + medium-zoom "^1.0.6" + validate-peer-dependencies "^2.2.0" + +dom-converter@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" + integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== + dependencies: + utila "~0.4" + +dom-serializer@0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +dom-serializer@^1.0.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^2.5.2, domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +domutils@^3.0.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" + integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +dot-prop@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" + integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== + dependencies: + is-obj "^2.0.0" + +dunder-proto@^1.0.0, dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +duplexer@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" + integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +ejs@^3.1.6: + version "3.1.10" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== + dependencies: + jake "^10.8.5" + +electron-to-chromium@^1.5.73: + version "1.5.80" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.80.tgz#ca7a8361d7305f0ec9e203ce4e633cbb8a8ef1b1" + integrity sha512-LTrKpW0AqIuHwmlVNV+cjFYTnXtM9K37OGhpe0ZI10ScPSxqVSryZHIY3WnCS5NSYbBODRTZyhRMS2h5FAEqAw== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +emojilib@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/emojilib/-/emojilib-2.4.0.tgz#ac518a8bb0d5f76dda57289ccb2fdf9d39ae721e" + integrity sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +emoticon@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/emoticon/-/emoticon-4.1.0.tgz#d5a156868ee173095627a33de3f1e914c3dde79e" + integrity sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enhanced-resolve@^5.17.1: + version "5.18.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz#91eb1db193896b9801251eeff1c6980278b1e404" + integrity sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +entities@^4.2.0, entities@^4.4.0, entities@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.17.2, es-abstract@^1.23.2, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9: + version "1.23.9" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.9.tgz#5b45994b7de78dada5c1bebf1379646b32b9d606" + integrity sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA== + dependencies: + array-buffer-byte-length "^1.0.2" + arraybuffer.prototype.slice "^1.0.4" + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" + data-view-buffer "^1.0.2" + data-view-byte-length "^1.0.2" + data-view-byte-offset "^1.0.1" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.1.0" + es-to-primitive "^1.3.0" + function.prototype.name "^1.1.8" + get-intrinsic "^1.2.7" + get-proto "^1.0.0" + get-symbol-description "^1.1.0" + globalthis "^1.0.4" + gopd "^1.2.0" + has-property-descriptors "^1.0.2" + has-proto "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + internal-slot "^1.1.0" + is-array-buffer "^3.0.5" + is-callable "^1.2.7" + is-data-view "^1.0.2" + is-regex "^1.2.1" + is-shared-array-buffer "^1.0.4" + is-string "^1.1.1" + is-typed-array "^1.1.15" + is-weakref "^1.1.0" + math-intrinsics "^1.1.0" + object-inspect "^1.13.3" + object-keys "^1.1.1" + object.assign "^4.1.7" + own-keys "^1.0.1" + regexp.prototype.flags "^1.5.3" + safe-array-concat "^1.1.3" + safe-push-apply "^1.0.0" + safe-regex-test "^1.1.0" + set-proto "^1.0.0" + string.prototype.trim "^1.2.10" + string.prototype.trimend "^1.0.9" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.3" + typed-array-byte-length "^1.0.3" + typed-array-byte-offset "^1.0.4" + typed-array-length "^1.0.7" + unbox-primitive "^1.1.0" + which-typed-array "^1.1.18" + +es-array-method-boxes-properly@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" + integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== + +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-module-lexer@^1.2.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.6.0.tgz#da49f587fd9e68ee2404fe4e256c0c7d3a81be21" + integrity sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ== + +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +es-to-primitive@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18" + integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== + dependencies: + is-callable "^1.2.7" + is-date-object "^1.0.5" + is-symbol "^1.0.4" + +esast-util-from-estree@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz#8d1cfb51ad534d2f159dc250e604f3478a79f1ad" + integrity sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ== + dependencies: + "@types/estree-jsx" "^1.0.0" + devlop "^1.0.0" + estree-util-visit "^2.0.0" + unist-util-position-from-estree "^2.0.0" + +esast-util-from-js@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz#5147bec34cc9da44accf52f87f239a40ac3e8225" + integrity sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw== + dependencies: + "@types/estree-jsx" "^1.0.0" + acorn "^8.0.0" + esast-util-from-estree "^2.0.0" + vfile-message "^4.0.0" + +escalade@^3.1.1, escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-goat@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-4.0.0.tgz#9424820331b510b0666b98f7873fe11ac4aa8081" + integrity sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg== + +escape-html@^1.0.3, escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escape-string-regexp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +estree-util-attach-comments@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz#344bde6a64c8a31d15231e5ee9e297566a691c2d" + integrity sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw== + dependencies: + "@types/estree" "^1.0.0" + +estree-util-build-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz#b6d0bced1dcc4f06f25cf0ceda2b2dcaf98168f1" + integrity sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ== + dependencies: + "@types/estree-jsx" "^1.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + estree-walker "^3.0.0" + +estree-util-is-identifier-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz#0b5ef4c4ff13508b34dcd01ecfa945f61fce5dbd" + integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg== + +estree-util-scope@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/estree-util-scope/-/estree-util-scope-1.0.0.tgz#9cbdfc77f5cb51e3d9ed4ad9c4adbff22d43e585" + integrity sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + +estree-util-to-js@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz#10a6fb924814e6abb62becf0d2bc4dea51d04f17" + integrity sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg== + dependencies: + "@types/estree-jsx" "^1.0.0" + astring "^1.8.0" + source-map "^0.7.0" + +estree-util-value-to-estree@^3.0.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/estree-util-value-to-estree/-/estree-util-value-to-estree-3.2.1.tgz#f8083e56f51efb4889794490730c036ba6167ee6" + integrity sha512-Vt2UOjyPbNQQgT5eJh+K5aATti0OjCIAGc9SgMdOFYbohuifsWclR74l0iZTJwePMgWYdX1hlVS+dedH9XV8kw== + dependencies: + "@types/estree" "^1.0.0" + +estree-util-visit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/estree-util-visit/-/estree-util-visit-2.0.0.tgz#13a9a9f40ff50ed0c022f831ddf4b58d05446feb" + integrity sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/unist" "^3.0.0" + +estree-walker@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" + integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== + +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +estree-walker@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +eta@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/eta/-/eta-2.2.0.tgz#eb8b5f8c4e8b6306561a455e62cd7492fe3a9b8a" + integrity sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eval@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eval/-/eval-0.1.8.tgz#2b903473b8cc1d1989b83a1e7923f883eb357f85" + integrity sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw== + dependencies: + "@types/node" "*" + require-like ">= 0.1.1" + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + +express@^4.17.3: + version "4.21.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.7.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.12" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.2" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== + dependencies: + is-extendable "^0.1.0" + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-fifo@^1.2.0, fast-fifo@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" + integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== + +fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + +fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-uri@^3.0.1: + version "3.0.5" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.5.tgz#19f5f9691d0dab9b85861a7bb5d98fca961da9cd" + integrity sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q== + +fastq@^1.6.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.18.0.tgz#d631d7e25faffea81887fe5ea8c9010e1b36fee0" + integrity sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw== + dependencies: + reusify "^1.0.4" + +fault@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fault/-/fault-2.0.1.tgz#d47ca9f37ca26e4bd38374a7c500b5a384755b6c" + integrity sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ== + dependencies: + format "^0.2.0" + +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +feed@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e" + integrity sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ== + dependencies: + xml-js "^1.6.11" + +figures@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +file-loader@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" + integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +filelist@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== + dependencies: + minimatch "^5.0.1" + +filesize@^8.0.6: + version "8.0.7" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-8.0.7.tgz#695e70d80f4e47012c132d57a059e80c6b580bd8" + integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-cache-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-4.0.0.tgz#a30ee0448f81a3990708f6453633c733e2f6eec2" + integrity sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg== + dependencies: + common-path-prefix "^3.0.0" + pkg-dir "^7.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +find-up@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-6.3.0.tgz#2abab3d3280b2dc7ac10199ef324c4e002c8c790" + integrity sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw== + dependencies: + locate-path "^7.1.0" + path-exists "^5.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +follow-redirects@^1.0.0: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +fork-ts-checker-webpack-plugin@^6.5.0: + version "6.5.3" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz#eda2eff6e22476a2688d10661688c47f611b37f3" + integrity sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ== + dependencies: + "@babel/code-frame" "^7.8.3" + "@types/json-schema" "^7.0.5" + chalk "^4.1.0" + chokidar "^3.4.2" + cosmiconfig "^6.0.0" + deepmerge "^4.2.2" + fs-extra "^9.0.0" + glob "^7.1.6" + memfs "^3.1.2" + minimatch "^3.0.4" + schema-utils "2.7.0" + semver "^7.3.2" + tapable "^1.0.0" + +form-data-encoder@^2.1.2: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5" + integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw== + +format@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" + integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fraction.js@^4.3.7: + version "4.3.7" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" + integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== + +framer-motion@6.2.4: + version "6.2.4" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-6.2.4.tgz#3d9c61be3fb8381a770efccdb56cc421de662979" + integrity sha512-1UfnSG4c4CefKft6QMYGx8AWt3TtaFoR/Ax4dkuDDD5BDDeIuUm7gesmJrF8GzxeX/i6fMm8+MEdPngUyPVdLA== + dependencies: + framesync "6.0.1" + hey-listen "^1.0.8" + popmotion "11.0.3" + style-value-types "5.0.0" + tslib "^2.1.0" + optionalDependencies: + "@emotion/is-prop-valid" "^0.8.2" + +framesync@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/framesync/-/framesync-6.0.1.tgz#5e32fc01f1c42b39c654c35b16440e07a25d6f20" + integrity sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA== + dependencies: + tslib "^2.1.0" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs-extra@^11.1.1, fs-extra@^11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^9.0.0, fs-extra@^9.0.1: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-monkey@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.6.tgz#8ead082953e88d992cf3ff844faa907b26756da2" + integrity sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz#e68e1df7b259a5c949eeef95cdbde53edffabb78" + integrity sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + functions-have-names "^1.2.3" + hasown "^2.0.2" + is-callable "^1.2.7" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.7.tgz#dcfcb33d3272e15f445d15124bc0a216189b9044" + integrity sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + function-bind "^1.1.2" + get-proto "^1.0.0" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-nonce@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" + integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== + +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== + +get-proto@^1.0.0, get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +get-stream@^6.0.0, get-stream@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-symbol-description@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" + integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== + +github-slugger@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.5.0.tgz#17891bbc73232051474d68bd867a34625c955f7d" + integrity sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw== + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.1, glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^10.3.10: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + +glob@^7.0.0, glob@^7.1.3, glob@^7.1.6: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-dirs@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" + integrity sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== + dependencies: + ini "2.0.0" + +global-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globalthis@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + +globby@^11.0.1, globby@^11.0.4, globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +globby@^13.1.1: + version "13.2.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592" + integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== + dependencies: + dir-glob "^3.0.1" + fast-glob "^3.3.0" + ignore "^5.2.4" + merge2 "^1.4.1" + slash "^4.0.0" + +gopd@^1.0.1, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +got@^12.1.0: + version "12.6.1" + resolved "https://registry.yarnpkg.com/got/-/got-12.6.1.tgz#8869560d1383353204b5a9435f782df9c091f549" + integrity sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ== + dependencies: + "@sindresorhus/is" "^5.2.0" + "@szmarczak/http-timer" "^5.0.1" + cacheable-lookup "^7.0.0" + cacheable-request "^10.2.8" + decompress-response "^6.0.0" + form-data-encoder "^2.1.2" + get-stream "^6.0.1" + http2-wrapper "^2.1.10" + lowercase-keys "^3.0.0" + p-cancelable "^3.0.0" + responselike "^3.0.0" + +graceful-fs@4.2.10: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +gray-matter@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798" + integrity sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q== + dependencies: + js-yaml "^3.13.1" + kind-of "^6.0.2" + section-matter "^1.0.0" + strip-bom-string "^1.0.0" + +gzip-size@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" + integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== + dependencies: + duplexer "^0.1.2" + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +has-bigints@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe" + integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5" + integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== + dependencies: + dunder-proto "^1.0.0" + +has-symbols@^1.0.1, has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +has-yarn@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-3.0.0.tgz#c3c21e559730d1d3b57e28af1f30d06fac38147d" + integrity sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA== + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +hast-util-from-parse5@^8.0.0: + version "8.0.2" + resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.2.tgz#29b42758ba96535fd6021f0f533c000886c0f00f" + integrity sha512-SfMzfdAi/zAoZ1KkFEyyeXBn7u/ShQrfd675ZEE9M3qj+PMFX05xubzRyF76CCSJu8au9jgVxDV1+okFvgZU4A== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + devlop "^1.0.0" + hastscript "^9.0.0" + property-information "^6.0.0" + vfile "^6.0.0" + vfile-location "^5.0.0" + web-namespaces "^2.0.0" + +hast-util-parse-selector@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27" + integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A== + dependencies: + "@types/hast" "^3.0.0" + +hast-util-raw@^9.0.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-9.1.0.tgz#79b66b26f6f68fb50dfb4716b2cdca90d92adf2e" + integrity sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + "@ungap/structured-clone" "^1.0.0" + hast-util-from-parse5 "^8.0.0" + hast-util-to-parse5 "^8.0.0" + html-void-elements "^3.0.0" + mdast-util-to-hast "^13.0.0" + parse5 "^7.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + web-namespaces "^2.0.0" + zwitch "^2.0.0" + +hast-util-to-estree@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/hast-util-to-estree/-/hast-util-to-estree-3.1.1.tgz#b7f0b247d9f62127bb5db34e3a86c93d17279071" + integrity sha512-IWtwwmPskfSmma9RpzCappDUitC8t5jhAynHhc1m2+5trOgsrp7txscUSavc5Ic8PATyAjfrCK1wgtxh2cICVQ== + dependencies: + "@types/estree" "^1.0.0" + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + estree-util-attach-comments "^3.0.0" + estree-util-is-identifier-name "^3.0.0" + hast-util-whitespace "^3.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + style-to-object "^1.0.0" + unist-util-position "^5.0.0" + zwitch "^2.0.0" + +hast-util-to-jsx-runtime@^2.0.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz#6d11b027473e69adeaa00ca4cfb5bb68e3d282fa" + integrity sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg== + dependencies: + "@types/estree" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + hast-util-whitespace "^3.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + style-to-object "^1.0.0" + unist-util-position "^5.0.0" + vfile-message "^4.0.0" + +hast-util-to-parse5@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz#477cd42d278d4f036bc2ea58586130f6f39ee6ed" + integrity sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw== + dependencies: + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + web-namespaces "^2.0.0" + zwitch "^2.0.0" + +hast-util-whitespace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" + integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== + dependencies: + "@types/hast" "^3.0.0" + +hastscript@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-9.0.0.tgz#2b76b9aa3cba8bf6d5280869f6f6f7165c230763" + integrity sha512-jzaLBGavEDKHrc5EfFImKN7nZKKBdSLIdGvCwDZ9TfzbF2ffXiov8CKE445L2Z1Ek2t/m4SKQ2j6Ipv7NyUolw== + dependencies: + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + hast-util-parse-selector "^4.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +hey-listen@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68" + integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q== + +history@^4.9.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" + integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== + dependencies: + "@babel/runtime" "^7.1.2" + loose-envify "^1.2.0" + resolve-pathname "^3.0.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + value-equal "^1.0.1" + +hoist-non-react-statics@^3.1.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-entities@^2.3.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" + integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== + +html-escaper@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +html-minifier-terser@^6.0.2: + version "6.1.0" + resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab" + integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw== + dependencies: + camel-case "^4.1.2" + clean-css "^5.2.2" + commander "^8.3.0" + he "^1.2.0" + param-case "^3.0.4" + relateurl "^0.2.7" + terser "^5.10.0" + +html-minifier-terser@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz#18752e23a2f0ed4b0f550f217bb41693e975b942" + integrity sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA== + dependencies: + camel-case "^4.1.2" + clean-css "~5.3.2" + commander "^10.0.0" + entities "^4.4.0" + param-case "^3.0.4" + relateurl "^0.2.7" + terser "^5.15.1" + +html-tags@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" + integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== + +html-void-elements@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" + integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== + +html-webpack-plugin@^5.6.0: + version "5.6.3" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz#a31145f0fee4184d53a794f9513147df1e653685" + integrity sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg== + dependencies: + "@types/html-minifier-terser" "^6.0.0" + html-minifier-terser "^6.0.2" + lodash "^4.17.21" + pretty-error "^4.0.0" + tapable "^2.0.0" + +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + +htmlparser2@^8.0.1: + version "8.0.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" + integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + entities "^4.4.0" + +http-cache-semantics@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.9" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.9.tgz#b817b3ca0edea6236225000d795378707c169cec" + integrity sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw== + +http-proxy-middleware@^2.0.3: + version "2.0.7" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz#915f236d92ae98ef48278a95dedf17e991936ec6" + integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA== + dependencies: + "@types/http-proxy" "^1.17.8" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +http2-wrapper@^2.1.10: + version "2.2.1" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-2.2.1.tgz#310968153dcdedb160d8b72114363ef5fce1f64a" + integrity sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.2.0" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +icss-utils@^5.0.0, icss-utils@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== + +idb@^7.0.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" + integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^5.2.0, ignore@^5.2.4: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +image-size@^1.0.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.2.0.tgz#312af27a2ff4ff58595ad00b9344dd684c910df6" + integrity sha512-4S8fwbO6w3GeCVN6OPtA9I5IGKkcDMPcKndtUlpJuCwu7JLjtj7JZpwqLuyY2nrmQT3AWsCJLSKPsc2mPBSl3w== + dependencies: + queue "6.0.2" + +immer@^9.0.7: + version "9.0.21" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" + integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== + +import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-lazy@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153" + integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw== + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +infima@0.2.0-alpha.45: + version "0.2.0-alpha.45" + resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.45.tgz#542aab5a249274d81679631b492973dd2c1e7466" + integrity sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +ini@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + +ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +inline-style-parser@0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.2.4.tgz#f4af5fe72e612839fcd453d989a586566d695f22" + integrity sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q== + +internal-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" + integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.2" + side-channel "^1.1.0" + +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" + integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== + +is-alphabetical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" + integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== + +is-alphanumerical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" + integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== + dependencies: + is-alphabetical "^2.0.0" + is-decimal "^2.0.0" + +is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" + integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-async-function@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.0.tgz#1d1080612c493608e93168fc4458c245074c06a6" + integrity sha512-GExz9MtyhlZyXYLxzlJRj5WUCE661zhDa1Yna52CN57AJsymh+DvXXjyveSioqSRdxvUrdKdvqB1b5cVKsNpWQ== + dependencies: + call-bound "^1.0.3" + get-proto "^1.0.1" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" + +is-bigint@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672" + integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== + dependencies: + has-bigints "^1.0.2" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.1.tgz#c20d0c654be05da4fbc23c562635c019e93daf89" + integrity sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + +is-callable@^1.1.3, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-ci@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" + integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== + dependencies: + ci-info "^3.2.0" + +is-core-module@^2.16.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + +is-data-view@^1.0.1, is-data-view@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e" + integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== + dependencies: + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + is-typed-array "^1.1.13" + +is-date-object@^1.0.5, is-date-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" + integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + +is-decimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" + integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extendable@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-finalizationregistry@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz#eefdcdc6c94ddd0674d9c85887bf93f944a97c90" + integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== + dependencies: + call-bound "^1.0.3" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-function@^1.0.10: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.0.tgz#bf3eeda931201394f57b5dba2800f91a238309ca" + integrity sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ== + dependencies: + call-bound "^1.0.3" + get-proto "^1.0.0" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" + +is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-hexadecimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" + integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== + +is-installed-globally@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" + integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== + dependencies: + global-dirs "^3.0.0" + is-path-inside "^3.0.2" + +is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + +is-npm@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-6.0.0.tgz#b59e75e8915543ca5d881ecff864077cba095261" + integrity sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ== + +is-number-object@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" + integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-cwd@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" + integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== + +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-plain-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== + dependencies: + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== + +is-root@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" + integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== + +is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f" + integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== + dependencies: + call-bound "^1.0.3" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-string@^1.0.7, is-string@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" + integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + +is-symbol@^1.0.4, is-symbol@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634" + integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== + dependencies: + call-bound "^1.0.2" + has-symbols "^1.1.0" + safe-regex-test "^1.1.0" + +is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== + dependencies: + which-typed-array "^1.1.16" + +is-typedarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + +is-weakref@^1.0.2, is-weakref@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.0.tgz#47e3472ae95a63fa9cf25660bcf0c181c39770ef" + integrity sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q== + dependencies: + call-bound "^1.0.2" + +is-weakset@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" + integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== + dependencies: + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +is-yarn-global@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.4.1.tgz#b312d902b313f81e4eaf98b6361ba2b45cd694bb" + integrity sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ== + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +isomorphic-rslog@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/isomorphic-rslog/-/isomorphic-rslog-0.0.6.tgz#abf13c77b545b03e5ab3bc376e6de720e07eb190" + integrity sha512-HM0q6XqQ93psDlqvuViNs/Ea3hAyGDkIdVAHlrEocjjAwGrs1fZ+EdQjS9eUPacnYB7Y8SoDdSY3H8p3ce205A== + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jake@^10.8.5: + version "10.9.2" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f" + integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.4" + minimatch "^3.1.2" + +jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest-worker@^29.4.3: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== + dependencies: + "@types/node" "*" + jest-util "^29.7.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jiti@^1.20.0, jiti@^1.21.6: + version "1.21.7" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.7.tgz#9dd81043424a3d28458b193d965f0d18a2300ba9" + integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A== + +joi@^17.9.2: + version "17.13.3" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.3.tgz#0f5cc1169c999b30d344366d384b12d92558bcec" + integrity sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA== + dependencies: + "@hapi/hoek" "^9.3.0" + "@hapi/topo" "^5.1.0" + "@sideway/address" "^4.1.5" + "@sideway/formula" "^3.0.1" + "@sideway/pinpoint" "^2.0.0" + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + +jsesc@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-schema@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json5@^2.1.2, json5@^2.2.0, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonpointer@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" + integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== + +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +latest-version@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-7.0.0.tgz#843201591ea81a4d404932eeb61240fe04e9e5da" + integrity sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg== + dependencies: + package-json "^8.1.0" + +launch-editor@^2.6.0: + version "2.9.1" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.9.1.tgz#253f173bd441e342d4344b4dae58291abb425047" + integrity sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w== + dependencies: + picocolors "^1.0.0" + shell-quote "^1.8.1" + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +lightningcss-darwin-arm64@1.29.1: + version "1.29.1" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.1.tgz#dce17349c7b9f968f396ec240503de14e7b4870b" + integrity sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw== + +lightningcss-darwin-x64@1.29.1: + version "1.29.1" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.1.tgz#e79c984180c57d00ee114210ceced83473d72dfc" + integrity sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA== + +lightningcss-freebsd-x64@1.29.1: + version "1.29.1" + resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.1.tgz#4b3aec9620684a60c45266d50fd843869320f42f" + integrity sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ== + +lightningcss-linux-arm-gnueabihf@1.29.1: + version "1.29.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.1.tgz#b80e9c4dd75652bec451ffd4d5779492a01791ff" + integrity sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg== + +lightningcss-linux-arm64-gnu@1.29.1: + version "1.29.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.1.tgz#7825eb119ddf580a4a4f011c6f384a3f9c992060" + integrity sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ== + +lightningcss-linux-arm64-musl@1.29.1: + version "1.29.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.1.tgz#389efccf80088dce2bb00e28bd7d1cfe36a71669" + integrity sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw== + +lightningcss-linux-x64-gnu@1.29.1: + version "1.29.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.1.tgz#98fc5df5e39ac8ddc51e51f785849eb21131f789" + integrity sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw== + +lightningcss-linux-x64-musl@1.29.1: + version "1.29.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.1.tgz#fb4f80895ba7dfa8048ee32e9716a1684fefd6b2" + integrity sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw== + +lightningcss-win32-arm64-msvc@1.29.1: + version "1.29.1" + resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.1.tgz#fd4409fd1505d89d0ff66511c36df5a1379eb7cd" + integrity sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog== + +lightningcss-win32-x64-msvc@1.29.1: + version "1.29.1" + resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.1.tgz#54dcd52884f6cbf205a53d49239559603f194927" + integrity sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q== + +lightningcss@^1.27.0: + version "1.29.1" + resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.29.1.tgz#1d4d62332fc5ba4b6c28e04a8c5638c76019702b" + integrity sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q== + dependencies: + detect-libc "^1.0.3" + optionalDependencies: + lightningcss-darwin-arm64 "1.29.1" + lightningcss-darwin-x64 "1.29.1" + lightningcss-freebsd-x64 "1.29.1" + lightningcss-linux-arm-gnueabihf "1.29.1" + lightningcss-linux-arm64-gnu "1.29.1" + lightningcss-linux-arm64-musl "1.29.1" + lightningcss-linux-x64-gnu "1.29.1" + lightningcss-linux-x64-musl "1.29.1" + lightningcss-win32-arm64-msvc "1.29.1" + lightningcss-win32-x64-msvc "1.29.1" + +lilconfig@^3.0.0, lilconfig@^3.1.1, lilconfig@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4" + integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +loader-utils@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" + integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +loader-utils@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.3.1.tgz#735b9a19fd63648ca7adbd31c2327dfe281304e5" + integrity sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg== + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +locate-path@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-7.2.0.tgz#69cb1779bd90b35ab1e771e1f2f89a202c2a8a8a" + integrity sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA== + dependencies: + p-locate "^6.0.0" + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== + +lodash@^4.17.20, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +longest-streak@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" + integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +lowercase-keys@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2" + integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ== + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lucide-react@^0.379.0: + version "0.379.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.379.0.tgz#29e34eeffae7fb241b64b09868cbe3ab888ef7cc" + integrity sha512-KcdeVPqmhRldldAAgptb8FjIunM2x2Zy26ZBh1RsEUcdLIvsEmbcw7KpzFYUy5BbpGeWhPu9Z9J5YXfStiXwhg== + +magic-string@^0.25.0, magic-string@^0.25.2, magic-string@^0.25.7: + version "0.25.9" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" + integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== + dependencies: + sourcemap-codec "^1.4.8" + +markdown-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz#34bebc83e9938cae16e0e017e4a9814a8330d3c4" + integrity sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q== + +markdown-table@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b" + integrity sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A== + dependencies: + repeat-string "^1.0.0" + +markdown-table@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.4.tgz#fe44d6d410ff9d6f2ea1797a3f60aa4d2b631c2a" + integrity sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw== + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +mdast-util-directive@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-directive/-/mdast-util-directive-3.0.0.tgz#3fb1764e705bbdf0afb0d3f889e4404c3e82561f" + integrity sha512-JUpYOqKI4mM3sZcNxmF/ox04XYFFkNwr0CFlrQIkCwbvH0xzMCqkMqAde9wRd80VAhaUrwFwKm2nxretdT1h7Q== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + parse-entities "^4.0.0" + stringify-entities "^4.0.0" + unist-util-visit-parents "^6.0.0" + +mdast-util-find-and-replace@^3.0.0, mdast-util-find-and-replace@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz#70a3174c894e14df722abf43bc250cbae44b11df" + integrity sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg== + dependencies: + "@types/mdast" "^4.0.0" + escape-string-regexp "^5.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + +mdast-util-from-markdown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz#4850390ca7cf17413a9b9a0fbefcd1bc0eb4160a" + integrity sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + mdast-util-to-string "^4.0.0" + micromark "^4.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-decode-string "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-stringify-position "^4.0.0" + +mdast-util-frontmatter@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz#f5f929eb1eb36c8a7737475c7eb438261f964ee8" + integrity sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + escape-string-regexp "^5.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + micromark-extension-frontmatter "^2.0.0" + +mdast-util-gfm-autolink-literal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz#abd557630337bd30a6d5a4bd8252e1c2dc0875d5" + integrity sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ== + dependencies: + "@types/mdast" "^4.0.0" + ccount "^2.0.0" + devlop "^1.0.0" + mdast-util-find-and-replace "^3.0.0" + micromark-util-character "^2.0.0" + +mdast-util-gfm-footnote@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz#25a1753c7d16db8bfd53cd84fe50562bd1e6d6a9" + integrity sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.1.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + +mdast-util-gfm-strikethrough@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz#d44ef9e8ed283ac8c1165ab0d0dfd058c2764c16" + integrity sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-gfm-table@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz#7a435fb6223a72b0862b33afbd712b6dae878d38" + integrity sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + markdown-table "^3.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-gfm-task-list-item@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz#e68095d2f8a4303ef24094ab642e1047b991a936" + integrity sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-gfm@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz#3f2aecc879785c3cb6a81ff3a243dc11eca61095" + integrity sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw== + dependencies: + mdast-util-from-markdown "^2.0.0" + mdast-util-gfm-autolink-literal "^2.0.0" + mdast-util-gfm-footnote "^2.0.0" + mdast-util-gfm-strikethrough "^2.0.0" + mdast-util-gfm-table "^2.0.0" + mdast-util-gfm-task-list-item "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-mdx-expression@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz#43f0abac9adc756e2086f63822a38c8d3c3a5096" + integrity sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-mdx-jsx@^3.0.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.3.tgz#76b957b3da18ebcfd0de3a9b4451dcd6fdec2320" + integrity sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + devlop "^1.1.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + parse-entities "^4.0.0" + stringify-entities "^4.0.0" + unist-util-stringify-position "^4.0.0" + vfile-message "^4.0.0" + +mdast-util-mdx@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz#792f9cf0361b46bee1fdf1ef36beac424a099c41" + integrity sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w== + dependencies: + mdast-util-from-markdown "^2.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-mdxjs-esm@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz#019cfbe757ad62dd557db35a695e7314bcc9fa97" + integrity sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-phrasing@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz#7cc0a8dec30eaf04b7b1a9661a92adb3382aa6e3" + integrity sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w== + dependencies: + "@types/mdast" "^4.0.0" + unist-util-is "^6.0.0" + +mdast-util-to-hast@^13.0.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz#5ca58e5b921cc0a3ded1bc02eed79a4fe4fe41f4" + integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@ungap/structured-clone" "^1.0.0" + devlop "^1.0.0" + micromark-util-sanitize-uri "^2.0.0" + trim-lines "^3.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +mdast-util-to-markdown@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz#f910ffe60897f04bb4b7e7ee434486f76288361b" + integrity sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^4.0.0" + mdast-util-to-string "^4.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-decode-string "^2.0.0" + unist-util-visit "^5.0.0" + zwitch "^2.0.0" + +mdast-util-to-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814" + integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg== + dependencies: + "@types/mdast" "^4.0.0" + +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + +mdn-data@2.0.28: + version "2.0.28" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba" + integrity sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g== + +mdn-data@2.0.30: + version "2.0.30" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" + integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== + +mdn-data@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" + integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +medium-zoom@^1.0.6: + version "1.1.0" + resolved "https://registry.yarnpkg.com/medium-zoom/-/medium-zoom-1.1.0.tgz#6efb6bbda861a02064ee71a2617a8dc4381ecc71" + integrity sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ== + +memfs@^3.1.2, memfs@^3.4.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" + integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== + dependencies: + fs-monkey "^1.0.4" + +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromark-core-commonmark@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.2.tgz#6a45bbb139e126b3f8b361a10711ccc7c6e15e93" + integrity sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w== + dependencies: + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-factory-destination "^2.0.0" + micromark-factory-label "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-factory-title "^2.0.0" + micromark-factory-whitespace "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-html-tag-name "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-directive@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz#2eb61985d1995a7c1ff7621676a4f32af29409e8" + integrity sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA== + dependencies: + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-factory-whitespace "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + parse-entities "^4.0.0" + +micromark-extension-frontmatter@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz#651c52ffa5d7a8eeed687c513cd869885882d67a" + integrity sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg== + dependencies: + fault "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-autolink-literal@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz#6286aee9686c4462c1e3552a9d505feddceeb935" + integrity sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-footnote@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz#4dab56d4e398b9853f6fe4efac4fc9361f3e0750" + integrity sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw== + dependencies: + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-strikethrough@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz#86106df8b3a692b5f6a92280d3879be6be46d923" + integrity sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw== + dependencies: + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-table@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz#5cadedfbb29fca7abf752447967003dc3b6583c9" + integrity sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g== + dependencies: + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-tagfilter@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz#f26d8a7807b5985fba13cf61465b58ca5ff7dc57" + integrity sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg== + dependencies: + micromark-util-types "^2.0.0" + +micromark-extension-gfm-task-list-item@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz#bcc34d805639829990ec175c3eea12bb5b781f2c" + integrity sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw== + dependencies: + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz#3e13376ab95dd7a5cfd0e29560dfe999657b3c5b" + integrity sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w== + dependencies: + micromark-extension-gfm-autolink-literal "^2.0.0" + micromark-extension-gfm-footnote "^2.0.0" + micromark-extension-gfm-strikethrough "^2.0.0" + micromark-extension-gfm-table "^2.0.0" + micromark-extension-gfm-tagfilter "^2.0.0" + micromark-extension-gfm-task-list-item "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-mdx-expression@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.0.tgz#1407b9ce69916cf5e03a196ad9586889df25302a" + integrity sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + micromark-factory-mdx-expression "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-mdx-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.1.tgz#5abb83da5ddc8e473a374453e6ea56fbd66b59ad" + integrity sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg== + dependencies: + "@types/acorn" "^4.0.0" + "@types/estree" "^1.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + micromark-factory-mdx-expression "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + vfile-message "^4.0.0" + +micromark-extension-mdx-md@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz#1d252881ea35d74698423ab44917e1f5b197b92d" + integrity sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ== + dependencies: + micromark-util-types "^2.0.0" + +micromark-extension-mdxjs-esm@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz#de21b2b045fd2059bd00d36746081de38390d54a" + integrity sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-position-from-estree "^2.0.0" + vfile-message "^4.0.0" + +micromark-extension-mdxjs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz#b5a2e0ed449288f3f6f6c544358159557549de18" + integrity sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ== + dependencies: + acorn "^8.0.0" + acorn-jsx "^5.0.0" + micromark-extension-mdx-expression "^3.0.0" + micromark-extension-mdx-jsx "^3.0.0" + micromark-extension-mdx-md "^2.0.0" + micromark-extension-mdxjs-esm "^3.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-destination@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz#8fef8e0f7081f0474fbdd92deb50c990a0264639" + integrity sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-label@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz#5267efa97f1e5254efc7f20b459a38cb21058ba1" + integrity sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg== + dependencies: + devlop "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-mdx-expression@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.2.tgz#2afaa8ba6d5f63e0cead3e4dee643cad184ca260" + integrity sha512-5E5I2pFzJyg2CtemqAbcyCktpHXuJbABnsb32wX2U8IQKhhVFBqkcZR5LRm1WVoFqa4kTueZK4abep7wdo9nrw== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-position-from-estree "^2.0.0" + vfile-message "^4.0.0" + +micromark-factory-space@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf" + integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-space@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz#36d0212e962b2b3121f8525fc7a3c7c029f334fc" + integrity sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-title@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz#237e4aa5d58a95863f01032d9ee9b090f1de6e94" + integrity sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-whitespace@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz#06b26b2983c4d27bfcc657b33e25134d4868b0b1" + integrity sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-character@^1.0.0, micromark-util-character@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc" + integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg== + dependencies: + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-character@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6" + integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q== + dependencies: + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-chunked@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz#47fbcd93471a3fccab86cff03847fc3552db1051" + integrity sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-classify-character@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz#d399faf9c45ca14c8b4be98b1ea481bced87b629" + integrity sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-combine-extensions@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz#2a0f490ab08bff5cc2fd5eec6dd0ca04f89b30a9" + integrity sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg== + dependencies: + micromark-util-chunked "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-decode-numeric-character-reference@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz#fcf15b660979388e6f118cdb6bf7d79d73d26fe5" + integrity sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-decode-string@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz#6cb99582e5d271e84efca8e61a807994d7161eb2" + integrity sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-encode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8" + integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== + +micromark-util-events-to-acorn@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.2.tgz#4275834f5453c088bd29cd72dfbf80e3327cec07" + integrity sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA== + dependencies: + "@types/acorn" "^4.0.0" + "@types/estree" "^1.0.0" + "@types/unist" "^3.0.0" + devlop "^1.0.0" + estree-util-visit "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + vfile-message "^4.0.0" + +micromark-util-html-tag-name@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz#e40403096481986b41c106627f98f72d4d10b825" + integrity sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA== + +micromark-util-normalize-identifier@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz#c30d77b2e832acf6526f8bf1aa47bc9c9438c16d" + integrity sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-resolve-all@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz#e1a2d62cdd237230a2ae11839027b19381e31e8b" + integrity sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg== + dependencies: + micromark-util-types "^2.0.0" + +micromark-util-sanitize-uri@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7" + integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-subtokenize@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.3.tgz#70ffb99a454bd8c913c8b709c3dc97baefb65f96" + integrity sha512-VXJJuNxYWSoYL6AJ6OQECCFGhIU2GGHMw8tahogePBrjkG8aCCas3ibkp7RnVOSTClg2is05/R7maAhF1XyQMg== + dependencies: + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-symbol@^1.0.0, micromark-util-symbol@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142" + integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== + +micromark-util-symbol@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8" + integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== + +micromark-util-types@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283" + integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== + +micromark-util-types@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.1.tgz#a3edfda3022c6c6b55bfb049ef5b75d70af50709" + integrity sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ== + +micromark@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.1.tgz#294c2f12364759e5f9e925a767ae3dfde72223ff" + integrity sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromatch@^4.0.2, micromatch@^4.0.5, micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +"mime-db@>= 1.43.0 < 2": + version "1.53.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" + integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== + +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" + integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== + +mime-types@2.1.18: + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" + integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== + dependencies: + mime-db "~1.33.0" + +mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + +mimic-response@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-4.0.0.tgz#35468b19e7c75d10f5165ea25e75a5ceea7cf70f" + integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== + +mini-css-extract-plugin@^2.9.1: + version "2.9.2" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz#966031b468917a5446f4c24a80854b2947503c5b" + integrity sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w== + dependencies: + schema-utils "^4.0.0" + tapable "^2.2.1" + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + +mkdirp@~0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mrmime@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.0.tgz#151082a6e06e59a9a39b46b3e14d5cfe92b3abb4" + integrity sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== + dependencies: + dns-packet "^5.2.2" + thunky "^1.0.2" + +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +nanoid@^3.3.7: + version "3.3.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== + +napi-build-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" + integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +negotiator@~0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" + integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +next-themes@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.3.0.tgz#b4d2a866137a67d42564b07f3a3e720e2ff3871a" + integrity sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w== + +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + +node-abi@^3.3.0: + version "3.71.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.71.0.tgz#52d84bbcd8575efb71468fbaa1f9a49b2c242038" + integrity sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw== + dependencies: + semver "^7.3.5" + +node-addon-api@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" + integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== + +node-emoji@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-2.2.0.tgz#1d000e3c76e462577895be1b436f4aa2d6760eb0" + integrity sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw== + dependencies: + "@sindresorhus/is" "^4.6.0" + char-regex "^1.0.2" + emojilib "^2.4.0" + skin-tone "^2.0.0" + +node-forge@^1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== + +normalize-url@^8.0.0: + version "8.0.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-8.0.1.tgz#9b7d96af9836577c58f5883e939365fa15623a4a" + integrity sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +npm-to-yarn@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/npm-to-yarn/-/npm-to-yarn-3.0.1.tgz#d1ed47551321ad5cd51342729fe21c8146644529" + integrity sha512-tt6PvKu4WyzPwWUzy/hvPFqn+uwXO0K1ZHka8az3NnrhWJDmSqI8ncWq0fkL0k/lmmi5tAC11FXwXuh0rFbt1A== + +nprogress@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1" + integrity sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA== + +nth-check@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + +null-loader@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/null-loader/-/null-loader-4.0.1.tgz#8e63bd3a2dd3c64236a4679428632edd0a6dbc6a" + integrity sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +object-assign@^4.0.1, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + +object-inspect@^1.13.3: + version "1.13.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" + integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.0, object.assign@^4.1.7: + version "4.1.7" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" + integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + has-symbols "^1.1.0" + object-keys "^1.1.1" + +object.getownpropertydescriptors@^2.1.0: + version "2.1.8" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.8.tgz#2f1fe0606ec1a7658154ccd4f728504f69667923" + integrity sha512-qkHIGe4q0lSYMv0XI4SsBTJz3WaURhLvd0lKSgtVuOsJ2krg4SgMw3PIRQFMp07yi++UR3se2mkcLqsBNpBb/A== + dependencies: + array.prototype.reduce "^1.0.6" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + gopd "^1.0.1" + safe-array-concat "^1.1.2" + +object.values@^1.1.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216" + integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@^8.0.9, open@^8.4.0: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +opener@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" + integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== + +os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ== + +own-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358" + integrity sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg== + dependencies: + get-intrinsic "^1.2.6" + object-keys "^1.1.1" + safe-push-apply "^1.0.0" + +p-cancelable@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" + integrity sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw== + +p-limit@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-limit@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644" + integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== + dependencies: + yocto-queue "^1.0.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-locate@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-6.0.0.tgz#3da9a49d4934b901089dca3302fa65dc5a05c04f" + integrity sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw== + dependencies: + p-limit "^4.0.0" + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +p-retry@^4.5.0: + version "4.6.2" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" + integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== + dependencies: + "@types/retry" "0.12.0" + retry "^0.13.1" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + +package-json@^8.1.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-8.1.1.tgz#3e9948e43df40d1e8e78a85485f1070bf8f03dc8" + integrity sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA== + dependencies: + got "^12.1.0" + registry-auth-token "^5.0.1" + registry-url "^6.0.0" + semver "^7.3.7" + +param-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" + integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-entities@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.2.tgz#61d46f5ed28e4ee62e9ddc43d6b010188443f159" + integrity sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw== + dependencies: + "@types/unist" "^2.0.0" + character-entities-legacy "^3.0.0" + character-reference-invalid "^2.0.0" + decode-named-character-reference "^1.0.0" + is-alphanumerical "^2.0.0" + is-decimal "^2.0.0" + is-hexadecimal "^2.0.0" + +parse-json@^5.0.0, parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parse-numeric-range@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz#7c63b61190d61e4d53a1197f0c83c47bb670ffa3" + integrity sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ== + +parse5-htmlparser2-tree-adapter@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz#b5a806548ed893a43e24ccb42fbb78069311e81b" + integrity sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g== + dependencies: + domhandler "^5.0.3" + parse5 "^7.0.0" + +parse5@^7.0.0: + version "7.2.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.2.1.tgz#8928f55915e6125f430cc44309765bf17556a33a" + integrity sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ== + dependencies: + entities "^4.5.0" + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascal-case@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-exists@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7" + integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-is-inside@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-root-regex@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d" + integrity sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ== + +path-root@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7" + integrity sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg== + dependencies: + path-root-regex "^0.1.0" + +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== + +path-to-regexp@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" + integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw== + +path-to-regexp@^1.7.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.9.0.tgz#5dc0753acbf8521ca2e0f137b4578b917b10cf24" + integrity sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g== + dependencies: + isarray "0.0.1" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pirates@^4.0.1: + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + +pkg-dir@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-7.0.0.tgz#8f0c08d6df4476756c5ff29b3282d0bab7517d11" + integrity sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA== + dependencies: + find-up "^6.3.0" + +pkg-up@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" + integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== + dependencies: + find-up "^3.0.0" + +popmotion@11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-11.0.3.tgz#565c5f6590bbcddab7a33a074bb2ba97e24b0cc9" + integrity sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA== + dependencies: + framesync "6.0.1" + hey-listen "^1.0.8" + style-value-types "5.0.0" + tslib "^2.1.0" + +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + +postcss-attribute-case-insensitive@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz#03d761b24afc04c09e757e92ff53716ae8ea2741" + integrity sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ== + dependencies: + postcss-selector-parser "^6.0.10" + +postcss-attribute-case-insensitive@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz#0c4500e3bcb2141848e89382c05b5a31c23033a3" + integrity sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-calc@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-9.0.1.tgz#a744fd592438a93d6de0f1434c572670361eb6c6" + integrity sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ== + dependencies: + postcss-selector-parser "^6.0.11" + postcss-value-parser "^4.2.0" + +postcss-clamp@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-clamp/-/postcss-clamp-4.1.0.tgz#7263e95abadd8c2ba1bd911b0b5a5c9c93e02363" + integrity sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-color-functional-notation@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz#21a909e8d7454d3612d1659e471ce4696f28caec" + integrity sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-color-functional-notation@^7.0.7: + version "7.0.7" + resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.7.tgz#c5362df010926f902ce4e7fb3da2a46cff175d1b" + integrity sha512-EZvAHsvyASX63vXnyXOIynkxhaHRSsdb7z6yiXKIovGXAolW4cMZ3qoh7k3VdTsLBS6VGdksGfIo3r6+waLoOw== + dependencies: + "@csstools/css-color-parser" "^3.0.7" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +postcss-color-hex-alpha@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz#5dd3eba1f8facb4ea306cba6e3f7712e876b0c76" + integrity sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-color-hex-alpha@^8.0.4: + version "8.0.4" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz#c66e2980f2fbc1a63f5b079663340ce8b55f25a5" + integrity sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-color-rebeccapurple@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz#5ada28406ac47e0796dff4056b0a9d5a6ecead98" + integrity sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-color-rebeccapurple@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz#63fdab91d878ebc4dd4b7c02619a0c3d6a56ced0" + integrity sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-colormin@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-6.1.0.tgz#076e8d3fb291fbff7b10e6b063be9da42ff6488d" + integrity sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw== + dependencies: + browserslist "^4.23.0" + caniuse-api "^3.0.0" + colord "^2.9.3" + postcss-value-parser "^4.2.0" + +postcss-convert-values@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz#3498387f8efedb817cbc63901d45bd1ceaa40f48" + integrity sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w== + dependencies: + browserslist "^4.23.0" + postcss-value-parser "^4.2.0" + +postcss-custom-media@^11.0.5: + version "11.0.5" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-11.0.5.tgz#2fcd88a9b1d4da41c67dac6f2def903063a3377d" + integrity sha512-SQHhayVNgDvSAdX9NQ/ygcDQGEY+aSF4b/96z7QUX6mqL5yl/JgG/DywcF6fW9XbnCRE+aVYk+9/nqGuzOPWeQ== + dependencies: + "@csstools/cascade-layer-name-parser" "^2.0.4" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/media-query-list-parser" "^4.0.2" + +postcss-custom-media@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz#c8f9637edf45fef761b014c024cee013f80529ea" + integrity sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-custom-properties@^12.1.10: + version "12.1.11" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz#d14bb9b3989ac4d40aaa0e110b43be67ac7845cf" + integrity sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-custom-properties@^14.0.4: + version "14.0.4" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-14.0.4.tgz#de9c663285a98833a946d7003a34369d3ce373a9" + integrity sha512-QnW8FCCK6q+4ierwjnmXF9Y9KF8q0JkbgVfvQEMa93x1GT8FvOiUevWCN2YLaOWyByeDX8S6VFbZEeWoAoXs2A== + dependencies: + "@csstools/cascade-layer-name-parser" "^2.0.4" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-custom-selectors@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz#1ab4684d65f30fed175520f82d223db0337239d9" + integrity sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-custom-selectors@^8.0.4: + version "8.0.4" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-8.0.4.tgz#95ef8268fdbbbd84f34cf84a4517c9d99d419c5a" + integrity sha512-ASOXqNvDCE0dAJ/5qixxPeL1aOVGHGW2JwSy7HyjWNbnWTQCl+fDc968HY1jCmZI0+BaYT5CxsOiUhavpG/7eg== + dependencies: + "@csstools/cascade-layer-name-parser" "^2.0.4" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + postcss-selector-parser "^7.0.0" + +postcss-dir-pseudo-class@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz#2bf31de5de76added44e0a25ecf60ae9f7c7c26c" + integrity sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA== + dependencies: + postcss-selector-parser "^6.0.10" + +postcss-dir-pseudo-class@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz#80d9e842c9ae9d29f6bf5fd3cf9972891d6cc0ca" + integrity sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-discard-comments@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz#e768dcfdc33e0216380623652b0a4f69f4678b6c" + integrity sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw== + +postcss-discard-duplicates@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz#d121e893c38dc58a67277f75bb58ba43fce4c3eb" + integrity sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw== + +postcss-discard-empty@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz#ee39c327219bb70473a066f772621f81435a79d9" + integrity sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ== + +postcss-discard-overridden@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz#4e9f9c62ecd2df46e8fdb44dc17e189776572e2d" + integrity sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ== + +postcss-discard-unused@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-6.0.5.tgz#c1b0e8c032c6054c3fbd22aaddba5b248136f338" + integrity sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA== + dependencies: + postcss-selector-parser "^6.0.16" + +postcss-double-position-gradients@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz#b96318fdb477be95997e86edd29c6e3557a49b91" + integrity sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" + postcss-value-parser "^4.2.0" + +postcss-double-position-gradients@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.0.tgz#eddd424ec754bb543d057d4d2180b1848095d4d2" + integrity sha512-JkIGah3RVbdSEIrcobqj4Gzq0h53GG4uqDPsho88SgY84WnpkTpI0k50MFK/sX7XqVisZ6OqUfFnoUO6m1WWdg== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-env-function@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-4.0.6.tgz#7b2d24c812f540ed6eda4c81f6090416722a8e7a" + integrity sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-focus-visible@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz#1f7904904368a2d1180b220595d77b6f8a957868" + integrity sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-focus-visible@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz#50c9ea9afa0ee657fb75635fabad25e18d76bf9e" + integrity sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw== + dependencies: + postcss-selector-parser "^6.0.9" + +postcss-focus-within@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz#5b1d2ec603195f3344b716c0b75f61e44e8d2e20" + integrity sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ== + dependencies: + postcss-selector-parser "^6.0.9" + +postcss-focus-within@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz#ac01ce80d3f2e8b2b3eac4ff84f8e15cd0057bc7" + integrity sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-font-variant@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz#efd59b4b7ea8bb06127f2d031bfbb7f24d32fa66" + integrity sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA== + +postcss-gap-properties@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz#f7e3cddcf73ee19e94ccf7cb77773f9560aa2fff" + integrity sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg== + +postcss-gap-properties@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz#d5ff0bdf923c06686499ed2b12e125fe64054fed" + integrity sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw== + +postcss-image-set-function@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz#08353bd756f1cbfb3b6e93182c7829879114481f" + integrity sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-image-set-function@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz#538e94e16716be47f9df0573b56bbaca86e1da53" + integrity sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-import@^14.0.2: + version "14.1.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0" + integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-import@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" + integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-initial@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-4.0.1.tgz#529f735f72c5724a0fb30527df6fb7ac54d7de42" + integrity sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ== + +postcss-js@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2" + integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== + dependencies: + camelcase-css "^2.0.1" + +postcss-lab-function@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz#6fe4c015102ff7cd27d1bd5385582f67ebdbdc98" + integrity sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" + postcss-value-parser "^4.2.0" + +postcss-lab-function@^7.0.7: + version "7.0.7" + resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-7.0.7.tgz#9c87c21ce5132c55824190b75d7d7adede9c2fac" + integrity sha512-+ONj2bpOQfsCKZE2T9VGMyVVdGcGUpr7u3SVfvkJlvhTRmDCfY25k4Jc8fubB9DclAPR4+w8uVtDZmdRgdAHig== + dependencies: + "@csstools/css-color-parser" "^3.0.7" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +postcss-load-config@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" + integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== + dependencies: + lilconfig "^3.0.0" + yaml "^2.3.4" + +postcss-loader@^7.3.3: + version "7.3.4" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-7.3.4.tgz#aed9b79ce4ed7e9e89e56199d25ad1ec8f606209" + integrity sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A== + dependencies: + cosmiconfig "^8.3.5" + jiti "^1.20.0" + semver "^7.5.4" + +postcss-logical@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-5.0.4.tgz#ec75b1ee54421acc04d5921576b7d8db6b0e6f73" + integrity sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g== + +postcss-logical@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-8.0.0.tgz#0db0b90c2dc53b485a8074a4b7a906297544f58d" + integrity sha512-HpIdsdieClTjXLOyYdUPAX/XQASNIwdKt5hoZW08ZOAiI+tbV0ta1oclkpVkW5ANU+xJvk3KkA0FejkjGLXUkg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-media-minmax@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz#7140bddec173e2d6d657edbd8554a55794e2a5b5" + integrity sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ== + +postcss-merge-idents@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz#7b9c31c7bc823c94bec50f297f04e3c2b838ea65" + integrity sha512-1oIoAsODUs6IHQZkLQGO15uGEbK3EAl5wi9SS8hs45VgsxQfMnxvt+L+zIr7ifZFIH14cfAeVe2uCTa+SPRa3g== + dependencies: + cssnano-utils "^4.0.2" + postcss-value-parser "^4.2.0" + +postcss-merge-longhand@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz#ba8a8d473617c34a36abbea8dda2b215750a065a" + integrity sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w== + dependencies: + postcss-value-parser "^4.2.0" + stylehacks "^6.1.1" + +postcss-merge-rules@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz#7aa539dceddab56019469c0edd7d22b64c3dea9d" + integrity sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ== + dependencies: + browserslist "^4.23.0" + caniuse-api "^3.0.0" + cssnano-utils "^4.0.2" + postcss-selector-parser "^6.0.16" + +postcss-minify-font-values@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz#a0e574c02ee3f299be2846369211f3b957ea4c59" + integrity sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-minify-gradients@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz#ca3eb55a7bdb48a1e187a55c6377be918743dbd6" + integrity sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q== + dependencies: + colord "^2.9.3" + cssnano-utils "^4.0.2" + postcss-value-parser "^4.2.0" + +postcss-minify-params@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz#54551dec77b9a45a29c3cb5953bf7325a399ba08" + integrity sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA== + dependencies: + browserslist "^4.23.0" + cssnano-utils "^4.0.2" + postcss-value-parser "^4.2.0" + +postcss-minify-selectors@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz#197f7d72e6dd19eed47916d575d69dc38b396aff" + integrity sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ== + dependencies: + postcss-selector-parser "^6.0.16" + +postcss-modules-extract-imports@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002" + integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== + +postcss-modules-local-by-default@^4.0.5: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz#d150f43837831dae25e4085596e84f6f5d6ec368" + integrity sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^7.0.0" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz#1bbccddcb398f1d7a511e0a2d1d047718af4078c" + integrity sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + +postcss-nested@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.2.0.tgz#4c2d22ab5f20b9cb61e2c5c5915950784d068131" + integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ== + dependencies: + postcss-selector-parser "^6.1.1" + +postcss-nesting@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-10.2.0.tgz#0b12ce0db8edfd2d8ae0aaf86427370b898890be" + integrity sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA== + dependencies: + "@csstools/selector-specificity" "^2.0.0" + postcss-selector-parser "^6.0.10" + +postcss-nesting@^13.0.1: + version "13.0.1" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-13.0.1.tgz#c405796d7245a3e4c267a9956cacfe9670b5d43e" + integrity sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ== + dependencies: + "@csstools/selector-resolve-nested" "^3.0.0" + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + +postcss-normalize-charset@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz#1ec25c435057a8001dac942942a95ffe66f721e1" + integrity sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ== + +postcss-normalize-display-values@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz#54f02764fed0b288d5363cbb140d6950dbbdd535" + integrity sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-positions@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz#e982d284ec878b9b819796266f640852dbbb723a" + integrity sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-repeat-style@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz#f8006942fd0617c73f049dd8b6201c3a3040ecf3" + integrity sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-string@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz#e3cc6ad5c95581acd1fc8774b309dd7c06e5e363" + integrity sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-timing-functions@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz#40cb8726cef999de984527cbd9d1db1f3e9062c0" + integrity sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-unicode@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz#aaf8bbd34c306e230777e80f7f12a4b7d27ce06e" + integrity sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg== + dependencies: + browserslist "^4.23.0" + postcss-value-parser "^4.2.0" + +postcss-normalize-url@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz#292792386be51a8de9a454cb7b5c58ae22db0f79" + integrity sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-whitespace@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz#fbb009e6ebd312f8b2efb225c2fcc7cf32b400cd" + integrity sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-opacity-percentage@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz#5b89b35551a556e20c5d23eb5260fbfcf5245da6" + integrity sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A== + +postcss-opacity-percentage@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz#0b0db5ed5db5670e067044b8030b89c216e1eb0a" + integrity sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ== + +postcss-ordered-values@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz#366bb663919707093451ab70c3f99c05672aaae5" + integrity sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q== + dependencies: + cssnano-utils "^4.0.2" + postcss-value-parser "^4.2.0" + +postcss-overflow-shorthand@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz#7ed6486fec44b76f0eab15aa4866cda5d55d893e" + integrity sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-overflow-shorthand@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz#f5252b4a2ee16c68cd8a9029edb5370c4a9808af" + integrity sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-page-break@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-3.0.4.tgz#7fbf741c233621622b68d435babfb70dd8c1ee5f" + integrity sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ== + +postcss-place@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-10.0.0.tgz#ba36ee4786ca401377ced17a39d9050ed772e5a9" + integrity sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-place@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-7.0.5.tgz#95dbf85fd9656a3a6e60e832b5809914236986c4" + integrity sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-preset-env@^10.1.0: + version "10.1.3" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-10.1.3.tgz#7d07adef2237a643162e751b00eb1e339aa3b82e" + integrity sha512-9qzVhcMFU/MnwYHyYpJz4JhGku/4+xEiPTmhn0hj3IxnUYlEF9vbh7OC1KoLAnenS6Fgg43TKNp9xcuMeAi4Zw== + dependencies: + "@csstools/postcss-cascade-layers" "^5.0.1" + "@csstools/postcss-color-function" "^4.0.7" + "@csstools/postcss-color-mix-function" "^3.0.7" + "@csstools/postcss-content-alt-text" "^2.0.4" + "@csstools/postcss-exponential-functions" "^2.0.6" + "@csstools/postcss-font-format-keywords" "^4.0.0" + "@csstools/postcss-gamut-mapping" "^2.0.7" + "@csstools/postcss-gradients-interpolation-method" "^5.0.7" + "@csstools/postcss-hwb-function" "^4.0.7" + "@csstools/postcss-ic-unit" "^4.0.0" + "@csstools/postcss-initial" "^2.0.0" + "@csstools/postcss-is-pseudo-class" "^5.0.1" + "@csstools/postcss-light-dark-function" "^2.0.7" + "@csstools/postcss-logical-float-and-clear" "^3.0.0" + "@csstools/postcss-logical-overflow" "^2.0.0" + "@csstools/postcss-logical-overscroll-behavior" "^2.0.0" + "@csstools/postcss-logical-resize" "^3.0.0" + "@csstools/postcss-logical-viewport-units" "^3.0.3" + "@csstools/postcss-media-minmax" "^2.0.6" + "@csstools/postcss-media-queries-aspect-ratio-number-values" "^3.0.4" + "@csstools/postcss-nested-calc" "^4.0.0" + "@csstools/postcss-normalize-display-values" "^4.0.0" + "@csstools/postcss-oklab-function" "^4.0.7" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/postcss-random-function" "^1.0.2" + "@csstools/postcss-relative-color-syntax" "^3.0.7" + "@csstools/postcss-scope-pseudo-class" "^4.0.1" + "@csstools/postcss-sign-functions" "^1.1.1" + "@csstools/postcss-stepped-value-functions" "^4.0.6" + "@csstools/postcss-text-decoration-shorthand" "^4.0.1" + "@csstools/postcss-trigonometric-functions" "^4.0.6" + "@csstools/postcss-unset-value" "^4.0.0" + autoprefixer "^10.4.19" + browserslist "^4.23.1" + css-blank-pseudo "^7.0.1" + css-has-pseudo "^7.0.2" + css-prefers-color-scheme "^10.0.0" + cssdb "^8.2.3" + postcss-attribute-case-insensitive "^7.0.1" + postcss-clamp "^4.1.0" + postcss-color-functional-notation "^7.0.7" + postcss-color-hex-alpha "^10.0.0" + postcss-color-rebeccapurple "^10.0.0" + postcss-custom-media "^11.0.5" + postcss-custom-properties "^14.0.4" + postcss-custom-selectors "^8.0.4" + postcss-dir-pseudo-class "^9.0.1" + postcss-double-position-gradients "^6.0.0" + postcss-focus-visible "^10.0.1" + postcss-focus-within "^9.0.1" + postcss-font-variant "^5.0.0" + postcss-gap-properties "^6.0.0" + postcss-image-set-function "^7.0.0" + postcss-lab-function "^7.0.7" + postcss-logical "^8.0.0" + postcss-nesting "^13.0.1" + postcss-opacity-percentage "^3.0.0" + postcss-overflow-shorthand "^6.0.0" + postcss-page-break "^3.0.4" + postcss-place "^10.0.0" + postcss-pseudo-class-any-link "^10.0.1" + postcss-replace-overflow-wrap "^4.0.0" + postcss-selector-not "^8.0.1" + +postcss-preset-env@^7.4.3: + version "7.8.3" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz#2a50f5e612c3149cc7af75634e202a5b2ad4f1e2" + integrity sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag== + dependencies: + "@csstools/postcss-cascade-layers" "^1.1.1" + "@csstools/postcss-color-function" "^1.1.1" + "@csstools/postcss-font-format-keywords" "^1.0.1" + "@csstools/postcss-hwb-function" "^1.0.2" + "@csstools/postcss-ic-unit" "^1.0.1" + "@csstools/postcss-is-pseudo-class" "^2.0.7" + "@csstools/postcss-nested-calc" "^1.0.0" + "@csstools/postcss-normalize-display-values" "^1.0.1" + "@csstools/postcss-oklab-function" "^1.1.1" + "@csstools/postcss-progressive-custom-properties" "^1.3.0" + "@csstools/postcss-stepped-value-functions" "^1.0.1" + "@csstools/postcss-text-decoration-shorthand" "^1.0.0" + "@csstools/postcss-trigonometric-functions" "^1.0.2" + "@csstools/postcss-unset-value" "^1.0.2" + autoprefixer "^10.4.13" + browserslist "^4.21.4" + css-blank-pseudo "^3.0.3" + css-has-pseudo "^3.0.4" + css-prefers-color-scheme "^6.0.3" + cssdb "^7.1.0" + postcss-attribute-case-insensitive "^5.0.2" + postcss-clamp "^4.1.0" + postcss-color-functional-notation "^4.2.4" + postcss-color-hex-alpha "^8.0.4" + postcss-color-rebeccapurple "^7.1.1" + postcss-custom-media "^8.0.2" + postcss-custom-properties "^12.1.10" + postcss-custom-selectors "^6.0.3" + postcss-dir-pseudo-class "^6.0.5" + postcss-double-position-gradients "^3.1.2" + postcss-env-function "^4.0.6" + postcss-focus-visible "^6.0.4" + postcss-focus-within "^5.0.4" + postcss-font-variant "^5.0.0" + postcss-gap-properties "^3.0.5" + postcss-image-set-function "^4.0.7" + postcss-initial "^4.0.1" + postcss-lab-function "^4.2.1" + postcss-logical "^5.0.4" + postcss-media-minmax "^5.0.0" + postcss-nesting "^10.2.0" + postcss-opacity-percentage "^1.1.2" + postcss-overflow-shorthand "^3.0.4" + postcss-page-break "^3.0.4" + postcss-place "^7.0.5" + postcss-pseudo-class-any-link "^7.1.6" + postcss-replace-overflow-wrap "^4.0.0" + postcss-selector-not "^6.0.1" + postcss-value-parser "^4.2.0" + +postcss-pseudo-class-any-link@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz#06455431171bf44b84d79ebaeee9fd1c05946544" + integrity sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-pseudo-class-any-link@^7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz#2693b221902da772c278def85a4d9a64b6e617ab" + integrity sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w== + dependencies: + postcss-selector-parser "^6.0.10" + +postcss-reduce-idents@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz#b0d9c84316d2a547714ebab523ec7d13704cd486" + integrity sha512-G3yCqZDpsNPoQgbDUy3T0E6hqOQ5xigUtBQyrmq3tn2GxlyiL0yyl7H+T8ulQR6kOcHJ9t7/9H4/R2tv8tJbMA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-reduce-initial@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz#4401297d8e35cb6e92c8e9586963e267105586ba" + integrity sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw== + dependencies: + browserslist "^4.23.0" + caniuse-api "^3.0.0" + +postcss-reduce-transforms@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz#6fa2c586bdc091a7373caeee4be75a0f3e12965d" + integrity sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-replace-overflow-wrap@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz#d2df6bed10b477bf9c52fab28c568b4b29ca4319" + integrity sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw== + +postcss-selector-not@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz#8f0a709bf7d4b45222793fc34409be407537556d" + integrity sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ== + dependencies: + postcss-selector-parser "^6.0.10" + +postcss-selector-not@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz#f2df9c6ac9f95e9fe4416ca41a957eda16130172" + integrity sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.9, postcss-selector-parser@^6.1.1, postcss-selector-parser@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" + integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-selector-parser@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz#41bd8b56f177c093ca49435f65731befe25d6b9c" + integrity sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-sort-media-queries@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz#4556b3f982ef27d3bac526b99b6c0d3359a6cf97" + integrity sha512-AZ5fDMLD8SldlAYlvi8NIqo0+Z8xnXU2ia0jxmuhxAU+Lqt9K+AlmLNJ/zWEnE9x+Zx3qL3+1K20ATgNOr3fAA== + dependencies: + sort-css-media-queries "2.2.0" + +postcss-svgo@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-6.0.3.tgz#1d6e180d6df1fa8a3b30b729aaa9161e94f04eaa" + integrity sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g== + dependencies: + postcss-value-parser "^4.2.0" + svgo "^3.2.0" + +postcss-unique-selectors@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz#983ab308896b4bf3f2baaf2336e14e52c11a2088" + integrity sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg== + dependencies: + postcss-selector-parser "^6.0.16" + +postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss-zindex@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-6.0.2.tgz#e498304b83a8b165755f53db40e2ea65a99b56e1" + integrity sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg== + +postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.33, postcss@^8.4.38, postcss@^8.4.47: + version "8.4.49" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19" + integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA== + dependencies: + nanoid "^3.3.7" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +prebuild-install@^7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.2.tgz#a5fd9986f5a6251fbc47e1e5c65de71e68c0a056" + integrity sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + +pretty-bytes@^5.3.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + +pretty-error@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" + integrity sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw== + dependencies: + lodash "^4.17.20" + renderkid "^3.0.0" + +pretty-time@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e" + integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== + +prism-react-renderer@^2.1.0, prism-react-renderer@^2.3.0, prism-react-renderer@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz#ac63b7f78e56c8f2b5e76e823a976d5ede77e35f" + integrity sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig== + dependencies: + "@types/prismjs" "^1.26.0" + clsx "^2.0.0" + +prismjs@^1.29.0: + version "1.29.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" + integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +prompts@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +prop-types@^15.0.0, prop-types@^15.6.2, prop-types@^15.7.2: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +property-information@^6.0.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec" + integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +pump@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8" + integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +pupa@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/pupa/-/pupa-3.1.0.tgz#f15610274376bbcc70c9a3aa8b505ea23f41c579" + integrity sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug== + dependencies: + escape-goat "^4.0.0" + +q@^1.1.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== + +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +queue-tick@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" + integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== + +queue@6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65" + integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA== + dependencies: + inherits "~2.0.3" + +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A== + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc@1.2.8, rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +react-day-picker@^8.10.1: + version "8.10.1" + resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-8.10.1.tgz#4762ec298865919b93ec09ba69621580835b8e80" + integrity sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA== + +react-dev-utils@^12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73" + integrity sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ== + dependencies: + "@babel/code-frame" "^7.16.0" + address "^1.1.2" + browserslist "^4.18.1" + chalk "^4.1.2" + cross-spawn "^7.0.3" + detect-port-alt "^1.1.6" + escape-string-regexp "^4.0.0" + filesize "^8.0.6" + find-up "^5.0.0" + fork-ts-checker-webpack-plugin "^6.5.0" + global-modules "^2.0.0" + globby "^11.0.4" + gzip-size "^6.0.0" + immer "^9.0.7" + is-root "^2.1.0" + loader-utils "^3.2.0" + open "^8.4.0" + pkg-up "^3.1.0" + prompts "^2.4.2" + react-error-overlay "^6.0.11" + recursive-readdir "^2.2.2" + shell-quote "^1.7.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +react-dom@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.2" + +react-error-overlay@^6.0.11: + version "6.0.11" + resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" + integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== + +react-fast-compare@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" + integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== + +react-helmet-async@^1.3.0, "react-helmet-async@npm:@slorber/react-helmet-async@*", "react-helmet-async@npm:@slorber/react-helmet-async@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz#11fbc6094605cf60aa04a28c17e0aab894b4ecff" + integrity sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A== + dependencies: + "@babel/runtime" "^7.12.5" + invariant "^2.2.4" + prop-types "^15.7.2" + react-fast-compare "^3.2.0" + shallowequal "^1.1.0" + +react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +"react-is@^17.0.1 || ^18.0.0": + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + +react-json-view-lite@^1.2.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/react-json-view-lite/-/react-json-view-lite-1.5.0.tgz#377cc302821717ac79a1b6d099e1891df54c8662" + integrity sha512-nWqA1E4jKPklL2jvHWs6s+7Na0qNgw9HCP6xehdQJeg6nPBTFZgGwyko9Q0oj+jQWKTTVRS30u0toM5wiuL3iw== + +react-live@^4.1.6: + version "4.1.8" + resolved "https://registry.yarnpkg.com/react-live/-/react-live-4.1.8.tgz#287fb6c5127c2d89a6fe39380278d95cc8e661b6" + integrity sha512-B2SgNqwPuS2ekqj4lcxi5TibEcjWkdVyYykBEUBshPAPDQ527x2zPEZg560n8egNtAjUpwXFQm7pcXV65aAYmg== + dependencies: + prism-react-renderer "^2.4.0" + sucrase "^3.35.0" + use-editable "^2.3.3" + +react-loadable-ssr-addon-v5-slorber@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz#2cdc91e8a744ffdf9e3556caabeb6e4278689883" + integrity sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A== + dependencies: + "@babel/runtime" "^7.10.3" + +"react-loadable@npm:@docusaurus/react-loadable@6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz#de6c7f73c96542bd70786b8e522d535d69069dc4" + integrity sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ== + dependencies: + "@types/react" "*" + +react-remove-scroll-bar@^2.3.7: + version "2.3.8" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223" + integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q== + dependencies: + react-style-singleton "^2.2.2" + tslib "^2.0.0" + +react-remove-scroll@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz#2518d2c5112e71ea8928f1082a58459b5c7a2a97" + integrity sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw== + dependencies: + react-remove-scroll-bar "^2.3.7" + react-style-singleton "^2.2.1" + tslib "^2.1.0" + use-callback-ref "^1.3.3" + use-sidecar "^1.1.2" + +react-router-config@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988" + integrity sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg== + dependencies: + "@babel/runtime" "^7.1.2" + +react-router-dom@^5.3.4: + version "5.3.4" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.4.tgz#2ed62ffd88cae6db134445f4a0c0ae8b91d2e5e6" + integrity sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ== + dependencies: + "@babel/runtime" "^7.12.13" + history "^4.9.0" + loose-envify "^1.3.1" + prop-types "^15.6.2" + react-router "5.3.4" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-router@5.3.4, react-router@^5.3.4: + version "5.3.4" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.4.tgz#8ca252d70fcc37841e31473c7a151cf777887bb5" + integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA== + dependencies: + "@babel/runtime" "^7.12.13" + history "^4.9.0" + hoist-non-react-statics "^3.1.0" + loose-envify "^1.3.1" + path-to-regexp "^1.7.0" + prop-types "^15.6.2" + react-is "^16.6.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-shepherd@6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/react-shepherd/-/react-shepherd-6.1.1.tgz#41d384cc4e97e26c9625b221d8b5289f4f924626" + integrity sha512-lylVKsH8w9gV7674RznDhl4uPrTXLYuc2E0+gYJPrz4FymHrhUpDqYvYvqESPODigRK+TFFpTZAUdAZzwzPvRg== + dependencies: + shepherd.js "13.0.3" + +react-style-singleton@^2.2.1, react-style-singleton@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388" + integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ== + dependencies: + get-nonce "^1.0.0" + tslib "^2.0.0" + +react-waypoint@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/react-waypoint/-/react-waypoint-10.3.0.tgz#fcc60e86c6c9ad2174fa58d066dc6ae54e3df71d" + integrity sha512-iF1y2c1BsoXuEGz08NoahaLFIGI9gTUAAOKip96HUmylRT6DUtpgoBPjk/Y8dfcFVmfVDvUzWjNXpZyKTOV0SQ== + dependencies: + "@babel/runtime" "^7.12.5" + consolidated-events "^1.1.0 || ^2.0.0" + prop-types "^15.0.0" + react-is "^17.0.1 || ^18.0.0" + +react@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== + dependencies: + pify "^2.3.0" + +readable-stream@^2.0.1: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +reading-time@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/reading-time/-/reading-time-1.5.0.tgz#d2a7f1b6057cb2e169beaf87113cc3411b5bc5bb" + integrity sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg== + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== + dependencies: + resolve "^1.1.6" + +recma-build-jsx@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz#c02f29e047e103d2fab2054954e1761b8ea253c4" + integrity sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew== + dependencies: + "@types/estree" "^1.0.0" + estree-util-build-jsx "^3.0.0" + vfile "^6.0.0" + +recma-jsx@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/recma-jsx/-/recma-jsx-1.0.0.tgz#f7bef02e571a49d6ba3efdfda8e2efab48dbe3aa" + integrity sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q== + dependencies: + acorn-jsx "^5.0.0" + estree-util-to-js "^2.0.0" + recma-parse "^1.0.0" + recma-stringify "^1.0.0" + unified "^11.0.0" + +recma-parse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/recma-parse/-/recma-parse-1.0.0.tgz#c351e161bb0ab47d86b92a98a9d891f9b6814b52" + integrity sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ== + dependencies: + "@types/estree" "^1.0.0" + esast-util-from-js "^2.0.0" + unified "^11.0.0" + vfile "^6.0.0" + +recma-stringify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/recma-stringify/-/recma-stringify-1.0.0.tgz#54632030631e0c7546136ff9ef8fde8e7b44f130" + integrity sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g== + dependencies: + "@types/estree" "^1.0.0" + estree-util-to-js "^2.0.0" + unified "^11.0.0" + vfile "^6.0.0" + +recursive-readdir@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" + integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA== + dependencies: + minimatch "^3.0.5" + +reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" + integrity sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.9" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.7" + get-proto "^1.0.1" + which-builtin-type "^1.2.1" + +regenerate-unicode-properties@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz#626e39df8c372338ea9b8028d1f99dc3fd9c3db0" + integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== + dependencies: + regenerate "^1.4.2" + +regenerate-unicode-properties@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326" + integrity sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +regenerator-transform@^0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" + integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== + dependencies: + "@babel/runtime" "^7.8.4" + +regexp.prototype.flags@^1.5.3: + version "1.5.4" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" + integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-errors "^1.3.0" + get-proto "^1.0.1" + gopd "^1.2.0" + set-function-name "^2.0.2" + +regexpu-core@^4.5.4: + version "4.8.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.8.0.tgz#e5605ba361b67b1718478501327502f4479a98f0" + integrity sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^9.0.0" + regjsgen "^0.5.2" + regjsparser "^0.7.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.0.0" + +regexpu-core@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.2.0.tgz#0e5190d79e542bf294955dccabae04d3c7d53826" + integrity sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.2.0" + regjsgen "^0.8.0" + regjsparser "^0.12.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +registry-auth-token@^5.0.1: + version "5.0.3" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-5.0.3.tgz#417d758c8164569de8cf5cabff16cc937902dcc6" + integrity sha512-1bpc9IyC+e+CNFRaWyn77tk4xGG4PPUyfakSmA6F6cvUDjrm58dfyJ3II+9yb10EDkHoy1LaPSmHaWLOH3m6HA== + dependencies: + "@pnpm/npm-conf" "^2.1.0" + +registry-url@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-6.0.1.tgz#056d9343680f2f64400032b1e199faa692286c58" + integrity sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q== + dependencies: + rc "1.2.8" + +regjsgen@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" + integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== + +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== + +regjsparser@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.12.0.tgz#0e846df6c6530586429377de56e0475583b088dc" + integrity sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ== + dependencies: + jsesc "~3.0.2" + +regjsparser@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.7.0.tgz#a6b667b54c885e18b52554cb4960ef71187e9968" + integrity sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ== + dependencies: + jsesc "~0.5.0" + +rehype-raw@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-7.0.0.tgz#59d7348fd5dbef3807bbaa1d443efd2dd85ecee4" + integrity sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww== + dependencies: + "@types/hast" "^3.0.0" + hast-util-raw "^9.0.0" + vfile "^6.0.0" + +rehype-recma@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rehype-recma/-/rehype-recma-1.0.0.tgz#d68ef6344d05916bd96e25400c6261775411aa76" + integrity sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw== + dependencies: + "@types/estree" "^1.0.0" + "@types/hast" "^3.0.0" + hast-util-to-estree "^3.0.0" + +relateurl@^0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== + +remark-directive@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/remark-directive/-/remark-directive-3.0.0.tgz#34452d951b37e6207d2e2a4f830dc33442923268" + integrity sha512-l1UyWJ6Eg1VPU7Hm/9tt0zKtReJQNOA4+iDMAxTyZNWnJnFlbS/7zhiel/rogTLQ2vMYwDzSJa4BiVNqGlqIMA== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-directive "^3.0.0" + micromark-extension-directive "^3.0.0" + unified "^11.0.0" + +remark-emoji@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/remark-emoji/-/remark-emoji-4.0.1.tgz#671bfda668047689e26b2078c7356540da299f04" + integrity sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg== + dependencies: + "@types/mdast" "^4.0.2" + emoticon "^4.0.1" + mdast-util-find-and-replace "^3.0.1" + node-emoji "^2.1.0" + unified "^11.0.4" + +remark-frontmatter@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz#b68d61552a421ec412c76f4f66c344627dc187a2" + integrity sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-frontmatter "^2.0.0" + micromark-extension-frontmatter "^2.0.0" + unified "^11.0.0" + +remark-gfm@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-4.0.0.tgz#aea777f0744701aa288b67d28c43565c7e8c35de" + integrity sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-gfm "^3.0.0" + micromark-extension-gfm "^3.0.0" + remark-parse "^11.0.0" + remark-stringify "^11.0.0" + unified "^11.0.0" + +remark-mdx@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/remark-mdx/-/remark-mdx-3.1.0.tgz#f979be729ecb35318fa48e2135c1169607a78343" + integrity sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA== + dependencies: + mdast-util-mdx "^3.0.0" + micromark-extension-mdxjs "^3.0.0" + +remark-parse@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-11.0.0.tgz#aa60743fcb37ebf6b069204eb4da304e40db45a1" + integrity sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + micromark-util-types "^2.0.0" + unified "^11.0.0" + +remark-rehype@^11.0.0: + version "11.1.1" + resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-11.1.1.tgz#f864dd2947889a11997c0a2667cd6b38f685bca7" + integrity sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + mdast-util-to-hast "^13.0.0" + unified "^11.0.0" + vfile "^6.0.0" + +remark-stringify@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-11.0.0.tgz#4c5b01dd711c269df1aaae11743eb7e2e7636fd3" + integrity sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-to-markdown "^2.0.0" + unified "^11.0.0" + +renderkid@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" + integrity sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg== + dependencies: + css-select "^4.1.3" + dom-converter "^0.2.0" + htmlparser2 "^6.1.0" + lodash "^4.17.21" + strip-ansi "^6.0.1" + +repeat-string@^1.0.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +"require-like@>= 0.1.1": + version "0.1.2" + resolved "https://registry.yarnpkg.com/require-like/-/require-like-0.1.2.tgz#ad6f30c13becd797010c468afa775c0c0a6b47fa" + integrity sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-alpn@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" + integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-package-path@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/resolve-package-path/-/resolve-package-path-4.0.3.tgz#31dab6897236ea6613c72b83658d88898a9040aa" + integrity sha512-SRpNAPW4kewOaNUt8VPqhJ0UMxawMwzJD8V7m1cJfdSTK9ieZwS6K7Dabsm4bmLFM96Z5Y/UznrpG5kt1im8yA== + dependencies: + path-root "^0.1.1" + +resolve-pathname@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" + integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== + +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.22.1, resolve@^1.22.8: + version "1.22.10" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== + dependencies: + is-core-module "^2.16.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +responselike@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-3.0.0.tgz#20decb6c298aff0dbee1c355ca95461d42823626" + integrity sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg== + dependencies: + lowercase-keys "^3.0.0" + +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rollup@^2.43.1: + version "2.79.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.2.tgz#f150e4a5db4b121a21a747d762f701e5e9f49090" + integrity sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ== + optionalDependencies: + fsevents "~2.3.2" + +rtlcss@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/rtlcss/-/rtlcss-4.3.0.tgz#f8efd4d5b64f640ec4af8fa25b65bacd9e07cc97" + integrity sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + postcss "^8.4.21" + strip-json-comments "^3.1.1" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-array-concat@^1.1.2, safe-array-concat@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" + integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + has-symbols "^1.1.0" + isarray "^2.0.5" + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-push-apply@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz#01850e981c1602d398c85081f360e4e6d03d27f5" + integrity sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA== + dependencies: + es-errors "^1.3.0" + isarray "^2.0.5" + +safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sax@^1.2.4: + version "1.4.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" + integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== + +sax@~1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== + dependencies: + loose-envify "^1.1.0" + +schema-utils@2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" + integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== + dependencies: + "@types/json-schema" "^7.0.4" + ajv "^6.12.2" + ajv-keywords "^3.4.1" + +schema-utils@^3.0.0, schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.0.0, schema-utils@^4.0.1, schema-utils@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.0.tgz#3b669f04f71ff2dfb5aba7ce2d5a9d79b35622c0" + integrity sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +section-matter@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" + integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA== + dependencies: + extend-shallow "^2.0.1" + kind-of "^6.0.0" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +selfsigned@^2.1.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== + dependencies: + "@types/node-forge" "^1.3.0" + node-forge "^1" + +semver-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-4.0.0.tgz#3afcf5ed6d62259f5c72d0d5d50dffbdc9680df5" + integrity sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA== + dependencies: + semver "^7.3.5" + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.4: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^6.0.0, serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +serve-handler@^6.1.6: + version "6.1.6" + resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.6.tgz#50803c1d3e947cd4a341d617f8209b22bd76cfa1" + integrity sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ== + dependencies: + bytes "3.0.0" + content-disposition "0.5.2" + mime-types "2.1.18" + minimatch "3.1.2" + path-is-inside "1.0.2" + path-to-regexp "3.3.0" + range-parser "1.2.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.19.0" + +set-function-length@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + +set-proto@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/set-proto/-/set-proto-1.0.0.tgz#0760dbcff30b2d7e801fd6e19983e56da337565e" + integrity sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw== + dependencies: + dunder-proto "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shallowequal@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== + +sharp@^0.32.3: + version "0.32.6" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.32.6.tgz#6ad30c0b7cd910df65d5f355f774aa4fce45732a" + integrity sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w== + dependencies: + color "^4.2.3" + detect-libc "^2.0.2" + node-addon-api "^6.1.0" + prebuild-install "^7.1.1" + semver "^7.5.4" + simple-get "^4.0.1" + tar-fs "^3.0.4" + tunnel-agent "^0.6.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.7.3, shell-quote@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.2.tgz#d2d83e057959d53ec261311e9e9b8f51dcb2934a" + integrity sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA== + +shelljs@^0.8.5: + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +shepherd.js@13.0.3: + version "13.0.3" + resolved "https://registry.yarnpkg.com/shepherd.js/-/shepherd.js-13.0.3.tgz#78fc4b7a8c9df5a03b83c7893967c92b39396553" + integrity sha512-1lQtQUNQYi+8k9BAmbUZh7D2QxFfkxiWKU0XFTbzYaIrCkB4nR0DLQuarH5G7Ym6L8wfbadxP3hJhZ2HzVktaA== + dependencies: + "@floating-ui/dom" "^1.6.5" + "@scarf/scarf" "^1.3.0" + deepmerge-ts "^5.1.0" + +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.6, side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +signal-exit@^3.0.2, signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^4.0.0, simple-get@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + dependencies: + is-arrayish "^0.3.1" + +sirv@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.4.tgz#5dd9a725c578e34e449f332703eb2a74e46a29b0" + integrity sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ== + dependencies: + "@polka/url" "^1.0.0-next.24" + mrmime "^2.0.0" + totalist "^3.0.0" + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +sitemap@^7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-7.1.2.tgz#6ce1deb43f6f177c68bc59cf93632f54e3ae6b72" + integrity sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw== + dependencies: + "@types/node" "^17.0.5" + "@types/sax" "^1.2.1" + arg "^5.0.0" + sax "^1.2.4" + +skin-tone@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/skin-tone/-/skin-tone-2.0.0.tgz#4e3933ab45c0d4f4f781745d64b9f4c208e41237" + integrity sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA== + dependencies: + unicode-emoji-modifier-base "^1.0.0" + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + +smob@^1.0.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/smob/-/smob-1.5.0.tgz#85d79a1403abf128d24d3ebc1cdc5e1a9548d3ab" + integrity sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig== + +snake-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +sockjs@^0.3.24: + version "0.3.24" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +sonner@^1.4.41: + version "1.7.1" + resolved "https://registry.yarnpkg.com/sonner/-/sonner-1.7.1.tgz#737110a3e6211d8d766442076f852ddde1725205" + integrity sha512-b6LHBfH32SoVasRFECrdY8p8s7hXPDn3OHUFbZZbiB1ctLS9Gdh6rpX2dVrpQA0kiL5jcRzDDldwwLkSKk3+QQ== + +sort-css-media-queries@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz#aa33cf4a08e0225059448b6c40eddbf9f1c8334c" + integrity sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA== + +source-map-js@^1.0.1, source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.7.0: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + +source-map@^0.8.0-beta.0: + version "0.8.0-beta.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" + integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== + dependencies: + whatwg-url "^7.0.0" + +sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +srcset@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/srcset/-/srcset-4.0.0.tgz#336816b665b14cd013ba545b6fe62357f86e65f4" + integrity sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw== + +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +std-env@^3.7.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5" + integrity sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w== + +streamx@^2.15.0, streamx@^2.21.0: + version "2.21.1" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.21.1.tgz#f02979d8395b6b637d08a589fb514498bed55845" + integrity sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw== + dependencies: + fast-fifo "^1.3.2" + queue-tick "^1.0.1" + text-decoder "^1.1.0" + optionalDependencies: + bare-events "^2.2.0" + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string.prototype.matchall@^4.0.6: + version "4.0.12" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz#6c88740e49ad4956b1332a911e949583a275d4c0" + integrity sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-abstract "^1.23.6" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.6" + gopd "^1.2.0" + has-symbols "^1.1.0" + internal-slot "^1.1.0" + regexp.prototype.flags "^1.5.3" + set-function-name "^2.0.2" + side-channel "^1.1.0" + +string.prototype.trim@^1.2.10: + version "1.2.10" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81" + integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + define-data-property "^1.1.4" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-object-atoms "^1.0.0" + has-property-descriptors "^1.0.2" + +string.prototype.trimend@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942" + integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +stringify-entities@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" + integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== + dependencies: + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.0" + +stringify-object@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-bom-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" + integrity sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g== + +strip-comments@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-2.0.1.tgz#4ad11c3fbcac177a67a40ac224ca339ca1c1ba9b" + integrity sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + +style-to-object@^1.0.0: + version "1.0.8" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-1.0.8.tgz#67a29bca47eaa587db18118d68f9d95955e81292" + integrity sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g== + dependencies: + inline-style-parser "0.2.4" + +style-value-types@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-5.0.0.tgz#76c35f0e579843d523187989da866729411fc8ad" + integrity sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA== + dependencies: + hey-listen "^1.0.8" + tslib "^2.1.0" + +stylehacks@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-6.1.1.tgz#543f91c10d17d00a440430362d419f79c25545a6" + integrity sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg== + dependencies: + browserslist "^4.23.0" + postcss-selector-parser "^6.0.16" + +sucrase@^3.35.0: + version "3.35.0" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263" + integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== + dependencies: + "@jridgewell/gen-mapping" "^0.3.2" + commander "^4.0.0" + glob "^10.3.10" + lines-and-columns "^1.1.6" + mz "^2.7.0" + pirates "^4.0.1" + ts-interface-checker "^0.1.9" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svg-parser@^2.0.2, svg-parser@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" + integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== + +svgo@^1.2.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" + integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== + dependencies: + chalk "^2.4.1" + coa "^2.0.2" + css-select "^2.0.0" + css-select-base-adapter "^0.1.1" + css-tree "1.0.0-alpha.37" + csso "^4.0.2" + js-yaml "^3.13.1" + mkdirp "~0.5.1" + object.values "^1.1.0" + sax "~1.2.4" + stable "^0.1.8" + unquote "~1.1.1" + util.promisify "~1.0.0" + +svgo@^3.0.2, svgo@^3.2.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.3.2.tgz#ad58002652dffbb5986fc9716afe52d869ecbda8" + integrity sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^5.1.0" + css-tree "^2.3.1" + css-what "^6.1.0" + csso "^5.0.5" + picocolors "^1.0.0" + +swc-loader@^0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/swc-loader/-/swc-loader-0.2.6.tgz#bf0cba8eeff34bb19620ead81d1277fefaec6bc8" + integrity sha512-9Zi9UP2YmDpgmQVbyOPJClY0dwf58JDyDMQ7uRc4krmc72twNI2fvlBWHLqVekBpPc7h5NJkGVT1zNDxFrqhvg== + dependencies: + "@swc/counter" "^0.1.3" + +tailwind-merge@^2.3.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz#ac5fb7e227910c038d458f396b7400d93a3142d5" + integrity sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA== + +tailwindcss-animate@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4" + integrity sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA== + +tailwindcss@^3.4.13: + version "3.4.17" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.17.tgz#ae8406c0f96696a631c790768ff319d46d5e5a63" + integrity sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og== + dependencies: + "@alloc/quick-lru" "^5.2.0" + arg "^5.0.2" + chokidar "^3.6.0" + didyoumean "^1.2.2" + dlv "^1.1.3" + fast-glob "^3.3.2" + glob-parent "^6.0.2" + is-glob "^4.0.3" + jiti "^1.21.6" + lilconfig "^3.1.3" + micromatch "^4.0.8" + normalize-path "^3.0.0" + object-hash "^3.0.0" + picocolors "^1.1.1" + postcss "^8.4.47" + postcss-import "^15.1.0" + postcss-js "^4.0.1" + postcss-load-config "^4.0.2" + postcss-nested "^6.2.0" + postcss-selector-parser "^6.1.2" + resolve "^1.22.8" + sucrase "^3.35.0" + +tapable@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== + +tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +tar-fs@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-fs@^3.0.4: + version "3.0.6" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.6.tgz#eaccd3a67d5672f09ca8e8f9c3d2b89fa173f217" + integrity sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w== + dependencies: + pump "^3.0.0" + tar-stream "^3.1.5" + optionalDependencies: + bare-fs "^2.1.1" + bare-path "^2.1.0" + +tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tar-stream@^3.1.5: + version "3.1.7" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b" + integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ== + dependencies: + b4a "^1.6.4" + fast-fifo "^1.2.0" + streamx "^2.15.0" + +temp-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" + integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg== + +tempy@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tempy/-/tempy-0.6.0.tgz#65e2c35abc06f1124a97f387b08303442bde59f3" + integrity sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw== + dependencies: + is-stream "^2.0.0" + temp-dir "^2.0.0" + type-fest "^0.16.0" + unique-string "^2.0.0" + +terser-webpack-plugin@^5.3.10, terser-webpack-plugin@^5.3.9: + version "5.3.11" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz#93c21f44ca86634257cac176f884f942b7ba3832" + integrity sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ== + dependencies: + "@jridgewell/trace-mapping" "^0.3.25" + jest-worker "^27.4.5" + schema-utils "^4.3.0" + serialize-javascript "^6.0.2" + terser "^5.31.1" + +terser@^5.10.0, terser@^5.15.1, terser@^5.17.4, terser@^5.31.1: + version "5.37.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.37.0.tgz#38aa66d1cfc43d0638fab54e43ff8a4f72a21ba3" + integrity sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +text-decoder@^1.1.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.3.tgz#b19da364d981b2326d5f43099c310cc80d770c65" + integrity sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA== + dependencies: + b4a "^1.6.4" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +tiny-invariant@^1.0.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + +tiny-warning@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +totalist@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" + integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== + +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA== + dependencies: + punycode "^2.1.0" + +trim-lines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" + integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== + +trough@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" + integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== + +ts-interface-checker@^0.1.9: + version "0.1.13" + resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" + integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== + +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.6.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +type-fest@^0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860" + integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^1.0.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" + integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== + +type-fest@^2.13.0, type-fest@^2.5.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typed-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" + integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-typed-array "^1.1.14" + +typed-array-byte-length@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz#8407a04f7d78684f3d252aa1a143d2b77b4160ce" + integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== + dependencies: + call-bind "^1.0.8" + for-each "^0.3.3" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.14" + +typed-array-byte-offset@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz#ae3698b8ec91a8ab945016108aef00d5bff12355" + integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + for-each "^0.3.3" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.15" + reflect.getprototypeof "^1.0.9" + +typed-array-length@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d" + integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + reflect.getprototypeof "^1.0.6" + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +typescript@~5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" + integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== + +unbox-primitive@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" + integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== + dependencies: + call-bound "^1.0.3" + has-bigints "^1.0.2" + has-symbols "^1.1.0" + which-boxed-primitive "^1.1.1" + +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" + integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== + +unicode-emoji-modifier-base@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz#dbbd5b54ba30f287e2a8d5a249da6c0cef369459" + integrity sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.0.0, unicode-match-property-value-ecmascript@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz#a0401aee72714598f739b68b104e4fe3a0cb3c71" + integrity sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +unified@^11.0.0, unified@^11.0.3, unified@^11.0.4: + version "11.0.5" + resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.5.tgz#f66677610a5c0a9ee90cab2b8d4d66037026d9e1" + integrity sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA== + dependencies: + "@types/unist" "^3.0.0" + bail "^2.0.0" + devlop "^1.0.0" + extend "^3.0.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^6.0.0" + +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + +unique-string@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-3.0.0.tgz#84a1c377aff5fd7a8bc6b55d8244b2bd90d75b9a" + integrity sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ== + dependencies: + crypto-random-string "^4.0.0" + +unist-util-is@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" + integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-position-from-estree@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz#d94da4df596529d1faa3de506202f0c9a23f2200" + integrity sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-position@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" + integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-stringify-position@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" + integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-visit-parents@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" + integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + +unist-util-visit@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" + integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +unquote@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" + integrity sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg== + +upath@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" + integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== + +update-browserslist-db@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz#97e9c96ab0ae7bcac08e9ae5151d26e6bc6b5580" + integrity sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +update-notifier@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-6.0.2.tgz#a6990253dfe6d5a02bd04fbb6a61543f55026b60" + integrity sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og== + dependencies: + boxen "^7.0.0" + chalk "^5.0.1" + configstore "^6.0.0" + has-yarn "^3.0.0" + import-lazy "^4.0.0" + is-ci "^3.0.1" + is-installed-globally "^0.4.0" + is-npm "^6.0.0" + is-yarn-global "^0.4.0" + latest-version "^7.0.0" + pupa "^3.1.0" + semver "^7.3.7" + semver-diff "^4.0.0" + xdg-basedir "^5.1.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +url-loader@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2" + integrity sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA== + dependencies: + loader-utils "^2.0.0" + mime-types "^2.1.27" + schema-utils "^3.0.0" + +use-callback-ref@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf" + integrity sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg== + dependencies: + tslib "^2.0.0" + +use-editable@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/use-editable/-/use-editable-2.3.3.tgz#a292fe9ba4c291cd28d1cc2728c75a5fc8d9a33f" + integrity sha512-7wVD2JbfAFJ3DK0vITvXBdpd9JAz5BcKAAolsnLBuBn6UDDwBGuCIAGvR3yA2BNKm578vAMVHFCWaOcA+BhhiA== + +use-sidecar@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.3.tgz#10e7fd897d130b896e2c546c63a5e8233d00efdb" + integrity sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ== + dependencies: + detect-node-es "^1.1.0" + tslib "^2.0.0" + +use-sync-external-store@^1.2.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz#adbc795d8eeb47029963016cefdf89dc799fcebc" + integrity sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw== + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +util.promisify@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" + integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.2" + has-symbols "^1.0.1" + object.getownpropertydescriptors "^2.1.0" + +utila@~0.4: + version "0.4.0" + resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA== + +utility-types@^3.10.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.11.0.tgz#607c40edb4f258915e901ea7995607fdf319424c" + integrity sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +validate-peer-dependencies@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/validate-peer-dependencies/-/validate-peer-dependencies-2.2.0.tgz#47b8ff008f66a66fc5d8699123844522c1d874f4" + integrity sha512-8X1OWlERjiUY6P6tdeU9E0EwO8RA3bahoOVG7ulOZT5MqgNDUO/BQoVjYiHPcNe+v8glsboZRIw9iToMAA2zAA== + dependencies: + resolve-package-path "^4.0.3" + semver "^7.3.8" + +value-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" + integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +vfile-location@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.3.tgz#cb9eacd20f2b6426d19451e0eafa3d0a846225c3" + integrity sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg== + dependencies: + "@types/unist" "^3.0.0" + vfile "^6.0.0" + +vfile-message@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" + integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + +vfile@^6.0.0, vfile@^6.0.1: + version "6.0.3" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab" + integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== + dependencies: + "@types/unist" "^3.0.0" + vfile-message "^4.0.0" + +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +web-namespaces@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" + integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== + +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + +webpack-bundle-analyzer@^4.10.2: + version "4.10.2" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz#633af2862c213730be3dbdf40456db171b60d5bd" + integrity sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw== + dependencies: + "@discoveryjs/json-ext" "0.5.7" + acorn "^8.0.4" + acorn-walk "^8.0.0" + commander "^7.2.0" + debounce "^1.2.1" + escape-string-regexp "^4.0.0" + gzip-size "^6.0.0" + html-escaper "^2.0.2" + opener "^1.5.2" + picocolors "^1.0.0" + sirv "^2.0.3" + ws "^7.3.1" + +webpack-dev-middleware@^5.3.4: + version "5.3.4" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" + integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== + dependencies: + colorette "^2.0.10" + memfs "^3.4.3" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@^4.15.2: + version "4.15.2" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz#9e0c70a42a012560860adb186986da1248333173" + integrity sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g== + dependencies: + "@types/bonjour" "^3.5.9" + "@types/connect-history-api-fallback" "^1.3.5" + "@types/express" "^4.17.13" + "@types/serve-index" "^1.9.1" + "@types/serve-static" "^1.13.10" + "@types/sockjs" "^0.3.33" + "@types/ws" "^8.5.5" + ansi-html-community "^0.0.8" + bonjour-service "^1.0.11" + chokidar "^3.5.3" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^2.0.0" + default-gateway "^6.0.3" + express "^4.17.3" + graceful-fs "^4.2.6" + html-entities "^2.3.2" + http-proxy-middleware "^2.0.3" + ipaddr.js "^2.0.1" + launch-editor "^2.6.0" + open "^8.0.9" + p-retry "^4.5.0" + rimraf "^3.0.2" + schema-utils "^4.0.0" + selfsigned "^2.1.1" + serve-index "^1.9.1" + sockjs "^0.3.24" + spdy "^4.0.2" + webpack-dev-middleware "^5.3.4" + ws "^8.13.0" + +webpack-merge@^5.9.0: + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.0" + +webpack-merge@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-6.0.1.tgz#50c776868e080574725abc5869bd6e4ef0a16c6a" + integrity sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.1" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@^5.88.1, webpack@^5.95.0: + version "5.97.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.97.1.tgz#972a8320a438b56ff0f1d94ade9e82eac155fa58" + integrity sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg== + dependencies: + "@types/eslint-scope" "^3.7.7" + "@types/estree" "^1.0.6" + "@webassemblyjs/ast" "^1.14.1" + "@webassemblyjs/wasm-edit" "^1.14.1" + "@webassemblyjs/wasm-parser" "^1.14.1" + acorn "^8.14.0" + browserslist "^4.24.0" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.1" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.11" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" + webpack-sources "^3.2.3" + +webpackbar@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-6.0.1.tgz#5ef57d3bf7ced8b19025477bc7496ea9d502076b" + integrity sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q== + dependencies: + ansi-escapes "^4.3.2" + chalk "^4.1.2" + consola "^3.2.3" + figures "^3.2.0" + markdown-table "^2.0.0" + pretty-time "^1.1.0" + std-env "^3.7.0" + wrap-ansi "^7.0.0" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +whatwg-url@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" + integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== + dependencies: + is-bigint "^1.1.0" + is-boolean-object "^1.2.1" + is-number-object "^1.1.1" + is-string "^1.1.1" + is-symbol "^1.1.1" + +which-builtin-type@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz#89183da1b4907ab089a6b02029cc5d8d6574270e" + integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== + dependencies: + call-bound "^1.0.2" + function.prototype.name "^1.1.6" + has-tostringtag "^1.0.2" + is-async-function "^2.0.0" + is-date-object "^1.1.0" + is-finalizationregistry "^1.1.0" + is-generator-function "^1.0.10" + is-regex "^1.2.1" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.1.0" + which-collection "^1.0.2" + which-typed-array "^1.1.16" + +which-collection@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + +which-typed-array@^1.1.16, which-typed-array@^1.1.18: + version "1.1.18" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.18.tgz#df2389ebf3fbb246a71390e90730a9edb6ce17ad" + integrity sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" + for-each "^0.3.3" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + +which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +widest-line@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-4.0.1.tgz#a0fc673aaba1ea6f0a0d35b3c2795c9a9cc2ebf2" + integrity sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig== + dependencies: + string-width "^5.0.1" + +wildcard@^2.0.0, wildcard@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +workbox-background-sync@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-7.3.0.tgz#b6340731a8d5b42b9e75a8a87c8806928e6e6303" + integrity sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg== + dependencies: + idb "^7.0.1" + workbox-core "7.3.0" + +workbox-broadcast-update@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-7.3.0.tgz#bff86b91795c4b9fa46a758d1a7a151828623280" + integrity sha512-T9/F5VEdJVhwmrIAE+E/kq5at2OY6+OXXgOWQevnubal6sO92Gjo24v6dCVwQiclAF5NS3hlmsifRrpQzZCdUA== + dependencies: + workbox-core "7.3.0" + +workbox-build@^7.0.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-7.3.0.tgz#ab688f3241b32862236aeeb62b240195f1fe4b62" + integrity sha512-JGL6vZTPlxnlqZRhR/K/msqg3wKP+m0wfEUVosK7gsYzSgeIxvZLi1ViJJzVL7CEeI8r7rGFV973RiEqkP3lWQ== + dependencies: + "@apideck/better-ajv-errors" "^0.3.1" + "@babel/core" "^7.24.4" + "@babel/preset-env" "^7.11.0" + "@babel/runtime" "^7.11.2" + "@rollup/plugin-babel" "^5.2.0" + "@rollup/plugin-node-resolve" "^15.2.3" + "@rollup/plugin-replace" "^2.4.1" + "@rollup/plugin-terser" "^0.4.3" + "@surma/rollup-plugin-off-main-thread" "^2.2.3" + ajv "^8.6.0" + common-tags "^1.8.0" + fast-json-stable-stringify "^2.1.0" + fs-extra "^9.0.1" + glob "^7.1.6" + lodash "^4.17.20" + pretty-bytes "^5.3.0" + rollup "^2.43.1" + source-map "^0.8.0-beta.0" + stringify-object "^3.3.0" + strip-comments "^2.0.1" + tempy "^0.6.0" + upath "^1.2.0" + workbox-background-sync "7.3.0" + workbox-broadcast-update "7.3.0" + workbox-cacheable-response "7.3.0" + workbox-core "7.3.0" + workbox-expiration "7.3.0" + workbox-google-analytics "7.3.0" + workbox-navigation-preload "7.3.0" + workbox-precaching "7.3.0" + workbox-range-requests "7.3.0" + workbox-recipes "7.3.0" + workbox-routing "7.3.0" + workbox-strategies "7.3.0" + workbox-streams "7.3.0" + workbox-sw "7.3.0" + workbox-window "7.3.0" + +workbox-cacheable-response@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-7.3.0.tgz#557b0f5fdfceb22fe243e3f19807c76a0ae646e3" + integrity sha512-eAFERIg6J2LuyELhLlmeRcJFa5e16Mj8kL2yCDbhWE+HUun9skRQrGIFVUagqWj4DMaaPSMWfAolM7XZZxNmxA== + dependencies: + workbox-core "7.3.0" + +workbox-core@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-7.3.0.tgz#f24fb92041a0b7482fe2dd856544aaa9fa105248" + integrity sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw== + +workbox-expiration@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-7.3.0.tgz#2c1ee1fdada34aa7e7474f706d5429c914bd10d2" + integrity sha512-lpnSSLp2BM+K6bgFCWc5bS1LR5pAwDWbcKt1iL87/eTSJRdLdAwGQznZE+1czLgn/X05YChsrEegTNxjM067vQ== + dependencies: + idb "^7.0.1" + workbox-core "7.3.0" + +workbox-google-analytics@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-7.3.0.tgz#3c4d4956c0a9800dfb587d82ec8bc0f9cf963791" + integrity sha512-ii/tSfFdhjLHZ2BrYgFNTrb/yk04pw2hasgbM70jpZfLk0vdJAXgaiMAWsoE+wfJDNWoZmBYY0hMVI0v5wWDbg== + dependencies: + workbox-background-sync "7.3.0" + workbox-core "7.3.0" + workbox-routing "7.3.0" + workbox-strategies "7.3.0" + +workbox-navigation-preload@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-7.3.0.tgz#9d54693b9179d5175e66af5ef9a92d1b7cf3e605" + integrity sha512-fTJzogmFaTv4bShZ6aA7Bfj4Cewaq5rp30qcxl2iYM45YD79rKIhvzNHiFj1P+u5ZZldroqhASXwwoyusnr2cg== + dependencies: + workbox-core "7.3.0" + +workbox-precaching@7.3.0, workbox-precaching@^7.0.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-7.3.0.tgz#a84663d69efdb334f25c04dba0a72ed3391c4da8" + integrity sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw== + dependencies: + workbox-core "7.3.0" + workbox-routing "7.3.0" + workbox-strategies "7.3.0" + +workbox-range-requests@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-7.3.0.tgz#1b3d5c235a0ff5271418c3a7183281dc131ccd0d" + integrity sha512-EyFmM1KpDzzAouNF3+EWa15yDEenwxoeXu9bgxOEYnFfCxns7eAxA9WSSaVd8kujFFt3eIbShNqa4hLQNFvmVQ== + dependencies: + workbox-core "7.3.0" + +workbox-recipes@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-7.3.0.tgz#fa407101e8ce52850dfba8e17a5afccb733a3942" + integrity sha512-BJro/MpuW35I/zjZQBcoxsctgeB+kyb2JAP5EB3EYzePg8wDGoQuUdyYQS+CheTb+GhqJeWmVs3QxLI8EBP1sg== + dependencies: + workbox-cacheable-response "7.3.0" + workbox-core "7.3.0" + workbox-expiration "7.3.0" + workbox-precaching "7.3.0" + workbox-routing "7.3.0" + workbox-strategies "7.3.0" + +workbox-routing@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-7.3.0.tgz#fc86296bc1155c112ee2c16b3180853586c30208" + integrity sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A== + dependencies: + workbox-core "7.3.0" + +workbox-strategies@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-7.3.0.tgz#bb1530f205806895aacdea3639e6cf6bfb3a6cb0" + integrity sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg== + dependencies: + workbox-core "7.3.0" + +workbox-streams@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-7.3.0.tgz#a4c0ae51b66121a2aa6f89229e237aca6dc27eb5" + integrity sha512-SZnXucyg8x2Y61VGtDjKPO5EgPUG5NDn/v86WYHX+9ZqvAsGOytP0Jxp1bl663YUuMoXSAtsGLL+byHzEuMRpw== + dependencies: + workbox-core "7.3.0" + workbox-routing "7.3.0" + +workbox-sw@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-7.3.0.tgz#39215017e868d7cfe6835b2961f55369d89b3e73" + integrity sha512-aCUyoAZU9IZtH05mn0ACUpyHzPs0lMeJimAYkQkBsOWiqaJLgusfDCR+yllkPkFRxWpZKF8vSvgHYeG7LwhlmA== + +workbox-window@7.3.0, workbox-window@^7.0.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-7.3.0.tgz#e71bb0b4d880d2295c96bf1ccadb6cea0df51c07" + integrity sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA== + dependencies: + "@types/trusted-types" "^2.0.2" + workbox-core "7.3.0" + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +ws@^7.3.1: + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== + +ws@^8.13.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +xdg-basedir@^5.0.1, xdg-basedir@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-5.1.0.tgz#1efba19425e73be1bc6f2a6ceb52a3d2c884c0c9" + integrity sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ== + +xml-js@^1.6.11: + version "1.6.11" + resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" + integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g== + dependencies: + sax "^1.2.4" + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yaml@^1.10.0, yaml@^1.7.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yaml@^2.3.4: + version "2.7.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.0.tgz#aef9bb617a64c937a9a748803786ad8d3ffe1e98" + integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +yocto-queue@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110" + integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== + +zwitch@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== diff --git a/platform/i18n/.gitignore b/platform/i18n/.gitignore new file mode 100644 index 0000000..dd616b8 --- /dev/null +++ b/platform/i18n/.gitignore @@ -0,0 +1 @@ +.locize \ No newline at end of file diff --git a/platform/i18n/.webpack/webpack.dev.js b/platform/i18n/.webpack/webpack.dev.js new file mode 100644 index 0000000..4bf848b --- /dev/null +++ b/platform/i18n/.webpack/webpack.dev.js @@ -0,0 +1,12 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.js`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/platform/i18n/.webpack/webpack.prod.js b/platform/i18n/.webpack/webpack.prod.js new file mode 100644 index 0000000..3302563 --- /dev/null +++ b/platform/i18n/.webpack/webpack.prod.js @@ -0,0 +1,41 @@ +const { merge } = require('webpack-merge'); +const path = require('path'); + +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const pkg = require('./../package.json'); + +const ROOT_DIR = path.join(__dirname, './..'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.js`, +}; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: false, + }, + output: { + path: ROOT_DIR, + library: 'ohif-i18n', + libraryTarget: 'umd', + filename: pkg.main, + }, + }); +}; diff --git a/platform/i18n/CHANGELOG.md b/platform/i18n/CHANGELOG.md new file mode 100644 index 0000000..b12a384 --- /dev/null +++ b/platform/i18n/CHANGELOG.md @@ -0,0 +1,3265 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + + +### Features + +* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1)) + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + + +### Features + +* **i18n:** enhanced i18n support ([#3761](https://github.com/OHIF/Viewers/issues/3761)) ([d14a8f0](https://github.com/OHIF/Viewers/commit/d14a8f0199db95cd9e85866a011b64d6bf830d57)) + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + + +### Bug Fixes + +* **SM:** drag and drop is now fixed for SM ([#3813](https://github.com/OHIF/Viewers/issues/3813)) ([f1a6764](https://github.com/OHIF/Viewers/commit/f1a67647aed635437b188cea7cf5d5a8fb974bbe)) + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + + +### Features + +* **i18n:** enhanced i18n support ([#3730](https://github.com/OHIF/Viewers/issues/3730)) ([330e11c](https://github.com/OHIF/Viewers/commit/330e11c7ff0151e1096e19b8ffdae7d64cae280e)) + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + + +### Bug Fixes + +* **i18n:** display set(s) are two words for English messages ([#3711](https://github.com/OHIF/Viewers/issues/3711)) ([c3a5847](https://github.com/OHIF/Viewers/commit/c3a5847dcd3dce4f1c8d8b11af95f79e3f93f70d)) + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + + +### Features + +* **locale:** add German translations - community PR ([#3697](https://github.com/OHIF/Viewers/issues/3697)) ([ebe8f71](https://github.com/OHIF/Viewers/commit/ebe8f71da22c1d24b58f889c5d803951e19817b6)) + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + + +### Features + +* **locale:** Added Turkish language support (tr-TR) - Community PR ([#3695](https://github.com/OHIF/Viewers/issues/3695)) ([745050a](https://github.com/OHIF/Viewers/commit/745050a28ec7c2ef2e9a4d4e590040050b2177b2)) + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + + +### Bug Fixes + +* **translation:** Side panel translate fix ([#3156](https://github.com/OHIF/Viewers/issues/3156)) ([29748d4](https://github.com/OHIF/Viewers/commit/29748d46a14d23817dbe196e0f64363fc61a8aed)) + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + + +### Features + +* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1)) + + + + + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + +**Note:** Version bump only for package @ohif/i18n + + + + + +## [0.52.8](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.7...@ohif/i18n@0.52.8) (2020-04-07) + +**Note:** Version bump only for package @ohif/i18n + + + + + +## [0.52.7](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.6...@ohif/i18n@0.52.7) (2020-03-09) + +**Note:** Version bump only for package @ohif/i18n + + + + + +## [0.52.6](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.5...@ohif/i18n@0.52.6) (2020-02-12) + + +### Bug Fixes + +* Combined Hotkeys for special characters ([#1233](https://github.com/OHIF/Viewers/issues/1233)) ([2f30e7a](https://github.com/OHIF/Viewers/commit/2f30e7a821a238144c49c56f37d8e5565540b4bd)) + + + + + +## [0.52.5](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.4...@ohif/i18n@0.52.5) (2020-01-30) + + +### Bug Fixes + +* download tool fixes & improvements ([#1235](https://github.com/OHIF/Viewers/issues/1235)) ([b9574b6](https://github.com/OHIF/Viewers/commit/b9574b6efcfeb85cde35b5cae63282f8e1b35be6)) + + + + + +## [0.52.4](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.3...@ohif/i18n@0.52.4) (2019-12-16) + +**Note:** Version bump only for package @ohif/i18n + + + + + +## [0.52.3](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.2...@ohif/i18n@0.52.3) (2019-12-12) + + +### Bug Fixes + +* translations ([#1234](https://github.com/OHIF/Viewers/issues/1234)) ([30b9e44](https://github.com/OHIF/Viewers/commit/30b9e4422073557287ef26a80b38eeb3f3fcff4c)) + + + + + +## [0.52.2](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.1...@ohif/i18n@0.52.2) (2019-11-28) + + +### Bug Fixes + +* User Preferences Issues ([#1207](https://github.com/OHIF/Viewers/issues/1207)) ([1df21a9](https://github.com/OHIF/Viewers/commit/1df21a9e075b5e6dfc10a429ae825826f46c71b8)), closes [#1161](https://github.com/OHIF/Viewers/issues/1161) [#1164](https://github.com/OHIF/Viewers/issues/1164) [#1177](https://github.com/OHIF/Viewers/issues/1177) [#1179](https://github.com/OHIF/Viewers/issues/1179) [#1180](https://github.com/OHIF/Viewers/issues/1180) [#1181](https://github.com/OHIF/Viewers/issues/1181) [#1182](https://github.com/OHIF/Viewers/issues/1182) [#1183](https://github.com/OHIF/Viewers/issues/1183) [#1184](https://github.com/OHIF/Viewers/issues/1184) [#1185](https://github.com/OHIF/Viewers/issues/1185) + + + + + +## [0.52.1](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.0...@ohif/i18n@0.52.1) (2019-11-18) + + +### Bug Fixes + +* minor date picker UX improvements ([813ee5e](https://github.com/OHIF/Viewers/commit/813ee5ed4d78b7bda234922d5f3389efe346451c)) + + + + + +# [0.52.0](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.51.0...@ohif/i18n@0.52.0) (2019-11-06) + + +### Features + +* modal provider ([#1151](https://github.com/OHIF/Viewers/issues/1151)) ([75d88bc](https://github.com/OHIF/Viewers/commit/75d88bc454710d2dcdbc7d68c4d9df041159c840)), closes [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1146](https://github.com/OHIF/Viewers/issues/1146) [#1142](https://github.com/OHIF/Viewers/issues/1142) [#1143](https://github.com/OHIF/Viewers/issues/1143) [#1110](https://github.com/OHIF/Viewers/issues/1110) [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1119](https://github.com/OHIF/Viewers/issues/1119) + + + + + +# [0.51.0](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.5...@ohif/i18n@0.51.0) (2019-10-15) + + +### Features + +* Add browser info and app version ([#1046](https://github.com/OHIF/Viewers/issues/1046)) ([c217b8b](https://github.com/OHIF/Viewers/commit/c217b8b)) + + + + + +## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.4...@ohif/i18n@0.50.5) (2019-10-04) + + +### Bug Fixes + +* CineDialog buttons label ([#998](https://github.com/OHIF/Viewers/issues/998)) ([4df624b](https://github.com/OHIF/Viewers/commit/4df624b)) + + + + + +## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.3...@ohif/i18n@0.50.4) (2019-09-10) + +**Note:** Version bump only for package @ohif/i18n + + + + + +## [0.50.3](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.2...@ohif/i18n@0.50.3) (2019-09-04) + +**Note:** Version bump only for package @ohif/i18n + + + + + +## [0.50.2](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.1...@ohif/i18n@0.50.2) (2019-09-04) + + +### Bug Fixes + +* measurementsAPI issue caused by production build ([#842](https://github.com/OHIF/Viewers/issues/842)) ([49d3439](https://github.com/OHIF/Viewers/commit/49d3439)) + + + + + +## [0.50.1](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.0-alpha.11...@ohif/i18n@0.50.1) (2019-08-14) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# [0.50.0-alpha.11](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.0-alpha.10...@ohif/i18n@0.50.0-alpha.11) (2019-08-14) + + +### Bug Fixes + +* Update i18n locales to include Japanese ([da725a8](https://github.com/OHIF/Viewers/commit/da725a8)) + + + + + +# [0.50.0-alpha.10](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.2.3-alpha.9...@ohif/i18n@0.50.0-alpha.10) (2019-08-14) + +**Note:** Version bump only for package @ohif/i18n + + + + + +## [0.2.3-alpha.9](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.2.3-alpha.8...@ohif/i18n@0.2.3-alpha.9) (2019-08-14) + +**Note:** Version bump only for package @ohif/i18n + + + + + +## 0.2.3-alpha.8 (2019-08-14) + +**Note:** Version bump only for package @ohif/i18n + + + + + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.2.3-alpha.7](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.2.3-alpha.6...@ohif/i18n@0.2.3-alpha.7) (2019-08-08) + +**Note:** Version bump only for package @ohif/i18n + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.2.3-alpha.6](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.2.3-alpha.5...@ohif/i18n@0.2.3-alpha.6) (2019-08-08) + +**Note:** Version bump only for package @ohif/i18n + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.2.3-alpha.5](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.2.3-alpha.4...@ohif/i18n@0.2.3-alpha.5) (2019-08-08) + +**Note:** Version bump only for package @ohif/i18n + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.2.3-alpha.4](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.2.3-alpha.3...@ohif/i18n@0.2.3-alpha.4) (2019-08-08) + +**Note:** Version bump only for package @ohif/i18n + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.2.3-alpha.3](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.2.3-alpha.2...@ohif/i18n@0.2.3-alpha.3) (2019-08-08) + +**Note:** Version bump only for package @ohif/i18n + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.2.3-alpha.2](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.2.3-alpha.1...@ohif/i18n@0.2.3-alpha.2) (2019-08-07) + +**Note:** Version bump only for package @ohif/i18n + +## [0.2.3-alpha.1](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.2.3-alpha.0...@ohif/i18n@0.2.3-alpha.1) (2019-08-07) + +**Note:** Version bump only for package @ohif/i18n + +## 0.2.3-alpha.0 (2019-08-05) + +**Note:** Version bump only for package @ohif/i18n diff --git a/platform/i18n/LICENSE b/platform/i18n/LICENSE new file mode 100644 index 0000000..19e20dd --- /dev/null +++ b/platform/i18n/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Open Health Imaging Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/platform/i18n/README.md b/platform/i18n/README.md new file mode 100644 index 0000000..183a6d4 --- /dev/null +++ b/platform/i18n/README.md @@ -0,0 +1,4 @@ +# @ohif/i18n + +More information available at +[OHIF Docs](https://docs.ohif.org/platform/internationalization/). diff --git a/platform/i18n/babel.config.js b/platform/i18n/babel.config.js new file mode 100644 index 0000000..325ca2a --- /dev/null +++ b/platform/i18n/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/platform/i18n/package.json b/platform/i18n/package.json new file mode 100644 index 0000000..26ae4e4 --- /dev/null +++ b/platform/i18n/package.json @@ -0,0 +1,54 @@ +{ + "name": "@ohif/i18n", + "version": "3.10.0-beta.111", + "description": "Internationalization library for The OHIF Viewer", + "author": "OHIF", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/ohif-i18n.umd.js", + "module": "src/index.js", + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "files": [ + "dist", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "clean": "shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", + "dev:i18n": "yarn run dev", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "pullTranslations": "./pullTranslations.sh", + "test:unit": "echo 'platform/i18n: missing unit tests'", + "test:unit:ci": "echo 'platform/i18n: missing unit tests'" + }, + "peerDependencies": { + "i18next": "^17.0.3", + "i18next-browser-languagedetector": "^3.0.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^12.2.2" + }, + "dependencies": { + "@babel/runtime": "^7.20.13", + "i18next-locize-backend": "^2.0.0", + "locize-editor": "^2.0.0", + "locize-lastused": "^1.1.0" + }, + "devDependencies": { + "i18next": "^17.0.3", + "i18next-browser-languagedetector": "^3.0.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^12.2.2", + "webpack-merge": "^5.7.3" + } +} diff --git a/platform/i18n/pullTranslations.sh b/platform/i18n/pullTranslations.sh new file mode 100755 index 0000000..4cd0d98 --- /dev/null +++ b/platform/i18n/pullTranslations.sh @@ -0,0 +1,8 @@ +cp -r src/locales/test-LNG src/temp +rm -rf src/locales/ +mkdir -p src/locales/ +cd src/locales/ +npx locize --config-path ../../.locize download --ver latest +cd ../../ +node ./writeLocaleIndexFiles.js +cp -r src/temp src/locales/test-LNG diff --git a/platform/i18n/src/config.js b/platform/i18n/src/config.js new file mode 100644 index 0000000..e5e4842 --- /dev/null +++ b/platform/i18n/src/config.js @@ -0,0 +1,22 @@ +const debugMode = !!(process.env.NODE_ENV !== 'production' && process.env.REACT_APP_I18N_DEBUG); + +const detectionOptions = { + // order and from where user language should be detected + order: ['querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag', 'path', 'subdomain'], + + // keys or params to lookup language from + lookupQuerystring: 'lng', + lookupCookie: 'i18next', + lookupLocalStorage: 'i18nextLng', + lookupFromPathIndex: 0, + lookupFromSubdomainIndex: 0, + + // cache user language on + caches: ['localStorage', 'cookie'], + excludeCacheFor: ['cimode'], // languages to not persist (cookie, localStorage) + + // optional htmlTag with lang attribute, the default is: + htmlTag: document.documentElement, +}; + +export { debugMode, detectionOptions }; diff --git a/platform/i18n/src/debugger.js b/platform/i18n/src/debugger.js new file mode 100644 index 0000000..9b6881e --- /dev/null +++ b/platform/i18n/src/debugger.js @@ -0,0 +1,8 @@ +import { debugMode } from './config'; + +export default (message, level = 'log') => { + if (debugMode) { + // eslint-disable-next-line + console[level]('@ohif/i18n: ', message); + } +}; diff --git a/platform/i18n/src/index.js b/platform/i18n/src/index.js new file mode 100644 index 0000000..b2ccc6f --- /dev/null +++ b/platform/i18n/src/index.js @@ -0,0 +1,152 @@ +import i18n from 'i18next'; +import Backend from 'i18next-locize-backend'; +import LastUsed from 'locize-lastused'; +import Editor from 'locize-editor'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import { initReactI18next } from 'react-i18next'; +import customDebug from './debugger'; +import pkg from '../package.json'; +import { debugMode, detectionOptions } from './config'; +import { getLanguageLabel, getAvailableLanguagesInfo } from './utils.js'; + +// Note: The index.js files inside src/locales are dynamically generated +// by the pullTranslations.sh script +import locales from './locales'; + +function addLocales(newLocales) { + customDebug(`Adding locales ${newLocales}`, 'info'); + + let resourceBundle = []; + + Object.keys(newLocales).map(key => { + Object.keys(newLocales[key]).map(namespace => { + const locale = newLocales[key][namespace]; + resourceBundle.push({ key, namespace, locale }); + i18n.addResourceBundle(key, namespace, locale, true, true); + }); + }); + + customDebug(`Locales added successfully`, 'info'); + customDebug(resourceBundle, 'info'); +} + +/* + * Note: Developers can add the API key to use the + * in-context editor using environment variables. + * (DO NOT commit the API key) + */ +const locizeOptions = { + projectId: process.env.LOCIZE_PROJECTID, + apiKey: process.env.LOCIZE_API_KEY, + referenceLng: 'en-US', + fallbacklng: 'en-US', +}; + +const envUseLocize = !!process.env.USE_LOCIZE; +const envApiKeyAvailable = !!process.env.LOCIZE_API_KEY; +const DEFAULT_LANGUAGE = 'en-US'; + +function initI18n( + detection = detectionOptions, + useLocize = envUseLocize, + apiKeyAvailable = envApiKeyAvailable +) { + let initialized; + + if (useLocize) { + customDebug(`Using Locize for translation files`, 'info'); + initialized = i18n + // i18next-locize-backend + // loads translations from your project, saves new keys to it (saveMissing: true) + // https://github.com/locize/i18next-locize-backend + .use(Backend) + // locize-lastused + // sets a timestamp of last access on every translation segment on locize + // -> safely remove the ones not being touched for weeks/months + // https://github.com/locize/locize-lastused + .use(LastUsed) + // locize-editor + // InContext Editor of locize ?locize=true to show it + // https://github.com/locize/locize-editor + .use(Editor) + // detect user language + // learn more: https://github.com/i18next/i18next-browser-languageDetector + .use(LanguageDetector) + // pass the i18n instance to react-i18next. + .use(initReactI18next) + // init i18next + // for all options read: https://www.i18next.com/overview/configuration-options + .init({ + fallbackLng: DEFAULT_LANGUAGE, + saveMissing: apiKeyAvailable, + debug: debugMode, + keySeparator: false, + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + }, + detection, + backend: locizeOptions, + locizeLastUsed: locizeOptions, + editor: { + ...locizeOptions, + onEditorSaved: async (lng, ns) => { + // reload that namespace in given language + await i18n.reloadResources(lng, ns); + // trigger an event on i18n which triggers a rerender + // based on bindI18n below in react options + i18n.emit('editorSaved'); + }, + }, + react: { + useSuspense: false, // TODO: Was seeing weird errors without this + wait: true, + bindI18n: 'languageChanged editorSaved', + }, + }); + } else { + customDebug(`Using local translation files`, 'info'); + initialized = i18n + // detect user language + // learn more: https://github.com/i18next/i18next-browser-languageDetector + .use(LanguageDetector) + // pass the i18n instance to react-i18next. + .use(initReactI18next) + // init i18next + // for all options read: https://www.i18next.com/overview/configuration-options + .init({ + fallbackLng: DEFAULT_LANGUAGE, + resources: locales, + debug: debugMode, + keySeparator: false, + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + }, + detection, + react: { + wait: true, + }, + }); + } + + return initialized.then(function (t) { + i18n.T = t; + customDebug(`T function available.`, 'info'); + }); +} + +customDebug(`version ${pkg.version} loaded.`, 'info'); + +i18n.initializing = initI18n(); +i18n.initI18n = initI18n; +i18n.addLocales = addLocales; +i18n.availableLanguages = getAvailableLanguagesInfo(locales); +i18n.defaultLanguage = { + label: getLanguageLabel(DEFAULT_LANGUAGE), + value: DEFAULT_LANGUAGE, +}; +i18n.currentLanguage = () => ({ + label: getLanguageLabel(i18n.language), + value: i18n.language, +}); + +export default i18n; diff --git a/platform/i18n/src/locales/ar/UserPreferencesModal.json b/platform/i18n/src/locales/ar/UserPreferencesModal.json new file mode 100644 index 0000000..86db274 --- /dev/null +++ b/platform/i18n/src/locales/ar/UserPreferencesModal.json @@ -0,0 +1,3 @@ +{ + "No hotkeys found": "Nenhuma tecla de atalho estรก configurada para este aplicativo. As teclas de atalho podem ser configuradas no arquivo app-config.js do aplicativo." +} diff --git a/platform/i18n/src/locales/ar/index.js b/platform/i18n/src/locales/ar/index.js new file mode 100644 index 0000000..2d37bdc --- /dev/null +++ b/platform/i18n/src/locales/ar/index.js @@ -0,0 +1,7 @@ +import UserPreferencesModal from './UserPreferencesModal.json'; + +export default { + ar: { + UserPreferencesModal, + }, +}; diff --git a/platform/i18n/src/locales/de/AboutModal.json b/platform/i18n/src/locales/de/AboutModal.json new file mode 100644 index 0000000..c64654d --- /dev/null +++ b/platform/i18n/src/locales/de/AboutModal.json @@ -0,0 +1,14 @@ +{ + "About OHIF Viewer": "รœber OHIF Viewer", + "Browser": "Browser", + "Build number": "Build-Nummer", + "Last master commits": "Letzter Master Commit", + "More details": "Mehr Details", + "Name": "Name", + "OS": "OS", + "Report an issue": "Ein Problem melden", + "Repository URL": "Repository URL", + "Value": "Wert", + "Version information": "Informationen zur Version", + "Visit the forum": "Besuchen Sie das Forum" +} diff --git a/platform/i18n/src/locales/de/Buttons.json b/platform/i18n/src/locales/de/Buttons.json new file mode 100644 index 0000000..9adac93 --- /dev/null +++ b/platform/i18n/src/locales/de/Buttons.json @@ -0,0 +1,50 @@ +{ + "Acquired": "Akquiriert", + "Angle": "Winkel", + "Axial": "Axial", + "Bidirectional": "Bidirektional", + "Brush": "Pinsel", + "CINE": "CINE", + "Cancel": "Abbrechen", + "Circle": "Kreis", + "Clear": "Leeren", + "Coronal": "Koronal", + "Crosshairs": "Fadenkreuz", + "Download": "Download", + "Ellipse": "Ellipse", + "Elliptical": "Elliptisch", + "Flip Horizontally": "Horizontal spiegeln", + "Flip Vertically": "Vertikal spiegeln", + "Freehand": "Freihand", + "Invert": "Invertieren", + "Invert Colors": "Invertieren", + "Layout": "$t(Common:Layout)", + "Grid Layout": "Rasterlayout", + "Length": "Lรคnge", + "Levels": "Level", + "Window Level": "Helligkeit/Kontrast", + "Magnify": "Vergrรถssern", + "Manual": "Manuell", + "Measurements": "Messungen", + "More": "$t(Common:More)", + "Next": "$t(Common:Next)", + "Pan": "Schwenken", + "Play": "$t(Common:Play)", + "Previous": "$t(Common:Previous)", + "Probe": "Probe", + "ROI Window": "ROI Fenster", + "Rectangle": "Rechteck", + "Reset": "$t(Common:Reset)", + "Reset View": "$t(Common:Reset)", + "Reset to defaults": "Auf Default zurรผcksetzen", + "Rotate Right": "Nach rechts drehen", + "Rotate +90": "Drehen +90", + "Sagittal": "Sagittal", + "Save": "Speichern", + "Stack Scroll": "Stack Scroll", + "Stop": "$t(Common:Stop)", + "Themes": "Themen", + "Zoom": "Zoomen", + "More Tools": "Weitere Werkzeuge", + "More Measure Tools": "Weitere Messwerkzeuge" +} diff --git a/platform/i18n/src/locales/de/CineDialog.json b/platform/i18n/src/locales/de/CineDialog.json new file mode 100644 index 0000000..fe1554e --- /dev/null +++ b/platform/i18n/src/locales/de/CineDialog.json @@ -0,0 +1,8 @@ +{ + "Next image": "Nรคchstes Bild", + "Play / Stop": "$t(Common:Play) / $t(Common:Stop)", + "Previous image": "Vorheriges Bild", + "Skip to first image": "Zum ersten Bild springen", + "Skip to last image": "Zum letzten Bild springen", + "fps": "fps" +} diff --git a/platform/i18n/src/locales/de/Common.json b/platform/i18n/src/locales/de/Common.json new file mode 100644 index 0000000..4e4423c --- /dev/null +++ b/platform/i18n/src/locales/de/Common.json @@ -0,0 +1,16 @@ +{ + "Close": "Schliessen", + "Image": "Bild", + "Layout": "Layout", + "Measurements": "Messungen", + "More": "Mehr", + "Next": "Nรคchste", + "Play": "Abspielen", + "Previous": "Vorherige", + "Reset": "Zurรผcksetzen", + "RowsPerPage": "Zeilen pro Seite", + "Series": "Serien", + "Show": "Anzeigen", + "Stop": "Stoppen", + "StudyDate": "Studiendatum" +} diff --git a/platform/i18n/src/locales/de/DatePicker.json b/platform/i18n/src/locales/de/DatePicker.json new file mode 100644 index 0000000..424da8f --- /dev/null +++ b/platform/i18n/src/locales/de/DatePicker.json @@ -0,0 +1,5 @@ +{ + "Clear dates": "Daten lรถschen", + "End Date": "Enddatum", + "Start Date": "Startdatum" +} diff --git a/platform/i18n/src/locales/de/Header.json b/platform/i18n/src/locales/de/Header.json new file mode 100644 index 0000000..4eff584 --- /dev/null +++ b/platform/i18n/src/locales/de/Header.json @@ -0,0 +1,8 @@ +{ + "About": "รœber", + "Back to Viewer": "Zurรผck zum Viewer", + "INVESTIGATIONAL USE ONLY": "NUR FรœR FORSCHUNGSZWECKE", + "Options": "Optionen", + "Preferences": "Einstellungen", + "Study list": "Studienliste" +} diff --git a/platform/i18n/src/locales/de/MeasurementTable.json b/platform/i18n/src/locales/de/MeasurementTable.json new file mode 100644 index 0000000..0677709 --- /dev/null +++ b/platform/i18n/src/locales/de/MeasurementTable.json @@ -0,0 +1,9 @@ +{ + "Criteria nonconformities": "Kriterien fรผr Nichtkonformitรคten", + "Delete": "Lรถschen", + "Description": "Beschreibung", + "MAX": "MAX", + "NonTargets": "NonTargets", + "Relabel": "Relabel", + "Targets": "Targets" +} diff --git a/platform/i18n/src/locales/de/StudyList.json b/platform/i18n/src/locales/de/StudyList.json new file mode 100644 index 0000000..0759eb8 --- /dev/null +++ b/platform/i18n/src/locales/de/StudyList.json @@ -0,0 +1,13 @@ +{ + "AccessionNumber": "Eingangsnummer", + "Accession": "Eingangsnummer", + "Empty": "Leer", + "MRN": "MRN", + "Modality": "Modalitรคt", + "Patient Name": "Patientenname", + "Study date": "Studiendatum", + "Description": "Beschreibung", + "Study list": "Studienliste", + "Instances": "Instanzen", + "Number of studies": "Studien" +} diff --git a/platform/i18n/src/locales/de/UserPreferencesModal.json b/platform/i18n/src/locales/de/UserPreferencesModal.json new file mode 100644 index 0000000..04790fd --- /dev/null +++ b/platform/i18n/src/locales/de/UserPreferencesModal.json @@ -0,0 +1,11 @@ +{ + "Cancel": "$t(Buttons:Cancel)", + "No hotkeys found": "Keine Hotkeys gefunden.", + "Reset to defaults": "$t(Buttons:Reset to defaults)", + "ResetDefaultMessage": "Einstellungen zurรผckgesetzt. Bitte speichern.", + "Save": "$t(Buttons:Save)", + "SaveMessage": "Gespeichert", + "User preferences": "Benutzereinstellungen", + "Language": "Sprache", + "General": "Allgemein" +} diff --git a/platform/i18n/src/locales/de/ViewportDownloadForm.json b/platform/i18n/src/locales/de/ViewportDownloadForm.json new file mode 100644 index 0000000..cceb825 --- /dev/null +++ b/platform/i18n/src/locales/de/ViewportDownloadForm.json @@ -0,0 +1,14 @@ +{ + "emptyFilenameError": "Der Dateiname darf nicht leer sein.", + "fileType": "Dateityp", + "filename": "Dateiname", + "formTitle": "Bitte geben Sie die Grรถsse, den Dateinamen und den gewรผnschten Typ fรผr das Bild an.", + "imageHeight": "Hรถhe (px)", + "imagePreview": "Vorschau", + "imageWidth": "Breite (px)", + "keepAspectRatio": "Seitenverhรคltnis beibehalten", + "loadingPreview": "Vorschau laden...", + "minHeightError": "Die Mindesthรถhe betrรคgt 100px.", + "minWidthError": "Die Mindestbreite betrรคgt 100px.", + "showAnnotations": "Annotationen anzeigen" +} diff --git a/platform/i18n/src/locales/de/index.js b/platform/i18n/src/locales/de/index.js new file mode 100644 index 0000000..a265fa4 --- /dev/null +++ b/platform/i18n/src/locales/de/index.js @@ -0,0 +1,25 @@ +import AboutModal from './AboutModal.json'; +import Buttons from './Buttons.json'; +import CineDialog from './CineDialog.json'; +import Common from './Common.json'; +import DatePicker from './DatePicker.json'; +import Header from './Header.json'; +import MeasurementTable from './MeasurementTable.json'; +import StudyList from './StudyList.json'; +import UserPreferencesModal from './UserPreferencesModal.json'; +import ViewportDownloadForm from './ViewportDownloadForm.json'; + +export default { + de: { + AboutModal, + Buttons, + CineDialog, + Common, + DatePicker, + Header, + MeasurementTable, + StudyList, + UserPreferencesModal, + ViewportDownloadForm, + }, +}; diff --git a/platform/i18n/src/locales/en-US/AboutModal.json b/platform/i18n/src/locales/en-US/AboutModal.json new file mode 100644 index 0000000..aa00fc0 --- /dev/null +++ b/platform/i18n/src/locales/en-US/AboutModal.json @@ -0,0 +1,18 @@ +{ + "About OHIF Viewer": "About OHIF Viewer", + "Browser": "Browser", + "Build number": "Build Number", + "Commit hash": "Commit hash", + "Data citation": "Data citation", + "Important links": "Important links", + "Last master commits": "Latest Master Commits", + "More details": "More details", + "Name": "Name", + "OS": "OS", + "Report an issue": "Report an issue", + "Repository URL": "Repository URL", + "Value": "Value", + "Version information": "Version Information", + "Version number": "Version number", + "Visit the forum": "Visit the forum" +} diff --git a/platform/i18n/src/locales/en-US/Buttons.json b/platform/i18n/src/locales/en-US/Buttons.json new file mode 100644 index 0000000..4a2d259 --- /dev/null +++ b/platform/i18n/src/locales/en-US/Buttons.json @@ -0,0 +1,51 @@ +{ + "Acquired": "Acquired", + "Angle": "Angle", + "Annotation": "Annotation", + "Axial": "Axial", + "Bidirectional": "Bidirectional", + "Brush": "Brush", + "Cine": "Cine", + "CINE": "CINE", + "Cancel": "Cancel", + "Capture": "Capture", + "Circle": "Circle", + "Clear": "Clear", + "Coronal": "Coronal", + "Crosshairs": "Crosshairs", + "Download": "Download", + "Ellipse": "Ellipse", + "Elliptical": "Elliptical", + "Flip H": "Flip H", + "Flip Horizontally": "Flip Horizontally", + "Flip V": "Flip V", + "Freehand": "Freehand", + "Grid Layout": "Grid Layout", + "Invert": "Invert", + "Layout": "$t(Common:Layout)", + "Length": "Length", + "Levels": "Levels", + "Magnify": "Magnify", + "Manual": "Manual", + "Measurements": "Measurements", + "More": "$t(Common:More)", + "More Tools": "More Tools", + "Next": "$t(Common:Next)", + "Pan": "Pan", + "Play": "$t(Common:Play)", + "Previous": "$t(Common:Previous)", + "Probe": "Probe", + "ROI Window": "ROI Window", + "Rectangle": "Rectangle", + "Reference Lines": "Reference Lines", + "Reset": "$t(Common:Reset)", + "Reset to defaults": "$t(Common:Reset) to Defaults", + "Rotate Right": "Rotate Right", + "Sagittal": "Sagittal", + "Save": "Save", + "Stack Scroll": "Stack Scroll", + "Stack Image Sync": "Stack Image Sync", + "Stop": "$t(Common:Stop)", + "Themes": "Themes", + "Zoom": "Zoom" +} diff --git a/platform/i18n/src/locales/en-US/CineDialog.json b/platform/i18n/src/locales/en-US/CineDialog.json new file mode 100644 index 0000000..4839826 --- /dev/null +++ b/platform/i18n/src/locales/en-US/CineDialog.json @@ -0,0 +1,8 @@ +{ + "Next image": "$t(Common:Next) $t(Common:Image)", + "Play / Stop": "$t(Common:Play) / $t(Common:Stop)", + "Previous image": "$t(Common:Previous) $t(Common:Image)", + "Skip to first image": "Skip to first $t(Common:Image)", + "Skip to last image": "Skip to last $t(Common:Image)", + "fps": "fps" +} diff --git a/platform/i18n/src/locales/en-US/Common.json b/platform/i18n/src/locales/en-US/Common.json new file mode 100644 index 0000000..a0b5ce3 --- /dev/null +++ b/platform/i18n/src/locales/en-US/Common.json @@ -0,0 +1,22 @@ +{ + "Back to": "Back to {{location}}", + "Close": "Close", + "Image": "Image", + "Layout": "Layout", + "LOAD": "LOAD", + "Measurements": "Measurements", + "mm": "mm", + "More": "More", + "Next": "Next", + "No": "No", + "NoStudyDate": "No Study Date", + "Play": "Play", + "Previous": "Previous", + "Reset": "Reset", + "RowsPerPage": "rows per page", + "Series": "Series", + "Show": "Show", + "Stop": "Stop", + "StudyDate": "Study Date", + "Yes": "Yes" +} diff --git a/platform/i18n/src/locales/en-US/DataSourceConfiguration.json b/platform/i18n/src/locales/en-US/DataSourceConfiguration.json new file mode 100644 index 0000000..092ea55 --- /dev/null +++ b/platform/i18n/src/locales/en-US/DataSourceConfiguration.json @@ -0,0 +1,24 @@ +{ + "Configure Data Source": "Configure Data Source", + "Data set": "Data set", + "DICOM store": "DICOM store", + "Location": "Location", + "Project": "Project", + "Error fetching Data set list": "Error fetching data sets", + "Error fetching DICOM store list": "Error fetching DICOM stores", + "Error fetching Location list": "Error fetching locations", + "Error fetching Project list": "Error fetching projects", + "No Project available": "No projects available", + "No Location available": "No locations available", + "No Data set available": "No data sets available", + "No DICOM store available": "No DICOM stores available", + "Select": "Select", + "Search Data set list": "Search data sets", + "Search DICOM store list": "Search DICOM stores", + "Search Location list": "Search locations", + "Search Project list": "Search projects", + "Select Data set": "Select a data Set", + "Select DICOM store": "Select a DICOM store", + "Select Location": "Select a location", + "Select Project": "Select a project" +} diff --git a/platform/i18n/src/locales/en-US/DatePicker.json b/platform/i18n/src/locales/en-US/DatePicker.json new file mode 100644 index 0000000..3add53f --- /dev/null +++ b/platform/i18n/src/locales/en-US/DatePicker.json @@ -0,0 +1,9 @@ +{ + "Clear dates": "Clear dates", + "Close": "$t(Common:Close)", + "End Date": "End Date", + "Last 7 days": "Last 7 days", + "Last 30 days": "Last 30 days", + "Start Date": "Start Date", + "Today": "Today" +} diff --git a/platform/i18n/src/locales/en-US/ErrorBoundary.json b/platform/i18n/src/locales/en-US/ErrorBoundary.json new file mode 100644 index 0000000..dcefb50 --- /dev/null +++ b/platform/i18n/src/locales/en-US/ErrorBoundary.json @@ -0,0 +1,8 @@ +{ + "Context": "Context", + "Error Message": "Error Message", + "Something went wrong": "Something went wrong", + "in": "in", + "Sorry, something went wrong there. Try again.": "Sorry, something went wrong there. Try again.", + "Stack Trace": "Stack Trace" +} diff --git a/platform/i18n/src/locales/en-US/Header.json b/platform/i18n/src/locales/en-US/Header.json new file mode 100644 index 0000000..e3210b3 --- /dev/null +++ b/platform/i18n/src/locales/en-US/Header.json @@ -0,0 +1,9 @@ +{ + "About": "About", + "Back to Viewer": "Back to Viewer", + "INVESTIGATIONAL USE ONLY": "INVESTIGATIONAL USE ONLY", + "Options": "Options", + "Preferences": "Preferences", + "Study list": "Study list", + "Logout": "Logout" +} diff --git a/platform/i18n/src/locales/en-US/HotkeysValidators.json b/platform/i18n/src/locales/en-US/HotkeysValidators.json new file mode 100644 index 0000000..74665c1 --- /dev/null +++ b/platform/i18n/src/locales/en-US/HotkeysValidators.json @@ -0,0 +1,6 @@ +{ + "Field can't be empty": "Field can't be empty", + "Hotkey is already in use": "\"{{action}}\" is already using the \"{{pressedKeys}}\" shortcut.", + "It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut": "It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut", + "Shortcut combination is not allowed": "{{pressedKeys}} shortcut combination is not allowed" +} diff --git a/platform/i18n/src/locales/en-US/MeasurementTable.json b/platform/i18n/src/locales/en-US/MeasurementTable.json new file mode 100644 index 0000000..008a8c8 --- /dev/null +++ b/platform/i18n/src/locales/en-US/MeasurementTable.json @@ -0,0 +1,12 @@ +{ + "Criteria nonconformities": "Criteria nonconformities", + "Delete": "Delete", + "Description": "Description", + "MAX": "MAX", + "Measurements": "Measurements", + "No, do not ask again": "No, do not ask again", + "NonTargets": "NonTargets", + "Relabel": "Relabel", + "Targets": "Targets", + "Track measurements for this series?": "Track measurements for this series?" +} diff --git a/platform/i18n/src/locales/en-US/Messages.json b/platform/i18n/src/locales/en-US/Messages.json new file mode 100644 index 0000000..931bd93 --- /dev/null +++ b/platform/i18n/src/locales/en-US/Messages.json @@ -0,0 +1,16 @@ +{ + "Display Set Messages": "Display Set Messages", + "1": "No valid instances found in series.", + "2": "Display set has missing position information.", + "3": "Display set is not a reconstructable 3D volume.", + "4": "Multi frame display sets do not have pixel measurement information.", + "5": "Multi frame display sets do not have orientation information.", + "6": "Multi frame display sets do not have position information.", + "7": "Display set has missing frames.", + "8": "Display set has irregular spacing.", + "9": "Display set has inconsistent dimensions between frames.", + "10": "Display set has frames with inconsistent number of components.", + "11": "Display set has frames with inconsistent orientations.", + "12": "Display set has inconsistent position information.", + "13": "Unsupported display set." +} diff --git a/platform/i18n/src/locales/en-US/Modes.json b/platform/i18n/src/locales/en-US/Modes.json new file mode 100644 index 0000000..813dfe9 --- /dev/null +++ b/platform/i18n/src/locales/en-US/Modes.json @@ -0,0 +1,8 @@ +{ + "Basic Dev Viewer": "Basic Dev Viewer", + "Basic Test Mode": "Basic Test Mode", + "Basic Viewer": "Basic Viewer", + "Microscopy": "Microscopy", + "Segmentation": "Segmentation", + "Total Metabolic Tumor Volume": "Total Metabolic Tumor Volume" +} diff --git a/platform/i18n/src/locales/en-US/SegmentationTable.json b/platform/i18n/src/locales/en-US/SegmentationTable.json new file mode 100644 index 0000000..8d73016 --- /dev/null +++ b/platform/i18n/src/locales/en-US/SegmentationTable.json @@ -0,0 +1,18 @@ +{ + "Active": "Active", + "Add new segmentation": "Add new segmentation", + "Add segment": "Add segment", + "Add segmentation": "Add segmentation", + "Delete": "Delete", + "Display inactive segmentations": "Display inactive segmentations", + "Export DICOM SEG": "Export DICOM SEG", + "Download DICOM SEG": "Download DICOM SEG", + "Download DICOM RTSTRUCT": "Download DICOM RTSTRUCT", + "Fill": "Fill", + "Inactive segmentations": "Inactive segmentations", + "Opacity": "Opacity", + "Outline": "Outline", + "Rename": "Rename", + "Segmentation": "Segmentation", + "Size": "Size" +} diff --git a/platform/i18n/src/locales/en-US/SidePanel.json b/platform/i18n/src/locales/en-US/SidePanel.json new file mode 100644 index 0000000..e468fb5 --- /dev/null +++ b/platform/i18n/src/locales/en-US/SidePanel.json @@ -0,0 +1,4 @@ +{ + "Measurements": "Measurements", + "Studies": "Studies" +} diff --git a/platform/i18n/src/locales/en-US/StudyBrowser.json b/platform/i18n/src/locales/en-US/StudyBrowser.json new file mode 100644 index 0000000..b12c087 --- /dev/null +++ b/platform/i18n/src/locales/en-US/StudyBrowser.json @@ -0,0 +1,5 @@ +{ + "Primary": "Primary", + "Recent": "Recent", + "All": "All" +} diff --git a/platform/i18n/src/locales/en-US/StudyItem.json b/platform/i18n/src/locales/en-US/StudyItem.json new file mode 100644 index 0000000..d689137 --- /dev/null +++ b/platform/i18n/src/locales/en-US/StudyItem.json @@ -0,0 +1,3 @@ +{ + "Tracked series": "{{trackedSeries}} Tracked series" +} diff --git a/platform/i18n/src/locales/en-US/StudyList.json b/platform/i18n/src/locales/en-US/StudyList.json new file mode 100644 index 0000000..c64afdb --- /dev/null +++ b/platform/i18n/src/locales/en-US/StudyList.json @@ -0,0 +1,20 @@ +{ + "AccessionNumber": "Accession #", + "ClearFilters": "Clear Filters", + "Description": "Description", + "Empty": "Empty", + "Filter list to 100 studies or less to enable sorting": "Filter the list to 100 studies or less to enable sorting", + "Instances": "Instances", + "Modality": "Modality", + "MRN": "MRN", + "Next": "Next >", + "No studies available": "No studies available", + "Number of studies": "Number of studies", + "Page": "Page", + "PatientName": "Patient Name", + "Previous": "< Back", + "Results per page": "Results per page", + "StudyDate": "Study Date", + "StudyList": "Study List", + "Upload": "Upload" +} diff --git a/platform/i18n/src/locales/en-US/ThumbnailTracked.json b/platform/i18n/src/locales/en-US/ThumbnailTracked.json new file mode 100644 index 0000000..0fef346 --- /dev/null +++ b/platform/i18n/src/locales/en-US/ThumbnailTracked.json @@ -0,0 +1,5 @@ +{ + "Series is tracked": "Series is tracked", + "Series is untracked": "Series is untracked", + "Viewport": "Viewport" +} diff --git a/platform/i18n/src/locales/en-US/TooltipClipboard.json b/platform/i18n/src/locales/en-US/TooltipClipboard.json new file mode 100644 index 0000000..0d85da6 --- /dev/null +++ b/platform/i18n/src/locales/en-US/TooltipClipboard.json @@ -0,0 +1,4 @@ +{ + "Copied": "Copied", + "Failed to copy": "Failed to copy" +} diff --git a/platform/i18n/src/locales/en-US/TrackedCornerstoneViewport.json b/platform/i18n/src/locales/en-US/TrackedCornerstoneViewport.json new file mode 100644 index 0000000..6da525f --- /dev/null +++ b/platform/i18n/src/locales/en-US/TrackedCornerstoneViewport.json @@ -0,0 +1,6 @@ +{ + "Series is tracked and can be viewed in the measurement panel": + "Series is tracked and can be viewed in the measurement panel", + "Measurements for untracked series will not be shown in the measurements panel": + "Measurements for untracked series will not be shown in the measurements panel" +} diff --git a/platform/i18n/src/locales/en-US/UserPreferencesModal.json b/platform/i18n/src/locales/en-US/UserPreferencesModal.json new file mode 100644 index 0000000..5e73354 --- /dev/null +++ b/platform/i18n/src/locales/en-US/UserPreferencesModal.json @@ -0,0 +1,9 @@ +{ + "Cancel": "$t(Buttons:Cancel)", + "No hotkeys found": "No hotkeys are configured for this application. Hotkeys can be configured in the application's app-config.js file.", + "Reset to defaults": "$t(Buttons:Reset to defaults)", + "ResetDefaultMessage": "Preferences successfully reset to default.
You must Save to perform this action.", + "Save": "$t(Buttons:Save)", + "SaveMessage": "Preferences saved", + "User preferences": "User Preferences" +} diff --git a/platform/i18n/src/locales/en-US/ViewportDownloadForm.json b/platform/i18n/src/locales/en-US/ViewportDownloadForm.json new file mode 100644 index 0000000..85001a4 --- /dev/null +++ b/platform/i18n/src/locales/en-US/ViewportDownloadForm.json @@ -0,0 +1,14 @@ +{ + "emptyFilenameError": "The file name cannot be empty.", + "fileType": "File Type", + "filename": "File Name", + "formTitle": "Please specify the dimensions, filename, and desired type for the output image.", + "imageHeight": "Image height (px)", + "imagePreview": "Image Preview", + "imageWidth": "Image width (px)", + "keepAspectRatio": "Keep aspect ratio", + "loadingPreview": "Loading Image Preview...", + "minHeightError": "The minimum valid height is 100px.", + "minWidthError": "The minimum valid width is 100px.", + "showAnnotations": "Show Annotations" +} diff --git a/platform/i18n/src/locales/en-US/WindowLevelActionMenu.json b/platform/i18n/src/locales/en-US/WindowLevelActionMenu.json new file mode 100644 index 0000000..9eba89b --- /dev/null +++ b/platform/i18n/src/locales/en-US/WindowLevelActionMenu.json @@ -0,0 +1,5 @@ +{ + "Back to Display Options": "Back to Display Options", + "Modality Presets": "{{modality}} Presets", + "Modality Window Presets": "{{modality}} Window Presets" +} diff --git a/platform/i18n/src/locales/en-US/index.js b/platform/i18n/src/locales/en-US/index.js new file mode 100644 index 0000000..6004ac6 --- /dev/null +++ b/platform/i18n/src/locales/en-US/index.js @@ -0,0 +1,51 @@ +import AboutModal from './AboutModal.json'; +import Buttons from './Buttons.json'; +import CineDialog from './CineDialog.json'; +import Common from './Common.json'; +import DataSourceConfiguration from './DataSourceConfiguration.json'; +import DatePicker from './DatePicker.json'; +import ErrorBoundary from './ErrorBoundary.json'; +import Header from './Header.json'; +import HotkeysValidators from './HotkeysValidators.json'; +import MeasurementTable from './MeasurementTable.json'; +import Modes from './Modes.json'; +import SegmentationTable from './SegmentationTable.json'; +import SidePanel from './SidePanel.json'; +import StudyBrowser from './StudyBrowser.json'; +import StudyItem from './StudyItem.json'; +import StudyList from './StudyList.json'; +import TooltipClipboard from './TooltipClipboard.json'; +import ThumbnailTracked from './ThumbnailTracked.json'; +import TrackedCornerstoneViewport from './TrackedCornerstoneViewport.json'; +import UserPreferencesModal from './UserPreferencesModal.json'; +import ViewportDownloadForm from './ViewportDownloadForm.json'; +import Messages from './Messages.json'; +import WindowLevelActionMenu from './WindowLevelActionMenu.json'; + +export default { + 'en-US': { + AboutModal, + Buttons, + CineDialog, + Common, + DataSourceConfiguration, + DatePicker, + ErrorBoundary, + Header, + HotkeysValidators, + MeasurementTable, + Modes, + SegmentationTable, + SidePanel, + StudyBrowser, + StudyItem, + StudyList, + TooltipClipboard, + ThumbnailTracked, + TrackedCornerstoneViewport, + UserPreferencesModal, + ViewportDownloadForm, + Messages, + WindowLevelActionMenu, + }, +}; diff --git a/platform/i18n/src/locales/es/AboutModal.json b/platform/i18n/src/locales/es/AboutModal.json new file mode 100644 index 0000000..22153c9 --- /dev/null +++ b/platform/i18n/src/locales/es/AboutModal.json @@ -0,0 +1,14 @@ +{ + "About OHIF Viewer": "Sobre OHIF Viewer", + "Browser": "Navegador", + "Build number": "Nรบmero de compilaciรณn", + "Last master commits": "รšltimos Master Commits", + "More details": "Mรกs detalles", + "Name": "Nombre", + "OS": "SO", + "Report an issue": "Informar un problema", + "Repository URL": "URL del repositorio", + "Value": "Valor", + "Version information": "Informaciรณn de la versiรณn", + "Visit the forum": "Visita el foro" +} diff --git a/platform/i18n/src/locales/es/Buttons.json b/platform/i18n/src/locales/es/Buttons.json new file mode 100644 index 0000000..ce1a1a9 --- /dev/null +++ b/platform/i18n/src/locales/es/Buttons.json @@ -0,0 +1,43 @@ +{ + "Acquired": "Adquirido", + "Angle": "รngulo", + "Axial": "Axial", + "Bidirectional": "Bidireccional", + "Brush": "Cepillo", + "CINE": "CINE", + "Cancel": "Cancelar", + "Circle": "Cรญrculo", + "Clear": "Limpiar", + "Coronal": "Coronal", + "Crosshairs": "Punto de mira", + "Download": "Descargar", + "Ellipse": "Elipse", + "Elliptical": "Elรญptico", + "Flip H": "Voltear H", + "Flip V": "Voltear V", + "Freehand": "Mano alzada", + "Invert": "Negativo", + "Layout": "$t(Common:Layout)", + "Length": "Longitud", + "Levels": "W/L", + "Magnify": "Lupa", + "Manual": "Manual", + "Measurements": "Medidas", + "More": "$t(Common:More)", + "Next": "$t(Common:Next)", + "Pan": "Mover", + "Play": "$t(Common:Play)", + "Previous": "$t(Common:Previous)", + "Probe": "Probar", + "ROI Window": "Ventana ROI", + "Rectangle": "Rectรกngulo", + "Reset": "$t(Common:Reset)", + "Reset to defaults": "$t(Common:Reset) por defecto", + "Rotate Right": "Girar ->", + "Sagittal": "Sagital", + "Save": "Guardar", + "Stack Scroll": "Scroll", + "Stop": "$t(Common:Stop)", + "Themes": "Temas", + "Zoom": "Ampliar" +} diff --git a/platform/i18n/src/locales/es/CineDialog.json b/platform/i18n/src/locales/es/CineDialog.json new file mode 100644 index 0000000..a9cbf4f --- /dev/null +++ b/platform/i18n/src/locales/es/CineDialog.json @@ -0,0 +1,8 @@ +{ + "Next image": "$t(Common:Image) $t(Common:Next)", + "Play / Stop": "$t(Common:Play) / Stop", + "Previous image": "$t(Common:Image) $t(Common:Previous)", + "Skip to first image": "Ir a la primera $t(Common:Image)", + "Skip to last image": "Ir a la รบltima $t(Common:Image)", + "fps": "imรกgenes/seg." +} diff --git a/platform/i18n/src/locales/es/Common.json b/platform/i18n/src/locales/es/Common.json new file mode 100644 index 0000000..65481c8 --- /dev/null +++ b/platform/i18n/src/locales/es/Common.json @@ -0,0 +1,15 @@ +{ + "Image": "Imagen", + "Layout": "Formato", + "Measurements": "Medidas", + "More": "Mรกs", + "Next": "Siguiente", + "Play": "Play", + "Previous": "Anterior", + "Reset": "Restaurar", + "RowsPerPage": "filas por pรกgina", + "Series": "Secuencia", + "Show": "Mostrar", + "Stop": "Detener", + "StudyDate": "Fecha de estudo" +} diff --git a/platform/i18n/src/locales/es/DatePicker.json b/platform/i18n/src/locales/es/DatePicker.json new file mode 100644 index 0000000..2c89998 --- /dev/null +++ b/platform/i18n/src/locales/es/DatePicker.json @@ -0,0 +1,5 @@ +{ + "Clear dates": "Borrar fechas", + "End Date": "Fecha fin", + "Start Date": "Fecha inicio" +} diff --git a/platform/i18n/src/locales/es/Header.json b/platform/i18n/src/locales/es/Header.json new file mode 100644 index 0000000..8a4308c --- /dev/null +++ b/platform/i18n/src/locales/es/Header.json @@ -0,0 +1,8 @@ +{ + "About": "Acerca de", + "Back to Viewer": "Volver al visor", + "INVESTIGATIONAL USE ONLY": "SOLO USO PARA INVESTIGACIร“N", + "Options": "Opciones", + "Preferences": "Preferencias", + "Study list": "Lista de estudios" +} diff --git a/platform/i18n/src/locales/es/MeasurementTable.json b/platform/i18n/src/locales/es/MeasurementTable.json new file mode 100644 index 0000000..a67bada --- /dev/null +++ b/platform/i18n/src/locales/es/MeasurementTable.json @@ -0,0 +1,11 @@ +{ + "Criteria nonconformities": "Criterios disconformes", + "Delete": "Borrar", + "Description": "Descripciรณn", + "MAX": "Mรกximo", + "NonTargets": "No objetivos", + "Relabel": "Re-etiquetar", + "Targets": "Objetivos", + "Export": "Exportar", + "Create Report": "Crear reporte" +} diff --git a/platform/i18n/src/locales/es/SidePanel.json b/platform/i18n/src/locales/es/SidePanel.json new file mode 100644 index 0000000..5581ad1 --- /dev/null +++ b/platform/i18n/src/locales/es/SidePanel.json @@ -0,0 +1,4 @@ +{ + "Measurements": "Mediciones", + "Studies": "Estudios" +} diff --git a/platform/i18n/src/locales/es/StudyBrowser.json b/platform/i18n/src/locales/es/StudyBrowser.json new file mode 100644 index 0000000..72f6def --- /dev/null +++ b/platform/i18n/src/locales/es/StudyBrowser.json @@ -0,0 +1,6 @@ +{ + "Primary": "Primario", + "Recent": "Reciente", + "All": "Todos", + "Studies": "Estudios" +} diff --git a/platform/i18n/src/locales/es/StudyList.json b/platform/i18n/src/locales/es/StudyList.json new file mode 100644 index 0000000..8ff58c1 --- /dev/null +++ b/platform/i18n/src/locales/es/StudyList.json @@ -0,0 +1,18 @@ +{ + "AccessionNumber": "Num. Adhesiรณn", + "ClearFilters": "Limpiar filtros", + "Description": "Descripciรณn", + "Empty": "vacรญo", + "Filter list to 100 studies or less to enable sorting": "Filtre la lista a 100 estudios o menos para habilitar la clasificaciรณn", + "Instances": "Instancias", + "MRN": "MRN", + "Modality": "Modalidad", + "PatientName": "Nombre paciente", + "Previous": "< Anterior", + "Page": "Pรกgina", + "Next": "Siguiente >", + "Results per page": "Resultados por pรกgina", + "Number of studies": "Estudios", + "StudyDate": "Fecha del estudio", + "StudyList": "Lista de Estudios" +} diff --git a/platform/i18n/src/locales/es/UserPreferencesModal.json b/platform/i18n/src/locales/es/UserPreferencesModal.json new file mode 100644 index 0000000..f0a9fe8 --- /dev/null +++ b/platform/i18n/src/locales/es/UserPreferencesModal.json @@ -0,0 +1,6 @@ +{ + "Cancel": "$t(Buttons:Cancel)", + "Reset to defaults": "$t(Buttons:Reset to defaults)", + "Save": "$t(Buttons:Save)", + "User preferences": "Preferencias de Usuario" +} diff --git a/platform/i18n/src/locales/es/ViewportDownloadForm.json b/platform/i18n/src/locales/es/ViewportDownloadForm.json new file mode 100644 index 0000000..95dfb83 --- /dev/null +++ b/platform/i18n/src/locales/es/ViewportDownloadForm.json @@ -0,0 +1,14 @@ +{ + "emptyFilenameError": "El nombre del fichero no puede ser vacรญo.", + "fileType": "Tipo de fichero", + "filename": "Nombre del fichero", + "formTitle": "Por favor especifica las dimensiones, nombre del fichero, y el tipo deseado para el fichero generado.", + "imageHeight": "Altura de la imagen (px)", + "imagePreview": "Preview de la imagen", + "imageWidth": "Anchura de la imagen (px)", + "keepAspectRatio": "Mantener el ratio de aspecto", + "loadingPreview": "Cargando el preview de la imagen...", + "minHeightError": "La altura mรญnima es 100px.", + "minWidthError": "La anchura mรญnima es 100px.", + "showAnnotations": "Mostrar las anotaciones" +} diff --git a/platform/i18n/src/locales/es/index.js b/platform/i18n/src/locales/es/index.js new file mode 100644 index 0000000..80a1aaa --- /dev/null +++ b/platform/i18n/src/locales/es/index.js @@ -0,0 +1,29 @@ +import AboutModal from './AboutModal.json'; +import Buttons from './Buttons.json'; +import CineDialog from './CineDialog.json'; +import Common from './Common.json'; +import DatePicker from './DatePicker.json'; +import Header from './Header.json'; +import MeasurementTable from './MeasurementTable.json'; +import SidePanel from './SidePanel.json'; +import StudyBrowser from './StudyBrowser.json'; +import StudyList from './StudyList.json'; +import UserPreferencesModal from './UserPreferencesModal.json'; +import ViewportDownloadForm from './ViewportDownloadForm.json'; + +export default { + es: { + AboutModal, + Buttons, + CineDialog, + Common, + DatePicker, + Header, + MeasurementTable, + SidePanel, + StudyBrowser, + StudyList, + UserPreferencesModal, + ViewportDownloadForm, + }, +}; diff --git a/platform/i18n/src/locales/fr/Buttons.json b/platform/i18n/src/locales/fr/Buttons.json new file mode 100644 index 0000000..c97dfaa --- /dev/null +++ b/platform/i18n/src/locales/fr/Buttons.json @@ -0,0 +1,42 @@ +{ + "Acquired": "Acquis", + "Angle": "Angle", + "Axial": "Axial", + "Bidirectional": "Bi-directionel", + "Brush": "Brosse", + "CINE": "Cinรฉ", + "Cancel": "Annuler", + "Circle": "Cercle", + "Clear": "Effacer", + "Coronal": "Coronal", + "Crosshairs": "Repรจre", + "Ellipse": "Ellipse", + "Elliptical": "Elliptique", + "Flip H": "Flip H", + "Flip V": "Flip V", + "Freehand": "Main levรฉe", + "Invert": "Inverser", + "Layout": "$t(Common:Layout)", + "Length": "Longueur", + "Levels": "Niveaux", + "Magnify": "Agrandir", + "Manual": "Manuel", + "Measurements": "Mesures", + "More": "$t(Common:More)", + "Next": "$t(Common:Next)", + "Pan": "Dรฉplacer", + "Play": "$t(Common:Play)", + "Previous": "$t(Common:Previous)", + "Probe": "Sonde", + "ROI Window": "ROI fenรชtrage", + "Rectangle": "Rectangle", + "Reset": "$t(Common:Reset)", + "Reset to defaults": "Valeurs d'usine", + "Rotate Right": "Tourner ร  droite", + "Sagittal": "Sagittal", + "Save": "Sauvegarder", + "Stack Scroll": "Dรฉfilement", + "Stop": "$t(Common:Stop)", + "Themes": "Themes", + "Zoom": "Zoom" +} diff --git a/platform/i18n/src/locales/fr/CineDialog.json b/platform/i18n/src/locales/fr/CineDialog.json new file mode 100644 index 0000000..257e8a2 --- /dev/null +++ b/platform/i18n/src/locales/fr/CineDialog.json @@ -0,0 +1,8 @@ +{ + "Next image": "$t(Common:Play) $t(Common:Image)", + "Play / Stop": "$t(Common:Play) / $t(Common:Stop)", + "Previous image": "$t(Common:Previous) $t(Common:Image)", + "Skip to first image": "Retour ร  la premiรจre $t(Common:Image)", + "Skip to last image": "Aller ร  la derniรจre $t(Common:Image)", + "fps": "ips" +} diff --git a/platform/i18n/src/locales/fr/Common.json b/platform/i18n/src/locales/fr/Common.json new file mode 100644 index 0000000..e1220f5 --- /dev/null +++ b/platform/i18n/src/locales/fr/Common.json @@ -0,0 +1,10 @@ +{ + "Image": "Image", + "Layout": "Disposition", + "More": "Plus", + "Next": "Suivant", + "Play": "Play", + "Previous": "Prรฉcรฉdent", + "Reset": "Reset", + "Stop": "Stop" +} diff --git a/platform/i18n/src/locales/fr/Header.json b/platform/i18n/src/locales/fr/Header.json new file mode 100644 index 0000000..e9b8948 --- /dev/null +++ b/platform/i18n/src/locales/fr/Header.json @@ -0,0 +1,8 @@ +{ + "About": "A Propos", + "Back to Viewer": "Retour au viewer", + "INVESTIGATIONAL USE ONLY": "Seulement pour utilisation expรฉrimentale", + "Options": "Options", + "Preferences": "Prรฉfรฉrences", + "Study list": "Liste d'รฉtudes" +} diff --git a/platform/i18n/src/locales/fr/UserPreferencesModal.json b/platform/i18n/src/locales/fr/UserPreferencesModal.json new file mode 100644 index 0000000..424cfe9 --- /dev/null +++ b/platform/i18n/src/locales/fr/UserPreferencesModal.json @@ -0,0 +1,6 @@ +{ + "Cancel": "$t(Buttons:Cancel)", + "Reset to defaults": "$t(Buttons:Reset to defaults)", + "Save": "$t(Buttons:Save)", + "User preferences": "Prรฉfรฉrences utilisateur" +} diff --git a/platform/i18n/src/locales/fr/index.js b/platform/i18n/src/locales/fr/index.js new file mode 100644 index 0000000..2fabaff --- /dev/null +++ b/platform/i18n/src/locales/fr/index.js @@ -0,0 +1,15 @@ +import Buttons from './Buttons.json'; +import CineDialog from './CineDialog.json'; +import Common from './Common.json'; +import Header from './Header.json'; +import UserPreferencesModal from './UserPreferencesModal.json'; + +export default { + fr: { + Buttons, + CineDialog, + Common, + Header, + UserPreferencesModal, + }, +}; diff --git a/platform/i18n/src/locales/index.js b/platform/i18n/src/locales/index.js new file mode 100644 index 0000000..25b8425 --- /dev/null +++ b/platform/i18n/src/locales/index.js @@ -0,0 +1,27 @@ +import tr_TR from './tr-TR/'; +import ar from './ar/'; +import de from './de'; +import en_US from './en-US/'; +import es from './es/'; +import fr from './fr/'; +import ja_JP from './ja-JP/'; +import nl from './nl/'; +import pt_BR from './pt-BR/'; +import vi from './vi/'; +import zh from './zh/'; +import test_lng from './test-LNG/'; + +export default { + ...ar, + ...tr_TR, + ...de, + ...en_US, + ...es, + ...fr, + ...ja_JP, + ...nl, + ...pt_BR, + ...vi, + ...zh, + ...test_lng, +}; diff --git a/platform/i18n/src/locales/ja-JP/Buttons.json b/platform/i18n/src/locales/ja-JP/Buttons.json new file mode 100644 index 0000000..43934aa --- /dev/null +++ b/platform/i18n/src/locales/ja-JP/Buttons.json @@ -0,0 +1,42 @@ +{ + "Acquired": "ๅ–ๅพ—ๆธˆ", + "Angle": "ๅˆ†ๅบฆๅ™จ", + "Axial": "ใ‚ขใ‚ญใ‚ทใƒฃใƒซ", + "Bidirectional": "ไธกๆ–นๅ‘", + "Brush": "ใƒ–ใƒฉใ‚ท", + "CINE": "ใ‚ทใƒ", + "Cancel": "ใ‚ญใƒฃใƒณใ‚ปใƒซ", + "Circle": "ใ‚ตใƒผใ‚ฏใƒซ", + "Clear": "ใ‚ฏใƒชใ‚ข", + "Coronal": "ใ‚ณใƒญใƒŠใƒซ", + "Crosshairs": "ใ‚ฏใƒญใ‚นใƒ˜ใ‚ขใƒผ", + "Ellipse": "ๆฅ•ๅ††", + "Elliptical": "ๆฅ•ๅ††", + "Flip H": "ๅทฆๅณๅ่ปข", + "Flip V": "ไธŠไธ‹ๅ่ปข", + "Freehand": "ใƒ•ใƒชใƒผใƒใƒณใƒ‰", + "Invert": "ๅ่ปข", + "Layout": "$t(Common:Layout)", + "Length": "้•ทใ•", + "Levels": "ใƒฌใƒ™ใƒซ", + "Magnify": "ๆ‹กๅคง", + "Manual": "ใƒžใƒ‹ใƒฅใ‚ขใƒซ", + "Measurements": "ๆธฌๅฎš", + "More": "$t(Common:More)", + "Next": "$t(Common:Next)", + "Pan": "ใƒ‘ใƒณ", + "Play": "$t(Common:Play)", + "Previous": "$t(Common:Previous)", + "Probe": "ใƒ—ใƒญใƒผใƒ–", + "ROI Window": "ROIใ‚ฆใ‚ฃใƒณใƒ‰ใ‚ฆ", + "Rectangle": "้•ทๆ–นๅฝข", + "Reset": "$t(Common:Reset)", + "Reset to defaults": "ใƒ‡ใƒ•ใ‚ฉใƒซใƒˆใธ$t(Common:Reset)", + "Rotate Right": "ๅณใซๅ›ž่ปข", + "Sagittal": "ใ‚ตใ‚ธใ‚ฟใƒซ", + "Save": "ไฟๅญ˜", + "Stack Scroll": "ใ‚นใ‚ฟใƒƒใ‚ฏใ‚นใ‚ฏใƒญใƒผใƒซ", + "Stop": "$t(Common:Stop)", + "Themes": "ใƒ†ใƒผใƒž", + "Zoom": "ใ‚บใƒผใƒ " +} diff --git a/platform/i18n/src/locales/ja-JP/CineDialog.json b/platform/i18n/src/locales/ja-JP/CineDialog.json new file mode 100644 index 0000000..be38a7b --- /dev/null +++ b/platform/i18n/src/locales/ja-JP/CineDialog.json @@ -0,0 +1,8 @@ +{ + "Next image": "$t(Common:Next) $t(Common:Image)", + "Play / Stop": "$t(Common:Play) / $t(Common:Stop)", + "Previous image": "$t(Common:Previous) $t(Common:Image)", + "Skip to first image": "$t(Common:Image)ๆœ€ๅˆใซใ‚นใ‚ญใƒƒใƒ—", + "Skip to last image": "$t(Common:Image)ๆœ€ๅพŒใซใ‚นใ‚ญใƒƒใƒ—", + "fps": "fps" +} diff --git a/platform/i18n/src/locales/ja-JP/Common.json b/platform/i18n/src/locales/ja-JP/Common.json new file mode 100644 index 0000000..fa56db2 --- /dev/null +++ b/platform/i18n/src/locales/ja-JP/Common.json @@ -0,0 +1,10 @@ +{ + "Image": "็”ปๅƒ", + "Layout": "ใƒฌใ‚คใ‚ขใ‚ฆใƒˆ", + "More": "ใ‚‚ใฃใจ", + "Next": "ๆฌกใธ", + "Play": "ใƒ—ใƒฌใ‚ค", + "Previous": "ๅ‰ใธ", + "Reset": "ใƒชใ‚ปใƒƒใƒˆ", + "Stop": "ใ‚นใƒˆใƒƒใƒ—" +} diff --git a/platform/i18n/src/locales/ja-JP/Header.json b/platform/i18n/src/locales/ja-JP/Header.json new file mode 100644 index 0000000..1562950 --- /dev/null +++ b/platform/i18n/src/locales/ja-JP/Header.json @@ -0,0 +1,8 @@ +{ + "About": "ใซใคใ„", + "Back to Viewer": "ๅ‰ใฎใƒ“ใƒฅใƒผ", + "INVESTIGATIONAL USE ONLY": "่ชฟๆŸป็”จใฎใฟ", + "Options": "ใ‚ชใƒ—ใ‚ทใƒงใƒณ", + "Preferences": "ใƒ—ใƒฌใƒ•ใ‚กใƒฌใƒณใ‚น", + "Study list": "ใ‚นใ‚ฟใƒ‡ใ‚ฃใƒชใ‚นใƒˆ" +} diff --git a/platform/i18n/src/locales/ja-JP/UserPreferencesModal.json b/platform/i18n/src/locales/ja-JP/UserPreferencesModal.json new file mode 100644 index 0000000..80a545b --- /dev/null +++ b/platform/i18n/src/locales/ja-JP/UserPreferencesModal.json @@ -0,0 +1,6 @@ +{ + "Cancel": "$t(Buttons:Cancel)", + "Reset to defaults": "$t(Buttons:Reset to defaults)", + "Save": "$t(Buttons:Save)", + "User preferences": "ใƒฆใƒผใ‚ถใƒ—ใƒฌใƒ•ใ‚กใƒฌใƒณใ‚น" +} diff --git a/platform/i18n/src/locales/ja-JP/index.js b/platform/i18n/src/locales/ja-JP/index.js new file mode 100644 index 0000000..38f2661 --- /dev/null +++ b/platform/i18n/src/locales/ja-JP/index.js @@ -0,0 +1,15 @@ +import Buttons from './Buttons.json'; +import CineDialog from './CineDialog.json'; +import Common from './Common.json'; +import Header from './Header.json'; +import UserPreferencesModal from './UserPreferencesModal.json'; + +export default { + 'ja-JP': { + Buttons, + CineDialog, + Common, + Header, + UserPreferencesModal, + }, +}; diff --git a/platform/i18n/src/locales/nl/Buttons.json b/platform/i18n/src/locales/nl/Buttons.json new file mode 100644 index 0000000..0443cff --- /dev/null +++ b/platform/i18n/src/locales/nl/Buttons.json @@ -0,0 +1,6 @@ +{ + "Circle": "Cirkel", + "More": "Meer", + "Pan": "Pan", + "Zoom": "Inzoomen" +} diff --git a/platform/i18n/src/locales/nl/Common.json b/platform/i18n/src/locales/nl/Common.json new file mode 100644 index 0000000..d35764a --- /dev/null +++ b/platform/i18n/src/locales/nl/Common.json @@ -0,0 +1,3 @@ +{ + "More": "Meer" +} diff --git a/platform/i18n/src/locales/nl/Header.json b/platform/i18n/src/locales/nl/Header.json new file mode 100644 index 0000000..38995e7 --- /dev/null +++ b/platform/i18n/src/locales/nl/Header.json @@ -0,0 +1,7 @@ +{ + "About": "Over", + "INVESTIGATIONAL USE ONLY": "ALLEEN VOOR ONDERZOEK", + "Options": "Opties", + "Preferences": "Voorkeuren", + "Study list": "Studie Overzicht" +} diff --git a/platform/i18n/src/locales/nl/index.js b/platform/i18n/src/locales/nl/index.js new file mode 100644 index 0000000..de42f56 --- /dev/null +++ b/platform/i18n/src/locales/nl/index.js @@ -0,0 +1,11 @@ +import Buttons from './Buttons.json'; +import Common from './Common.json'; +import Header from './Header.json'; + +export default { + nl: { + Buttons, + Common, + Header, + }, +}; diff --git a/platform/i18n/src/locales/pt-BR/AboutModal.json b/platform/i18n/src/locales/pt-BR/AboutModal.json new file mode 100644 index 0000000..00e56c6 --- /dev/null +++ b/platform/i18n/src/locales/pt-BR/AboutModal.json @@ -0,0 +1,14 @@ +{ + "About OHIF Viewer": "OHIF Viewer - Sobre", + "Browser": "Navegador", + "Build number": "Nรบmero da compilaรงรฃo", + "Last master commits": "รšltimos Commits na Master", + "More details": "Mais detalhes", + "Name": "Nome", + "OS": "SO", + "Report an issue": "Informar um problema", + "Repository URL": "URL do Repositรณrio", + "Value": "Valor", + "Version information": "Informaรงรฃo da Versรฃo", + "Visit the forum": "Visite o fรณrum" +} diff --git a/platform/i18n/src/locales/pt-BR/Buttons.json b/platform/i18n/src/locales/pt-BR/Buttons.json new file mode 100644 index 0000000..838e42b --- /dev/null +++ b/platform/i18n/src/locales/pt-BR/Buttons.json @@ -0,0 +1,43 @@ +{ + "Acquired": "Adquirido", + "Angle": "ร‚ngulo", + "Axial": "Axial", + "Bidirectional": "Bidirecional", + "Brush": "Pincel", + "CINE": "CINE", + "Cancel": "Cancelar", + "Circle": "Cรญrculo", + "Clear": "Limpar", + "Coronal": "Coronal", + "Crosshairs": "Localizador", + "Download": "Baixar", + "Ellipse": "Elipse", + "Elliptical": "Elรญptico", + "Flip H": "Inverter H", + "Flip V": "Inverter V", + "Freehand": "Desenho livre", + "Invert": "Inverter", + "Layout": "Layout", + "Length": "Tamanho", + "Levels": "Nรญveis", + "Magnify": "Ampliar", + "Manual": "Manual", + "Measurements": "Medidas", + "More": "Mais", + "Next": "Prรณximo", + "Pan": "Arrastar", + "Play": "Tocar", + "Previous": "Anterior", + "Probe": "Prova", + "ROI Window": "Janela ROI", + "Rectangle": "Retรขngulo", + "Reset": "$t(Common:Reset)", + "Reset to defaults": "$t(Common:Reset) para o Padrรฃo", + "Rotate Right": "Girar ร  direita", + "Sagittal": "Sagital", + "Save": "Salvar", + "Stack Scroll": "Navegar Stacks", + "Stop": "Parar", + "Themes": "Temas", + "Zoom": "Zoom" +} diff --git a/platform/i18n/src/locales/pt-BR/CineDialog.json b/platform/i18n/src/locales/pt-BR/CineDialog.json new file mode 100644 index 0000000..d4d653e --- /dev/null +++ b/platform/i18n/src/locales/pt-BR/CineDialog.json @@ -0,0 +1,8 @@ +{ + "Next image": "Prรณxima imagem", + "Play / Stop": "Tocar / Parar", + "Previous image": "Imagem Anterior", + "Skip to first image": "Pular para a primeira imagem", + "Skip to last image": "Pular para a รบltima imagem", + "fps": "fps" +} diff --git a/platform/i18n/src/locales/pt-BR/Common.json b/platform/i18n/src/locales/pt-BR/Common.json new file mode 100644 index 0000000..e50b07e --- /dev/null +++ b/platform/i18n/src/locales/pt-BR/Common.json @@ -0,0 +1,11 @@ +{ + "Close": "Fechar", + "Image": "Imagem", + "Layout": "Layout", + "More": "Mais", + "Next": "Prรณximo", + "Play": "Play", + "Previous": "Anterior", + "Reset": "Restaurar", + "Stop": "Stop" +} diff --git a/platform/i18n/src/locales/pt-BR/DatePicker.json b/platform/i18n/src/locales/pt-BR/DatePicker.json new file mode 100644 index 0000000..dbd86e2 --- /dev/null +++ b/platform/i18n/src/locales/pt-BR/DatePicker.json @@ -0,0 +1,5 @@ +{ + "Clear dates": "Limpar datas", + "End Date": "Data Final", + "Start Date": "Data Inicial" +} diff --git a/platform/i18n/src/locales/pt-BR/Header.json b/platform/i18n/src/locales/pt-BR/Header.json new file mode 100644 index 0000000..8f292e0 --- /dev/null +++ b/platform/i18n/src/locales/pt-BR/Header.json @@ -0,0 +1,8 @@ +{ + "About": "Quem somos", + "Back to Viewer": "Voltar para o Viewer", + "INVESTIGATIONAL USE ONLY": "APENAS PARA USO INVESTIGATIVO", + "Options": "Opรงรตes", + "Preferences": "Preferรชncias", + "Study list": "Lista de estudos" +} diff --git a/platform/i18n/src/locales/pt-BR/MeasurementTable.json b/platform/i18n/src/locales/pt-BR/MeasurementTable.json new file mode 100644 index 0000000..b1cd2bd --- /dev/null +++ b/platform/i18n/src/locales/pt-BR/MeasurementTable.json @@ -0,0 +1,4 @@ +{ + "Export": "Exportar", + "Create Report": "Criar relatรณrio" +} diff --git a/platform/i18n/src/locales/pt-BR/Messages.json b/platform/i18n/src/locales/pt-BR/Messages.json new file mode 100644 index 0000000..649b863 --- /dev/null +++ b/platform/i18n/src/locales/pt-BR/Messages.json @@ -0,0 +1,15 @@ +{ + "1": "Sรฉrie sem imagens.", + "2": "Sรฉrie nao possui informaรงรฃo de posiรงรฃo.", + "3": "Serie nรฃo รฉ reconstruรญvel.", + "4": "Sรฉrie nulti frame nรฃo possui informaรงรฃo de medidas.", + "5": "Sรฉrie multi frame nรฃo possui informaรงรฃo de orientaรงรฃo.", + "6": "Sรฉrie multi frame nรฃo possui informaรงรฃo de posiรงรฃo.", + "7": "Sรฉrie nรฃo possui algumas imagens.", + "8": "Sรฉrie possui espaรงamento irregular.", + "9": "Sรฉrie possui dimensรตes inconsistentes entre frames.", + "10": "Sรฉrie possui frames com componentes inconsistentes.", + "11": "Sรฉrie possui frames com orientaรงรตes inconsistentes.", + "12": "Sรฉrie possui informaรงรฃo de posiรงรฃo inconsistentes.", + "13": "Sรฉrie nรฃo suportada." +} diff --git a/platform/i18n/src/locales/pt-BR/UserPreferencesModal.json b/platform/i18n/src/locales/pt-BR/UserPreferencesModal.json new file mode 100644 index 0000000..6d85f2a --- /dev/null +++ b/platform/i18n/src/locales/pt-BR/UserPreferencesModal.json @@ -0,0 +1,8 @@ +{ + "Cancel": "Cancelar", + "Reset to defaults": "$t(Common:Reset) para Padrรฃo", + "ResetDefaultMessage": "Preferรชncias resetadas com sucesso.
Vocรช deve Salvar para que essa aรงรฃo seja realizada.", + "Save": "Salvar", + "SaveMessage": "Preferรชncias salvas", + "User preferences": "Preferรชncias do Usuรกrio" +} diff --git a/platform/i18n/src/locales/pt-BR/index.js b/platform/i18n/src/locales/pt-BR/index.js new file mode 100644 index 0000000..2dcc04c --- /dev/null +++ b/platform/i18n/src/locales/pt-BR/index.js @@ -0,0 +1,23 @@ +import AboutModal from './AboutModal.json'; +import Buttons from './Buttons.json'; +import CineDialog from './CineDialog.json'; +import Common from './Common.json'; +import DatePicker from './DatePicker.json'; +import Header from './Header.json'; +import UserPreferencesModal from './UserPreferencesModal.json'; +import MeasurementTable from './MeasurementTable.json'; +import Messages from './Messages.json'; + +export default { + 'pt-BR': { + AboutModal, + Buttons, + CineDialog, + Common, + DatePicker, + Header, + UserPreferencesModal, + MeasurementTable, + Messages, + }, +}; diff --git a/platform/i18n/src/locales/test-LNG/AboutModal.json b/platform/i18n/src/locales/test-LNG/AboutModal.json new file mode 100644 index 0000000..6608ef6 --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/AboutModal.json @@ -0,0 +1,18 @@ +{ + "About OHIF Viewer": "About OHIF Viewer", + "Browser": "Browser", + "Build Number": "Build Number", + "Commit hash": "Commit hash", + "Data citation": "Data citation", + "Important links": "Important links", + "Last master commits": "Latest Master Commits", + "More details": "More details", + "Name": "Name", + "OS": "OS", + "Report an issue": "Report an issue", + "Repository URL": "Repository URL", + "Value": "Value", + "Version information": "Version Information", + "Version number": "Version number", + "Visit the forum": "Visit the forum" +} diff --git a/platform/i18n/src/locales/test-LNG/Buttons.json b/platform/i18n/src/locales/test-LNG/Buttons.json new file mode 100644 index 0000000..999309a --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/Buttons.json @@ -0,0 +1,54 @@ +{ + "Acquired": "Test Acquired", + "Angle": "Test Angle", + "Axial": "Test Axial", + "Bidirectional": "Test Bidirectional", + "Brush": "Test Brush", + "CINE": "Test CINE", + "Cancel": "Test Cancel", + "Circle": "Test Circle", + "Clear": "Test Clear", + "Coronal": "Test Coronal", + "Crosshairs": "Test Crosshairs", + "Download": "Test Download", + "Ellipse": "Test Ellipse", + "Elliptical": "Test Elliptical", + "Flip H": "Test Flip H", + "Flip V": "Test Flip V", + "Freehand": "Test Freehand", + "Invert": "Test Invert", + "Layout": "Test $t(Common:Layout)", + "Length": "Test Length", + "Levels": "Test Levels", + "Magnify": "Test Magnify", + "Manual": "Test Manual", + "Measurements": "Test Measurements", + "More": "Test $t(Common:More)", + "Next": "Test $t(Common:Next)", + "Pan": "Test Pan", + "Play": "Test $t(Common:Play)", + "Previous": "Test $t(Common:Previous)", + "Probe": "Test Probe", + "ROI Window": "Test ROI Window", + "Rectangle": "Test Rectangle", + "Reset": "Test $t(Common:Reset)", + "Reset to defaults": "Test $t(Common:Reset) to Defaults", + "Rotate Right": "Test Rotate Right", + "Sagittal": "Test Sagittal", + "Save": "Test Save", + "Stack Scroll": "Test Stack Scroll", + "Stop": "Test $t(Common:Stop)", + "Themes": "Test Themes", + "Zoom": "Test Zoom", + "Grid Layout": "Test Grid Layout", + "W/L Presets": "Test W/L Presets", + "More Measure Tools": "Test More Measure Tools", + "More Tools": "Test More Tools", + "Capture": "Test Capture", + "Annotation": "Test Annotation", + "Soft Tissue": "Test Soft Tissue", + "Lung": "Test Lung", + "Liver": "Test Liver", + "Bone": "Test Bone", + "Cine": "Test Cine" +} diff --git a/platform/i18n/src/locales/test-LNG/CineDialog.json b/platform/i18n/src/locales/test-LNG/CineDialog.json new file mode 100644 index 0000000..4839826 --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/CineDialog.json @@ -0,0 +1,8 @@ +{ + "Next image": "$t(Common:Next) $t(Common:Image)", + "Play / Stop": "$t(Common:Play) / $t(Common:Stop)", + "Previous image": "$t(Common:Previous) $t(Common:Image)", + "Skip to first image": "Skip to first $t(Common:Image)", + "Skip to last image": "Skip to last $t(Common:Image)", + "fps": "fps" +} diff --git a/platform/i18n/src/locales/test-LNG/Common.json b/platform/i18n/src/locales/test-LNG/Common.json new file mode 100644 index 0000000..a809080 --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/Common.json @@ -0,0 +1,19 @@ +{ + "Close": "Test Close", + "Image": "Test Image", + "Layout": "Test Layout", + "Measurements": "Test Measurements", + "mm": "Test mm", + "More": "Test More", + "Next": "Test Next", + "No": "Test No", + "Play": "Test Play", + "Previous": "Test Previous", + "Reset": "Test Reset", + "RowsPerPage": "Test rows per page", + "Series": "Test Series", + "Show": "Test Show", + "Stop": "Test Stop", + "StudyDate": "Test Study Date", + "Yes": "Test Yes" +} diff --git a/platform/i18n/src/locales/test-LNG/DatePicker.json b/platform/i18n/src/locales/test-LNG/DatePicker.json new file mode 100644 index 0000000..3add53f --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/DatePicker.json @@ -0,0 +1,9 @@ +{ + "Clear dates": "Clear dates", + "Close": "$t(Common:Close)", + "End Date": "End Date", + "Last 7 days": "Last 7 days", + "Last 30 days": "Last 30 days", + "Start Date": "Start Date", + "Today": "Today" +} diff --git a/platform/i18n/src/locales/test-LNG/ErrorBoundary.json b/platform/i18n/src/locales/test-LNG/ErrorBoundary.json new file mode 100644 index 0000000..4077306 --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/ErrorBoundary.json @@ -0,0 +1,8 @@ +{ + "Context": "Test Context", + "Error Message": "Test Error Message", + "Something went wrong": "Test Something went wrong", + "in": "in", + "Sorry, something went wrong there. Try again.": "Test Sorry, something went wrong there. Try again.", + "Stack Trace": "Test Stack Trace" +} diff --git a/platform/i18n/src/locales/test-LNG/Header.json b/platform/i18n/src/locales/test-LNG/Header.json new file mode 100644 index 0000000..b36dcae --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/Header.json @@ -0,0 +1,6 @@ +{ + "About": "Test About", + "INVESTIGATIONAL USE ONLY": "Test Investigational", + "Options": "Test Options", + "Preferences": "Test Preferences" +} diff --git a/platform/i18n/src/locales/test-LNG/HotkeysValidators.json b/platform/i18n/src/locales/test-LNG/HotkeysValidators.json new file mode 100644 index 0000000..9734884 --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/HotkeysValidators.json @@ -0,0 +1,6 @@ +{ + "Field can't be empty": "Test Field can't be empty", + "Hotkey is already in use": "Test \"{{action}}\" is already using the \"{{pressedKeys}}\" shortcut.", + "It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut": "Test It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut", + "Shortcut combination is not allowed": "Test {{pressedKeys}} shortcut combination is not allowed" +} diff --git a/platform/i18n/src/locales/test-LNG/MeasurementTable.json b/platform/i18n/src/locales/test-LNG/MeasurementTable.json new file mode 100644 index 0000000..f65da48 --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/MeasurementTable.json @@ -0,0 +1,14 @@ +{ + "Measurements": "Test Measurements", + "No tracked measurements": "Test No tracked measurements", + "Create Report": "Test Create Report", + "Export": "Test Export", + "Delete": "Delete", + "Description": "Description", + "MAX": "MAX", + "No, do not ask again": "Test No, do not ask again", + "NonTargets": "NonTargets", + "Relabel": "Relabel", + "Targets": "Targets", + "Track measurements for this series?": "Test Track measurements for this series?" +} diff --git a/platform/i18n/src/locales/test-LNG/Messages.json b/platform/i18n/src/locales/test-LNG/Messages.json new file mode 100644 index 0000000..acf153c --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/Messages.json @@ -0,0 +1,16 @@ +{ + "Display Set Messages": "Test Display Set Messages", + "1": "Test No valid instances found in series.", + "2": "Test Display set has missing position information.", + "3": "Test Display set is not a reconstructable 3D volume.", + "4": "Test Multi frame display sets do not have pixel measurement information.", + "5": "Test Multi frame display sets do not have orientation information.", + "6": "Test Multi frame display sets do not have position information.", + "7": "Test Display set has missing frames.", + "8": "Test Display set has irregular spacing.", + "9": "Test Display set has inconsistent dimensions between frames.", + "10": "Test Display set has frames with inconsistent number of components.", + "11": "Test Display set has frames with inconsistent orientations.", + "12": "Test Display set has inconsistent position information.", + "13": "Test Unsupported display set." +} diff --git a/platform/i18n/src/locales/test-LNG/Modals.json b/platform/i18n/src/locales/test-LNG/Modals.json new file mode 100644 index 0000000..7bbbe08 --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/Modals.json @@ -0,0 +1,13 @@ +{ + "Download High Quality Image": "Test Download High Quality Image", + "Cancel": "Test Cancel", + "Download": "Test Download", + "File Name": "Test File Name", + "Active viewport has no displayed image": "Test Active viewport has no displayed image", + "Image preview": "Test Image preview", + "Show Annotations": "Test Show Annotations", + "File Type": "Test File Type", + "Image height (px)": "Test Image height (px)", + "Image width (px)": "Test Image width (px)", + "Please specify the dimensions, filename, and desired type for the output image.": "Test Please specify the dimensions, filename, and desired type for the output image." +} diff --git a/platform/i18n/src/locales/test-LNG/Modes.json b/platform/i18n/src/locales/test-LNG/Modes.json new file mode 100644 index 0000000..a9f03e2 --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/Modes.json @@ -0,0 +1,8 @@ +{ + "Basic Dev Viewer": "Test Basic Dev Viewer", + "Basic Test Mode": "Test Basic Test Mode", + "Basic Viewer": "Test Basic Viewer", + "Microscopy": "Test Microscopy", + "Segmentation": "Test Segmentation", + "Total Metabolic Tumor Volume": "Test Total Metabolic Tumor Volume" +} diff --git a/platform/i18n/src/locales/test-LNG/PatientInfo.json b/platform/i18n/src/locales/test-LNG/PatientInfo.json new file mode 100644 index 0000000..658013f --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/PatientInfo.json @@ -0,0 +1,8 @@ +{ + "Age": "Test Age", + "Sex": "Test Sex", + "MRN": "Test MRN", + "Thickness": "Test Thickness", + "Spacing": "Test Spacing", + "Scanner": "Test Scanner" +} diff --git a/platform/i18n/src/locales/test-LNG/SegmentationTable.json b/platform/i18n/src/locales/test-LNG/SegmentationTable.json new file mode 100644 index 0000000..45f6aae --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/SegmentationTable.json @@ -0,0 +1,18 @@ +{ + "Active": "Test Active", + "Add new segmentation": "Test Add new segmentation", + "Add segment": "Test Add segment", + "Add segmentation": "Test Add segmentation", + "Delete": "Test Delete", + "Display inactive segmentations": "Test Display inactive segmentations", + "Export DICOM SEG": "Test Export DICOM SEG", + "Download DICOM SEG": "Test Download DICOM SEG", + "Download DICOM RTSTRUCT": "Test Download DICOM RTSTRUCT", + "Fill": "Test Fill", + "Inactive segmentations": "Test Inactive segmentations", + "Opacity": "Test Opacity", + "Outline": "Test Outline", + "Rename": "Test Rename", + "Segmentation": "Test Segmentation", + "Size": "Test Size" +} diff --git a/platform/i18n/src/locales/test-LNG/SidePanel.json b/platform/i18n/src/locales/test-LNG/SidePanel.json new file mode 100644 index 0000000..be26718 --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/SidePanel.json @@ -0,0 +1,4 @@ +{ + "Studies": "Test Studies", + "Measurements": "Test Measurements" +} diff --git a/platform/i18n/src/locales/test-LNG/StudyBrowser.json b/platform/i18n/src/locales/test-LNG/StudyBrowser.json new file mode 100644 index 0000000..160a466 --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/StudyBrowser.json @@ -0,0 +1,5 @@ +{ + "Primary": "Test Primary", + "Recent": "Test Recent", + "All": "Test All" +} diff --git a/platform/i18n/src/locales/test-LNG/StudyItem.json b/platform/i18n/src/locales/test-LNG/StudyItem.json new file mode 100644 index 0000000..b8a1896 --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/StudyItem.json @@ -0,0 +1,3 @@ +{ + "Tracked series": "{{trackedSeries}} Test Tracked series" +} diff --git a/platform/i18n/src/locales/test-LNG/StudyList.json b/platform/i18n/src/locales/test-LNG/StudyList.json new file mode 100644 index 0000000..5f9bf94 --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/StudyList.json @@ -0,0 +1,23 @@ +{ + "Previous": "Test < Previous", + "AccessionNumber": "Test Accession #", + "Accession": "Test Accession", + "Description": "Test Description", + "Empty": "Test Empty", + "Filter list to 100 studies or less to enable sorting": "Test Filter list to 100 studies or less to enable sorting", + "Instances": "Test Instances", + "MRN": "Test MRN", + "Modality": "Test Modality", + "Next": "Test Next >", + "No studies available": "Test No studies available", + "Number of studies": "Test Number of studies", + "PatientName": "Test PatientName", + "Patient Name": "Test Patient Name", + "Results per page": "Test Results per page", + "StudyDate": "Test Study Date", + "Study date": "Test Study Date", + "Series": "Test Series", + "Study List": "Test Study List", + "Study list": "Test Study list", + "Upload": "Test Upload" +} diff --git a/platform/i18n/src/locales/test-LNG/ThumbnailTracked.json b/platform/i18n/src/locales/test-LNG/ThumbnailTracked.json new file mode 100644 index 0000000..bfda286 --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/ThumbnailTracked.json @@ -0,0 +1,5 @@ +{ + "Series is tracked": "Test Series is tracked", + "Series is untracked": "Test Series is untracked", + "Viewport": "Test Viewport" +} diff --git a/platform/i18n/src/locales/test-LNG/ToolTip.json b/platform/i18n/src/locales/test-LNG/ToolTip.json new file mode 100644 index 0000000..d3d6574 --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/ToolTip.json @@ -0,0 +1,3 @@ +{ + "Zoom": "toolTip1" +} diff --git a/platform/i18n/src/locales/test-LNG/TooltipClipboard.json b/platform/i18n/src/locales/test-LNG/TooltipClipboard.json new file mode 100644 index 0000000..b21a54d --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/TooltipClipboard.json @@ -0,0 +1,4 @@ +{ + "Copied": "Test Copied", + "Failed to copy": "Test Failed to copy" +} diff --git a/platform/i18n/src/locales/test-LNG/TrackedCornerstoneViewport.json b/platform/i18n/src/locales/test-LNG/TrackedCornerstoneViewport.json new file mode 100644 index 0000000..2b6c50e --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/TrackedCornerstoneViewport.json @@ -0,0 +1,6 @@ +{ + "Series is tracked and can be viewed in the measurement panel": + "Test Series is tracked and can be viewed in the measurement panel", + "Measurements for untracked series will not be shown in the measurements panel": + "Test Measurements for untracked series will not be shown in the measurements panel" +} diff --git a/platform/i18n/src/locales/test-LNG/UserPreferencesModal.json b/platform/i18n/src/locales/test-LNG/UserPreferencesModal.json new file mode 100644 index 0000000..e6c003f --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/UserPreferencesModal.json @@ -0,0 +1,14 @@ +{ + "Cancel": "$t(Buttons:Cancel)", + "No hotkeys found": "No hotkeys are configured for this application. Hotkeys can be configured in the application's app-config.js file.", + "Reset to defaults": "$t(Buttons:Reset to defaults)", + "ResetDefaultMessage": "Preferences successfully reset to default.
You must Save to perform this action.", + "Save": "$t(Buttons:Save)", + "SaveMessage": "Test Preferences saved", + "User preferences": "Test User Preferences", + "Function": "Test function", + "Shortcut": "Test shortcut", + "Language": "Test language", + "Hotkeys": "Test hotkeys", + "General": "Test general" +} diff --git a/platform/i18n/src/locales/test-LNG/ViewportDownloadForm.json b/platform/i18n/src/locales/test-LNG/ViewportDownloadForm.json new file mode 100644 index 0000000..85001a4 --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/ViewportDownloadForm.json @@ -0,0 +1,14 @@ +{ + "emptyFilenameError": "The file name cannot be empty.", + "fileType": "File Type", + "filename": "File Name", + "formTitle": "Please specify the dimensions, filename, and desired type for the output image.", + "imageHeight": "Image height (px)", + "imagePreview": "Image Preview", + "imageWidth": "Image width (px)", + "keepAspectRatio": "Keep aspect ratio", + "loadingPreview": "Loading Image Preview...", + "minHeightError": "The minimum valid height is 100px.", + "minWidthError": "The minimum valid width is 100px.", + "showAnnotations": "Show Annotations" +} diff --git a/platform/i18n/src/locales/test-LNG/WindowLevelActionMenu.json b/platform/i18n/src/locales/test-LNG/WindowLevelActionMenu.json new file mode 100644 index 0000000..13c1c82 --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/WindowLevelActionMenu.json @@ -0,0 +1,5 @@ +{ + "Back to Display Options": "Test Back to Display Options", + "Modality Presets": "Test {{modality}} Presets", + "Modality Window Presets": "Test {{modality}} Window Presets" +} diff --git a/platform/i18n/src/locales/test-LNG/index.js b/platform/i18n/src/locales/test-LNG/index.js new file mode 100644 index 0000000..16997ce --- /dev/null +++ b/platform/i18n/src/locales/test-LNG/index.js @@ -0,0 +1,55 @@ +import AboutModal from './AboutModal.json'; +import Buttons from './Buttons.json'; +import CineDialog from './CineDialog.json'; +import Common from './Common.json'; +import DatePicker from './DatePicker.json'; +import ErrorBoundary from './ErrorBoundary.json'; +import Header from './Header.json'; +import HotkeysValidators from './HotkeysValidators.json'; +import MeasurementTable from './MeasurementTable.json'; +import Messages from './Messages.json'; +import Modals from './Modals.json'; +import Modes from './Modes.json'; +import PatientInfo from './PatientInfo.json'; +import SegmentationTable from './SegmentationTable.json'; +import SidePanel from './SidePanel.json'; +import StudyBrowser from './StudyBrowser.json'; +import StudyItem from './StudyItem.json'; +import StudyList from './StudyList.json'; +import ToolTip from './ToolTip.json'; +import TooltipClipboard from './TooltipClipboard.json'; +import ThumbnailTracked from './ThumbnailTracked.json'; +import TrackedCornerstoneViewport from './TrackedCornerstoneViewport.json'; +import UserPreferencesModal from './UserPreferencesModal.json'; +import ViewportDownloadForm from './ViewportDownloadForm.json'; +import WindowLevelActionMenu from './WindowLevelActionMenu.json'; + +export default { + 'test-LNG': { + AboutModal, + Buttons, + CineDialog, + Common, + DatePicker, + ErrorBoundary, + Header, + HotkeysValidators, + MeasurementTable, + Messages, + Modals, + Modes, + PatientInfo, + SegmentationTable, + SidePanel, + StudyBrowser, + StudyItem, + StudyList, + ToolTip, + TooltipClipboard, + ThumbnailTracked, + TrackedCornerstoneViewport, + UserPreferencesModal, + ViewportDownloadForm, + WindowLevelActionMenu, + }, +}; diff --git a/platform/i18n/src/locales/tr-TR/AboutModal.json b/platform/i18n/src/locales/tr-TR/AboutModal.json new file mode 100644 index 0000000..b60829b --- /dev/null +++ b/platform/i18n/src/locales/tr-TR/AboutModal.json @@ -0,0 +1,14 @@ +{ + "About OHIF Viewer": "OHIF Viewer - Hakkฤฑnda", + "Browser": "Tarayฤฑcฤฑ", + "Build number": "Derleme Numarasฤฑ", + "Last master commits": "Son Kaynak Kod Gรผncellemesi", + "More details": "Daha Fazla Detay", + "Name": "ฤฐsim", + "OS": "ฤฐลŸletim Sistemi", + "Report an issue": "Sorun Bildir", + "Repository URL": "Kaynak Kod URL", + "Value": "DeฤŸer", + "Version information": "Sรผrรผm Bilgisi", + "Visit the forum": "Forumu ziyaret et" +} diff --git a/platform/i18n/src/locales/tr-TR/Buttons.json b/platform/i18n/src/locales/tr-TR/Buttons.json new file mode 100644 index 0000000..f397797 --- /dev/null +++ b/platform/i18n/src/locales/tr-TR/Buttons.json @@ -0,0 +1,43 @@ +{ + "Acquired": "Edinilen", + "Angle": "Aรงฤฑ", + "Axial": "Eksenel", + "Bidirectional": "ร‡ift Yรถnlรผ", + "Brush": "Fฤฑrรงa", + "CINE": "CINE", + "Cancel": "Vazgeรง", + "Circle": "Daire", + "Clear": "Temizle", + "Coronal": "Koronal", + "Crosshairs": "KesiลŸim", + "Download": "ฤฐndir", + "Ellipse": "Elips", + "Elliptical": "Eliptik", + "Flip H": "ร‡evir D", + "Flip V": "ร‡evir Y", + "Freehand": "Serbest El", + "Invert": "Tersini ร‡evir", + "Layout": "$t(Common:Layout)", + "Length": "Uzunluk", + "Levels": "Seviyeler", + "Magnify": "Bรผyรผt", + "Manual": "Manuel", + "Measurements": "ร–lรงรผmler", + "More": "$t(Common:More)", + "Next": "$t(Common:Next)", + "Pan": "Tut", + "Play": "$t(Common:Play)", + "Previous": "$t(Common:Previous)", + "Probe": "ฤฐncele", + "ROI Window": "ROI Penceresi", + "Rectangle": "Diktรถrtgen", + "Reset": "$t(Common:Reset)", + "Reset to defaults": "Varsayฤฑlana $t(Common:Reset)", + "Rotate Right": "SaฤŸa Dรถndรผr", + "Sagittal": "Sagital", + "Save": "Kaydet", + "Stack Scroll": "YฤฑฤŸฤฑn Kaydฤฑrma", + "Stop": "$t(Common:Stop)", + "Themes": "Temalar", + "Zoom": "YakฤฑnlaลŸtฤฑr" +} diff --git a/platform/i18n/src/locales/tr-TR/CineDialog.json b/platform/i18n/src/locales/tr-TR/CineDialog.json new file mode 100644 index 0000000..7f93001 --- /dev/null +++ b/platform/i18n/src/locales/tr-TR/CineDialog.json @@ -0,0 +1,8 @@ +{ + "Next image": "$t(Common:Next) $t(Common:Image)", + "Play / Stop": "$t(Common:Play) / $t(Common:Stop)", + "Previous image": "$t(Common:Previous) $t(Common:Image)", + "Skip to first image": "ฤฐlk $t(Common:Image) Geรง", + "Skip to last image": "Son $t(Common:Image) Geรง", + "fps": "fps" +} diff --git a/platform/i18n/src/locales/tr-TR/Common.json b/platform/i18n/src/locales/tr-TR/Common.json new file mode 100644 index 0000000..eddaacc --- /dev/null +++ b/platform/i18n/src/locales/tr-TR/Common.json @@ -0,0 +1,16 @@ +{ + "Close": "Kapat", + "Image": "Gรถrรผntรผ", + "Layout": "Dรผzen", + "Measurements": "ร–lรงรผmler", + "More": "Daha Fazla", + "Next": "Sonraki", + "Play": "Oynat", + "Previous": "ร–nceki", + "Reset": "Sฤฑfฤฑrla", + "RowsPerPage": "Sayfa baลŸฤฑna satฤฑr", + "Series": "Seriler", + "Show": "Gรถster", + "Stop": "Durdur", + "StudyDate": "ร‡alฤฑลŸma Zamanฤฑ" +} diff --git a/platform/i18n/src/locales/tr-TR/DatePicker.json b/platform/i18n/src/locales/tr-TR/DatePicker.json new file mode 100644 index 0000000..ca1073d --- /dev/null +++ b/platform/i18n/src/locales/tr-TR/DatePicker.json @@ -0,0 +1,5 @@ +{ + "Clear dates": "Tarihleri Temizle", + "End Date": "BitiลŸ Tarih", + "Start Date": "BaลŸlangฤฑรง Tarihi" +} diff --git a/platform/i18n/src/locales/tr-TR/Header.json b/platform/i18n/src/locales/tr-TR/Header.json new file mode 100644 index 0000000..d4a5815 --- /dev/null +++ b/platform/i18n/src/locales/tr-TR/Header.json @@ -0,0 +1,8 @@ +{ + "About": "Hakkฤฑnda", + "Back to Viewer": "Gรถrรผntรผleyiciye Dรถn", + "INVESTIGATIONAL USE ONLY": "SADECE ARAลžTIRMA AMAร‡LI KULLANIM", + "Options": "Seรงenekler", + "Preferences": "Tercihler", + "Study list": "ร‡alฤฑลŸma Listesi" +} diff --git a/platform/i18n/src/locales/tr-TR/MeasurementTable.json b/platform/i18n/src/locales/tr-TR/MeasurementTable.json new file mode 100644 index 0000000..ac89358 --- /dev/null +++ b/platform/i18n/src/locales/tr-TR/MeasurementTable.json @@ -0,0 +1,9 @@ +{ + "Criteria nonconformities": "Kriter uygunsuzluklarฤฑ", + "Delete": "Sil", + "Description": "Aรงฤฑklama", + "MAX": "Enfazla", + "NonTargets": "Hedefsiz", + "Relabel": "Tekrar Etiketle", + "Targets": "Hedefler" +} diff --git a/platform/i18n/src/locales/tr-TR/StudyList.json b/platform/i18n/src/locales/tr-TR/StudyList.json new file mode 100644 index 0000000..9566214 --- /dev/null +++ b/platform/i18n/src/locales/tr-TR/StudyList.json @@ -0,0 +1,10 @@ +{ + "AccessionNumber": "Accession #", + "Empty": "BoลŸ", + "MRN": "MRN", + "Modality": "Modalite", + "PatientName": "Hasta Adฤฑ", + "StudyDate": "ร‡alฤฑลŸma Zamanฤฑ", + "Description": "Aรงฤฑklama", + "StudyList": "ร‡alฤฑลŸma Listesi" +} diff --git a/platform/i18n/src/locales/tr-TR/UserPreferencesModal.json b/platform/i18n/src/locales/tr-TR/UserPreferencesModal.json new file mode 100644 index 0000000..16cf465 --- /dev/null +++ b/platform/i18n/src/locales/tr-TR/UserPreferencesModal.json @@ -0,0 +1,9 @@ +{ + "Cancel": "$t(Buttons:Cancel)", + "No hotkeys found": "Bu uygulama iรงin hiรงbir kฤฑsayol tuลŸu yapฤฑlandฤฑrฤฑlmamฤฑลŸ. Kฤฑsayol tuลŸlarฤฑ, uygulamanฤฑn app-config.js dosyasฤฑnda yapฤฑlandฤฑrฤฑlabilir.", + "Reset to defaults": "$t(Buttons:Reset to defaults)", + "ResetDefaultMessage": "Tercihler baลŸarฤฑyla varsayฤฑlana sฤฑfฤฑrlandฤฑ.
Bu eylemi gerรงekleลŸtirmek iรงin Kaydetmelisiniz.", + "Save": "$t(Buttons:Save)", + "SaveMessage": "Tercihler kaydedildi", + "User preferences": "Kullanฤฑcฤฑ tercihleri" +} diff --git a/platform/i18n/src/locales/tr-TR/ViewportDownloadForm.json b/platform/i18n/src/locales/tr-TR/ViewportDownloadForm.json new file mode 100644 index 0000000..22ce0ef --- /dev/null +++ b/platform/i18n/src/locales/tr-TR/ViewportDownloadForm.json @@ -0,0 +1,14 @@ +{ + "emptyFilenameError": "Dosya adฤฑ boลŸ olamaz.", + "fileType": "Dosya Tipi", + "filename": "Dosya Adฤฑ", + "formTitle": "Lรผtfen รงฤฑktฤฑ gรถrรผntรผsรผ iรงin boyutlarฤฑ, dosya adฤฑnฤฑ ve istediฤŸiniz tรผrรผ belirtin.", + "imageHeight": "Gรถrรผntรผ YรผksekliฤŸi (px)", + "imagePreview": "Gรถrรผntรผ ร–nizleme", + "imageWidth": "Gรถrรผntรผ GeniลŸliฤŸi (px)", + "keepAspectRatio": "En-boy oranฤฑnฤฑ koru", + "loadingPreview": "Gรถrรผntรผ ร–nzilemesi Yรผkleniyor...", + "minHeightError": "Minimum geรงerli yรผkseklik 100 pikseldir.", + "minWidthError": "Minimum geรงerli geniลŸlik 100 pikseldir.", + "showAnnotations": "Ek Aรงฤฑklamalarฤฑ Gรถster" +} diff --git a/platform/i18n/src/locales/tr-TR/index.js b/platform/i18n/src/locales/tr-TR/index.js new file mode 100644 index 0000000..174822b --- /dev/null +++ b/platform/i18n/src/locales/tr-TR/index.js @@ -0,0 +1,25 @@ +import AboutModal from './AboutModal.json'; +import Buttons from './Buttons.json'; +import CineDialog from './CineDialog.json'; +import Common from './Common.json'; +import DatePicker from './DatePicker.json'; +import Header from './Header.json'; +import MeasurementTable from './MeasurementTable.json'; +import StudyList from './StudyList.json'; +import UserPreferencesModal from './UserPreferencesModal.json'; +import ViewportDownloadForm from './ViewportDownloadForm.json'; + +export default { + 'tr-TR': { + AboutModal, + Buttons, + CineDialog, + Common, + DatePicker, + Header, + MeasurementTable, + StudyList, + UserPreferencesModal, + ViewportDownloadForm, + }, +}; diff --git a/platform/i18n/src/locales/vi/Buttons.json b/platform/i18n/src/locales/vi/Buttons.json new file mode 100644 index 0000000..952ec81 --- /dev/null +++ b/platform/i18n/src/locales/vi/Buttons.json @@ -0,0 +1,42 @@ +{ + "Acquired": "ฤรฃ lแบฅy", + "Angle": "Gรณc", + "Axial": "Trแปฅc", + "Bidirectional": "Hai hฦฐแป›ng", + "Brush": "Bรบt lรดng", + "CINE": "Duyแป‡t tแปฑ ฤ‘แป™ng", + "Cancel": "Hแปงy bแป", + "Circle": "Vรฒng trรฒn", + "Clear": "Xรณa", + "Coronal": "Mแบทt phแบณng vร nh", + "Crosshairs": "Vแป‹ trรญ tฦฐฦกng quan", + "Ellipse": "ฤo Elip", + "Elliptical": "Elip", + "Flip H": "Lแบญt ngang", + "Flip V": "Lแบญt dแปc", + "Freehand": "Bแบฑng tay", + "Invert": "แบขnh dฦฐฦกng bแบฃn", + "Layout": "$t(Common:Layout)", + "Length": "Thฦฐแป›c ฤ‘o chiแปu dร i", + "Levels": "ฤแป™ sรกng", + "Magnify": "Phรณng ฤ‘แบกi mแป™t phแบงn", + "Manual": "Thแปง cรดng", + "Measurements": "ฤo lฦฐแปng", + "More": "$t(Common:More)", + "Next": "$t(Common:Next)", + "Pan": "Di chuyแปƒn", + "Play": "$t(Common:Play)", + "Previous": "$t(Common:Previous)", + "Probe": "Thรดng tin ฤ‘iแปƒm แบฃnh", + "ROI Window": "ROI Window", + "Rectangle": "ฤo chแปฏ nhแบญt", + "Reset": "$t(Common:Reset)", + "Reset to defaults": "$t(Common:Reset) ฤ‘แบฟn mแบทc ฤ‘แป‹nh", + "Rotate Right": "Xoay phแบฃi", + "Sagittal": "Mแบทt phแบณng ฤ‘แปฉng dแปc", + "Save": "Lฦฐu", + "Stack Scroll": "Duyแป‡t", + "Stop": "$t(Common:Stop)", + "Themes": "Giao diแป‡n", + "Zoom": "Thu phรณng" +} diff --git a/platform/i18n/src/locales/vi/CineDialog.json b/platform/i18n/src/locales/vi/CineDialog.json new file mode 100644 index 0000000..d2e7366 --- /dev/null +++ b/platform/i18n/src/locales/vi/CineDialog.json @@ -0,0 +1,8 @@ +{ + "Next image": "$t(Common:Next) $t(Common:Image)", + "Play / Stop": "$t(Common:Play) / $t(Common:Stop)", + "Previous image": "$t(Common:Previous) $t(Common:Image)", + "Skip to first image": "Bแป qua ฤ‘แบฟn ฤ‘แบงu $t(Common:Image)", + "Skip to last image": "Bแป qua ฤ‘แบฟn cuแป‘i $t(Common:Image)", + "fps": "fps" +} diff --git a/platform/i18n/src/locales/vi/Common.json b/platform/i18n/src/locales/vi/Common.json new file mode 100644 index 0000000..5ae2c4c --- /dev/null +++ b/platform/i18n/src/locales/vi/Common.json @@ -0,0 +1,15 @@ +{ + "Image": "แบขnh", + "Layout": "Cรกch bแป‘ trรญ", + "Measurements": "ฤo lฦฐแปng", + "More": "Thรชm", + "Next": "Tiแบฟp theo", + "Play": "Phรกt", + "Previous": "Vแป sau", + "Reset": "ฤแบทt lแบกi", + "RowsPerPage": "trรชn 1 trang", + "Series": "Tแบญp แบฃnh", + "Show": "Hiแปƒn thแป‹", + "Stop": "Dแปซng", + "StudyDate": "Ngร y chแปฅp" +} diff --git a/platform/i18n/src/locales/vi/Header.json b/platform/i18n/src/locales/vi/Header.json new file mode 100644 index 0000000..07ffb42 --- /dev/null +++ b/platform/i18n/src/locales/vi/Header.json @@ -0,0 +1,8 @@ +{ + "About": "Vแป chรบng tรดi", + "Back to Viewer": "แบขnh vแปซa xem", + "INVESTIGATIONAL USE ONLY": "Chแป‰ dรนng cho nghiรชn cแปฉu", + "Options": "Lแปฑa chแปn", + "Preferences": "Thiแบฟt lแบญp", + "Study list": "Danh sรกch" +} diff --git a/platform/i18n/src/locales/vi/StudyList.json b/platform/i18n/src/locales/vi/StudyList.json new file mode 100644 index 0000000..4f13a5e --- /dev/null +++ b/platform/i18n/src/locales/vi/StudyList.json @@ -0,0 +1,10 @@ +{ + "AccessionNumber": "Mรฃ phiแปƒu", + "Empty": "Rแป—ng", + "MRN": "Mรฃ Bแป‡nh nhรขn", + "Modality": "Thiแบฟt bแป‹", + "PatientName": "Tรชn Bแป‡nh nhรขn", + "StudyDate": "Ngร y chแปฅp", + "Description": "Diแป…n giแบฃi", + "StudyList": "Danh sรกch" +} diff --git a/platform/i18n/src/locales/vi/UserPreferencesModal.json b/platform/i18n/src/locales/vi/UserPreferencesModal.json new file mode 100644 index 0000000..38ad6ed --- /dev/null +++ b/platform/i18n/src/locales/vi/UserPreferencesModal.json @@ -0,0 +1,6 @@ +{ + "Cancel": "$t(Buttons:Cancel)", + "Reset to defaults": "$t(Buttons:Reset to defaults)", + "Save": "$t(Buttons:Save)", + "User preferences": "Thiแบฟt lแบญp theo ngฦฐแปi dรนng" +} diff --git a/platform/i18n/src/locales/vi/index.js b/platform/i18n/src/locales/vi/index.js new file mode 100644 index 0000000..93283fd --- /dev/null +++ b/platform/i18n/src/locales/vi/index.js @@ -0,0 +1,17 @@ +import Buttons from './Buttons.json'; +import CineDialog from './CineDialog.json'; +import Common from './Common.json'; +import Header from './Header.json'; +import StudyList from './StudyList.json'; +import UserPreferencesModal from './UserPreferencesModal.json'; + +export default { + vi: { + Buttons, + CineDialog, + Common, + Header, + StudyList, + UserPreferencesModal, + }, +}; diff --git a/platform/i18n/src/locales/zh/AboutModal.json b/platform/i18n/src/locales/zh/AboutModal.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/platform/i18n/src/locales/zh/AboutModal.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/platform/i18n/src/locales/zh/Buttons.json b/platform/i18n/src/locales/zh/Buttons.json new file mode 100644 index 0000000..fe99f01 --- /dev/null +++ b/platform/i18n/src/locales/zh/Buttons.json @@ -0,0 +1,68 @@ +{ + "Acquired": "ๅทฒ่Žทๅ–", + "Angle": "่ง’ๅบฆ", + "Annotation": "ๆณจ้‡Š", + "Arrow Annotate": "ๆ ‡ๆณจ", + "Axial": "่ฝด็Šถ้ข", + "Bidirectional Tool": "ๅŒๅ‘", + "Bidirectional": "ๅŒๅ‘", + "Bone": "้ชจ็ช—", + "Brain": "่„‘็ช—", + "Brush": "ๆฉก็šฎๆ“ฆ", + "Cancel": "ๅ–ๆถˆ", + "Capture": "ไธ‹่ฝฝ", + "CINE": "ๆ’ญๆ”พๅŠจ็”ป", + "Cine": "่ฟž็ปญๆ’ญๆ”พ", + "Circle": "ๅœ†", + "Clear": "ๆธ…้™ค", + "Coronal": "ๅ† ็Šถ้ข", + "Crosshairs": "ๅๅญ—็บฟ", + "Dismiss Aspect": "่งฃ้™คAspect", + "Ellipse Tool": "ๆคญๅœ†", + "Ellipse": "ๆคญๅœ†", + "Elliptical": "ๆคญๅœ†็š„", + "Flip H": "ๅทฆๅณ็ฟป่ฝฌ", + "Flip Horizontal": "ๆฐดๅนณ็ฟป่ฝฌ", + "Flip Horizontally": "ๅทฆๅณ็ฟป่ฝฌ", + "Flip V": "ไธŠไธ‹็ฟป่ฝฌ", + "Freehand": "่‡ช็”ฑ็”ป็บฟ", + "Grid Layout": "็ช—ๅฃๅธƒๅฑ€", + "Invert Colors": "็ฐๅบฆๅ่ฝฌ", + "Invert": "็ฐๅบฆๅ่ฝฌ", + "Keep Aspect": "ไฟๆŒAspect", + "Layout": "ๆ˜พ็คบ็ช—ๅฃ", + "Length Tool": "้•ฟๅบฆ", + "Length": "้•ฟๅบฆ", + "Levels": "ๅฑ‚็บง", + "Liver": "่‚็ช—", + "Lung": "่‚บ็ช—", + "Magnify": "ๆ”พๅคง้•œ", + "Manual": "ๆ‰‹ๅŠจ", + "Measurements": "ๆต‹้‡", + "More Measure Tools": "ๆ›ดๅคšๆต‹้‡ๅทฅๅ…ท", + "More Tools": "ๆ›ดๅคšๅทฅๅ…ท", + "More": "ๆ›ดๅคš", + "Next": "ไธ‹ไธ€ไธช", + "Pan": "็งปๅŠจ", + "Play": "ๆ’ญๆ”พ", + "Previous": "ไธŠไธ€ไธช", + "Probe": "ๆŽข้’ˆ", + "Rectangle": "็Ÿฉๅฝข", + "Reference Lines": "ๅ‚่€ƒ็บฟ", + "Reset to defaults": "่ฟ”ๅ›ž้ป˜่ฎค", + "Reset View": "ๅคๅŽŸ", + "Reset": "ๅคๅŽŸ", + "ROI Window": "้€‰ๆ‹ฉๅฏนๆฏ”ๅบฆ", + "Rotate +90": "้กบๆ—ถ้’ˆๆ—‹่ฝฌ", + "Rotate Right": "้กบๆ—ถ้’ˆๆ—‹่ฝฌ", + "Sagittal": "็Ÿข็Šถ้ข", + "Save": "ไฟๅญ˜", + "Soft tissue": "่ฝฏ็ป„็ป‡็ช—", + "Stack Image Sync": "ๅฝฑๅƒ่”ๅŠจ", + "Stack Scroll": "ๆป‘ๅŠจๅˆ‡ๆขๅ›พๅฑ‚", + "Stop": "ๅœๆญข", + "Themes": "ไธป้ข˜", + "W/L Presets": "็ช—ไฝ้ข„่ฎพ", + "Window Level": "็ช—ไฝ", + "Zoom": "ๆ”พๅคง" +} diff --git a/platform/i18n/src/locales/zh/CineDialog.json b/platform/i18n/src/locales/zh/CineDialog.json new file mode 100644 index 0000000..6bdce24 --- /dev/null +++ b/platform/i18n/src/locales/zh/CineDialog.json @@ -0,0 +1,8 @@ +{ + "Next image": "ไธ‹ไธ€ไธชๅ›พๅƒ", + "Play / Stop": "ๆ’ญๆ”พ/ๅœๆญข", + "Previous image": "ไธŠไธ€ไธชๅ›พๅƒ", + "Skip to first image": "่ทณ่ฝฌๅˆฐ็ฌฌไธ€ไธชๅ›พๅƒ", + "Skip to last image": "่ทณ่ฝฌๅˆฐๆœ€ๅŽไธ€ไธชๅ›พๅƒ", + "fps": "ๅธง็އ" +} diff --git a/platform/i18n/src/locales/zh/Common.json b/platform/i18n/src/locales/zh/Common.json new file mode 100644 index 0000000..10ce931 --- /dev/null +++ b/platform/i18n/src/locales/zh/Common.json @@ -0,0 +1,15 @@ +{ + "Image": "ๅ›พๅƒ", + "Layout": "ๆ˜พ็คบ็ช—ๅฃ", + "Measurements": "ๆต‹้‡ๅ€ผ", + "More": "ๆ›ดๅคš", + "Next": "ไธ‹ไธ€ไธช", + "Play": "ๆ’ญๆ”พ", + "Previous": "ไธŠไธ€ไธช", + "Reset": "ๅคๅŽŸ", + "RowsPerPage": "ๆฏ้กตๆกๆ•ฐ", + "Series": "ๅบๅˆ—", + "Show": "ๆ˜พ็คบ", + "Stop": "ๅœๆญข", + "StudyDate": "ๆ—ถ้—ด" +} diff --git a/platform/i18n/src/locales/zh/ContextMenu.json b/platform/i18n/src/locales/zh/ContextMenu.json new file mode 100644 index 0000000..144e5d5 --- /dev/null +++ b/platform/i18n/src/locales/zh/ContextMenu.json @@ -0,0 +1,4 @@ +{ + "Add Label": "ๆทปๅŠ ๆ ‡ๆณจ", + "Delete measurement": "ๅˆ ้™คๆต‹้‡" +} diff --git a/platform/i18n/src/locales/zh/DatePicker.json b/platform/i18n/src/locales/zh/DatePicker.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/platform/i18n/src/locales/zh/DatePicker.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/platform/i18n/src/locales/zh/Dialog.json b/platform/i18n/src/locales/zh/Dialog.json new file mode 100644 index 0000000..7794304 --- /dev/null +++ b/platform/i18n/src/locales/zh/Dialog.json @@ -0,0 +1,6 @@ +{ + "Enter your annotation": "$t(Common:Enter your annotation)", + "Cancel": "$t(Common:Cancel)", + "Save": "$t(Common:Save)", + "Provide a name for your report": "่พ“ๅ…ฅๆŠฅๅ‘Šๅ็งฐ" +} diff --git a/platform/i18n/src/locales/zh/ErrorBoundary.json b/platform/i18n/src/locales/zh/ErrorBoundary.json new file mode 100644 index 0000000..ac9289c --- /dev/null +++ b/platform/i18n/src/locales/zh/ErrorBoundary.json @@ -0,0 +1,7 @@ +{ + "Sorry, something went wrong there. Try again.": "ๅ‘็”Ÿ้”™่ฏฏ๏ผŒ่ฏท้‡่ฏ•ใ€‚", + "Context": "ไธŠไธ‹ๆ–‡", + "Error Message": "้”™่ฏฏไฟกๆฏ", + "Stack": "ๅ †ๆ ˆ", + "Something went wrong": "ๅ‘็”Ÿ้”™่ฏฏ" +} diff --git a/platform/i18n/src/locales/zh/Header.json b/platform/i18n/src/locales/zh/Header.json new file mode 100644 index 0000000..1ecb90e --- /dev/null +++ b/platform/i18n/src/locales/zh/Header.json @@ -0,0 +1,8 @@ +{ + "About": "ๅ…ณไบŽ", + "Back to Viewer": "่ฟ”ๅ›ž่ง†ๅ›พ", + "INVESTIGATIONAL USE ONLY": "็ ”็ฉถ็”จ้€”", + "Options": "้€‰้กน", + "Preferences": "ๅๅฅฝ", + "Study list": "็ ”็ฉถๅˆ—่กจ" +} diff --git a/platform/i18n/src/locales/zh/Local.json b/platform/i18n/src/locales/zh/Local.json new file mode 100644 index 0000000..439189b --- /dev/null +++ b/platform/i18n/src/locales/zh/Local.json @@ -0,0 +1,4 @@ +{ + "Load files": "ๅŠ ่ฝฝๆ–‡ไปถ", + "Load folders": "ๅŠ ่ฝฝๆ–‡ไปถๅคน" +} diff --git a/platform/i18n/src/locales/zh/MeasurementTable.json b/platform/i18n/src/locales/zh/MeasurementTable.json new file mode 100644 index 0000000..ef6c2db --- /dev/null +++ b/platform/i18n/src/locales/zh/MeasurementTable.json @@ -0,0 +1,16 @@ +{ + "Criteria nonconformities": "ไธๅˆๆ ‡ๅ‡†", + "Delete": "ๅˆ ้™ค", + "Description": "ๆ่ฟฐ", + "MAX": "ๆœ€ๅคง", + "NonTargets": "้ž้ถๅ‘", + "Relabel": "้‡ๆ–ฐๆ ‡่ฎฐ", + "Measurements": "ๆต‹้‡", + "Targets": "้ถๅ‘", + "Export CSV": "ๅฏผๅ‡บCSV", + "No tracked measurements": "ๆฒกๆœ‰่ทŸ่ธช็š„ๆต‹้‡ๅ€ผ", + "Export": "ๅฏผๅ‡บ", + "Create Report": "ๆ–ฐๅปบๆŠฅๅ‘Š", + "Do you want to add this measurement to the existing report?": "ๅฐ†ๆต‹้‡ๆทปๅŠ ๅˆฐๆŠฅๅ‘Šไธญ๏ผŸ", + "Track measurements for this series?": "่ฆๅฏนไธชๅบๅˆ—่ฟ›่กŒ่ทŸ่ธชๅ—๏ผŸ" +} diff --git a/platform/i18n/src/locales/zh/Modals.json b/platform/i18n/src/locales/zh/Modals.json new file mode 100644 index 0000000..c80bfae --- /dev/null +++ b/platform/i18n/src/locales/zh/Modals.json @@ -0,0 +1,19 @@ +{ + "Active viewport has no displayed image": "่ง†ๅ›พ็ช—ๅฃๆฒกๆœ‰ๅ›พๅƒ", + "Cancel": "ๅ–ๆถˆ", + "Download": "ไธ‹่ฝฝ", + "The file name cannot be empty.": "ๆ–‡ไปถๅ็งฐไธ่ƒฝไธบ็ฉบ", + "File Type": "ๅ›พ็‰‡็ฑปๅž‹", + "File Name": "ๆ–‡ไปถๅ", + "formTitle": "Please specify the dimensions, filename, and desired type for the output image.", + "Image height (px)": "้ซ˜(px)", + "Image Preview": "้ข„่งˆ", + "Image preview": "้ข„่งˆ", + "Image width (px)": "ๅฎฝ(px)", + "keepAspectRatio": "Keep aspect ratio", + "loadingPreview": "Loading Image Preview...", + "The minimum valid height is 100px.": "ๅ›พ็‰‡ๆœ€ๅฐ้ซ˜ๅบฆๅ€ผไธบ100px", + "The minimum valid width is 100px.": "ๅ›พ็‰‡ๆœ€ๅฐๅฎฝๅบฆๅ€ผไธบ100px", + "Show Annotations": "ๆ˜พ็คบๆ ‡ๆณจ", + "Please specify the dimensions, filename, and desired type for the output image.": "่ฏทๆŒ‡ๅฎš่พ“ๅ‡บๅ›พๅƒ็š„ๅฐบๅฏธใ€ๆ–‡ไปถๅๅ’Œๆ‰€้œ€็ฑปๅž‹ใ€‚" +} diff --git a/platform/i18n/src/locales/zh/Modes.json b/platform/i18n/src/locales/zh/Modes.json new file mode 100644 index 0000000..9e33c94 --- /dev/null +++ b/platform/i18n/src/locales/zh/Modes.json @@ -0,0 +1,5 @@ +{ + "Basic Viewer": "ๅŸบ็ก€ๆŸฅ็œ‹ๅ™จ", + "Total Metabolic Tumor Volume": "ๆ€ปไปฃ่ฐข่‚ฟ็˜คไฝ“็งฏ", + "Download High Quality Image": "A" +} diff --git a/platform/i18n/src/locales/zh/Notification.json b/platform/i18n/src/locales/zh/Notification.json new file mode 100644 index 0000000..de9c406 --- /dev/null +++ b/platform/i18n/src/locales/zh/Notification.json @@ -0,0 +1,18 @@ +{ + "Do you want to add this measurement to the existing report?": "ๆทปๅŠ ๆต‹้‡ๅ€ผๅˆฐๅฝ“ๅ‰ๆŠฅๅ‘Šไธญ๏ผŸ", + "Create new report": "ๅˆ›ๅปบๆ–ฐๆŠฅๅ‘Š", + "Add to existing report": "ๆทปๅŠ ", + "Discard": "ๆ”พๅผƒ", + "You have existing tracked measurements. What would you like to do with your existing tracked measurements?": "ๅทฒ็ปๅญ˜ๅœจ่ทŸ่ธช็š„ๆต‹้‡๏ผŒๅฆ‚ไฝ•ๅค„็†่ฟ™ไบ›ๆต‹้‡ๆ•ฐๆฎ๏ผŸ", + "No, do not ask again for this series": "ๅฆ๏ผŒไธๅ†่ฏข้—ฎ", + "No": "ๅฆ", + "Track measurements for this series?": "ๅฏนๅบๅˆ—็š„ๆต‹้‡ๅ€ผ่ฟ›่กŒ่ทŸ่ธช?", + "Yes": "ๆ˜ฏ", + "Cancel": "ๅ–ๆถˆ", + "Measurements cannot span across multiple studies. Do you want to save your tracked measurements?": "ๆต‹้‡ไธ่ƒฝ่ทจๅคšไธชๆฃ€ๆŸฅ๏ผŒๆ˜ฏๅฆ่ฆไฟๅญ˜่ทŸ่ธช็š„ๆต‹้‡ๅ€ผ๏ผŸ", + "No, discard previously tracked series & measurements": "ๅฆ๏ผŒๆ”พๅผƒไน‹ๅ‰่ทŸ่ธช็š„ๅบๅˆ—ๅ’Œๆต‹้‡ๅ€ผใ€‚", + "Do you want to continue tracking measurements for this study?": "็ปง็ปญๅฏน่ฏฅๆฃ€ๆŸฅ่ฟ›่กŒๆต‹้‡่ทŸ่ธชๅ—๏ผŸ", + "Create Report": "ๆ–ฐๅปบๆŠฅๅ‘Š", + "Measurements saved successfully": "ๆต‹้‡ๅ€ผไฟๅญ˜ๆˆๅŠŸ", + "Failed to store measurements": "ๆต‹้‡ๅ€ผไฟๅญ˜ๅคฑ่ดฅ" +} diff --git a/platform/i18n/src/locales/zh/PatientInfo.json b/platform/i18n/src/locales/zh/PatientInfo.json new file mode 100644 index 0000000..6e4a57e --- /dev/null +++ b/platform/i18n/src/locales/zh/PatientInfo.json @@ -0,0 +1,8 @@ +{ + "Sex": "ๆ€งๅˆซ", + "Age": "ๅนด้พ„", + "MRN": "็—…ไพ‹ๅท", + "Thickness": "ๅŽšๅบฆ", + "Spacing": "้—ด่ท", + "Scanner": "ๆ‰ซๆๅ™จ" +} diff --git a/platform/i18n/src/locales/zh/SidePanel.json b/platform/i18n/src/locales/zh/SidePanel.json new file mode 100644 index 0000000..11a5310 --- /dev/null +++ b/platform/i18n/src/locales/zh/SidePanel.json @@ -0,0 +1,6 @@ +{ + "Studies": "ๆฃ€ๆŸฅ", + "Measurements": "ๆต‹้‡", + "Measure": "ๆต‹้‡", + "Segmentation": "ๅˆ†ๅ‰ฒ" +} diff --git a/platform/i18n/src/locales/zh/StudyBrowser.json b/platform/i18n/src/locales/zh/StudyBrowser.json new file mode 100644 index 0000000..a4a1e78 --- /dev/null +++ b/platform/i18n/src/locales/zh/StudyBrowser.json @@ -0,0 +1,6 @@ +{ + "Primary": "ๅฝ“ๅ‰", + "Recent": "ๆœ€่ฟ‘", + "All": "ๅ…จ้ƒจ", + "Tracked Series": "ไธช่ทŸ่ธชๅบๅˆ—" +} diff --git a/platform/i18n/src/locales/zh/StudyList.json b/platform/i18n/src/locales/zh/StudyList.json new file mode 100644 index 0000000..bbdf8be --- /dev/null +++ b/platform/i18n/src/locales/zh/StudyList.json @@ -0,0 +1,27 @@ +{ + "Empty": "ๆ— ", + "Filter list to 100 studies or less to enable sorting": "ๅฐ†ๆฃ€ๆŸฅๅˆ—่กจ่ฟ‡ๆปคๅˆฐ 100 ไธชๆˆ–ๆ›ดๅฐ‘ไปฅๅฏ็”จๆŽ’ๅบ", + "Modality": "ๆˆๅƒ่ฎพๅค‡", + "PatientName": "ๆ‚ฃ่€…ๅง“ๅ", + "StudyDate": "ๆฃ€ๆŸฅๆ—ฅๆœŸ", + "StudyList": "ๆฃ€ๆŸฅๅˆ—่กจ", + "Patient Name": "ๆ‚ฃ่€…ๅง“ๅ", + "MRN": "็—…ไพ‹ๅท", + "Study date": "ๆฃ€ๆŸฅๆ—ฅๆœŸ", + "Description": "ๆ่ฟฐ", + "Study list": "ๆฃ€ๆŸฅๅˆ—่กจ", + "Clear filters": "ๆธ…็ฉบๆกไปถ", + "Number of studies": "", + "Instances": "ๅ›พๅƒๆ•ฐ", + "Accession": "ๆฃ€ๆŸฅๅท", + "Results per page": "ๆฏ้กตๆกๆ•ฐ", + "Previous": "ไธŠไธ€้กต", + "Next": "ไธ‹ไธ€้กต", + "Page": "้กต็ ", + "Start Date": "ๅผ€ๅง‹ๆ—ฅๆœŸ", + "Series": "ๅบๅˆ—", + "No studies available": "ๆฒกๆœ‰ๆ•ฐๆฎ", + "Loading...": "ๅŠ ่ฝฝไธญ...", + "Select...": "้€‰ๆ‹ฉ...", + "InstitutionName": "ๆฃ€ๆŸฅๆœบๆž„" +} diff --git a/platform/i18n/src/locales/zh/UserPreferencesModal.json b/platform/i18n/src/locales/zh/UserPreferencesModal.json new file mode 100644 index 0000000..41ddedd --- /dev/null +++ b/platform/i18n/src/locales/zh/UserPreferencesModal.json @@ -0,0 +1,6 @@ +{ + "Cancel": "ๅ–ๆถˆ", + "Reset to defaults": "่ฟ”ๅ›ž้ป˜่ฎค", + "Save": "ไฟๅญ˜", + "User preferences": "็”จๆˆทๅๅฅฝ" +} diff --git a/platform/i18n/src/locales/zh/ViewportDownloadForm.json b/platform/i18n/src/locales/zh/ViewportDownloadForm.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/platform/i18n/src/locales/zh/ViewportDownloadForm.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/platform/i18n/src/locales/zh/index.js b/platform/i18n/src/locales/zh/index.js new file mode 100644 index 0000000..4e2517c --- /dev/null +++ b/platform/i18n/src/locales/zh/index.js @@ -0,0 +1,45 @@ +import AboutModal from './AboutModal.json'; +import Buttons from './Buttons.json'; +import CineDialog from './CineDialog.json'; +import Common from './Common.json'; +import DatePicker from './DatePicker.json'; +import Header from './Header.json'; +import MeasurementTable from './MeasurementTable.json'; +import StudyList from './StudyList.json'; +import UserPreferencesModal from './UserPreferencesModal.json'; +import ViewportDownloadForm from './ViewportDownloadForm.json'; +import StudyBrowser from './StudyBrowser.json'; +import SidePanel from './SidePanel.json'; +import Modes from './Modes.json'; +import PatientInfo from './PatientInfo.json'; +import Notification from './Notification.json'; +import ContextMenu from './ContextMenu.json'; +import Dialog from './Dialog.json'; +import Modals from './Modals.json'; +import Local from './Local.json'; +import ErrorBoundary from './ErrorBoundary.json'; + +export default { + 'zh': { + AboutModal, + Buttons, + CineDialog, + Common, + DatePicker, + Header, + MeasurementTable, + StudyList, + UserPreferencesModal, + ViewportDownloadForm, + StudyBrowser, + SidePanel, + Modes, + PatientInfo, + Notification, + ContextMenu, + Dialog, + Modals, + Local, + ErrorBoundary, + }, +}; \ No newline at end of file diff --git a/platform/i18n/src/utils.js b/platform/i18n/src/utils.js new file mode 100644 index 0000000..b3fbacb --- /dev/null +++ b/platform/i18n/src/utils.js @@ -0,0 +1,78 @@ +const languagesMap = { + ar: 'Arabic', + am: 'Amharic', + bg: 'Bulgarian', + bn: 'Bengali', + ca: 'Catalan', + cs: 'Czech', + da: 'Danish', + de: 'German', + el: 'Greek', + en: 'English', + 'en-GB': 'English (Great Britain)', + 'en-US': 'English (USA)', + es: 'Spanish', + et: 'Estonian', + fa: 'Persian', + fi: 'Finnish', + fil: 'Filipino', + fr: 'French', + gu: 'Gujarati', + he: 'Hebrew', + hi: 'Hindi', + hr: 'Croatian', + hu: 'Hungarian', + id: 'Indonesian', + it: 'Italian', + ja: 'Japanese', + 'ja-JP': 'Japanese (Japan)', + kn: 'Kannada', + ko: 'Korean', + lt: 'Lithuanian', + lv: 'Latvian', + ml: 'Malayalam', + mr: 'Marathi', + ms: 'Malay', + nl: 'Dutch', + no: 'Norwegian', + pl: 'Polish', + 'pt-BR': 'Portuguese (Brazil)', + 'pt-PT': 'Portuguese (Portugal)', + ro: 'Romanian', + ru: 'Russian', + sk: 'Slovak', + sl: 'Slovenian', + sr: 'Serbian', + sv: 'Swedish', + sw: 'Swahili', + ta: 'Tamil', + te: 'Telugu', + th: 'Thai', + tr: 'Turkish', + 'tr-TR': 'Turkish (Turkey)', + uk: 'Ukrainian', + vi: 'Vietnamese', + zh: 'Chinese', + 'zh-CN': 'Chinese (China)', + 'zh-TW': 'Chinese (Taiwan)', + 'test-LNG': 'Test Language', +}; + +const getLanguageLabel = language => { + return languagesMap[language]; +}; + +export default function getAvailableLanguagesInfo(locales) { + const availableLanguagesInfo = []; + + Object.keys(locales).forEach(key => { + availableLanguagesInfo.push({ + value: key, + label: getLanguageLabel(key) || key, + }); + }); + + return availableLanguagesInfo; +} + +export { getAvailableLanguagesInfo, getLanguageLabel }; diff --git a/platform/i18n/writeLocaleIndexFiles.js b/platform/i18n/writeLocaleIndexFiles.js new file mode 100644 index 0000000..12e7c3a --- /dev/null +++ b/platform/i18n/writeLocaleIndexFiles.js @@ -0,0 +1,94 @@ +const path = require('path'); +const fs = require('fs'); + +const directoryPath = path.join(__dirname, 'src', 'locales'); +const { lstatSync, readdirSync } = require('fs'); +const { join } = require('path'); + +const isDirectory = source => lstatSync(source).isDirectory(); +const getDirectories = source => + readdirSync(source) + .map(name => join(source, name)) + .filter(isDirectory); + +const getJSONFiles = source => + readdirSync(source) + .filter(name => name.includes('.json')) + .map(name => join(source, name)) + .filter(a => !isDirectory(a)); + +const directories = getDirectories(directoryPath); + +function writeFile(filepath, name, content) { + fs.writeFile(path.join(filepath, name), content, err => { + if (err) { + throw err; + } + }); +} + +// For each language directory +const languages = []; +directories.forEach(directory => { + const language = path.basename(directory); + languages.push(language); + const name = 'index.js'; + + // Create one file (index.js) inside the language folder + // For each namespace + let content = ''; + + const files = getJSONFiles(directory); + const namespaces = files.map(file => path.basename(file, '.json')); + + files.forEach(file => { + const filename = path.basename(file); + const namespace = path.basename(file, '.json'); + + content += `import ${namespace} from './${filename}';\n`; + }); + + content += '\n'; + let exportLines = `export default { \n '${language}': {\n`; + namespaces.forEach(namespace => { + exportLines += ` ${namespace},\n`; + }); + exportLines += ' }\n};\n'; + + content += exportLines; + + // If the file {namespace}.json is present, + // create a file to import the namespace and + // export all of the namespaces for the language + // e.g. + // import namespace from './{namespace}.json'; + // export { namespace1, namespace2, ... } + writeFile(directory, name, content); +}); + +let fileContent = ''; +const languageVariables = languages.map(language => { + const languageVariable = language.replace('-', '_'); + + fileContent += `import ${languageVariable} from './${language}/';\n`; + + return languageVariable; +}); + +fileContent += '\n'; + +fileContent += 'export default {\n'; +languageVariables.forEach(language => { + fileContent += ` ...${language},\n`; +}); + +fileContent += '};\n'; + +// Create one file (index.js) inside the locales folder +// which exports each of the languages +// e.g. +// import language1 from './{language1}/' +// import language2 from './{language2}/' +// +// export { language1, language2 } +writeFile(directoryPath, 'index.js', fileContent); diff --git a/platform/ui-next/.webpack/template.html b/platform/ui-next/.webpack/template.html new file mode 100644 index 0000000..683a0c6 --- /dev/null +++ b/platform/ui-next/.webpack/template.html @@ -0,0 +1,17 @@ + + + + + + <%= htmlWebpackPlugin.options.title %> + + +
+ + diff --git a/platform/ui-next/.webpack/webpack.dev.js b/platform/ui-next/.webpack/webpack.dev.js new file mode 100644 index 0000000..4b875ed --- /dev/null +++ b/platform/ui-next/.webpack/webpack.dev.js @@ -0,0 +1,11 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); +const ENTRY = { + app: `${SRC_DIR}/index.ts`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/platform/ui-next/.webpack/webpack.prod.js b/platform/ui-next/.webpack/webpack.prod.js new file mode 100644 index 0000000..9f86a9d --- /dev/null +++ b/platform/ui-next/.webpack/webpack.prod.js @@ -0,0 +1,60 @@ +const { merge } = require('webpack-merge'); +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; + +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const pkg = require('./../package.json'); + +const ROOT_DIR = path.join(__dirname, './..'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.ts`, +}; + +const outputName = `ohif-${pkg.name.split('/').pop()}`; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: false, + }, + output: { + path: ROOT_DIR, + library: 'ohif-ui', + libraryTarget: 'umd', + filename: pkg.main, + }, + externals: [ + /\b(dcmjs)/, + /\b(gl-matrix)/, + { + react: 'React', + 'react-dom': 'ReactDOM', + }, + ], + plugins: [ + new MiniCssExtractPlugin({ + filename: `./dist/${outputName}.css`, + chunkFilename: `./dist/${outputName}.css`, + }), + // new BundleAnalyzerPlugin({}), + ], + }); +}; diff --git a/platform/ui-next/CHANGELOG.md b/platform/ui-next/CHANGELOG.md new file mode 100644 index 0000000..15f533a --- /dev/null +++ b/platform/ui-next/CHANGELOG.md @@ -0,0 +1,1717 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + + +### Features + +* **hotkeys:** Migrate hotkeys to customization service and fix issues with overrides ([#4777](https://github.com/OHIF/Viewers/issues/4777)) ([3e6913b](https://github.com/OHIF/Viewers/commit/3e6913b097569280a5cc2fa5bbe4add52f149305)) + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + + +### Bug Fixes + +* **button:** fix for className ([#4604](https://github.com/OHIF/Viewers/issues/4604)) ([125f11f](https://github.com/OHIF/Viewers/commit/125f11fc737f70ec9324798245787f44198e3bd4)) + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + + +### Bug Fixes + +* **core:** Address 3D reconstruction and Android compatibility issues and clean up 4D data mode ([#4762](https://github.com/OHIF/Viewers/issues/4762)) ([149d6d0](https://github.com/OHIF/Viewers/commit/149d6d049cd333b9e5846576b403ff387558a66f)) + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + + +### Bug Fixes + +* Remove non-functional Tailwind class for SegmentationPanel ([#4745](https://github.com/OHIF/Viewers/issues/4745)) ([32017d1](https://github.com/OHIF/Viewers/commit/32017d15a4c11e0cb7717c13da022a01ee152ba5)) + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + + +### Features + +* **side-panels:** Added resize handle interactive feedback for side panels ([#4734](https://github.com/OHIF/Viewers/issues/4734)) ([6abb095](https://github.com/OHIF/Viewers/commit/6abb095b8a39c5ae4f8df8852b3ddb3249ec463f)) + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + + +### Features + +* **customization:** new customization service api ([#4688](https://github.com/OHIF/Viewers/issues/4688)) ([55ad8ef](https://github.com/OHIF/Viewers/commit/55ad8efbabc3fabd8031fc08927b2f92ae5aec69)) + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + + +### Features + +* **panels:** responsive thumbnails based on panel size ([#4723](https://github.com/OHIF/Viewers/issues/4723)) ([d9abc3d](https://github.com/OHIF/Viewers/commit/d9abc3da8d94d6c5ab0cc5af25a5f61849905a35)) + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + + +### Features + +* **resizable-side-panels:** Make the left and right side panels (optionally) resizable. ([#4672](https://github.com/OHIF/Viewers/issues/4672)) ([d90a4cf](https://github.com/OHIF/Viewers/commit/d90a4cfb16cc0daed9b905de9780f44cca1323f9)) + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + + +### Bug Fixes + +* **ui:** Update dependencies and add missing icons ([#4699](https://github.com/OHIF/Viewers/issues/4699)) ([cf97fa9](https://github.com/OHIF/Viewers/commit/cf97fa9b7b9687a9b73c1cf6926bc9fbc39b6512)) + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + + +### Bug Fixes + +* context menu icon ([#4696](https://github.com/OHIF/Viewers/issues/4696)) ([1993161](https://github.com/OHIF/Viewers/commit/19931614dc53da440718e512d39a87ca9118b96e)) + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + + +### Features + +* **multimonitor:** Add simple multi-monitor support to open another study([#4178](https://github.com/OHIF/Viewers/issues/4178)) ([07c628e](https://github.com/OHIF/Viewers/commit/07c628e689b28f831317a7c28d712509b69c6b13)) + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + + +### Features + +* **dev:** move to rsbuild for dev - faster ([#4674](https://github.com/OHIF/Viewers/issues/4674)) ([d4a4267](https://github.com/OHIF/Viewers/commit/d4a4267429c02916dd51f6aefb290d96dd1c3b04)) + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + + +### Bug Fixes + +* **icons:** icons missing for volume presets and others ([#4671](https://github.com/OHIF/Viewers/issues/4671)) ([01924b8](https://github.com/OHIF/Viewers/commit/01924b8bf27da045d39dfaeb126b73cb4efcdb08)) + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + + +### Bug Fixes + +* year 2025 missing in date picker ([#4647](https://github.com/OHIF/Viewers/issues/4647)) ([4a8e8ca](https://github.com/OHIF/Viewers/commit/4a8e8cacf2fa5a7e2ed2429cba79edcd3f2a6d78)) + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + + +### Bug Fixes + +* **icons:** Add Clipboard icon and update MetadataProvider for null checks ([#4615](https://github.com/OHIF/Viewers/issues/4615)) ([93d7076](https://github.com/OHIF/Viewers/commit/93d707690104ae099df6e08156e2efd8c1a6e076)) + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + + +### Features + +* migrate icons to ui-next ([#4606](https://github.com/OHIF/Viewers/issues/4606)) ([4e2ae32](https://github.com/OHIF/Viewers/commit/4e2ae328744ed95589c2cdf7a531454a25bf88b5)) + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + + +### Bug Fixes + +* **multiframe:** metadata handling of NM studies and loading order ([#4554](https://github.com/OHIF/Viewers/issues/4554)) ([7624ccb](https://github.com/OHIF/Viewers/commit/7624ccb5e495c0a151227a458d8d5bfb8babb22c)) + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + + +### Features + +* **segmentation:** Enhance dropdown menu functionality in SegmentationTable ([#4553](https://github.com/OHIF/Viewers/issues/4553)) ([397fd85](https://github.com/OHIF/Viewers/commit/397fd856539cd3b949a9614a9ea32d0d04a90000)) + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + + +### Bug Fixes + +* **error-boundray:** prevent stack trace from overflowing and make it scrollable ([#4541](https://github.com/OHIF/Viewers/issues/4541)) ([27ae385](https://github.com/OHIF/Viewers/commit/27ae385fd7787bf34af00366c5d490ac33abeff9)) + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + + +### Bug Fixes + +* Have an addIcon that adds to both ui and ui-next ([#4490](https://github.com/OHIF/Viewers/issues/4490)) ([4a12523](https://github.com/OHIF/Viewers/commit/4a125236ddbf8a4a95fb9c5820f511d0224e663f)) +* Measurement Tracking: Various UI and functionality improvements ([#4481](https://github.com/OHIF/Viewers/issues/4481)) ([62b2748](https://github.com/OHIF/Viewers/commit/62b27488471c9d5979142e2d15872a85778b90ed)) +* **styles:** several fixes for different styles ([#4483](https://github.com/OHIF/Viewers/issues/4483)) ([a5f0376](https://github.com/OHIF/Viewers/commit/a5f03764d1fe07b55635c52c5dac38ab5e694ba1)) + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + + +### Bug Fixes + +* **bugs:** Update dependencies and enhance UI components ([#4478](https://github.com/OHIF/Viewers/issues/4478)) ([05d41c5](https://github.com/OHIF/Viewers/commit/05d41c52068a3b7ba249f15ecdf71838c352fd30)) + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + + +### Features + +* **new-study-panel:** default to list view for non thumbnail series, change default fitler to all, and add more menu to thumbnail items with a dicom tag browser ([#4417](https://github.com/OHIF/Viewers/issues/4417)) ([a7fd9fa](https://github.com/OHIF/Viewers/commit/a7fd9fa5bfff7a1b533d99cb96f7147a35fd528f)) + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + + +### Features + +* **tours:** freeze versions and add licensings doc ([#4407](https://github.com/OHIF/Viewers/issues/4407)) ([60a8d51](https://github.com/OHIF/Viewers/commit/60a8d5154a5d6d2b121bd93aeacf12d97ef9f8cb)) + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + + +### Bug Fixes + +* ๐Ÿ› Fix imports for ui-next ([#4394](https://github.com/OHIF/Viewers/issues/4394)) ([43efed2](https://github.com/OHIF/Viewers/commit/43efed207e0d8d13bcbf52fab14c1be034d22d0c)) + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + + +### Bug Fixes + +* **ui:** Fixed study component open and closed feedback in Studies panel ([#4384](https://github.com/OHIF/Viewers/issues/4384)) ([365d824](https://github.com/OHIF/Viewers/commit/365d824b98e03b87db294878abde6823abdcf409)) + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + + +### Features + +* **studies-panel:** New OHIF study panel - under experimental flag ([#4254](https://github.com/OHIF/Viewers/issues/4254)) ([7a96406](https://github.com/OHIF/Viewers/commit/7a96406a116e46e62c396855fa64f434e2984b58)) + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + + +### Bug Fixes + +* **ui:** Tailwind build errors ([#4329](https://github.com/OHIF/Viewers/issues/4329)) ([8e7cc11](https://github.com/OHIF/Viewers/commit/8e7cc1152917f562ea7e6a5f3f7e492b300dc564)) + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + + +### Features + +* **ui:** Created design and added core components for ui-next ([#4324](https://github.com/OHIF/Viewers/issues/4324)) ([9036418](https://github.com/OHIF/Viewers/commit/90364189b865514cc471786d2f91c270517e98fc)) + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + + +### Bug Fixes + +* **ui:** remove border-border class ([#4317](https://github.com/OHIF/Viewers/issues/4317)) ([d402ded](https://github.com/OHIF/Viewers/commit/d402ded8c36631f8009b7b15b2f1c7a02cd09f6c)) + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + + +### Bug Fixes + +* webpack import bugs showing warnings on import ([#4265](https://github.com/OHIF/Viewers/issues/4265)) ([24c511f](https://github.com/OHIF/Viewers/commit/24c511f4bc04c4143bbd3d0d48029f41f7f36014)) + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + + +### Bug Fixes + +* **seg:** maintain algorithm name and algorithm type when DICOM seg is exported or downloaded ([#4203](https://github.com/OHIF/Viewers/issues/4203)) ([a29e94d](https://github.com/OHIF/Viewers/commit/a29e94de803f79bbb3372d00ad8eb14b4224edc2)) + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/ui-next + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) diff --git a/platform/ui-next/assets/data/index.ts b/platform/ui-next/assets/data/index.ts new file mode 100644 index 0000000..8d63a4e --- /dev/null +++ b/platform/ui-next/assets/data/index.ts @@ -0,0 +1,256 @@ +const actionOptionsMap: { [key: string]: string[] } = { + Measurement: ['Rename', 'Lock', 'Delete'], + Segmentation: ['Rename', 'Lock', 'Export', 'Delete'], + 'ROI Tools': ['Rename', 'Lock', 'Delete'], + 'Organ Segmentation': ['Rename', 'Lock', 'Export', 'Delete'], + // Add more types and their corresponding actions as needed +}; + +const dataList = [ + { + type: 'Measurement', + items: [ + { + id: 1, + title: 'Measurement Label', + description: 'Description for Measurement One.', + optionalField: 'Optional Info 1', + details: { primary: ['Data'], secondary: [] }, + }, + { + id: 2, + title: 'Measurement Label', + description: 'Description for Measurement Two.', + details: { primary: ['Data'], secondary: [] }, + }, + ], + }, + { + type: 'Segmentation', + items: [ + { + id: 3, + title: 'Segmentation One', + colorHex: '#FF5733', + description: 'Description for Segmentation One.', + }, + { + id: 4, + title: 'Segmentation Two', + colorHex: '#FF5733', + description: 'Description for Segmentation Two.', + }, + { + id: 5, + title: 'Segmentation Three', + colorHex: '#FF5733', + description: 'Description for Segmentation Three.', + }, + ], + }, + { + type: 'ROI Tools', + items: [ + { + id: 6, + title: 'Linear', + description: 'Description for Linear.', + details: { primary: ['49.2 mm'], secondary: ['S2 I:1'] }, + }, + { + id: 7, + title: 'Bidirectional', + description: 'Description for Bidirectional.', + details: { primary: ['L: 34.5 mm', 'W: 23.0 mm'], secondary: ['S:2 I:2'] }, + }, + { + id: 8, + title: 'Ellipse', + description: 'Description for Ellipse.', + details: { primary: ['2641 mmยฒ', 'Max: 1087 HU'], secondary: ['S:2 I:4'] }, + }, + { + id: 9, + title: 'Rectangle', + description: 'Description for Rectangle.', + details: { primary: ['1426 mmยฒ', 'Max: 718 HU'], secondary: ['S:2 I:5'] }, + }, + { + id: 10, + title: 'Circle', + description: 'Description for Circle.', + details: { primary: ['7339 mmยฒ', 'Max: 871 HU'], secondary: ['S:2 I:6'] }, + }, + { + id: 11, + title: 'Freehand ROI', + description: 'Description for Freehand ROI.', + details: { + primary: ['Mean: 215 HU', 'Max: 947 HU', 'Area: 839 mmยฒ'], + secondary: ['S:2 I:7', 'S:3 I:7'], + }, + }, + { + id: 12, + title: 'Spline Tool', + description: 'Description for Spline Tool.', + details: { primary: ['Area: 203 mmยฒ'], secondary: ['S:2 I:8'] }, + }, + { + id: 13, + title: 'Livewire Tool', + description: 'Description for Livewire Tool.', + details: { primary: ['Area: 203 mmยฒ'], secondary: ['S:2 I:3'] }, + }, + { + id: 14, + title: 'Annotation Lorem ipsum dolor sit amet long measurement name continues here', + description: 'Description for Annotation.', + details: { primary: ['Area: 203 mmยฒ'], secondary: ['S:2 I:3'] }, + }, + ], + }, + { + type: 'Organ Segmentation', + items: [ + { + id: 15, + title: 'Spleen', + description: 'Description for Spleen.', + colorHex: '#6B8E23', + }, + { + id: 16, + title: 'Kidney', + description: 'Description for Kidney.', + colorHex: '#4682B4', + }, + { + id: 17, + title: 'Kidney very long title name lorem ipsum dolor sit amet segmentation', + description: 'Description for Kidney.', + colorHex: '#9ACD32', + }, + { + id: 18, + title: 'Gallbladder', + description: 'Description for Gallbladder.', + colorHex: '#20B2AA', + }, + { + id: 19, + title: 'Esophagus', + description: 'Description for Esophagus.', + colorHex: '#DAA520', + }, + { + id: 20, + title: 'Liver', + description: 'Description for Liver.', + colorHex: '#CD5C5C', + }, + { + id: 21, + title: 'Stomach', + description: 'Description for Stomach.', + colorHex: '#778899', + }, + { + id: 22, + title: 'Abdominal aorta', + description: 'Description for Abdominal Aorta.', + colorHex: '#B8860B', + }, + { + id: 23, + title: 'Inferior vena cava', + description: 'Description for Inferior Vena Cava.', + colorHex: '#556B2F', + }, + { + id: 24, + title: 'Portal vein', + description: 'Description for Portal Vein.', + colorHex: '#8B4513', + }, + { + id: 25, + title: 'Pancreas', + description: 'Description for Pancreas.', + colorHex: '#2F4F4F', + }, + { + id: 26, + title: 'Adrenal gland', + description: 'Description for Adrenal Gland.', + colorHex: '#708090', + }, + { + id: 27, + title: 'Adrenal gland', + description: 'Description for Adrenal Gland.', + colorHex: '#6A5ACD', + }, + { + id: 28, + title: 'New Seg Test New Seg Test New Seg Test New Seg Test New Seg Test New Seg Test ', + description: 'Description for New Seg Test.', + colorHex: '#4682B4', + }, + ], + }, + { + type: 'TMTV1', + items: [ + { + id: 29, + title: 'Segment 1', + colorHex: '#FF6F61', + description: 'Description for Segmentation One.', + details: { primary: ['SUV Peak: NaN', 'Volume: 21.56mmยณ'], secondary: [] }, + }, + { + id: 30, + title: 'Segment 2', + colorHex: '#00CED1', + description: 'Description for Segmentation Two.', + details: { primary: ['SUV Peak: NaN', 'Volume: 21.56mmยณ'], secondary: [] }, + }, + { + id: 31, + title: 'Segment 3', + colorHex: '#88B04B', + description: 'Description for Segmentation One.', + details: { primary: ['SUV Peak: NaN', 'Volume: 21.56mmยณ'], secondary: [] }, + }, + ], + }, + { + type: 'TMTV2', + items: [ + { + id: 32, + title: 'Segment A', + colorHex: '#FF6F61', + description: 'Description for Segmentation One.', + details: { primary: ['SUV Peak: NaN', 'Volume: 21.56mmยณ'], secondary: [] }, + }, + { + id: 33, + title: 'Segment B', + colorHex: '#00CED1', + description: 'Description for Segmentation Two.', + details: { primary: ['SUV Peak: NaN', 'Volume: 21.56mmยณ'], secondary: [] }, + }, + { + id: 34, + title: 'Segment C', + colorHex: '#88B04B', + description: 'Description for Segmentation One.', + details: { primary: ['SUV Peak: NaN', 'Volume: 21.56mmยณ'], secondary: [] }, + }, + ], + }, +]; + +export { actionOptionsMap, dataList }; diff --git a/platform/ui-next/assets/images/CT-AAA.png b/platform/ui-next/assets/images/CT-AAA.png new file mode 100644 index 0000000..67c6bf7 Binary files /dev/null and b/platform/ui-next/assets/images/CT-AAA.png differ diff --git a/platform/ui-next/assets/images/CT-AAA2.png b/platform/ui-next/assets/images/CT-AAA2.png new file mode 100644 index 0000000..4c51a6c Binary files /dev/null and b/platform/ui-next/assets/images/CT-AAA2.png differ diff --git a/platform/ui-next/assets/images/CT-Air.png b/platform/ui-next/assets/images/CT-Air.png new file mode 100644 index 0000000..a65680a Binary files /dev/null and b/platform/ui-next/assets/images/CT-Air.png differ diff --git a/platform/ui-next/assets/images/CT-Bone.png b/platform/ui-next/assets/images/CT-Bone.png new file mode 100644 index 0000000..7c3f8c9 Binary files /dev/null and b/platform/ui-next/assets/images/CT-Bone.png differ diff --git a/platform/ui-next/assets/images/CT-Bones.png b/platform/ui-next/assets/images/CT-Bones.png new file mode 100644 index 0000000..441d6bf Binary files /dev/null and b/platform/ui-next/assets/images/CT-Bones.png differ diff --git a/platform/ui-next/assets/images/CT-Cardiac.png b/platform/ui-next/assets/images/CT-Cardiac.png new file mode 100644 index 0000000..3f9daad Binary files /dev/null and b/platform/ui-next/assets/images/CT-Cardiac.png differ diff --git a/platform/ui-next/assets/images/CT-Cardiac2.png b/platform/ui-next/assets/images/CT-Cardiac2.png new file mode 100644 index 0000000..a281b24 Binary files /dev/null and b/platform/ui-next/assets/images/CT-Cardiac2.png differ diff --git a/platform/ui-next/assets/images/CT-Cardiac3.png b/platform/ui-next/assets/images/CT-Cardiac3.png new file mode 100644 index 0000000..0b8773e Binary files /dev/null and b/platform/ui-next/assets/images/CT-Cardiac3.png differ diff --git a/platform/ui-next/assets/images/CT-Chest-Contrast-Enhanced.png b/platform/ui-next/assets/images/CT-Chest-Contrast-Enhanced.png new file mode 100644 index 0000000..be165b4 Binary files /dev/null and b/platform/ui-next/assets/images/CT-Chest-Contrast-Enhanced.png differ diff --git a/platform/ui-next/assets/images/CT-Chest-Vessels.png b/platform/ui-next/assets/images/CT-Chest-Vessels.png new file mode 100644 index 0000000..23f8732 Binary files /dev/null and b/platform/ui-next/assets/images/CT-Chest-Vessels.png differ diff --git a/platform/ui-next/assets/images/CT-Coronary-Arteries-2.png b/platform/ui-next/assets/images/CT-Coronary-Arteries-2.png new file mode 100644 index 0000000..1b6b161 Binary files /dev/null and b/platform/ui-next/assets/images/CT-Coronary-Arteries-2.png differ diff --git a/platform/ui-next/assets/images/CT-Coronary-Arteries-3.png b/platform/ui-next/assets/images/CT-Coronary-Arteries-3.png new file mode 100644 index 0000000..088a286 Binary files /dev/null and b/platform/ui-next/assets/images/CT-Coronary-Arteries-3.png differ diff --git a/platform/ui-next/assets/images/CT-Coronary-Arteries.png b/platform/ui-next/assets/images/CT-Coronary-Arteries.png new file mode 100644 index 0000000..3b32f1b Binary files /dev/null and b/platform/ui-next/assets/images/CT-Coronary-Arteries.png differ diff --git a/platform/ui-next/assets/images/CT-Cropped-Volume-Bone.png b/platform/ui-next/assets/images/CT-Cropped-Volume-Bone.png new file mode 100644 index 0000000..13c0922 Binary files /dev/null and b/platform/ui-next/assets/images/CT-Cropped-Volume-Bone.png differ diff --git a/platform/ui-next/assets/images/CT-Fat.png b/platform/ui-next/assets/images/CT-Fat.png new file mode 100644 index 0000000..9cdd78a Binary files /dev/null and b/platform/ui-next/assets/images/CT-Fat.png differ diff --git a/platform/ui-next/assets/images/CT-Liver-Vasculature.png b/platform/ui-next/assets/images/CT-Liver-Vasculature.png new file mode 100644 index 0000000..b33856d Binary files /dev/null and b/platform/ui-next/assets/images/CT-Liver-Vasculature.png differ diff --git a/platform/ui-next/assets/images/CT-Lung.png b/platform/ui-next/assets/images/CT-Lung.png new file mode 100644 index 0000000..158f3d7 Binary files /dev/null and b/platform/ui-next/assets/images/CT-Lung.png differ diff --git a/platform/ui-next/assets/images/CT-MIP.png b/platform/ui-next/assets/images/CT-MIP.png new file mode 100644 index 0000000..30a9356 Binary files /dev/null and b/platform/ui-next/assets/images/CT-MIP.png differ diff --git a/platform/ui-next/assets/images/CT-Muscle.png b/platform/ui-next/assets/images/CT-Muscle.png new file mode 100644 index 0000000..76ecdc4 Binary files /dev/null and b/platform/ui-next/assets/images/CT-Muscle.png differ diff --git a/platform/ui-next/assets/images/CT-Pulmonary-Arteries.png b/platform/ui-next/assets/images/CT-Pulmonary-Arteries.png new file mode 100644 index 0000000..4558000 Binary files /dev/null and b/platform/ui-next/assets/images/CT-Pulmonary-Arteries.png differ diff --git a/platform/ui-next/assets/images/CT-Soft-Tissue.png b/platform/ui-next/assets/images/CT-Soft-Tissue.png new file mode 100644 index 0000000..f036900 Binary files /dev/null and b/platform/ui-next/assets/images/CT-Soft-Tissue.png differ diff --git a/platform/ui-next/assets/images/DTI-FA-Brain.png b/platform/ui-next/assets/images/DTI-FA-Brain.png new file mode 100644 index 0000000..9643546 Binary files /dev/null and b/platform/ui-next/assets/images/DTI-FA-Brain.png differ diff --git a/platform/ui-next/assets/images/MR-Angio.png b/platform/ui-next/assets/images/MR-Angio.png new file mode 100644 index 0000000..f54d6fa Binary files /dev/null and b/platform/ui-next/assets/images/MR-Angio.png differ diff --git a/platform/ui-next/assets/images/MR-Default.png b/platform/ui-next/assets/images/MR-Default.png new file mode 100644 index 0000000..f8bf302 Binary files /dev/null and b/platform/ui-next/assets/images/MR-Default.png differ diff --git a/platform/ui-next/assets/images/MR-MIP.png b/platform/ui-next/assets/images/MR-MIP.png new file mode 100644 index 0000000..8b3e91a Binary files /dev/null and b/platform/ui-next/assets/images/MR-MIP.png differ diff --git a/platform/ui-next/assets/images/MR-T2-Brain.png b/platform/ui-next/assets/images/MR-T2-Brain.png new file mode 100644 index 0000000..8b1f7a5 Binary files /dev/null and b/platform/ui-next/assets/images/MR-T2-Brain.png differ diff --git a/platform/ui-next/assets/images/VolumeRendering.png b/platform/ui-next/assets/images/VolumeRendering.png new file mode 100644 index 0000000..8d7313e Binary files /dev/null and b/platform/ui-next/assets/images/VolumeRendering.png differ diff --git a/platform/ui-next/babel.config.js b/platform/ui-next/babel.config.js new file mode 100644 index 0000000..0c8d921 --- /dev/null +++ b/platform/ui-next/babel.config.js @@ -0,0 +1,56 @@ +// https://babeljs.io/docs/en/options#babelrcroots +const { extendDefaultPlugins } = require('svgo'); + +module.exports = { + babelrcRoots: ['./platform/*', './extensions/*', './modes/*'], + presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'], + plugins: [ + ['@babel/plugin-proposal-class-properties', { loose: true }], + '@babel/plugin-transform-typescript', + ['@babel/plugin-proposal-private-methods', { loose: true }], + '@babel/plugin-transform-class-static-block', + ], + env: { + test: { + presets: [ + [ + // TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2 + '@babel/preset-env', + { + modules: 'commonjs', + debug: false, + }, + ], + '@babel/preset-react', + '@babel/preset-typescript', + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-regenerator', + '@babel/transform-destructuring', + '@babel/plugin-transform-runtime', + '@babel/plugin-transform-typescript', + '@babel/plugin-transform-class-static-block', + ], + }, + production: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + development: { + presets: [ + // WebPack handles ES6 --> Target Syntax + ['@babel/preset-env', { modules: false }], + '@babel/preset-react', + '@babel/preset-typescript', + ], + ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'], + }, + }, +}; diff --git a/platform/ui-next/components.json b/platform/ui-next/components.json new file mode 100644 index 0000000..e66d5c8 --- /dev/null +++ b/platform/ui-next/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/tailwind.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "src/components", + "utils": "src/lib/utils" + } +} diff --git a/platform/ui-next/package.json b/platform/ui-next/package.json new file mode 100644 index 0000000..ae4070c --- /dev/null +++ b/platform/ui-next/package.json @@ -0,0 +1,70 @@ +{ + "name": "@ohif/ui-next", + "version": "3.10.0-beta.111", + "description": "Next version of OHIF Viewers UI, more customizable using shadcn/ui", + "main": "dist/ohif-ui-next.umd.js", + "module": "src/index.ts", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "clean": "rm -rf node_modules/.cache/storybook && shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "start": "yarn run build --watch", + "dev": "cross-env NODE_ENV=development webpack serve --config .webpack/webpack.playground.js", + "test": "echo \"Error: no test specified\" && exit 1", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build" + }, + "exports": { + "./tailwind.config": "./tailwind.config.ts", + "./lib/*": "./src/lib/*.ts", + "./components/*": "./src/components/*.tsx", + ".": "./src/index.ts" + }, + "dependencies": { + "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-context-menu": "^2.2.4", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-scroll-area": "^1.1.0", + "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slider": "^1.2.0", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.0", + "@radix-ui/react-toggle": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.2", + "class-variance-authority": "^0.7.0", + "clsx": "*", + "cmdk": "^1.0.0", + "date-fns": "^3.6.0", + "framer-motion": "6.2.4", + "lucide-react": "^0.379.0", + "next-themes": "^0.3.0", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-resizable-panels": "^2.1.7", + "react-shepherd": "6.1.1", + "shepherd.js": "13.0.3", + "sonner": "^1.5.0", + "tailwind-merge": "^2.3.0", + "tailwindcss": "3.2.4", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.16.7" + }, + "keywords": [], + "author": "OHIF", + "license": "MIT" +} diff --git a/platform/ui-next/src/components/Accordion/Accordion.tsx b/platform/ui-next/src/components/Accordion/Accordion.tsx new file mode 100644 index 0000000..e7e2dc3 --- /dev/null +++ b/platform/ui-next/src/components/Accordion/Accordion.tsx @@ -0,0 +1,59 @@ +'use client'; + +import * as React from 'react'; +import * as AccordionPrimitive from '@radix-ui/react-accordion'; +import { ChevronDownIcon } from '@radix-ui/react-icons'; + +import { cn } from '../../lib/utils'; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = 'AccordionItem'; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-270', + '[&[data-state=closed]>svg]:rotate-90' + )} + {...props} + > + {children} + + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/platform/ui-next/src/components/Accordion/index.ts b/platform/ui-next/src/components/Accordion/index.ts new file mode 100644 index 0000000..2a755dd --- /dev/null +++ b/platform/ui-next/src/components/Accordion/index.ts @@ -0,0 +1,3 @@ +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './Accordion'; + +export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }; diff --git a/platform/ui-next/src/components/BackgroundColorSelect/BackgroundColorSelect.tsx b/platform/ui-next/src/components/BackgroundColorSelect/BackgroundColorSelect.tsx new file mode 100644 index 0000000..eeb3e94 --- /dev/null +++ b/platform/ui-next/src/components/BackgroundColorSelect/BackgroundColorSelect.tsx @@ -0,0 +1,38 @@ +'use client'; + +import * as React from 'react'; +import { useState, useEffect } from 'react'; +import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../Select'; + +const BackgroundColorSelect: React.FC = () => { + const [selectedColor, setSelectedColor] = useState('#050615'); + + useEffect(() => { + const rows = document.querySelectorAll('.row') as NodeListOf; + rows.forEach(row => { + row.style.backgroundColor = selectedColor; + }); + }, [selectedColor]); + + const handleColorChange = (value: string) => { + setSelectedColor(value); + }; + + return ( +
+ +
+ ); +}; + +export default BackgroundColorSelect; diff --git a/platform/ui-next/src/components/BackgroundColorSelect/index.tsx b/platform/ui-next/src/components/BackgroundColorSelect/index.tsx new file mode 100644 index 0000000..0d3a1f4 --- /dev/null +++ b/platform/ui-next/src/components/BackgroundColorSelect/index.tsx @@ -0,0 +1 @@ +export { default as BackgroundColorSelect } from './BackgroundColorSelect'; diff --git a/platform/ui-next/src/components/Button/Button.tsx b/platform/ui-next/src/components/Button/Button.tsx new file mode 100644 index 0000000..8a8de63 --- /dev/null +++ b/platform/ui-next/src/components/Button/Button.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '../../lib/utils'; + +const buttonVariants = cva( + 'inline-flex items-center justify-center whitespace-nowrap rounded text-base font-normal leading-tight transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + default: 'bg-primary/60 text-primary-foreground hover:bg-primary/100', + destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + outline: + 'border border-primary/25 bg-background hover:bg-primary/25 text-primary hover:text-primary', + secondary: 'bg-primary/40 text-secondary-foreground hover:bg-primary/60', + ghost: 'font-normal text-primary hover:bg-primary/25', + link: 'font-normal text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-7 px-2 py-2', + sm: 'h-6 rounded px-2', + lg: 'h-9 rounded px-2', + icon: 'h-6 w-6', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, forwardRef) => { + const Comp = asChild ? Slot : 'button'; + return ( + + ); + } +); +Button.displayName = 'Button'; + +export { Button, buttonVariants }; diff --git a/platform/ui-next/src/components/Button/index.ts b/platform/ui-next/src/components/Button/index.ts new file mode 100644 index 0000000..2e2ea29 --- /dev/null +++ b/platform/ui-next/src/components/Button/index.ts @@ -0,0 +1 @@ +export { Button, buttonVariants } from './Button'; diff --git a/platform/ui-next/src/components/Calendar/Calendar.tsx b/platform/ui-next/src/components/Calendar/Calendar.tsx new file mode 100644 index 0000000..f6479d1 --- /dev/null +++ b/platform/ui-next/src/components/Calendar/Calendar.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { DayPicker } from 'react-day-picker'; + +import { cn } from '../../lib/utils'; + +import { buttonVariants } from '../Button'; + +export type CalendarProps = React.ComponentProps; + +function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) { + return ( + undefined, + labelYearDropdown: () => undefined, + }} + classNames={{ + months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0', + month: 'space-y-4', + caption: 'flex justify-between items-center px-2', + + caption_dropdowns: 'flex space-x-2 text-black', + caption_label: 'hidden', + nav: 'space-x-1 flex items-center', + table: 'w-full border-collapse space-y-1', + head_row: 'flex', + head_cell: 'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]', + row: 'flex w-full mt-2', + cell: 'h-9 w-9 text-center text-base p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20', + day: cn( + buttonVariants({ variant: 'ghost' }), + 'h-9 w-9 p-0 font-normal aria-selected:opacity-100' + ), + day_range_end: 'day-range-end', + day_selected: + 'bg-primary/60 text-primary-foreground hover:bg-primary/80 hover:text-primary-foreground focus:bg-primary/80 focus:text-primary-foreground', + day_today: 'bg-accent text-accent-foreground', + day_outside: + 'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30', + day_disabled: 'text-muted-foreground opacity-50', + day_range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground', + day_hidden: 'invisible', + ...classNames, + }} + components={{ + IconLeft: ({ ...props }) => , + IconRight: ({ ...props }) => , + }} + {...props} + /> + ); +} +Calendar.displayName = 'Calendar'; + +export { Calendar }; diff --git a/platform/ui-next/src/components/Calendar/index.tsx b/platform/ui-next/src/components/Calendar/index.tsx new file mode 100644 index 0000000..8608496 --- /dev/null +++ b/platform/ui-next/src/components/Calendar/index.tsx @@ -0,0 +1,3 @@ +import { Calendar } from './Calendar'; + +export { Calendar}; diff --git a/platform/ui-next/src/components/Card/Card.tsx b/platform/ui-next/src/components/Card/Card.tsx new file mode 100644 index 0000000..162bbcb --- /dev/null +++ b/platform/ui-next/src/components/Card/Card.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; + +import { cn } from '../../lib/utils'; + +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +Card.displayName = 'Card'; + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +CardHeader.displayName = 'CardHeader'; + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +); +CardTitle.displayName = 'CardTitle'; + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardDescription.displayName = 'CardDescription'; + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +); +CardContent.displayName = 'CardContent'; + +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +CardFooter.displayName = 'CardFooter'; + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/platform/ui-next/src/components/Card/index.ts b/platform/ui-next/src/components/Card/index.ts new file mode 100644 index 0000000..7c9b951 --- /dev/null +++ b/platform/ui-next/src/components/Card/index.ts @@ -0,0 +1,2 @@ +import { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from './Card'; +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/platform/ui-next/src/components/Checkbox/Checkbox.tsx b/platform/ui-next/src/components/Checkbox/Checkbox.tsx new file mode 100644 index 0000000..fd8f299 --- /dev/null +++ b/platform/ui-next/src/components/Checkbox/Checkbox.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; +import { CheckIcon } from '@radix-ui/react-icons'; + +import { cn } from '../../lib/utils'; + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export { Checkbox }; diff --git a/platform/ui-next/src/components/Checkbox/index.tsx b/platform/ui-next/src/components/Checkbox/index.tsx new file mode 100644 index 0000000..f4050fc --- /dev/null +++ b/platform/ui-next/src/components/Checkbox/index.tsx @@ -0,0 +1,3 @@ +import { Checkbox } from './Checkbox'; + +export { Checkbox }; diff --git a/platform/ui-next/src/components/Clipboard/Clipboard.tsx b/platform/ui-next/src/components/Clipboard/Clipboard.tsx new file mode 100644 index 0000000..0b515d8 --- /dev/null +++ b/platform/ui-next/src/components/Clipboard/Clipboard.tsx @@ -0,0 +1,55 @@ +import React, { ReactNode } from 'react'; +import { Button } from '../Button'; +import { Icons } from '../Icons'; + +interface ClipboardProps { + children: ReactNode; +} + +const Clipboard: React.FC = ({ children }) => { + const [copyState, setCopyState] = React.useState<'idle' | 'success' | 'error'>('idle'); + const copyText = React.useMemo(() => { + if (typeof children === 'string') { + return children.trim(); + } + return ''; + }, [children]); + + const handleCopy = React.useCallback(async () => { + if (!copyText) { + return; + } + try { + await navigator.clipboard.writeText(copyText); + setCopyState('success'); + } catch { + setCopyState('error'); + } finally { + setTimeout(() => setCopyState('idle'), 1500); // Reset state after feedback + } + }, [copyText]); + + return ( + + ); +}; + +export { Clipboard }; diff --git a/platform/ui-next/src/components/Clipboard/index.tsx b/platform/ui-next/src/components/Clipboard/index.tsx new file mode 100644 index 0000000..0c7b423 --- /dev/null +++ b/platform/ui-next/src/components/Clipboard/index.tsx @@ -0,0 +1,3 @@ +import { Clipboard } from './Clipboard'; + +export { Clipboard }; diff --git a/platform/ui-next/src/components/Combobox/Combobox.tsx b/platform/ui-next/src/components/Combobox/Combobox.tsx new file mode 100644 index 0000000..173b58e --- /dev/null +++ b/platform/ui-next/src/components/Combobox/Combobox.tsx @@ -0,0 +1,66 @@ +import * as React from 'react'; +import { Check, ChevronsUpDown } from 'lucide-react'; + +import { cn } from '../../lib/utils'; +import { Button } from '../Button/Button'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '../Command/Command'; +import { Popover, PopoverContent, PopoverTrigger } from '../Popover/Popover'; + +export function Combobox({ data = [], placeholder = 'Select item...' }) { + const [open, setOpen] = React.useState(false); + const [value, setValue] = React.useState(''); + + return ( + + + + + + + + No {placeholder.toLowerCase()} found. + + + {data.map(item => ( + { + setValue(currentValue === value ? '' : currentValue); + setOpen(false); + }} + > + + {item.label} + + ))} + + + + + + ); +} diff --git a/platform/ui-next/src/components/Combobox/index.ts b/platform/ui-next/src/components/Combobox/index.ts new file mode 100644 index 0000000..968ed4e --- /dev/null +++ b/platform/ui-next/src/components/Combobox/index.ts @@ -0,0 +1,3 @@ +import { Combobox } from './Combobox'; + +export { Combobox}; diff --git a/platform/ui-next/src/components/Command/Command.tsx b/platform/ui-next/src/components/Command/Command.tsx new file mode 100644 index 0000000..1a672a3 --- /dev/null +++ b/platform/ui-next/src/components/Command/Command.tsx @@ -0,0 +1,150 @@ +import * as React from 'react'; +import { type DialogProps } from '@radix-ui/react-dialog'; +import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; +import { Command as CommandPrimitive } from 'cmdk'; + +import { cn } from '../../lib/utils'; +import { Dialog, DialogContent } from '../Dialog/Dialog'; + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ); +}; + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; + +const CommandShortcut = ({ className, ...props }: React.HTMLAttributes) => { + return ( + + ); +}; +CommandShortcut.displayName = 'CommandShortcut'; + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/platform/ui-next/src/components/Command/index.ts b/platform/ui-next/src/components/Command/index.ts new file mode 100644 index 0000000..dabb8c4 --- /dev/null +++ b/platform/ui-next/src/components/Command/index.ts @@ -0,0 +1,23 @@ +import { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} from './Command'; + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/platform/ui-next/src/components/ContextMenu/ContextMenu.tsx b/platform/ui-next/src/components/ContextMenu/ContextMenu.tsx new file mode 100644 index 0000000..149596b --- /dev/null +++ b/platform/ui-next/src/components/ContextMenu/ContextMenu.tsx @@ -0,0 +1,187 @@ +import * as React from 'react'; +import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'; + +import { cn } from '../../lib/utils'; +import { CheckIcon, ChevronRightIcon, DotFilledIcon } from '@radix-ui/react-icons'; + +const ContextMenu = ContextMenuPrimitive.Root; + +const ContextMenuTrigger = ContextMenuPrimitive.Trigger; + +const ContextMenuGroup = ContextMenuPrimitive.Group; + +const ContextMenuPortal = ContextMenuPrimitive.Portal; + +const ContextMenuSub = ContextMenuPrimitive.Sub; + +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; + +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName; + +const ContextMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName; + +const ContextMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName; + +const ContextMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName; + +const ContextMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName; + +const ContextMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName; + +const ContextMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName; + +const ContextMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName; + +const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { + return ( + + ); +}; +ContextMenuShortcut.displayName = 'ContextMenuShortcut'; + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +}; diff --git a/platform/ui-next/src/components/ContextMenu/index.ts b/platform/ui-next/src/components/ContextMenu/index.ts new file mode 100644 index 0000000..36ed0d4 --- /dev/null +++ b/platform/ui-next/src/components/ContextMenu/index.ts @@ -0,0 +1,35 @@ +import { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +} from './ContextMenu'; + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +}; diff --git a/platform/ui-next/src/components/DataRow/DataRow.tsx b/platform/ui-next/src/components/DataRow/DataRow.tsx new file mode 100644 index 0000000..b0c3e93 --- /dev/null +++ b/platform/ui-next/src/components/DataRow/DataRow.tsx @@ -0,0 +1,338 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Button } from '../../components/Button/Button'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from '../../components/DropdownMenu'; +import { Icons } from '../../components/Icons/Icons'; +import { Tooltip, TooltipTrigger, TooltipContent } from '../../components/Tooltip/Tooltip'; + +/** + * DataRow is a complex UI component that displays a selectable, interactive row with hierarchical data. + * It's designed to show a numbered item with a title, optional color indicator, and expandable details. + * The row supports various interactive features like visibility toggling, locking, and contextual actions. + * + * @component + * @example + * ```tsx + * {}} + * onToggleLocked={() => {}} + * onRename={() => {}} + * onDelete={() => {}} + * onColor={() => {}} + * /> + * ``` + */ + +/** + * Props for the DataRow component + * @interface DataRowProps + * @property {number} number - The display number/index of the row + * @property {string} title - The main text label for the row + * @property {boolean} disableEditing - When true, prevents rename and delete operations + * @property {string} [colorHex] - Optional hex color code to display a color indicator + * @property {Object} [details] - Optional hierarchical details to display below the row + * @property {string[]} details.primary - Primary details shown immediately below the row + * @property {string[]} details.secondary - Secondary details (currently unused) + * @property {boolean} [isSelected] - Whether the row is currently selected + * @property {() => void} [onSelect] - Callback when the row is clicked/selected + * @property {boolean} isVisible - Controls the row's visibility state + * @property {() => void} onToggleVisibility - Callback to toggle visibility + * @property {boolean} isLocked - Controls the row's locked state + * @property {() => void} onToggleLocked - Callback to toggle locked state + * @property {() => void} onRename - Callback when rename is requested + * @property {() => void} onDelete - Callback when delete is requested + * @property {() => void} onColor - Callback when color change is requested + */ +interface DataRowProps { + number: number; + disableEditing: boolean; + description: string; + details?: { primary: string[]; secondary: string[] }; + // + isSelected?: boolean; + onSelect?: () => void; + // + isVisible: boolean; + onToggleVisibility: () => void; + // + isLocked: boolean; + onToggleLocked: () => void; + // + title: string; + onRename: () => void; + // + onDelete: () => void; + // + colorHex?: string; + onColor: () => void; +} + +const DataRow: React.FC = ({ + number, + title, + colorHex, + details, + onSelect, + isLocked, + onToggleVisibility, + onToggleLocked, + onRename, + onDelete, + onColor, + isSelected = false, + isVisible = true, + disableEditing = false, +}) => { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const isTitleLong = title?.length > 25; + const rowRef = useRef(null); + + useEffect(() => { + if (isSelected && rowRef.current) { + rowRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, [isSelected]); + + const handleAction = (action: string, e: React.MouseEvent) => { + e.stopPropagation(); + switch (action) { + case 'Rename': + onRename(); + break; + case 'Lock': + onToggleLocked(); + break; + case 'Delete': + onDelete(); + break; + case 'Color': + onColor(); + break; + } + }; + + const decodeHTML = (html: string) => { + const txt = document.createElement('textarea'); + txt.innerHTML = html; + return txt.value; + }; + + const renderDetailText = (text: string, indent: number = 0) => { + const indentation = ' '.repeat(indent); + if (text === '') { + return ( +
+ ); + } + const cleanText = decodeHTML(text); + return ( +
+ {indentation} + {cleanText} +
+ ); + }; + + const renderDetails = (details: string[]) => { + const visibleLines = details.slice(0, 4); + const hiddenLines = details.slice(4); + + return ( + + +
+
+ {visibleLines.map((line, lineIndex) => + renderDetailText(line, line.startsWith(' ') ? 1 : 0) + )} +
+ {hiddenLines.length > 0 && ( +
+ ... + +
+ )} +
+
+ +
+ {details.map((line, lineIndex) => + renderDetailText(line, line.startsWith(' ') ? 1 : 0) + )} +
+
+
+ ); + }; + + return ( +
+
+ {/* Hover Overlay */} +
+ + {/* Number Box */} +
+ {number} +
+ + {/* Color Circle (Optional) */} + {colorHex && ( +
+ +
+ )} + + {/* Label with Conditional Tooltip */} +
+ {isTitleLong ? ( + + + + {title} + + + + {title} + + + ) : ( + + {title} + + )} +
+ + {/* Actions and Visibility Toggle */} +
+ {/* Visibility Toggle Icon */} + + + {/* Lock Icon (if needed) */} + {isLocked && !disableEditing && } + + {/* Actions Dropdown Menu */} + {disableEditing &&
} + {!disableEditing && ( + setIsDropdownOpen(open)}> + + + + + <> + handleAction('Rename', e)}> + + Rename + + handleAction('Delete', e)}> + + Delete + + {onColor && ( + handleAction('Color', e)}> + + Change Color + + )} + handleAction('Lock', e)}> + + {isLocked ? 'Unlock' : 'Lock'} + + + + + )} +
+
+ + {/* Details Section */} + {details && (details.primary?.length > 0 || details.secondary?.length > 0) && ( +
+
+ {details.primary?.length > 0 && renderDetails(details.primary)} + {details.secondary?.length > 0 && ( +
+ {renderDetails(details.secondary)} +
+ )} +
+
+ )} +
+ ); +}; + +export default DataRow; diff --git a/platform/ui-next/src/components/DataRow/index.ts b/platform/ui-next/src/components/DataRow/index.ts new file mode 100644 index 0000000..1f3799b --- /dev/null +++ b/platform/ui-next/src/components/DataRow/index.ts @@ -0,0 +1,3 @@ +import DataRow from './DataRow'; + +export { DataRow }; diff --git a/platform/ui-next/src/components/DataRow/types.ts b/platform/ui-next/src/components/DataRow/types.ts new file mode 100644 index 0000000..60be942 --- /dev/null +++ b/platform/ui-next/src/components/DataRow/types.ts @@ -0,0 +1,31 @@ +/** + * Represents a single data item in a list or table structure + * + * @interface DataItem + */ +export type DataItem = { + /** Unique identifier for the data item */ + id: number; + /** Primary text or name of the data item */ + title: string; + /** Detailed text description of the data item */ + description: string; + /** Additional optional field for extra information */ + optionalField?: string; + /** Hex color code (e.g., '#FF0000') for visual representation */ + colorHex?: string; + /** Additional details or metadata about the item */ + details?: string; +}; + +/** + * Represents a group of related data items with a common type + * + * @interface ListGroup + */ +export type ListGroup = { + /** The type or category of the group */ + type: string; + /** Array of DataItem objects belonging to this group */ + items: DataItem[]; +}; diff --git a/platform/ui-next/src/components/DateRange/DateRange.tsx b/platform/ui-next/src/components/DateRange/DateRange.tsx new file mode 100644 index 0000000..eebc7b9 --- /dev/null +++ b/platform/ui-next/src/components/DateRange/DateRange.tsx @@ -0,0 +1,153 @@ +import * as React from 'react'; +import { format, parse, isValid } from 'date-fns'; +import { Calendar as CalendarIcon } from 'lucide-react'; +import { cn } from '../../lib/utils'; +import { Calendar } from '../Calendar'; +import * as Popover from '../Popover'; + +export type DatePickerWithRangeProps = { + id: string; + /** YYYYMMDD (19921022) */ + startDate: string; + /** YYYYMMDD (19921022) */ + endDate: string; + /** Callback that received { startDate: string(YYYYMMDD), endDate: string(YYYYMMDD)} */ + onChange: (value: { startDate: string; endDate: string }) => void; +}; + +export function DatePickerWithRange({ + className, + id, + startDate, + endDate, + onChange, + ...props +}: React.HTMLAttributes & DatePickerWithRangeProps) { + const [start, setStart] = React.useState( + startDate ? format(parse(startDate, 'yyyyMMdd', new Date()), 'yyyy-MM-dd') : '' + ); + const [end, setEnd] = React.useState( + endDate ? format(parse(endDate, 'yyyyMMdd', new Date()), 'yyyy-MM-dd') : '' + ); + const [openEnd, setOpenEnd] = React.useState(false); + + const handleStartSelect = (selectedDate: Date | undefined) => { + if (selectedDate) { + const formattedDate = format(selectedDate, 'yyyy-MM-dd'); + setStart(formattedDate); + setOpenEnd(true); + onChange({ + startDate: format(selectedDate, 'yyyyMMdd'), + endDate: end.replace(/-/g, ''), + }); + } + }; + + const handleEndSelect = (selectedDate: Date | undefined) => { + if (selectedDate) { + const formattedDate = format(selectedDate, 'yyyy-MM-dd'); + setEnd(formattedDate); + setOpenEnd(false); + onChange({ + startDate: start.replace(/-/g, ''), + endDate: format(selectedDate, 'yyyyMMdd'), + }); + } + }; + + const handleInputChange = (e: React.ChangeEvent, type: 'start' | 'end') => { + const value = e.target.value; + const date = parse(value, 'yyyy-MM-dd', new Date()); + if (type === 'start') { + setStart(value); + if (isValid(date)) { + handleStartSelect(date); + } + } else { + setEnd(value); + if (isValid(date)) { + handleEndSelect(date); + } + } + }; + + React.useEffect(() => { + setStart(startDate ? format(parse(startDate, 'yyyyMMdd', new Date()), 'yyyy-MM-dd') : ''); + setEnd(endDate ? format(parse(endDate, 'yyyyMMdd', new Date()), 'yyyy-MM-dd') : ''); + }, [startDate, endDate]); + + return ( +
+ + +
+ + handleInputChange(e, 'start')} + className={cn( + 'border-inputfield-main focus:border-inputfield-focus h-[32px] w-full justify-start rounded border bg-black py-[6.5px] pl-[6.5px] pr-[6.5px] text-left text-base font-normal hover:bg-black hover:text-white', + !start && 'text-muted-foreground' + )} + data-cy="input-date-range-start" + /> +
+
+ + + +
+ + + +
+ + handleInputChange(e, 'end')} + className={cn( + 'border-inputfield-main focus:border-inputfield-focus h-full w-full justify-start rounded border bg-black py-[6.5px] pl-[6.5px] pr-[6.5px] text-left text-base font-normal hover:bg-black hover:text-white', + !end && 'text-muted-foreground' + )} + data-cy="input-date-range-end" + /> +
+
+ + + +
+
+ ); +} diff --git a/platform/ui-next/src/components/DateRange/index.ts b/platform/ui-next/src/components/DateRange/index.ts new file mode 100644 index 0000000..fb10eac --- /dev/null +++ b/platform/ui-next/src/components/DateRange/index.ts @@ -0,0 +1,3 @@ +import { DatePickerWithRange } from './DateRange'; + +export { DatePickerWithRange }; diff --git a/platform/ui-next/src/components/Dialog/Dialog.tsx b/platform/ui-next/src/components/Dialog/Dialog.tsx new file mode 100644 index 0000000..8e69250 --- /dev/null +++ b/platform/ui-next/src/components/Dialog/Dialog.tsx @@ -0,0 +1,105 @@ +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { Cross2Icon } from '@radix-ui/react-icons'; + +import { cn } from '../../lib/utils'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = 'DialogHeader'; + +const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = 'DialogFooter'; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/platform/ui-next/src/components/Dialog/index.ts b/platform/ui-next/src/components/Dialog/index.ts new file mode 100644 index 0000000..ee5229d --- /dev/null +++ b/platform/ui-next/src/components/Dialog/index.ts @@ -0,0 +1,25 @@ +import { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} from './Dialog'; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/platform/ui-next/src/components/DisplaySetMessageListTooltip/DisplaySetMessageListTooltip.tsx b/platform/ui-next/src/components/DisplaySetMessageListTooltip/DisplaySetMessageListTooltip.tsx new file mode 100644 index 0000000..31f1d01 --- /dev/null +++ b/platform/ui-next/src/components/DisplaySetMessageListTooltip/DisplaySetMessageListTooltip.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Icons } from '../Icons'; +import { useTranslation } from 'react-i18next'; +import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip'; + +/** + * Displays a tooltip with a list of messages of a displaySet + * @param param0 + * @returns + */ +const DisplaySetMessageListTooltip = ({ messages, id }): React.ReactNode => { + const { t } = useTranslation('Messages'); + if (messages?.size()) { + return ( + <> + + + + +
+
+ {t('Display Set Messages')} +
+
    + {messages.messages.map((message, index) => ( +
  1. + {index + 1}. {t(message.id)} +
  2. + ))} +
+
+
+
+ + ); + } + return <>; +}; + +DisplaySetMessageListTooltip.propTypes = { + messages: PropTypes.object, + id: PropTypes.string, +}; + +export { DisplaySetMessageListTooltip }; diff --git a/platform/ui-next/src/components/DisplaySetMessageListTooltip/index.ts b/platform/ui-next/src/components/DisplaySetMessageListTooltip/index.ts new file mode 100644 index 0000000..b90a5c9 --- /dev/null +++ b/platform/ui-next/src/components/DisplaySetMessageListTooltip/index.ts @@ -0,0 +1,3 @@ +import { DisplaySetMessageListTooltip } from './DisplaySetMessageListTooltip'; + +export { DisplaySetMessageListTooltip }; diff --git a/platform/ui-next/src/components/DoubleSlider/DoubleSlider.tsx b/platform/ui-next/src/components/DoubleSlider/DoubleSlider.tsx new file mode 100644 index 0000000..20a5a14 --- /dev/null +++ b/platform/ui-next/src/components/DoubleSlider/DoubleSlider.tsx @@ -0,0 +1,136 @@ +import React from 'react'; +import * as SliderPrimitive from '@radix-ui/react-slider'; + +import { cn } from '../../lib/utils'; +import { Input } from '../Input'; + +interface DoubleSliderProps { + className?: string; + min: number; + max: number; + step?: number; + defaultValue?: [number, number]; + onValueChange?: (value: [number, number]) => void; + showNumberInputs?: boolean; +} + +const DoubleSlider = React.forwardRef( + ( + { + className, + min, + max, + onValueChange, + step = 1, + defaultValue = [min, max], + showNumberInputs = false, + }, + ref + ) => { + const [value, setValue] = React.useState<[number, number]>(defaultValue); + + const prevDefaultValueRef = React.useRef<[number, number] | null>(null); + + const isInteger = step % 1 === 0; + + React.useEffect(() => { + // Only update if defaultValue has actually changed + if ( + !prevDefaultValueRef.current || + prevDefaultValueRef.current[0] !== defaultValue[0] || + prevDefaultValueRef.current[1] !== defaultValue[1] + ) { + setValue(defaultValue); + prevDefaultValueRef.current = defaultValue; + } + }, [defaultValue]); + + const roundToStep = (num: number): number => { + const inverse = 1 / step; + return Math.round(num * inverse) / inverse; + }; + + const handleSliderChange = React.useCallback( + (newValue: number[]) => { + const clampedValue: [number, number] = [ + roundToStep(Math.max(min, Math.min(newValue[0], max))), + roundToStep(Math.min(max, Math.max(newValue[1], min))), + ]; + setValue(clampedValue); + onValueChange?.(clampedValue); + }, + [min, max, onValueChange, step] + ); + + const handleInputChange = React.useCallback( + (index: 0 | 1, inputValue: string) => { + const newValue = parseFloat(inputValue); + if (!isNaN(newValue)) { + const clampedValue: [number, number] = [...value]; + clampedValue[index] = roundToStep(Math.min(Math.max(newValue, min), max)); + if (index === 0 && clampedValue[0] > clampedValue[1]) { + clampedValue[1] = clampedValue[0]; + } else if (index === 1 && clampedValue[1] < clampedValue[0]) { + clampedValue[0] = clampedValue[1]; + } + setValue(clampedValue); + onValueChange?.(clampedValue); + } + }, + [value, min, max, onValueChange, step] + ); + + const formatValue = (val: number) => { + return isInteger ? Math.round(val) : val; + }; + + return ( +
+ {showNumberInputs && ( + handleInputChange(0, e.target.value)} + onBlur={() => handleInputChange(0, value[0].toString())} + className="w-14" + min={min} + max={max} + step={step} + /> + )} + + + + + + + + {showNumberInputs && ( + handleInputChange(1, e.target.value)} + onBlur={() => handleInputChange(1, value[1].toString())} + className="w-14" + min={min} + max={max} + step={step} + /> + )} +
+ ); + } +); +DoubleSlider.displayName = 'DoubleSlider'; + +export { DoubleSlider }; diff --git a/platform/ui-next/src/components/DoubleSlider/index.tsx b/platform/ui-next/src/components/DoubleSlider/index.tsx new file mode 100644 index 0000000..1c4d718 --- /dev/null +++ b/platform/ui-next/src/components/DoubleSlider/index.tsx @@ -0,0 +1,3 @@ +import { DoubleSlider } from './DoubleSlider'; + +export { DoubleSlider }; diff --git a/platform/ui-next/src/components/DropdownMenu/DropdownMenu.tsx b/platform/ui-next/src/components/DropdownMenu/DropdownMenu.tsx new file mode 100644 index 0000000..bbf8343 --- /dev/null +++ b/platform/ui-next/src/components/DropdownMenu/DropdownMenu.tsx @@ -0,0 +1,191 @@ +import * as React from 'react'; +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { CheckIcon, ChevronRightIcon, DotFilledIcon } from '@radix-ui/react-icons'; + +import { cn } from '../../lib/utils'; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; + +const DropdownMenuSub = DropdownMenuPrimitive.Sub; + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + disabled?: boolean; + } +>(({ className, inset, children, disabled, ...props }, ref) => ( + + {children} + + +)); +DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { + return ( + + ); +}; +DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'; + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}; diff --git a/platform/ui-next/src/components/DropdownMenu/index.ts b/platform/ui-next/src/components/DropdownMenu/index.ts new file mode 100644 index 0000000..e33a795 --- /dev/null +++ b/platform/ui-next/src/components/DropdownMenu/index.ts @@ -0,0 +1,35 @@ +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} from './DropdownMenu'; + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}; diff --git a/platform/ui-next/src/components/Errorboundary/ErrorBoundary.tsx b/platform/ui-next/src/components/Errorboundary/ErrorBoundary.tsx new file mode 100644 index 0000000..0263072 --- /dev/null +++ b/platform/ui-next/src/components/Errorboundary/ErrorBoundary.tsx @@ -0,0 +1,198 @@ +import React, { useState, useEffect } from 'react'; +import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from '../Dialog/Dialog'; +import { ScrollArea } from '../ScrollArea/ScrollArea'; +import { Icons } from '../Icons'; + +const isProduction = process.env.NODE_ENV === 'production'; + +interface ErrorBoundaryError extends Error { + message: string; + stack?: string; +} + +interface DefaultFallbackProps { + error: ErrorBoundaryError; + context: string; + resetErrorBoundary: () => void; +} + +interface ErrorBoundaryProps { + context?: string; + onReset?: () => void; + onError?: (error: ErrorBoundaryError, componentStack: string, context: string) => void; + fallbackComponent?: React.ComponentType; + children: React.ReactNode; + fallbackRoute?: string | null; + isPage?: boolean; +} + +const DefaultFallback = ({ + error, + context, + resetErrorBoundary = () => {}, +}: DefaultFallbackProps) => { + const { t } = useTranslation('ErrorBoundary'); + const [showDetails, setShowDetails] = useState(false); + const title = `${t('Something went wrong')}${!isProduction && ` ${t('in')} ${context}`}.`; + const subtitle = t('Sorry, something went wrong there. Try again.'); + + const copyErrorDetails = () => { + const errorDetails = ` +Context: ${context} +Error Message: ${error.message} +Stack: ${error.stack} + `; + navigator.clipboard.writeText(errorDetails); + toast.success(t('Copied to clipboard')); + }; + + useEffect(() => { + toast.error(title, { + description: subtitle, + action: { + label: t('Show Details'), + onClick: () => setShowDetails(true), + }, + duration: 0, + }); + }, [error]); + + if (isProduction) { + return null; + } + + return ( + <> + + + + +
{title}
+ +
+ + {subtitle} +
+ + +
+
+

+ {t('Context')}: {context} +

+

+ {t('Error Message')}: {error.message} +

+
+                  Stack: {error.stack}
+                
+
+
+
+
+
+ + ); +}; + +const ErrorBoundary = ({ + context = 'OHIF', + onReset = () => {}, + onError = () => {}, + fallbackComponent: FallbackComponent = DefaultFallback, + children, + fallbackRoute = null, + isPage, +}: ErrorBoundaryProps) => { + const [error, setError] = useState(null); + + const onResetHandler = () => { + setError(null); + onReset(); + }; + + // Add error event listener to window + useEffect(() => { + let errorTimeout: NodeJS.Timeout; + + const handleError = (event: ErrorEvent) => { + event.preventDefault(); + clearTimeout(errorTimeout); + errorTimeout = setTimeout(() => { + setError(event.error); + onErrorHandler(event.error, null); + }, 100); + }; + + const handleRejection = (event: PromiseRejectionEvent) => { + event.preventDefault(); + clearTimeout(errorTimeout); + errorTimeout = setTimeout(() => { + setError(event.reason); + onErrorHandler(event.reason, null); + }, 100); + }; + + window.addEventListener('error', handleError); + window.addEventListener('unhandledrejection', handleRejection); + + return () => { + clearTimeout(errorTimeout); + window.removeEventListener('error', handleError); + window.removeEventListener('unhandledrejection', handleRejection); + }; + }, []); + + const onErrorHandler = (error: ErrorBoundaryError, componentStack: string) => { + console.debug(`${context} Error Boundary`, error, componentStack, context); + onError(error, componentStack, context); + }; + + return ( + ( + + )} + onReset={onResetHandler} + onError={onErrorHandler} + > + <> + {children} + {error && ( + setError(null)} + /> + )} + + + ); +}; + +export { ErrorBoundary }; diff --git a/platform/ui-next/src/components/Errorboundary/index.tsx b/platform/ui-next/src/components/Errorboundary/index.tsx new file mode 100644 index 0000000..31fce40 --- /dev/null +++ b/platform/ui-next/src/components/Errorboundary/index.tsx @@ -0,0 +1,3 @@ +import { ErrorBoundary } from './ErrorBoundary'; + +export { ErrorBoundary }; diff --git a/platform/ui-next/src/components/Header/Header.tsx b/platform/ui-next/src/components/Header/Header.tsx new file mode 100644 index 0000000..279165c --- /dev/null +++ b/platform/ui-next/src/components/Header/Header.tsx @@ -0,0 +1,121 @@ +import React, { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; +import classNames from 'classnames'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + Icons, + Button, +} from '../'; + +import NavBar from '../NavBar'; + +// Todo: we should move this component to composition and remove props base + +interface HeaderProps { + children?: ReactNode; + menuOptions: Array<{ + title: string; + icon?: string; + onClick: () => void; + }>; + isReturnEnabled?: boolean; + onClickReturnButton?: () => void; + isSticky?: boolean; + WhiteLabeling?: { + createLogoComponentFn?: (React: any, props: any) => ReactNode; + }; + PatientInfo?: ReactNode; + Secondary?: ReactNode; +} + +function Header({ + children, + menuOptions, + isReturnEnabled = true, + onClickReturnButton, + isSticky = false, + WhiteLabeling, + PatientInfo, + Secondary, + ...props +}: HeaderProps): ReactNode { + const { t } = useTranslation('Header'); + + const onClickReturn = () => { + if (isReturnEnabled && onClickReturnButton) { + onClickReturnButton(); + } + }; + + return ( + +
+
+
+ {isReturnEnabled && } +
+ {WhiteLabeling?.createLogoComponentFn?.(React, props) || } +
+
+
+
{Secondary}
+
+
{children}
+
+
+ {PatientInfo} +
+
+ {/* + + + + + {menuOptions.map((option, index) => { + const IconComponent = option.icon + ? Icons[option.icon as keyof typeof Icons] + : null; + return ( + + {IconComponent && ( + + + + )} + {option.title} + + ); + })} + + */} +
+
+
+
+ ); +} + +export default Header; diff --git a/platform/ui-next/src/components/Header/index.js b/platform/ui-next/src/components/Header/index.js new file mode 100644 index 0000000..6a0251d --- /dev/null +++ b/platform/ui-next/src/components/Header/index.js @@ -0,0 +1,2 @@ +import Header from './Header'; +export { Header }; diff --git a/platform/ui-next/src/components/Icons/Icons.tsx b/platform/ui-next/src/components/Icons/Icons.tsx new file mode 100644 index 0000000..0e1dd5a --- /dev/null +++ b/platform/ui-next/src/components/Icons/Icons.tsx @@ -0,0 +1,741 @@ +import React from 'react'; +import Actions from './Sources/Actions'; +import Add from './Sources/Add'; +import Cancel from './Sources/Cancel'; +import ChevronClosed from './Sources/ChevronClosed'; +import ChevronOpen from './Sources/ChevronOpen'; +import Code from './Sources/Code'; +import ColorChange from './Sources/ColorChange'; +import Controls from './Sources/Controls'; +import Copy from './Sources/Copy'; +import Delete from './Sources/Delete'; +import DicomTagBrowser from './Sources/DicomTagBrowser'; +import DisplayFillAndOutline from './Sources/DisplayFillAndOutline'; +import DisplayFillOnly from './Sources/DisplayFillOnly'; +import DisplayOutlineOnly from './Sources/DisplayOutlineOnly'; +import Download from './Sources/Download'; +import Export from './Sources/Export'; +import EyeHidden from './Sources/EyeHidden'; +import EyeVisible from './Sources/EyeVisible'; +import FeedbackComplete from './Sources/FeedbackComplete'; +import GearSettings from './Sources/GearSettings'; +import Hide from './Sources/Hide'; +import IconMPR from './Sources/IconMPR'; +import Info from './Sources/Info'; +import InfoLink from './Sources/InfoLink'; +import InfoSeries from './Sources/InfoSeries'; +import ListView from './Sources/ListView'; +import LoadingSpinner from './Sources/LoadingSpinner'; +import Lock from './Sources/Lock'; +import Minus from './Sources/Minus'; +import MissingIcon from './Sources/MissingIcon'; +import More from './Sources/More'; +import MultiplePatients from './Sources/MultiplePatients'; +import NavigationPanelReveal from './Sources/NavigationPanelReveal'; +import OHIFLogo from './Sources/OHIFLogo'; +import Patient from './Sources/Patient'; +import Pin from './Sources/Pin'; +import PinFill from './Sources/PinFill'; +import Plus from './Sources/Plus'; +import PowerOff from './Sources/PowerOff'; +import Refresh from './Sources/Refresh'; +import Rename from './Sources/Rename'; +import Series from './Sources/Series'; +import Settings from './Sources/Settings'; +import Show from './Sources/Show'; +import SidePanelCloseLeft from './Sources/SidePanelCloseLeft'; +import SidePanelCloseRight from './Sources/SidePanelCloseRight'; +import SortingAscending from './Sources/SortingAscending'; +import SortingDescending from './Sources/SortingDescending'; +import StatusError from './Sources/StatusError'; +import StatusSuccess from './Sources/StatusSuccess'; +import StatusTracking from './Sources/StatusTracking'; +import StatusUntracked from './Sources/StatusUntracked'; +import StatusWarning from './Sources/StatusWarning'; +import Tab4D from './Sources/Tab4D'; +import TabLinear from './Sources/TabLinear'; +import TabPatientInfo from './Sources/TabPatientInfo'; +import TabRoiThreshold from './Sources/TabRoiThreshold'; +import TabSegmentation from './Sources/TabSegmentation'; +import TabStudies from './Sources/TabStudies'; +import ThumbnailView from './Sources/ThumbnailView'; +import Trash from './Sources/Trash'; +import ViewportViews from './Sources/ViewportViews'; +import Sorting from './Sources/Sorting'; +import Upload from './Sources/Upload'; +import LaunchArrow from './Sources/LaunchArrow'; +import LaunchInfo from './Sources/LaunchInfo'; +import GroupLayers from './Sources/GroupLayers'; +import Database from './Sources/Database'; +import InvestigationalUse from './Sources/InvestigationalUse'; +import IconTransferring from './Sources/IconTransferring'; +import Alert from './Sources/Alert'; +import AlertOutline from './Sources/AlertOutline'; +import Clipboard from './Sources/Clipboard'; +import { + Tool3DRotate, + ToolAngle, + ToolAnnotate, + ToolBidirectional, + ToolCalibrate, + ToolCapture, + ToolCine, + ToolCircle, + ToolCobbAngle, + ToolCreateThreshold, + ToolCrosshair, + ToolDicomTagBrowser, + ToolFlipHorizontal, + ToolFreehandPolygon, + ToolFreehandRoi, + ToolFreehand, + ToolFusionColor, + ToolInvert, + ToolLayoutDefault, + ToolLength, + ToolMagneticRoi, + ToolMagnify, + ToolMeasureEllipse, + ToolMoreMenu, + ToolMove, + ToolPolygon, + ToolQuickMagnify, + ToolRectangle, + ToolReferenceLines, + ToolReset, + ToolRotateRight, + ToolSegBrush, + ToolSegEraser, + ToolSegShape, + ToolSegThreshold, + ToolSplineRoi, + ToolStackImageSync, + ToolStackScroll, + ToolToggleDicomOverlay, + ToolUltrasoundBidirectional, + ToolWindowLevel, + ToolWindowRegion, + ToolZoom, + ToolLayout, + ToolProbe, + ToolEraser, + ToolBrush, + ToolThreshold, + ToolShape, + ToolLabelmapAssist, + ToolPETSegment, + ToolInterpolation, + ToolBidirectionalSegment, + ToolSegmentAnything, + ToolContract, + ToolExpand, +} from './Sources/Tools'; +import ActionNewDialog from './Sources/ActionNewDialog'; +import NotificationInfo from './Sources/NotificationInfo'; +import StatusLocked from './Sources/StatusLocked'; +import ContentPrev from './Sources/ContentPrev'; +import ContentNext from './Sources/ContentNext'; +import CheckBoxChecked from './Sources/CheckBoxChecked'; +import CheckBoxUnchecked from './Sources/CheckBoxUnChecked'; +import Close from './Sources/Close'; +import Pause from './Sources/Pause'; +import Play from './Sources/Play'; +import ViewportWindowLevel from './Sources/ViewportWindowLevel'; +import Search from './Sources/Search'; +import Clear from './Sources/Clear'; +import { + LayoutAdvanced3DOnly, + LayoutAdvanced3DPrimary, + LayoutAdvancedAxialPrimary, + LayoutAdvancedMPR, + LayoutCommon2x2, + LayoutCommon1x1, + LayoutCommon1x2, + LayoutCommon2x3, + LayoutAdvanced3DFourUp, + LayoutAdvanced3DMain, +} from './Sources/Layout'; +import Link from './Sources/Link'; +import IconColorLUT from './Sources/IconColorLUT'; +import CTAAA from '../../../assets/images/CT-AAA.png'; +import CTAAA2 from '../../../assets/images/CT-AAA2.png'; +import CTAir from '../../../assets/images/CT-Air.png'; +import CTBone from '../../../assets/images/CT-Bone.png'; +import CTBones from '../../../assets/images/CT-Bones.png'; +import CTCardiac from '../../../assets/images/CT-Cardiac.png'; +import CTCardiac2 from '../../../assets/images/CT-Cardiac2.png'; +import CTCardiac3 from '../../../assets/images/CT-Cardiac3.png'; +import CTChestContrastEnhanced from '../../../assets/images/CT-Chest-Contrast-Enhanced.png'; +import CTChestVessels from '../../../assets/images/CT-Chest-Vessels.png'; +import CTCoronaryArteries from '../../../assets/images/CT-Coronary-Arteries.png'; +import CTCoronaryArteries2 from '../../../assets/images/CT-Coronary-Arteries-2.png'; +import CTCoronaryArteries3 from '../../../assets/images/CT-Coronary-Arteries-3.png'; +import CTCroppedVolumeBone from '../../../assets/images/CT-Cropped-Volume-Bone.png'; +import CTFat from '../../../assets/images/CT-Fat.png'; +import CTLiverVasculature from '../../../assets/images/CT-Liver-Vasculature.png'; +import CTLung from '../../../assets/images/CT-Lung.png'; +import CTMIP from '../../../assets/images/CT-MIP.png'; +import CTMuscle from '../../../assets/images/CT-Muscle.png'; +import CTPulmonaryArteries from '../../../assets/images/CT-Pulmonary-Arteries.png'; +import CTSoftTissue from '../../../assets/images/CT-Soft-Tissue.png'; +import DTIFABrain from '../../../assets/images/DTI-FA-Brain.png'; +import MRAngio from '../../../assets/images/MR-Angio.png'; +import MRDefault from '../../../assets/images/MR-Default.png'; +import MRMIP from '../../../assets/images/MR-MIP.png'; +import MRT2Brain from '../../../assets/images/MR-T2-Brain.png'; +import VolumeRendering from '../../../assets/images/VolumeRendering.png'; +import ExternalLink from './Sources/ExternalLink'; +import OHIFLogoColorDarkBackground from './Sources/OHIFLogoColorDarkBackground'; +import Magnifier from './Sources/Magnifier'; +import LoadingOHIFMark from './Sources/LoadingOHIFMark'; +import ArrowLeftBold from './Sources/ArrowLeftBold'; +import Pencil from './Sources/Pencil'; +import NotificationWarning from './Sources/NotificationWarning'; +import ArrowRight from './Sources/ArrowRight'; +import ChevronLeft from './Sources/ChevronLeft'; +// +// +type IconProps = React.HTMLAttributes; +type ImageIconProps = React.ImgHTMLAttributes; + +const ImageWrapper = ({ src, ...props }: { src: string } & ImageIconProps) => { + return ( + + ); +}; + +export const Icons = { + 'CT-AAA': (props: ImageIconProps) => ( + + ), + 'CT-AAA2': (props: ImageIconProps) => ( + + ), + 'CT-Air': (props: ImageIconProps) => ( + + ), + 'CT-Bone': (props: ImageIconProps) => ( + + ), + 'CT-Bones': (props: ImageIconProps) => ( + + ), + 'CT-Cardiac': (props: ImageIconProps) => ( + + ), + 'CT-Cardiac2': (props: ImageIconProps) => ( + + ), + 'CT-Cardiac3': (props: ImageIconProps) => ( + + ), + 'CT-Chest-Contrast-Enhanced': (props: ImageIconProps) => ( + + ), + 'CT-Chest-Vessels': (props: ImageIconProps) => ( + + ), + 'CT-Coronary-Arteries': (props: ImageIconProps) => ( + + ), + 'CT-Coronary-Arteries-2': (props: ImageIconProps) => ( + + ), + 'CT-Coronary-Arteries-3': (props: ImageIconProps) => ( + + ), + 'CT-Cropped-Volume-Bone': (props: ImageIconProps) => ( + + ), + 'CT-Fat': (props: ImageIconProps) => ( + + ), + 'CT-Liver-Vasculature': (props: ImageIconProps) => ( + + ), + 'CT-Lung': (props: ImageIconProps) => ( + + ), + 'CT-MIP': (props: ImageIconProps) => ( + + ), + 'CT-Muscle': (props: ImageIconProps) => ( + + ), + 'CT-Pulmonary-Arteries': (props: ImageIconProps) => ( + + ), + 'CT-Soft-Tissue': (props: ImageIconProps) => ( + + ), + 'DTI-FA-Brain': (props: ImageIconProps) => ( + + ), + 'MR-Angio': (props: ImageIconProps) => ( + + ), + 'MR-Default': (props: ImageIconProps) => ( + + ), + 'MR-MIP': (props: ImageIconProps) => ( + + ), + 'MR-T2-Brain': (props: ImageIconProps) => ( + + ), + VolumeRendering: (props: ImageIconProps) => ( + + ), + // Icons + Clipboard, + ActionNewDialog, + GroupLayers, + Database, + InvestigationalUse, + Tool3DRotate, + ToolAngle, + ToolAnnotate, + ToolBidirectional, + ToolCalibrate, + ToolCapture, + ToolCine, + ToolCircle, + ToolCobbAngle, + ToolCreateThreshold, + ToolCrosshair, + ToolDicomTagBrowser, + ToolFlipHorizontal, + ToolFreehandPolygon, + ToolFreehandRoi, + ToolFreehand, + ToolFusionColor, + ToolInvert, + ToolLayoutDefault, + ToolLength, + ToolMagneticRoi, + ToolMagnify, + ToolMeasureEllipse, + ToolMoreMenu, + ToolMove, + ToolPolygon, + ToolQuickMagnify, + ToolRectangle, + ToolReferenceLines, + ToolReset, + ToolRotateRight, + ToolSegBrush, + ToolSegEraser, + ToolSegShape, + ToolSegThreshold, + ToolSplineRoi, + ToolStackImageSync, + ToolStackScroll, + ToolToggleDicomOverlay, + ToolUltrasoundBidirectional, + ToolWindowLevel, + ToolWindowRegion, + ToolZoom, + LaunchArrow, + LaunchInfo, + Upload, + Actions, + Add, + Cancel, + Code, + ColorChange, + Controls, + Copy, + Delete, + DicomTagBrowser, + DisplayFillAndOutline, + DisplayFillOnly, + DisplayOutlineOnly, + FillAndOutline: DisplayFillAndOutline, + FillOnly: DisplayFillOnly, + OutlineOnly: DisplayOutlineOnly, + Download, + Export, + EyeHidden, + EyeVisible, + FeedbackComplete, + GearSettings, + Hide, + IconMPR, + Info, + InfoLink, + InfoSeries, + ListView, + LoadingSpinner, + Lock, + Minus, + MissingIcon, + More, + MultiplePatients, + NavigationPanelReveal, + OHIFLogo, + Patient, + Pin, + PinFill, + Plus, + PowerOff, + Refresh, + Rename, + Series, + Settings, + Show, + SidePanelCloseLeft, + SidePanelCloseRight, + SortingAscending, + SortingDescending, + Sorting, + StatusError, + StatusSuccess, + StatusTracking, + StatusWarning, + StatusUntracked, + Tab4D, + TabLinear, + TabPatientInfo, + TabRoiThreshold, + TabSegmentation, + TabStudies, + ThumbnailView, + Trash, + ViewportViews, + ChevronClosed, + ChevronOpen, + ChevronRight: (props: IconProps) => { + return ( + + ); + }, + ChevronLeft, + ChevronDown: (props: IconProps) => { + return ( + + ); + }, + Alert, + AlertOutline, + NotificationInfo, + StatusLocked, + ContentPrev, + ContentNext, + CheckBoxChecked, + CheckBoxUnchecked, + Close, + Pause, + Play, + Link, + LoadingOHIFMark, + ArrowLeft: ChevronClosed, + ArrowRight, + ArrowLeftBold, + ArrowRightBold: (props: IconProps) => { + return ( + + ); + }, + ArrowDown: (props: IconProps) => { + return ( + + ); + }, + ViewportWindowLevel, + Search, + Clear, + LayoutCommon2x3, + LayoutCommon2x2, + LayoutCommon1x1, + LayoutCommon1x2, + LayoutAdvanced3DFourUp, + LayoutAdvanced3DMain, + LayoutAdvanced3DOnly, + LayoutAdvanced3DPrimary, + LayoutAdvancedAxialPrimary, + LayoutAdvancedMPR, + ToolLayout, + IconColorLUT, + ToolEraser, + ToolBrush, + ToolThreshold, + ToolShape, + ToolLabelmapAssist, + ToolSegmentAnything, + ToolPETSegment, + ToolInterpolation, + ToolBidirectionalSegment, + ToolContract, + ToolExpand, + ExternalLink, + OHIFLogoColorDarkBackground, + Magnifier, + Pencil, + // + // + // + // + // + // + // + // + // + // Aliases + 'prev-arrow': (props: IconProps) => Icons.ArrowLeftBold(props), + 'next-arrow': (props: IconProps) => Icons.ArrowRightBold(props), + 'loading-ohif-mark': (props: IconProps) => LoadingOHIFMark(props), + magnifier: (props: IconProps) => Magnifier(props), + 'status-alert-warning': (props: IconProps) => StatusWarning(props), + 'logo-dark-background': (props: IconProps) => OHIFLogoColorDarkBackground(props), + 'external-link': (props: IconProps) => ExternalLink(props), + 'checkbox-checked': (props: IconProps) => CheckBoxChecked(props), + 'checkbox-unchecked': (props: IconProps) => CheckBoxUnchecked(props), + 'checkbox-default': (props: IconProps) => CheckBoxUnchecked(props), + 'checkbox-active': (props: IconProps) => CheckBoxChecked(props), + 'icon-tool-eraser': (props: IconProps) => ToolEraser(props), + 'icon-tool-brush': (props: IconProps) => ToolBrush(props), + 'icon-tool-labelmap-assist': (props: IconProps) => ToolLabelmapAssist(props), + 'icon-tool-segment-anything': (props: IconProps) => ToolSegmentAnything(props), + 'icon-tool-threshold': (props: IconProps) => ToolThreshold(props), + 'icon-tool-pet-segment': (props: IconProps) => ToolPETSegment(props), + 'icon-tool-interpolation': (props: IconProps) => ToolInterpolation(props), + 'icon-tool-bidirectional-segment': (props: IconProps) => ToolBidirectionalSegment(props), + 'icon-tool-expand': (props: IconProps) => ToolExpand(props), + 'icon-tool-contract': (props: IconProps) => ToolContract(props), + 'icon-tool-shape': (props: IconProps) => ToolShape(props), + link: (props: IconProps) => Link(props), + 'icon-color-lut': (props: IconProps) => IconColorLUT(props), + 'icon-link': (props: IconProps) => Link(props), + 'icon-clear': (props: IconProps) => Clear(props), + 'icon-search': (props: IconProps) => Search(props), + 'viewport-window-level': (props: IconProps) => ViewportWindowLevel(props), + 'action-new-dialog': (props: IconProps) => ActionNewDialog(props), + 'arrow-left': (props: IconProps) => Icons.ArrowLeft(props), + 'arrow-right': (props: IconProps) => Icons.ArrowRight(props), + 'arrow-down': (props: IconProps) => Icons.ArrowDown(props), + 'status-tracked': (props: IconProps) => StatusTracking(props), + 'status-untracked': (props: IconProps) => StatusUntracked(props), + 'status-locked': (props: IconProps) => StatusLocked(props), + 'tab-segmentation': (props: IconProps) => TabSegmentation(props), + 'tab-studies': (props: IconProps) => TabStudies(props), + 'tab-linear': (props: IconProps) => TabLinear(props), + 'tab-4d': (props: IconProps) => Tab4D(props), + 'tab-patient-info': (props: IconProps) => TabPatientInfo(props), + 'tab-roi-threshold': (props: IconProps) => TabRoiThreshold(props), + 'icon-mpr': (props: IconProps) => IconMPR(props), + 'power-off': (props: IconProps) => PowerOff(props), + 'icon-multiple-patients': (props: IconProps) => MultiplePatients(props), + 'icon-patient': (props: IconProps) => Patient(props), + 'chevron-down': (props: IconProps) => ChevronOpen(props), + 'tool-length': (props: IconProps) => ToolLength(props), + 'tool-3d-rotate': (props: IconProps) => Tool3DRotate(props), + 'tool-angle': (props: IconProps) => ToolAngle(props), + 'tool-annotate': (props: IconProps) => ToolAnnotate(props), + 'tool-bidirectional': (props: IconProps) => ToolBidirectional(props), + 'tool-calibration': (props: IconProps) => ToolCalibrate(props), + 'tool-capture': (props: IconProps) => ToolCapture(props), + 'tool-cine': (props: IconProps) => ToolCine(props), + 'tool-circle': (props: IconProps) => ToolCircle(props), + 'tool-cobb-angle': (props: IconProps) => ToolCobbAngle(props), + 'tool-create-threshold': (props: IconProps) => ToolCreateThreshold(props), + 'tool-crosshair': (props: IconProps) => ToolCrosshair(props), + 'dicom-tag-browser': (props: IconProps) => ToolDicomTagBrowser(props), + 'tool-flip-horizontal': (props: IconProps) => ToolFlipHorizontal(props), + 'tool-freehand-polygon': (props: IconProps) => ToolFreehandPolygon(props), + 'tool-freehand-roi': (props: IconProps) => ToolFreehandRoi(props), + 'icon-tool-freehand-roi': (props: IconProps) => ToolFreehandRoi(props), + 'icon-tool-spline-roi': (props: IconProps) => ToolSplineRoi(props), + 'tool-freehand': (props: IconProps) => ToolFreehand(props), + 'tool-fusion-color': (props: IconProps) => ToolFusionColor(props), + 'tool-invert': (props: IconProps) => ToolInvert(props), + 'tool-layout-default': (props: IconProps) => ToolLayoutDefault(props), + 'tool-magnetic-roi': (props: IconProps) => ToolMagneticRoi(props), + 'icon-tool-livewire': (props: IconProps) => ToolMagneticRoi(props), + 'tool-magnify': (props: IconProps) => ToolMagnify(props), + 'tool-measure-ellipse': (props: IconProps) => ToolMeasureEllipse(props), + 'tool-more-menu': (props: IconProps) => ToolMoreMenu(props), + 'tool-move': (props: IconProps) => ToolMove(props), + 'tool-polygon': (props: IconProps) => ToolPolygon(props), + 'tool-ellipse': (props: IconProps) => ToolMeasureEllipse(props), + 'tool-quick-magnify': (props: IconProps) => ToolQuickMagnify(props), + 'tool-rectangle': (props: IconProps) => ToolRectangle(props), + 'tool-referenceLines': (props: IconProps) => ToolReferenceLines(props), + 'tool-reset': (props: IconProps) => ToolReset(props), + 'tool-rotate-right': (props: IconProps) => ToolRotateRight(props), + 'tool-seg-brush': (props: IconProps) => ToolSegBrush(props), + 'tool-seg-eraser': (props: IconProps) => ToolSegEraser(props), + 'tool-seg-shape': (props: IconProps) => ToolSegShape(props), + 'tool-seg-threshold': (props: IconProps) => ToolSegThreshold(props), + 'tool-spline-roi': (props: IconProps) => ToolSplineRoi(props), + 'tool-stack-image-sync': (props: IconProps) => ToolStackImageSync(props), + 'tool-stack-scroll': (props: IconProps) => ToolStackScroll(props), + 'toggle-dicom-overlay': (props: IconProps) => ToolToggleDicomOverlay(props), + 'tool-ultrasound-bidirectional': (props: IconProps) => ToolUltrasoundBidirectional(props), + 'tool-window-level': (props: IconProps) => ToolWindowLevel(props), + 'tool-window-region': (props: IconProps) => ToolWindowRegion(props), + 'icon-tool-window-region': (props: IconProps) => ToolWindowRegion(props), + 'icon-tool-ultrasound-bidirectional': (props: IconProps) => ToolUltrasoundBidirectional(props), + 'icon-tool-cobb-angle': (props: IconProps) => ToolCobbAngle(props), + 'icon-tool-loupe': (props: IconProps) => ToolMagnify(props), + 'tool-probe': (props: IconProps) => ToolProbe(props), + 'icon-tool-probe': (props: IconProps) => ToolProbe(props), + 'tool-zoom': (props: IconProps) => ToolZoom(props), + 'tool-layout': (props: IconProps) => ToolLayout(props), + 'icon-transferring': (props: IconProps) => IconTransferring(props), + 'icon-alert-small': (props: IconProps) => Alert(props), + 'icon-alert-outline': (props: IconProps) => AlertOutline(props), + 'status-alert': (props: IconProps) => Alert(props), + info: (props: IconProps) => Info(props), + 'notifications-info': (props: IconProps) => NotificationInfo(props), + 'notificationwarning-diamond': (props: IconProps) => NotificationWarning(props), + 'content-prev': (props: IconProps) => ContentPrev(props), + 'content-next': (props: IconProps) => ContentNext(props), + 'icon-settings': (props: IconProps) => Settings(props), + close: (props: IconProps) => Close(props), + pause: (props: IconProps) => Pause(props), + 'icon-pause': (props: IconProps) => Pause(props), + settings: (props: IconProps) => Settings(props), + play: (props: IconProps) => Play(props), + 'icon-play': (props: IconProps) => Play(props), + 'layout-advanced-3d-four-up': (props: IconProps) => LayoutAdvanced3DFourUp(props), + 'layout-advanced-3d-main': (props: IconProps) => LayoutAdvanced3DMain(props), + 'layout-advanced-3d-only': (props: IconProps) => LayoutAdvanced3DOnly(props), + 'layout-advanced-3d-primary': (props: IconProps) => LayoutAdvanced3DPrimary(props), + 'layout-advanced-axial-primary': (props: IconProps) => LayoutAdvancedAxialPrimary(props), + 'layout-advanced-mpr': (props: IconProps) => LayoutAdvancedMPR(props), + 'layout-common-1x1': (props: IconProps) => LayoutCommon1x1(props), + 'layout-common-1x2': (props: IconProps) => LayoutCommon1x2(props), + 'layout-common-2x2': (props: IconProps) => LayoutCommon2x2(props), + 'layout-common-2x3': (props: IconProps) => LayoutCommon2x3(props), + pencil: (props: IconProps) => Pencil(props), + 'icon-list-view': (props: IconProps) => ListView(props), + 'chevron-menu': 'chevron-down', + 'icon-status-alert': (props: IconProps) => Alert(props), + 'info-link': (props: IconProps) => InfoLink(props), + 'launch-info': (props: IconProps) => LaunchInfo(props), + 'old-trash': (props: IconProps) => Trash(props), + 'tool-point': (props: IconProps) => ToolCircle(props), + 'tool-freehand-line': (props: IconProps) => ToolFreehand(props), + clipboard: (props: IconProps) => Clipboard(props), + + /** Adds an icon to the set of icons */ + addIcon: (name: string, icon) => { + if (Icons[name]) { + console.warn('Replacing icon', name); + } + Icons[name] = icon; + }, + + ByName: ({ name, className, ...props }: { name: string; className?: string }) => { + const IconComponent = Icons[name]; + + if (!IconComponent) { + console.debug(`Icon "${name}" not found.`); + return
Missing Icon
; + } + + return ( + + ); + }, +}; diff --git a/platform/ui-next/src/components/Icons/Sources/ActionNewDialog.tsx b/platform/ui-next/src/components/Icons/Sources/ActionNewDialog.tsx new file mode 100644 index 0000000..be4065e --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/ActionNewDialog.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const ActionNewDialog = (props: IconProps) => ( + + + + + + + + + + + + +); + +export default ActionNewDialog; diff --git a/platform/ui-next/src/components/Icons/Sources/Actions.tsx b/platform/ui-next/src/components/Icons/Sources/Actions.tsx new file mode 100644 index 0000000..63f2afd --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Actions.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Actions = (props: IconProps) => ( + + + + + + + + + +); + +export default Actions; diff --git a/platform/ui-next/src/components/Icons/Sources/Add.tsx b/platform/ui-next/src/components/Icons/Sources/Add.tsx new file mode 100644 index 0000000..6035dca --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Add.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Add = (props: IconProps) => ( + + + + + + + + + +); + +export default Add; diff --git a/platform/ui-next/src/components/Icons/Sources/Alert.tsx b/platform/ui-next/src/components/Icons/Sources/Alert.tsx new file mode 100644 index 0000000..50b5443 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Alert.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Alert = (props: IconProps) => ( + + + + + + + + +); + +export default Alert; diff --git a/platform/ui-next/src/components/Icons/Sources/AlertOutline.tsx b/platform/ui-next/src/components/Icons/Sources/AlertOutline.tsx new file mode 100644 index 0000000..c7e5883 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/AlertOutline.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const AlertOutline = (props: IconProps) => ( + + + + + + + + +); + +export default AlertOutline; diff --git a/platform/ui-next/src/components/Icons/Sources/ArrowLeftBold.tsx b/platform/ui-next/src/components/Icons/Sources/ArrowLeftBold.tsx new file mode 100644 index 0000000..5fe55ef --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/ArrowLeftBold.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const ArrowLeftBold = (props: IconProps) => ( + + arrow-left + + + + + + + + + +); + +export default ArrowLeftBold; diff --git a/platform/ui-next/src/components/Icons/Sources/ArrowRight.tsx b/platform/ui-next/src/components/Icons/Sources/ArrowRight.tsx new file mode 100644 index 0000000..526d093 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/ArrowRight.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const ArrowLeftBold = (props: IconProps) => ( + + + + + + +); + +export default ArrowLeftBold; diff --git a/platform/ui-next/src/components/Icons/Sources/Cancel.tsx b/platform/ui-next/src/components/Icons/Sources/Cancel.tsx new file mode 100644 index 0000000..d273641 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Cancel.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Cancel = (props: IconProps) => ( + + + + + + + + +); + +export default Cancel; diff --git a/platform/ui-next/src/components/Icons/Sources/CheckBoxChecked.tsx b/platform/ui-next/src/components/Icons/Sources/CheckBoxChecked.tsx new file mode 100644 index 0000000..9328367 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/CheckBoxChecked.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const CheckBoxChecked = (props: IconProps) => ( + + + + + + + + + +); + +export default CheckBoxChecked; diff --git a/platform/ui-next/src/components/Icons/Sources/CheckBoxUnChecked.tsx b/platform/ui-next/src/components/Icons/Sources/CheckBoxUnChecked.tsx new file mode 100644 index 0000000..776a8a7 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/CheckBoxUnChecked.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const CheckBoxUnchecked = (props: IconProps) => ( + + + +); + +export default CheckBoxUnchecked; diff --git a/platform/ui-next/src/components/Icons/Sources/ChevronClosed.tsx b/platform/ui-next/src/components/Icons/Sources/ChevronClosed.tsx new file mode 100644 index 0000000..1e33c3d --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/ChevronClosed.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const ChevronClosed = (props: IconProps) => ( + + + + + + +); + +export default ChevronClosed; diff --git a/platform/ui-next/src/components/Icons/Sources/ChevronLeft.tsx b/platform/ui-next/src/components/Icons/Sources/ChevronLeft.tsx new file mode 100644 index 0000000..6ec6843 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/ChevronLeft.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const ChevronLeft = (props: IconProps) => ( + + + + + + +); + +export default ChevronLeft; diff --git a/platform/ui-next/src/components/Icons/Sources/ChevronOpen.tsx b/platform/ui-next/src/components/Icons/Sources/ChevronOpen.tsx new file mode 100644 index 0000000..49e70fa --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/ChevronOpen.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const ChevronOpen = (props: IconProps) => ( + + + + + + +); + +export default ChevronOpen; diff --git a/platform/ui-next/src/components/Icons/Sources/Clear.tsx b/platform/ui-next/src/components/Icons/Sources/Clear.tsx new file mode 100644 index 0000000..bceffb9 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Clear.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Clear = (props: IconProps) => ( + + + + + + + + +); + +export default Clear; diff --git a/platform/ui-next/src/components/Icons/Sources/Clipboard.tsx b/platform/ui-next/src/components/Icons/Sources/Clipboard.tsx new file mode 100644 index 0000000..1fdcc50 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Clipboard.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Clipboard = (props: IconProps) => ( + + + +); + +export default Clipboard; diff --git a/platform/ui-next/src/components/Icons/Sources/Close.tsx b/platform/ui-next/src/components/Icons/Sources/Close.tsx new file mode 100644 index 0000000..308317d --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Close.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Close = (props: IconProps) => ( + + + +); + +export default Close; diff --git a/platform/ui-next/src/components/Icons/Sources/Code.tsx b/platform/ui-next/src/components/Icons/Sources/Code.tsx new file mode 100644 index 0000000..e14cddf --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Code.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { Code as LucideCode, LucideProps } from 'lucide-react'; +import type { IconProps } from '../types'; + +export const Code = (props: LucideProps) => ; + +export default Code; diff --git a/platform/ui-next/src/components/Icons/Sources/ColorChange.tsx b/platform/ui-next/src/components/Icons/Sources/ColorChange.tsx new file mode 100644 index 0000000..089ec40 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/ColorChange.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const ColorChange = (props: IconProps) => ( + + + + + + + + + + + + + + + + + + + + + + +); + +export default ColorChange; diff --git a/platform/ui-next/src/components/Icons/Sources/ContentNext.tsx b/platform/ui-next/src/components/Icons/Sources/ContentNext.tsx new file mode 100644 index 0000000..7c4fbba --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/ContentNext.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const ContentNext = (props: IconProps) => ( + + + + + + + +); + +export default ContentNext; diff --git a/platform/ui-next/src/components/Icons/Sources/ContentPrev.tsx b/platform/ui-next/src/components/Icons/Sources/ContentPrev.tsx new file mode 100644 index 0000000..c25ad6e --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/ContentPrev.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const ContentPrev = (props: IconProps) => ( + + + + + + + +); + +export default ContentPrev; diff --git a/platform/ui-next/src/components/Icons/Sources/Controls.tsx b/platform/ui-next/src/components/Icons/Sources/Controls.tsx new file mode 100644 index 0000000..427b39f --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Controls.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Controls = (props: IconProps) => ( + + + + + + + + + + + + + + + +); + +export default Controls; diff --git a/platform/ui-next/src/components/Icons/Sources/Copy.tsx b/platform/ui-next/src/components/Icons/Sources/Copy.tsx new file mode 100644 index 0000000..35198c4 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Copy.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Copy = (props: IconProps) => ( + + + + + + + +); + +export default Copy; diff --git a/platform/ui-next/src/components/Icons/Sources/Database.tsx b/platform/ui-next/src/components/Icons/Sources/Database.tsx new file mode 100644 index 0000000..0e143ab --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Database.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Database = (props: IconProps) => ( + + + + + + + +); + +export default Database; diff --git a/platform/ui-next/src/components/Icons/Sources/Delete.tsx b/platform/ui-next/src/components/Icons/Sources/Delete.tsx new file mode 100644 index 0000000..acded2d --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Delete.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Delete = (props: IconProps) => ( + + + + + + + + +); + +export default Delete; diff --git a/platform/ui-next/src/components/Icons/Sources/DicomTagBrowser.tsx b/platform/ui-next/src/components/Icons/Sources/DicomTagBrowser.tsx new file mode 100644 index 0000000..619a6c9 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/DicomTagBrowser.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const DicomTagBrowser = (props: IconProps) => ( + + + + + + + + + + + + + +); + +export default DicomTagBrowser; diff --git a/platform/ui-next/src/components/Icons/Sources/DisplayFillAndOutline.tsx b/platform/ui-next/src/components/Icons/Sources/DisplayFillAndOutline.tsx new file mode 100644 index 0000000..997dd1d --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/DisplayFillAndOutline.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const DisplayFillAndOutline = (props: IconProps) => ( + + + + + + + + + +); + +export default DisplayFillAndOutline; diff --git a/platform/ui-next/src/components/Icons/Sources/DisplayFillOnly.tsx b/platform/ui-next/src/components/Icons/Sources/DisplayFillOnly.tsx new file mode 100644 index 0000000..8640ea3 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/DisplayFillOnly.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const DisplayFillOnly = (props: IconProps) => ( + + + + + + + + +); + +export default DisplayFillOnly; diff --git a/platform/ui-next/src/components/Icons/Sources/DisplayOutlineOnly.tsx b/platform/ui-next/src/components/Icons/Sources/DisplayOutlineOnly.tsx new file mode 100644 index 0000000..718ea18 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/DisplayOutlineOnly.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const DisplayOutlineOnly = (props: IconProps) => ( + + + + + + + + +); + +export default DisplayOutlineOnly; diff --git a/platform/ui-next/src/components/Icons/Sources/Download.tsx b/platform/ui-next/src/components/Icons/Sources/Download.tsx new file mode 100644 index 0000000..73a7084 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Download.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Download = (props: IconProps) => ( + + + + + + + + + +); + +export default Download; diff --git a/platform/ui-next/src/components/Icons/Sources/Export.tsx b/platform/ui-next/src/components/Icons/Sources/Export.tsx new file mode 100644 index 0000000..7783722 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Export.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Export = (props: IconProps) => ( + + + + + + + + +); + +export default Export; diff --git a/platform/ui-next/src/components/Icons/Sources/ExternalLink.tsx b/platform/ui-next/src/components/Icons/Sources/ExternalLink.tsx new file mode 100644 index 0000000..08cb4bd --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/ExternalLink.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const ExternalLink = (props: IconProps) => ( + + + + + +); + +export default ExternalLink; diff --git a/platform/ui-next/src/components/Icons/Sources/EyeHidden.tsx b/platform/ui-next/src/components/Icons/Sources/EyeHidden.tsx new file mode 100644 index 0000000..61037e5 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/EyeHidden.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const EyeHidden = (props: IconProps) => ( + + + + + + + + +); + +export default EyeHidden; diff --git a/platform/ui-next/src/components/Icons/Sources/EyeVisible.tsx b/platform/ui-next/src/components/Icons/Sources/EyeVisible.tsx new file mode 100644 index 0000000..f1570c0 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/EyeVisible.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const EyeVisible = (props: IconProps) => ( + + + + + + +); + +export default EyeVisible; diff --git a/platform/ui-next/src/components/Icons/Sources/FeedbackComplete.tsx b/platform/ui-next/src/components/Icons/Sources/FeedbackComplete.tsx new file mode 100644 index 0000000..a3df59a --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/FeedbackComplete.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const FeedbackComplete = (props: IconProps) => ( + + + + + + +); + +export default FeedbackComplete; diff --git a/platform/ui-next/src/components/Icons/Sources/GearSettings.tsx b/platform/ui-next/src/components/Icons/Sources/GearSettings.tsx new file mode 100644 index 0000000..d2bc946 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/GearSettings.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const GearSettings = (props: IconProps) => ( + + + + + + + + + + + +); + +export default GearSettings; diff --git a/platform/ui-next/src/components/Icons/Sources/GroupLayers.tsx b/platform/ui-next/src/components/Icons/Sources/GroupLayers.tsx new file mode 100644 index 0000000..286c61e --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/GroupLayers.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const GroupLayers = (props: IconProps) => ( + + + + + + +); + +export default GroupLayers; diff --git a/platform/ui-next/src/components/Icons/Sources/Hide.tsx b/platform/ui-next/src/components/Icons/Sources/Hide.tsx new file mode 100644 index 0000000..df27b66 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Hide.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Hide = (props: IconProps) => ( + + + + + + + +); + +export default Hide; diff --git a/platform/ui-next/src/components/Icons/Sources/IconColorLUT.tsx b/platform/ui-next/src/components/Icons/Sources/IconColorLUT.tsx new file mode 100644 index 0000000..4571819 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/IconColorLUT.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const IconColorLUT = (props: IconProps) => ( + + + + + + + + + + + + + + + + + + + + + +); + +export default IconColorLUT; diff --git a/platform/ui-next/src/components/Icons/Sources/IconMPR.tsx b/platform/ui-next/src/components/Icons/Sources/IconMPR.tsx new file mode 100644 index 0000000..28dcaa7 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/IconMPR.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const IconMPR = (props: IconProps) => ( + + info-mpr + + + + + + + + + +); + +export default IconMPR; diff --git a/platform/ui-next/src/components/Icons/Sources/IconTransferring.tsx b/platform/ui-next/src/components/Icons/Sources/IconTransferring.tsx new file mode 100644 index 0000000..da55168 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/IconTransferring.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const IconTransferring = (props: IconProps) => ( + + + + + + +); + +export default IconTransferring; diff --git a/platform/ui-next/src/components/Icons/Sources/Info.tsx b/platform/ui-next/src/components/Icons/Sources/Info.tsx new file mode 100644 index 0000000..c72a2e5 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Info.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Info = (props: IconProps) => ( + + + + + + + + + +); + +export default Info; diff --git a/platform/ui-next/src/components/Icons/Sources/InfoLink.tsx b/platform/ui-next/src/components/Icons/Sources/InfoLink.tsx new file mode 100644 index 0000000..d7cd68a --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/InfoLink.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const InfoLink = (props: IconProps) => ( + + + + + + + +); + +export default InfoLink; diff --git a/platform/ui-next/src/components/Icons/Sources/InfoSeries.tsx b/platform/ui-next/src/components/Icons/Sources/InfoSeries.tsx new file mode 100644 index 0000000..140d2e4 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/InfoSeries.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const InfoSeries = (props: IconProps) => ( + + info-series + + + + + + + + +); + +export default InfoSeries; diff --git a/platform/ui-next/src/components/Icons/Sources/InvestigationalUse.tsx b/platform/ui-next/src/components/Icons/Sources/InvestigationalUse.tsx new file mode 100644 index 0000000..65b5a7a --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/InvestigationalUse.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const InvestigationalUse = (props: IconProps) => ( + + + + + + + + + + + + + + + + + + +); + +export default InvestigationalUse; diff --git a/platform/ui-next/src/components/Icons/Sources/LaunchArrow.tsx b/platform/ui-next/src/components/Icons/Sources/LaunchArrow.tsx new file mode 100644 index 0000000..31a4f93 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/LaunchArrow.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const LaunchArrow = (props: IconProps) => ( + + + + + + + +); + +export default LaunchArrow; diff --git a/platform/ui-next/src/components/Icons/Sources/LaunchInfo.tsx b/platform/ui-next/src/components/Icons/Sources/LaunchInfo.tsx new file mode 100644 index 0000000..69a0632 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/LaunchInfo.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const LaunchInfo = (props: IconProps) => ( + + + + + + + + +); + +export default LaunchInfo; diff --git a/platform/ui-next/src/components/Icons/Sources/Layout.tsx b/platform/ui-next/src/components/Icons/Sources/Layout.tsx new file mode 100644 index 0000000..958d26f --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Layout.tsx @@ -0,0 +1,433 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const LayoutAdvanced3DFourUp = (props: IconProps) => ( + + + + + + + + + + + +); + +export const LayoutAdvanced3DMain = (props: IconProps) => ( + + + + + + + + + + +); + +export const LayoutAdvanced3DOnly = (props: IconProps) => ( + + + + + + + +); + +export const LayoutAdvanced3DPrimary = (props: IconProps) => ( + + + + + + + + + + +); + +export const LayoutAdvancedAxialPrimary = (props: IconProps) => ( + + + + + + + + + +); + +export const LayoutAdvancedMPR = (props: IconProps) => ( + + + + + + + + + +); + +export const LayoutCommon1x1 = (props: IconProps) => ( + + + + + + + +); + +export const LayoutCommon1x2 = (props: IconProps) => ( + + + + + + + + +); + +export const LayoutCommon2x2 = (props: IconProps) => ( + + + + + + + + + +); + +export const LayoutCommon2x3 = (props: IconProps) => ( + + + + + + + + + + +); diff --git a/platform/ui-next/src/components/Icons/Sources/Link.tsx b/platform/ui-next/src/components/Icons/Sources/Link.tsx new file mode 100644 index 0000000..0d29c93 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Link.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Link = (props: IconProps) => ( + + + + + + + + +); + +export default Link; diff --git a/platform/ui-next/src/components/Icons/Sources/ListView.tsx b/platform/ui-next/src/components/Icons/Sources/ListView.tsx new file mode 100644 index 0000000..e1dc7c7 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/ListView.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const ListView = (props: IconProps) => ( + + + + + + + + + + + +); + +export default ListView; diff --git a/platform/ui-next/src/components/Icons/Sources/LoadingOHIFMark.tsx b/platform/ui-next/src/components/Icons/Sources/LoadingOHIFMark.tsx new file mode 100644 index 0000000..b9e8cdb --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/LoadingOHIFMark.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const LoadingOHIFMark = (props: IconProps) => ( + + + + + +); + +export default LoadingOHIFMark; diff --git a/platform/ui-next/src/components/Icons/Sources/LoadingSpinner.tsx b/platform/ui-next/src/components/Icons/Sources/LoadingSpinner.tsx new file mode 100644 index 0000000..45b8fd2 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/LoadingSpinner.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const LoadingSpinner = (props: IconProps) => ( + + + + + + + + + +); + +export default LoadingSpinner; diff --git a/platform/ui-next/src/components/Icons/Sources/Lock.tsx b/platform/ui-next/src/components/Icons/Sources/Lock.tsx new file mode 100644 index 0000000..b8a844b --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Lock.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Lock = (props: IconProps) => ( + + + + + + +); + +export default Lock; diff --git a/platform/ui-next/src/components/Icons/Sources/Magnifier.tsx b/platform/ui-next/src/components/Icons/Sources/Magnifier.tsx new file mode 100644 index 0000000..bce9400 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Magnifier.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +const Magnifier = (props: IconProps) => ( + + + + + + + +); + +export default Magnifier; diff --git a/platform/ui-next/src/components/Icons/Sources/Minus.tsx b/platform/ui-next/src/components/Icons/Sources/Minus.tsx new file mode 100644 index 0000000..368c5fa --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Minus.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Minus = (props: IconProps) => ( + + + + + + +); + +export default Minus; diff --git a/platform/ui-next/src/components/Icons/Sources/MissingIcon.tsx b/platform/ui-next/src/components/Icons/Sources/MissingIcon.tsx new file mode 100644 index 0000000..65a561d --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/MissingIcon.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const MissingIcon = (props: IconProps) =>
Missing icon
; + +export default MissingIcon; diff --git a/platform/ui-next/src/components/Icons/Sources/More.tsx b/platform/ui-next/src/components/Icons/Sources/More.tsx new file mode 100644 index 0000000..02988a0 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/More.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const More = (props: IconProps) => ( + + icon-more + + + + + + + +); + +export default More; diff --git a/platform/ui-next/src/components/Icons/Sources/MultiplePatients.tsx b/platform/ui-next/src/components/Icons/Sources/MultiplePatients.tsx new file mode 100644 index 0000000..5886779 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/MultiplePatients.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const MultiplePatients = (props: IconProps) => ( + + icon-multiple-patients + + + + + + + + + + + + + + + + + + +); + +export default MultiplePatients; diff --git a/platform/ui-next/src/components/Icons/Sources/NavigationPanelReveal.tsx b/platform/ui-next/src/components/Icons/Sources/NavigationPanelReveal.tsx new file mode 100644 index 0000000..e5eb571 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/NavigationPanelReveal.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const NavigationPanelReveal = (props: IconProps) => ( + + + + + +); + +export default NavigationPanelReveal; diff --git a/platform/ui-next/src/components/Icons/Sources/NotificationInfo.tsx b/platform/ui-next/src/components/Icons/Sources/NotificationInfo.tsx new file mode 100644 index 0000000..8bf8dc1 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/NotificationInfo.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const NotificationInfo = (props: IconProps) => ( + + + + + + + + +); + +export default NotificationInfo; diff --git a/platform/ui-next/src/components/Icons/Sources/NotificationWarning.tsx b/platform/ui-next/src/components/Icons/Sources/NotificationWarning.tsx new file mode 100644 index 0000000..aafaa92 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/NotificationWarning.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const NotificationInfo = (props: IconProps) => ( + + + + + + + + + + +); + +export default NotificationInfo; diff --git a/platform/ui-next/src/components/Icons/Sources/OHIFLogo.tsx b/platform/ui-next/src/components/Icons/Sources/OHIFLogo.tsx new file mode 100644 index 0000000..34fb295 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/OHIFLogo.tsx @@ -0,0 +1,122 @@ +// import React from 'react'; +// import type { IconProps } from '../types'; + +// export const OHIFLogo = (props: IconProps) => ( +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// ); + +// export default OHIFLogo; + +// * UBAH DENGAN LOGO SISMEDIKA +import React from 'react'; +import type { IconProps } from '../types'; + +export const OHIFLogo = (props: IconProps) => ( + + + +); + +export default OHIFLogo; diff --git a/platform/ui-next/src/components/Icons/Sources/OHIFLogoColorDarkBackground.tsx b/platform/ui-next/src/components/Icons/Sources/OHIFLogoColorDarkBackground.tsx new file mode 100644 index 0000000..15f02d7 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/OHIFLogoColorDarkBackground.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const OHIFLogoColorDarkBackground = (props: IconProps) => ( + + + + + + + + + + + + + + + + + + +); + +export default OHIFLogoColorDarkBackground; diff --git a/platform/ui-next/src/components/Icons/Sources/Patient.tsx b/platform/ui-next/src/components/Icons/Sources/Patient.tsx new file mode 100644 index 0000000..bf6be5f --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Patient.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Patient = (props: IconProps) => ( + + icon-patient + + + + + + + + + + +); + +export default Patient; diff --git a/platform/ui-next/src/components/Icons/Sources/Pause.tsx b/platform/ui-next/src/components/Icons/Sources/Pause.tsx new file mode 100644 index 0000000..065682e --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Pause.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Pause = (props: IconProps) => ( + + + + + + +); + +export default Pause; diff --git a/platform/ui-next/src/components/Icons/Sources/Pencil.tsx b/platform/ui-next/src/components/Icons/Sources/Pencil.tsx new file mode 100644 index 0000000..11fdf52 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Pencil.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Pencil = (props: IconProps) => ( + + + +); + +export default Pencil; diff --git a/platform/ui-next/src/components/Icons/Sources/Pin.tsx b/platform/ui-next/src/components/Icons/Sources/Pin.tsx new file mode 100644 index 0000000..6d367b3 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Pin.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Pin = (props: IconProps) => ( + + icon-pin + + + + + + +); + +export default Pin; diff --git a/platform/ui-next/src/components/Icons/Sources/PinFill.tsx b/platform/ui-next/src/components/Icons/Sources/PinFill.tsx new file mode 100644 index 0000000..9a4e9c3 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/PinFill.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const PinFill = (props: IconProps) => ( + + icon-pin-fill + + + + + + +); + +export default PinFill; diff --git a/platform/ui-next/src/components/Icons/Sources/Play.tsx b/platform/ui-next/src/components/Icons/Sources/Play.tsx new file mode 100644 index 0000000..2364b12 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Play.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Play = (props: IconProps) => ( + + + + + + +); + +export default Play; diff --git a/platform/ui-next/src/components/Icons/Sources/Plus.tsx b/platform/ui-next/src/components/Icons/Sources/Plus.tsx new file mode 100644 index 0000000..60f2cd7 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Plus.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Plus = (props: IconProps) => ( + + + + + + + + +); + +export default Plus; diff --git a/platform/ui-next/src/components/Icons/Sources/PowerOff.tsx b/platform/ui-next/src/components/Icons/Sources/PowerOff.tsx new file mode 100644 index 0000000..4de9a9f --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/PowerOff.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const PowerOff = (props: IconProps) => ( + + Power Off + + +); + +export default PowerOff; diff --git a/platform/ui-next/src/components/Icons/Sources/Refresh.tsx b/platform/ui-next/src/components/Icons/Sources/Refresh.tsx new file mode 100644 index 0000000..79c5ae9 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Refresh.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Refresh = (props: IconProps) => ( + + + + + + + +); + +export default Refresh; diff --git a/platform/ui-next/src/components/Icons/Sources/Rename.tsx b/platform/ui-next/src/components/Icons/Sources/Rename.tsx new file mode 100644 index 0000000..834ea9d --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Rename.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Rename = (props: IconProps) => ( + + + + + + + + + + +); + +export default Rename; diff --git a/platform/ui-next/src/components/Icons/Sources/Search.tsx b/platform/ui-next/src/components/Icons/Sources/Search.tsx new file mode 100644 index 0000000..f606aec --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Search.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Search = (props: IconProps) => ( + + + + + + + + + +); + +export default Search; diff --git a/platform/ui-next/src/components/Icons/Sources/Series.tsx b/platform/ui-next/src/components/Icons/Sources/Series.tsx new file mode 100644 index 0000000..a4b3465 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Series.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Series = (props: IconProps) => ( + + + + + + + +); + +export default Series; diff --git a/platform/ui-next/src/components/Icons/Sources/Settings.tsx b/platform/ui-next/src/components/Icons/Sources/Settings.tsx new file mode 100644 index 0000000..beaeed3 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Settings.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Settings = (props: IconProps) => ( + + + + + + + + + + + + + + + + + + + +); + +export default Settings; diff --git a/platform/ui-next/src/components/Icons/Sources/Show.tsx b/platform/ui-next/src/components/Icons/Sources/Show.tsx new file mode 100644 index 0000000..e0686de --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Show.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Show = (props: IconProps) => ( + + + + + + + + + + +); + +export default Show; diff --git a/platform/ui-next/src/components/Icons/Sources/SidePanelCloseLeft.tsx b/platform/ui-next/src/components/Icons/Sources/SidePanelCloseLeft.tsx new file mode 100644 index 0000000..886d831 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/SidePanelCloseLeft.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const SidePanelCloseLeft = (props: IconProps) => ( + + icon-panel-close-left + + + + + + + + + + + +); + +export default SidePanelCloseLeft; diff --git a/platform/ui-next/src/components/Icons/Sources/SidePanelCloseRight.tsx b/platform/ui-next/src/components/Icons/Sources/SidePanelCloseRight.tsx new file mode 100644 index 0000000..2d5ec86 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/SidePanelCloseRight.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const SidePanelCloseRight = (props: IconProps) => ( + + + + + + + + + + + + +); + +export default SidePanelCloseRight; diff --git a/platform/ui-next/src/components/Icons/Sources/Sorting.tsx b/platform/ui-next/src/components/Icons/Sources/Sorting.tsx new file mode 100644 index 0000000..819c77c --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Sorting.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const SortingDescending = (props: IconProps) => ( + + + + + + + + + + +); + +export default SortingDescending; diff --git a/platform/ui-next/src/components/Icons/Sources/SortingAscending.tsx b/platform/ui-next/src/components/Icons/Sources/SortingAscending.tsx new file mode 100644 index 0000000..2aa5021 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/SortingAscending.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const SortingAscending = (props: IconProps) => ( + + + + + + +); + +export default SortingAscending; diff --git a/platform/ui-next/src/components/Icons/Sources/SortingDescending.tsx b/platform/ui-next/src/components/Icons/Sources/SortingDescending.tsx new file mode 100644 index 0000000..d8dfd5d --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/SortingDescending.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const SortingDescending = (props: IconProps) => ( + + + + + + +); + +export default SortingDescending; diff --git a/platform/ui-next/src/components/Icons/Sources/StatusError.tsx b/platform/ui-next/src/components/Icons/Sources/StatusError.tsx new file mode 100644 index 0000000..07e42df --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/StatusError.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const StatusError = (props: IconProps) => ( + + + + + + + + + + +); + +export default StatusError; diff --git a/platform/ui-next/src/components/Icons/Sources/StatusLocked.tsx b/platform/ui-next/src/components/Icons/Sources/StatusLocked.tsx new file mode 100644 index 0000000..b448372 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/StatusLocked.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const StatusLocked = (props: IconProps) => ( + + + + + + + +); + +export default StatusLocked; diff --git a/platform/ui-next/src/components/Icons/Sources/StatusSuccess.tsx b/platform/ui-next/src/components/Icons/Sources/StatusSuccess.tsx new file mode 100644 index 0000000..f1fc07f --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/StatusSuccess.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const StatusSuccess = (props: IconProps) => ( + + + + + + + +); + +export default StatusSuccess; diff --git a/platform/ui-next/src/components/Icons/Sources/StatusTracking.tsx b/platform/ui-next/src/components/Icons/Sources/StatusTracking.tsx new file mode 100644 index 0000000..5626e1b --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/StatusTracking.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const StatusTracking = (props: IconProps) => ( + + + + + + + +); + +export default StatusTracking; diff --git a/platform/ui-next/src/components/Icons/Sources/StatusUntracked.tsx b/platform/ui-next/src/components/Icons/Sources/StatusUntracked.tsx new file mode 100644 index 0000000..0afe693 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/StatusUntracked.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const StatusUntracked = (props: IconProps) => ( + + + + + + +); + +export default StatusUntracked; diff --git a/platform/ui-next/src/components/Icons/Sources/StatusWarning.tsx b/platform/ui-next/src/components/Icons/Sources/StatusWarning.tsx new file mode 100644 index 0000000..8698cee --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/StatusWarning.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const StatusWarning = (props: IconProps) => ( + + + + + + + + + + + + +); + +export default StatusWarning; diff --git a/platform/ui-next/src/components/Icons/Sources/Tab4D.tsx b/platform/ui-next/src/components/Icons/Sources/Tab4D.tsx new file mode 100644 index 0000000..1c5c40d --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Tab4D.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Tab4D = (props: IconProps) => ( + + tab-4d + + + + + + + + + + +); + +export default Tab4D; diff --git a/platform/ui-next/src/components/Icons/Sources/TabLinear.tsx b/platform/ui-next/src/components/Icons/Sources/TabLinear.tsx new file mode 100644 index 0000000..79b5684 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/TabLinear.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const TabLinear = (props: IconProps) => ( + + + + + + + + +); + +export default TabLinear; diff --git a/platform/ui-next/src/components/Icons/Sources/TabPatientInfo.tsx b/platform/ui-next/src/components/Icons/Sources/TabPatientInfo.tsx new file mode 100644 index 0000000..a203c46 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/TabPatientInfo.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const TabPatientInfo = (props: IconProps) => ( + + + + + + + +); + +export default TabPatientInfo; diff --git a/platform/ui-next/src/components/Icons/Sources/TabRoiThreshold.tsx b/platform/ui-next/src/components/Icons/Sources/TabRoiThreshold.tsx new file mode 100644 index 0000000..a9521f8 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/TabRoiThreshold.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const TabRoiThreshold = (props: IconProps) => ( + + + + + + + + + + +); + +export default TabRoiThreshold; diff --git a/platform/ui-next/src/components/Icons/Sources/TabSegmentation.tsx b/platform/ui-next/src/components/Icons/Sources/TabSegmentation.tsx new file mode 100644 index 0000000..946e8e2 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/TabSegmentation.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const TabSegmentation = (props: IconProps) => ( + + + + + + + + + + + +); + +export default TabSegmentation; diff --git a/platform/ui-next/src/components/Icons/Sources/TabStudies.tsx b/platform/ui-next/src/components/Icons/Sources/TabStudies.tsx new file mode 100644 index 0000000..47cefcc --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/TabStudies.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const TabStudies = (props: IconProps) => ( + + tab-studies + + + + + + + + + + +); + +export default TabStudies; diff --git a/platform/ui-next/src/components/Icons/Sources/ThumbnailView.tsx b/platform/ui-next/src/components/Icons/Sources/ThumbnailView.tsx new file mode 100644 index 0000000..e8898db --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/ThumbnailView.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const ThumbnailView = (props: IconProps) => ( + + + + + + + + + +); + +export default ThumbnailView; diff --git a/platform/ui-next/src/components/Icons/Sources/Tools.tsx b/platform/ui-next/src/components/Icons/Sources/Tools.tsx new file mode 100644 index 0000000..e21e91a --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Tools.tsx @@ -0,0 +1,3499 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const ToolLayout = (props: IconProps) => ( + + tool-layout + + + + + + + + + + + + + + + + + + + +); + +export const ToolLength = (props: IconProps) => ( + + + + + + + + + + + + + + +); + +export const Tool3DRotate = (props: IconProps) => ( + + + + + + + + + + + + + + + + + + +); + +export const ToolAngle = (props: IconProps) => ( + + + + + + + + + + +); + +export const ToolAnnotate = (props: IconProps) => ( + + + + + + + + + +); + +export const ToolBidirectional = (props: IconProps) => ( + + tool-bidirectional + + + + + + + + + + + + + + + + + + +); + +export const ToolCalibrate = (props: IconProps) => ( + + + + + + + + + + + + + + + + + +); + +export const ToolCapture = (props: IconProps) => ( + + tool- + + + + + + + +); + +export const ToolCine = (props: IconProps) => ( + + + + + + + +); + +export const ToolCircle = (props: IconProps) => ( + + + + + + +); + +export const ToolCobbAngle = (props: IconProps) => ( + + + + + + + + + + + + +); + +export const ToolCreateThreshold = (props: IconProps) => ( + + + + + + + + + +); + +export const ToolCrosshair = (props: IconProps) => ( + + + + + + + + + + + + +); + +export const ToolDicomTagBrowser = (props: IconProps) => ( + + + + + + + + + + + + + +); + +export const ToolFlipHorizontal = (props: IconProps) => ( + + + + + + + + +); + +export const ToolFreehandPolygon = (props: IconProps) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export const ToolFreehandRoi = (props: IconProps) => ( + + + + + + + + + + +); + +export const ToolFreehand = (props: IconProps) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export const ToolFusionColor = (props: IconProps) => ( + + + + + + + + + + + + + +); + +export const ToolInvert = (props: IconProps) => ( + + + + + + + + +); + +export const ToolLayoutDefault = (props: IconProps) => ( + + + + + + + + + + + + + + + + + + + + +); + +export const ToolMagneticRoi = (props: IconProps) => ( + + + + + + + + + + + + + + + +); + +export const ToolMagnify = (props: IconProps) => ( + + + + + + + + + +); + +export const ToolMeasureEllipse = (props: IconProps) => ( + + + + + + + + + + +); + +export const ToolMoreMenu = (props: IconProps) => ( + + + + + + +); + +export const ToolMove = (props: IconProps) => ( + + + + + + + + + + + +); + +export const ToolPolygon = (props: IconProps) => ( + + + +); + +export const ToolQuickMagnify = (props: IconProps) => ( + + + + + + + + + +); + +export const ToolRectangle = (props: IconProps) => ( + + + + + + + + + + +); + +export const ToolReferenceLines = (props: IconProps) => ( + + + + + + + + + + + +); + +export const ToolProbe = (props: IconProps) => ( + + + + + + + +); + +export const ToolReset = (props: IconProps) => ( + + + + + + + +); + +export const ToolRotateRight = (props: IconProps) => ( + + + + + + + + + + +); + +export const ToolSegBrush = (props: IconProps) => ( + + + + + + + +); + +export const ToolSegEraser = (props: IconProps) => ( + + + + + + + + +); + +export const ToolSegShape = (props: IconProps) => ( + + + + + + + +); + +export const ToolSegThreshold = (props: IconProps) => ( + + + + + + + + + + + + + + + +); + +export const ToolSplineRoi = (props: IconProps) => ( + + + + + + + + + + + + +); + +export const ToolStackImageSync = (props: IconProps) => ( + + + + + + + + +); + +export const ToolStackScroll = (props: IconProps) => ( + + + + + + + + +); + +export const ToolToggleDicomOverlay = (props: IconProps) => ( + + + + + + + +); + +export const ToolUltrasoundBidirectional = (props: IconProps) => ( + + + + + + + + + + + + + +); + +export const ToolWindowLevel = (props: IconProps) => ( + + + + + + + +); + +export const ToolWindowRegion = (props: IconProps) => ( + + + + + + + + + + +); + +export const ToolZoom = (props: IconProps) => ( + + + + + + + +); + +export const ToolBrush = (props: IconProps) => ( + + + + + + + +); + +export const ToolEraser = (props: IconProps) => ( + + + + + + + + +); + +export const ToolThreshold = (props: IconProps) => ( + + + + + + + + + + + + + + + +); + +export const ToolShape = (props: IconProps) => ( + + + + + + + +); +export const ToolLabelmapAssist = (props: IconProps) => ( + + + + + + +); +export const ToolSegmentAnything = (props: IconProps) => ( + + + + + +); +export const ToolPETSegment = (props: IconProps) => ( + + + + + +); +export const ToolInterpolation = (props: IconProps) => ( + + + + + + + +); +export const ToolBidirectionalSegment = (props: IconProps) => ( + + + + + +); +export const ToolExpand = (props: IconProps) => ( + + + +); +export const ToolContract = (props: IconProps) => ( + + + +); diff --git a/platform/ui-next/src/components/Icons/Sources/Trash.tsx b/platform/ui-next/src/components/Icons/Sources/Trash.tsx new file mode 100644 index 0000000..95f2543 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Trash.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Trash = (props: IconProps) => ( + + Trash + + +); + +export default Trash; diff --git a/platform/ui-next/src/components/Icons/Sources/Upload.tsx b/platform/ui-next/src/components/Icons/Sources/Upload.tsx new file mode 100644 index 0000000..52732b6 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/Upload.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const Upload = (props: IconProps) => ( + + + + + + +); + +export default Upload; diff --git a/platform/ui-next/src/components/Icons/Sources/ViewportViews.tsx b/platform/ui-next/src/components/Icons/Sources/ViewportViews.tsx new file mode 100644 index 0000000..69a71a2 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/ViewportViews.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const ViewportViews = (props: IconProps) => ( + + + + + + + +); + +export default ViewportViews; diff --git a/platform/ui-next/src/components/Icons/Sources/ViewportWindowLevel.tsx b/platform/ui-next/src/components/Icons/Sources/ViewportWindowLevel.tsx new file mode 100644 index 0000000..d4b1601 --- /dev/null +++ b/platform/ui-next/src/components/Icons/Sources/ViewportWindowLevel.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import type { IconProps } from '../types'; + +export const ViewportWindowLevel = (props: IconProps) => ( + + + + + + + + + + + + + + + +); + +export default ViewportWindowLevel; diff --git a/platform/ui-next/src/components/Icons/index.ts b/platform/ui-next/src/components/Icons/index.ts new file mode 100644 index 0000000..5bb4131 --- /dev/null +++ b/platform/ui-next/src/components/Icons/index.ts @@ -0,0 +1,4 @@ +import { Icons } from './Icons'; + +export { Icons }; +export default Icons; diff --git a/platform/ui-next/src/components/Icons/types.ts b/platform/ui-next/src/components/Icons/types.ts new file mode 100644 index 0000000..db9ee6a --- /dev/null +++ b/platform/ui-next/src/components/Icons/types.ts @@ -0,0 +1,3 @@ +import { HTMLAttributes } from 'react'; + +export type IconProps = HTMLAttributes; diff --git a/platform/ui-next/src/components/Input/Input.tsx b/platform/ui-next/src/components/Input/Input.tsx new file mode 100644 index 0000000..9b4da76 --- /dev/null +++ b/platform/ui-next/src/components/Input/Input.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; + +import { cn } from '../../lib/utils'; + +export interface InputProps extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + } +); + +Input.displayName = 'Input'; + +export { Input }; diff --git a/platform/ui-next/src/components/Input/index.tsx b/platform/ui-next/src/components/Input/index.tsx new file mode 100644 index 0000000..6ed9dc0 --- /dev/null +++ b/platform/ui-next/src/components/Input/index.tsx @@ -0,0 +1,3 @@ +import { Input } from './Input'; + +export { Input }; diff --git a/platform/ui-next/src/components/Label/Label.tsx b/platform/ui-next/src/components/Label/Label.tsx new file mode 100644 index 0000000..60c0702 --- /dev/null +++ b/platform/ui-next/src/components/Label/Label.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '../../lib/utils'; + +const labelVariants = cva( + 'text-base text-foreground font-normal leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' +); + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & VariantProps +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/platform/ui-next/src/components/Label/index.tsx b/platform/ui-next/src/components/Label/index.tsx new file mode 100644 index 0000000..88d2bb8 --- /dev/null +++ b/platform/ui-next/src/components/Label/index.tsx @@ -0,0 +1,3 @@ +import { Label } from './Label'; + +export { Label }; diff --git a/platform/ui-next/src/components/MeasurementTable/MeasurementTable.tsx b/platform/ui-next/src/components/MeasurementTable/MeasurementTable.tsx new file mode 100644 index 0000000..fd371de --- /dev/null +++ b/platform/ui-next/src/components/MeasurementTable/MeasurementTable.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { DataRow, PanelSection } from '../../index'; +import { createContext } from '../../lib/createContext'; + +interface MeasurementTableContext { + data?: any[]; + onClick?: (uid: string) => void; + onDelete?: (uid: string) => void; + onToggleVisibility?: (uid: string) => void; + onToggleLocked?: (uid: string) => void; + onRename?: (uid: string) => void; + onColor?: (uid: string) => void; + disableEditing?: boolean; +} + +const [MeasurementTableProvider, useMeasurementTableContext] = + createContext('MeasurementTable', { data: [] }); + +interface MeasurementDataProps extends MeasurementTableContext { + title: string; + children: React.ReactNode; +} + +const MeasurementTable = ({ + data = [], + onClick, + onDelete, + onToggleVisibility, + onToggleLocked, + onRename, + onColor, + title, + children, + disableEditing = false, +}: MeasurementDataProps) => { + const { t } = useTranslation('MeasurementTable'); + const amount = data.length; + + return ( + + + + {`${t(title)} (${amount})`} + + {children} + + + ); +}; + +const Header = ({ children }: { children: React.ReactNode }) => { + return
{children}
; +}; + +const Body = () => { + const { data } = useMeasurementTableContext('MeasurementTable.Body'); + + if (!data || data.length === 0) { + return ( +
+ No tracked measurements +
+ ); + } + + return ( +
+ {data.map((item, index) => ( + + ))} +
+ ); +}; + +const Footer = ({ children }: { children: React.ReactNode }) => { + return
{children}
; +}; + +interface MeasurementItem { + uid: string; + label: string; + colorHex: string; + isSelected: boolean; + displayText: { primary: string[]; secondary: string[] }; + isVisible: boolean; + isLocked: boolean; + toolName: string; +} + +interface RowProps { + item: MeasurementItem; + index: number; +} + +const Row = ({ item, index }: RowProps) => { + const { + onClick, + onDelete, + onToggleVisibility, + onToggleLocked, + onRename, + onColor, + disableEditing, + } = useMeasurementTableContext('MeasurementTable.Row'); + + return ( + onClick(item.uid)} + onDelete={() => onDelete(item.uid)} + disableEditing={disableEditing} + isVisible={item.isVisible} + isLocked={item.isLocked} + onToggleVisibility={() => onToggleVisibility(item.uid)} + onToggleLocked={() => onToggleLocked(item.uid)} + onRename={() => onRename(item.uid)} + // onColor={() => onColor(item.uid)} + /> + ); +}; + +MeasurementTable.Header = Header; +MeasurementTable.Body = Body; +MeasurementTable.Footer = Footer; +MeasurementTable.Row = Row; + +export default MeasurementTable; diff --git a/platform/ui-next/src/components/MeasurementTable/index.js b/platform/ui-next/src/components/MeasurementTable/index.js new file mode 100644 index 0000000..42f9beb --- /dev/null +++ b/platform/ui-next/src/components/MeasurementTable/index.js @@ -0,0 +1,3 @@ +import MeasurementTable from './MeasurementTable'; + +export { MeasurementTable }; diff --git a/platform/ui-next/src/components/NavBar/NavBar.tsx b/platform/ui-next/src/components/NavBar/NavBar.tsx new file mode 100644 index 0000000..e1a3636 --- /dev/null +++ b/platform/ui-next/src/components/NavBar/NavBar.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +const stickyClasses = 'sticky top-0'; +const notStickyClasses = 'relative'; + +const NavBar = ({ + className, + children, + isSticky, +}: { + className?: string; + children?: React.ReactNode; + isSticky?: boolean; +}) => { + return ( +
+ {children} +
+ ); +}; + +NavBar.propTypes = { + className: PropTypes.string, + children: PropTypes.node, + isSticky: PropTypes.bool, +}; + +export default NavBar; diff --git a/platform/ui-next/src/components/NavBar/index.js b/platform/ui-next/src/components/NavBar/index.js new file mode 100644 index 0000000..35fb054 --- /dev/null +++ b/platform/ui-next/src/components/NavBar/index.js @@ -0,0 +1,2 @@ +import NavBar from './NavBar'; +export default NavBar; diff --git a/platform/ui-next/src/components/Numeric/Numeric.tsx b/platform/ui-next/src/components/Numeric/Numeric.tsx new file mode 100644 index 0000000..b2cf66c --- /dev/null +++ b/platform/ui-next/src/components/Numeric/Numeric.tsx @@ -0,0 +1,302 @@ +// Numeric.tsx +import React, { createContext, useContext, useCallback, PropsWithChildren } from 'react'; +import { useControllableState } from '@radix-ui/react-use-controllable-state'; +import { cn } from '../../lib/utils'; +import { Input } from '../Input/Input'; +import { Slider } from '../Slider/Slider'; +import { DoubleSlider } from '../DoubleSlider/DoubleSlider'; + +interface NumericMetaContextValue { + mode: 'number' | 'singleRange' | 'doubleRange'; + singleValue: number; + doubleValue: [number, number]; + setSingleValue: (val: number) => void; + setDoubleValue: (vals: [number, number]) => void; + min: number; + max: number; + step: number; +} + +const NumericMetaContext = createContext(null); + +/* ------------------------------------------------------------------------- + 1) Container +---------------------------------------------------------------------------*/ +interface NumericMetaContainerProps { + mode: 'number' | 'singleRange' | 'doubleRange'; + value?: number; // for controlled single-value usage from parent + defaultValue?: number; // for uncontrolled single-value usage + values?: [number, number]; // for controlled double-range usage from parent + defaultValues?: [number, number]; // for uncontrolled double-range usage + onChange?: (val: number | [number, number]) => void; + min?: number; + max?: number; + step?: number; + className?: string; +} + +function NumericMetaContainer({ + mode, + value, + defaultValue, + values, + defaultValues, + onChange, + min = 0, + max = 100, + step = 1, + className, + children, +}: PropsWithChildren) { + // Calculate default values based on min and max + const calculatedDefaultValue = defaultValue ?? min + (max - min) / 2; + const calculatedDefaultValues = defaultValues ?? [ + min + (max - min) * 0.3, + min + (max - min) * 0.7, + ]; + + // Use useControllableState for both single and double values + const [internalSingleValue, setInternalSingleValue] = useControllableState({ + prop: mode === 'number' || mode === 'singleRange' ? value : undefined, + defaultProp: calculatedDefaultValue, + onChange: newVal => { + if (mode === 'number' || mode === 'singleRange') { + onChange?.(newVal); + } + }, + }); + + const [internalDoubleValue, setInternalDoubleValue] = useControllableState({ + prop: mode === 'doubleRange' ? values : undefined, + defaultProp: calculatedDefaultValues, + onChange: newVals => { + if (mode === 'doubleRange') { + onChange?.(newVals); + } + }, + }); + + const handleSingleChange = useCallback( + (newVal: number) => { + setInternalSingleValue(newVal); + }, + [setInternalSingleValue] + ); + + const handleDoubleChange = useCallback( + (newVals: [number, number]) => { + setInternalDoubleValue(newVals); + }, + [setInternalDoubleValue] + ); + + return ( + +
{children}
+
+ ); +} + +/* ------------------------------------------------------------------------- + 2) Label sub-component +---------------------------------------------------------------------------*/ +interface NumericMetaLabelProps { + showValue?: boolean; // optionally show the current numeric value(s) + className?: string; + children: React.ReactNode; +} + +function NumericMetaLabel({ children, showValue, className }: NumericMetaLabelProps) { + const ctx = useContext(NumericMetaContext); + if (!ctx) { + throw new Error('NumericMetaLabel must be used inside .'); + } + + const { mode, singleValue, doubleValue } = ctx; + + let displayedValue = ''; + let valueClasses = ''; + if (mode === 'number' || mode === 'singleRange') { + displayedValue = singleValue.toString(); + valueClasses = 'w-10'; + } else if (mode === 'doubleRange') { + displayedValue = `[${doubleValue[0]} - ${doubleValue[1]}]`; + } + + return ( +
+ {children} + {showValue && ( + {`: ${displayedValue}`} + )} +
+ ); +} + +/* ------------------------------------------------------------------------- + 3) SingleRange sub-component +---------------------------------------------------------------------------*/ + +interface SingleRangeProps { + showNumberInput?: boolean; + sliderClassName?: string; + numberInputClassName?: string; +} + +function SingleRange({ showNumberInput, sliderClassName, numberInputClassName }: SingleRangeProps) { + const ctx = useContext(NumericMetaContext); + if (!ctx) { + throw new Error('SingleRange must be used inside .'); + } + + const { mode, singleValue, setSingleValue, min, max, step } = ctx; + + const handleSliderChange = useCallback( + (val: number[]) => { + setSingleValue(val[0]); + }, + [setSingleValue] + ); + + const handleNumberChange = useCallback( + (evt: React.ChangeEvent) => { + const parsed = parseFloat(evt.target.value); + if (!isNaN(parsed)) { + setSingleValue(Math.max(min, Math.min(parsed, max))); + } + }, + [min, max, setSingleValue] + ); + + if (mode !== 'singleRange') { + return null; + } + + return ( +
+ + {showNumberInput && ( + + )} +
+ ); +} + +/* ------------------------------------------------------------------------- + 4) DoubleRange sub-component +---------------------------------------------------------------------------*/ +interface DoubleRangeProps { + showNumberInputs?: boolean; + className?: string; +} + +function DoubleRange({ showNumberInputs, className }: DoubleRangeProps) { + const ctx = useContext(NumericMetaContext); + if (!ctx) { + throw new Error('DoubleRange must be used inside .'); + } + + const { mode, doubleValue, setDoubleValue, min, max, step } = ctx; + + const handleSliderChange = useCallback( + (values: [number, number]) => { + setDoubleValue(values); + }, + [setDoubleValue] + ); + + if (mode !== 'doubleRange') { + return null; + } + + return ( +
+ +
+ ); +} + +/* ------------------------------------------------------------------------- + 5) Basic NumberInput sub-component +---------------------------------------------------------------------------*/ +interface NumberInputProps { + className?: string; +} + +function NumberInput({ className }: NumberInputProps) { + const ctx = useContext(NumericMetaContext); + if (!ctx) { + throw new Error('NumberInput must be used inside .'); + } + + const { mode, singleValue, setSingleValue, min, max, step } = ctx; + if (mode !== 'number') { + return null; + } + + const handleChange = (evt: React.ChangeEvent) => { + const val = parseFloat(evt.target.value); + if (!isNaN(val)) { + setSingleValue(Math.max(min, Math.min(val, max))); + } + }; + + // Calculate width based on max value's length, with a minimum of 3 characters + const maxLength = Math.max(3, max?.toString().length ?? 3); + const calculatedWidth = `${maxLength + 1.5}ch`; + + return ( + + ); +} + +export const Numeric = { + Container: NumericMetaContainer, + Label: NumericMetaLabel, + SingleRange, + DoubleRange, + NumberInput, +}; + +export default Numeric; diff --git a/platform/ui-next/src/components/Numeric/index.ts b/platform/ui-next/src/components/Numeric/index.ts new file mode 100644 index 0000000..1241895 --- /dev/null +++ b/platform/ui-next/src/components/Numeric/index.ts @@ -0,0 +1,3 @@ +import Numeric from './Numeric'; + +export default Numeric; diff --git a/platform/ui-next/src/components/OHIFToolSettings/RowDoubleRange.tsx b/platform/ui-next/src/components/OHIFToolSettings/RowDoubleRange.tsx new file mode 100644 index 0000000..58cc3f3 --- /dev/null +++ b/platform/ui-next/src/components/OHIFToolSettings/RowDoubleRange.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import Numeric from '../Numeric'; +import { cn } from '../../lib/utils'; + +interface RowDoubleRangeProps { + values: [number, number]; + onChange: (values: [number, number]) => void; + minValue: number; + maxValue: number; + step: number; + showLabel?: boolean; + label?: string; + className?: string; +} + +const RowDoubleRange: React.FC = ({ + values, + onChange, + minValue, + maxValue, + step, + showLabel = false, + label = '', + className, +}) => { + return ( + + {showLabel && {label}} + + + ); +}; + +export default RowDoubleRange; diff --git a/platform/ui-next/src/components/OHIFToolSettings/RowInputRange.tsx b/platform/ui-next/src/components/OHIFToolSettings/RowInputRange.tsx new file mode 100644 index 0000000..395fa3a --- /dev/null +++ b/platform/ui-next/src/components/OHIFToolSettings/RowInputRange.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import Numeric from '../Numeric'; +import { cn } from '../../lib/utils'; + +interface RowInputRangeProps { + value: number; + onChange: (newValue: number) => void; + minValue?: number; + maxValue?: number; + step?: number; + label?: string; + showLabel?: boolean; + labelPosition?: 'left' | 'right'; + allowNumberEdit?: boolean; + showNumberInput?: boolean; + className?: string; + containerClassName?: string; +} + +const RowInputRange: React.FC = ({ + value, + onChange, + minValue = 0, + maxValue = 100, + step = 1, + label = '', + showLabel = false, + labelPosition = 'right', + allowNumberEdit = false, + showNumberInput = true, + className, + containerClassName, +}) => { + const handleChange = (newValue: number | [number, number]) => { + if (typeof newValue === 'number') { + onChange(newValue); + } else { + onChange(newValue[0]); + } + }; + + const content = ( + + {showLabel && label && labelPosition === 'left' && ( + {label} + )} + + {showLabel && label && labelPosition === 'right' && ( + {label} + )} + + ); + + return containerClassName ?
{content}
: content; +}; + +export default RowInputRange; diff --git a/platform/ui-next/src/components/OHIFToolSettings/RowSegmentedControl.tsx b/platform/ui-next/src/components/OHIFToolSettings/RowSegmentedControl.tsx new file mode 100644 index 0000000..918c3da --- /dev/null +++ b/platform/ui-next/src/components/OHIFToolSettings/RowSegmentedControl.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { Label } from '../Label'; +import { Tabs, TabsList, TabsTrigger } from '../Tabs'; +import { cn } from '../../lib/utils'; + +interface RadioValue { + value: string; + label: string; +} + +interface RadioOption { + id: string; + name: string; + value: string; + values: RadioValue[]; + commands?: (val: string) => void; +} + +interface RowSegmentedControlProps { + option: RadioOption; + className?: string; +} + +export const RowSegmentedControl: React.FC = ({ option, className }) => { + const handleValueChange = (newVal: string) => { + if (option.commands) { + option.commands(newVal); + } + }; + + return ( +
+ +
+ + + {option.values.map(({ label, value: itemValue }, index) => ( + + {label} + + ))} + + +
+
+ ); +}; + +export default RowSegmentedControl; diff --git a/platform/ui-next/src/components/OHIFToolSettings/ToolSettings.tsx b/platform/ui-next/src/components/OHIFToolSettings/ToolSettings.tsx new file mode 100644 index 0000000..33e3c09 --- /dev/null +++ b/platform/ui-next/src/components/OHIFToolSettings/ToolSettings.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import RowInputRange from './RowInputRange'; +import RowSegmentedControl from './RowSegmentedControl'; +import RowDoubleRange from './RowDoubleRange'; + +const SETTING_TYPES = { + RANGE: 'range', + RADIO: 'radio', + CUSTOM: 'custom', + DOUBLE_RANGE: 'double-range', +}; + +function ToolSettings({ options }) { + if (!options) { + return null; + } + + if (typeof options === 'function') { + return options(); + } + + return ( +
+ {options?.map(option => { + if (option.condition && option.condition?.({ options }) === false) { + return null; + } + + switch (option.type) { + case SETTING_TYPES.RANGE: + return renderRangeSetting(option); + case SETTING_TYPES.RADIO: + return renderRadioSetting(option); + case SETTING_TYPES.DOUBLE_RANGE: + return renderDoubleRangeSetting(option); + case SETTING_TYPES.CUSTOM: + return renderCustomSetting(option); + default: + return null; + } + })} +
+ ); +} + +const renderRangeSetting = option => { + return ( +
+
{option.name}
+
+ option.commands?.(value)} + allowNumberEdit={true} + inputClassName="ml-1 w-4/5 cursor-pointer" + /> +
+
+ ); +}; + +const renderRadioSetting = option => { + return ( + + ); +}; + +function renderDoubleRangeSetting(option) { + return ( + + ); +} + +const renderCustomSetting = option => { + return ( +
+ {typeof option.children === 'function' ? option.children() : option.children} +
+ ); +}; + +export default ToolSettings; diff --git a/platform/ui-next/src/components/OHIFToolSettings/index.ts b/platform/ui-next/src/components/OHIFToolSettings/index.ts new file mode 100644 index 0000000..5922213 --- /dev/null +++ b/platform/ui-next/src/components/OHIFToolSettings/index.ts @@ -0,0 +1 @@ +export { default as ToolSettings } from './ToolSettings'; diff --git a/platform/ui-next/src/components/OHIFToolbox/Toolbox.tsx b/platform/ui-next/src/components/OHIFToolbox/Toolbox.tsx new file mode 100644 index 0000000..93871ec --- /dev/null +++ b/platform/ui-next/src/components/OHIFToolbox/Toolbox.tsx @@ -0,0 +1,162 @@ +import React, { useEffect, useRef } from 'react'; +import { ToolboxUI, useToolbox } from '../../'; +import { useToolbar } from '@ohif/core'; + +/** + * A toolbox is a collection of buttons and commands that they invoke, used to provide + * custom control panels to users. This component is a generic UI component that + * interacts with services and commands in a generic fashion. While it might + * seem unconventional to import it from the UI and integrate it into the JSX, + * it belongs in the UI components as there isn't anything in this component that + * couldn't be used for a completely different type of app. It plays a crucial + * role in enhancing the app with a toolbox by providing a way to integrate + * and display various tools and their corresponding options + */ +function Toolbox({ + servicesManager, + buttonSectionId, + commandsManager, + title, + ...props +}: withAppTypes) { + // We should move these outside of the platform/ui-next, no file here + // should rely on the managers and services + const { state: toolboxState, api } = useToolbox(buttonSectionId); + const { onInteraction, toolbarButtons } = useToolbar({ + servicesManager, + buttonSection: buttonSectionId, + }); + + const prevButtonIdsRef = useRef(''); + const prevToolboxStateRef = useRef(''); + + useEffect(() => { + const currentButtonIdsStr = JSON.stringify( + toolbarButtons.map(button => { + const { id, componentProps } = button; + if (componentProps.items?.length) { + return componentProps.items.map(item => `${item.id}-${item.disabled}`); + } + return `${id}-${componentProps.disabled}`; + }) + ); + + const currentToolBoxStateStr = JSON.stringify( + Object.keys(toolboxState.toolOptions).map(tool => { + const options = toolboxState.toolOptions[tool]; + if (Array.isArray(options)) { + return options?.map(option => `${option.id}-${option.value}`); + } + }) + ); + + if ( + prevButtonIdsRef.current === currentButtonIdsStr && + prevToolboxStateRef.current === currentToolBoxStateStr + ) { + return; + } + + prevButtonIdsRef.current = currentButtonIdsStr; + prevToolboxStateRef.current = currentToolBoxStateStr; + + const initializeOptionsWithEnhancements = toolbarButtons.reduce( + (accumulator, toolbarButton) => { + const { id: buttonId, componentProps } = toolbarButton; + + const createEnhancedOptions = (options, parentId) => { + const optionsToUse = Array.isArray(options) ? options : [options]; + + return optionsToUse.map(option => { + if (typeof option.optionComponent === 'function') { + return option; + } + + const value = + toolboxState.toolOptions?.[parentId]?.find(prop => prop.id === option.id)?.value ?? + option.value; + + const updatedOptions = toolboxState.toolOptions?.[parentId]; + + return { + ...option, + value, + commands: value => { + api.handleToolOptionChange(parentId, option.id, value); + + const { isArray } = Array; + const cmds = isArray(option.commands) ? option.commands : [option.commands]; + + cmds.forEach(command => { + const isString = typeof command === 'string'; + const isObject = typeof command === 'object'; + const isFunction = typeof command === 'function'; + + if (isString) { + commandsManager.run(command, { value }); + } else if (isObject) { + commandsManager.run({ + ...command, + commandOptions: { + ...command.commandOptions, + ...option, + value, + options: updatedOptions, + }, + }); + } else if (isFunction) { + command({ value, commandsManager, servicesManager, options: updatedOptions }); + } + }); + }, + }; + }); + }; + + const { items, options } = componentProps; + + if (items?.length) { + items.forEach(({ options, id }) => { + if (!options) { + return; + } + accumulator[id] = createEnhancedOptions(options, id); + }); + } else if (options?.length) { + accumulator[buttonId] = createEnhancedOptions(options, buttonId); + } else if (options?.optionComponent) { + accumulator[buttonId] = options.optionComponent; + } + + return accumulator; + }, + {} + ); + + api.initializeToolOptions(initializeOptionsWithEnhancements); + }, [toolbarButtons, api, toolboxState, commandsManager, servicesManager]); + + const handleToolOptionChange = (toolName, optionName, newValue) => { + api.handleToolOptionChange(toolName, optionName, newValue); + }; + + useEffect(() => { + return () => { + api.handleToolSelect(null); + }; + }, []); + + return ( + api.handleToolSelect(id)} + handleToolOptionChange={handleToolOptionChange} + onInteraction={onInteraction} + /> + ); +} + +export default Toolbox; diff --git a/platform/ui-next/src/components/OHIFToolbox/ToolboxUI.tsx b/platform/ui-next/src/components/OHIFToolbox/ToolboxUI.tsx new file mode 100644 index 0000000..32b522b --- /dev/null +++ b/platform/ui-next/src/components/OHIFToolbox/ToolboxUI.tsx @@ -0,0 +1,122 @@ +import React, { useEffect, useRef } from 'react'; +import classnames from 'classnames'; + +import { PanelSection } from '../../components'; +import { ToolSettings } from '../OHIFToolSettings'; + +const ItemsPerRow = 4; + +function usePrevious(value) { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +} + +/** + * Just refactoring from the toolbox component to make it more readable + */ +function ToolboxUI(props: withAppTypes) { + const { + toolbarButtons, + handleToolSelect, + toolboxState, + numRows, + servicesManager, + title, + useCollapsedPanel = true, + } = props; + + const { activeTool, toolOptions, selectedEvent } = toolboxState; + const activeToolOptions = toolOptions?.[activeTool]; + + const prevToolOptions = usePrevious(activeToolOptions); + + useEffect(() => { + if (!activeToolOptions || Array.isArray(activeToolOptions) === false) { + return; + } + + activeToolOptions.forEach((option, index) => { + const prevOption = prevToolOptions ? prevToolOptions[index] : undefined; + if (!prevOption || option.value !== prevOption.value || selectedEvent) { + const isOptionValid = option.condition + ? option.condition({ options: activeToolOptions }) + : true; + if (isOptionValid) { + const { commands } = option; + commands(option.value); + } + } + }); + }, [activeToolOptions, selectedEvent]); + + const render = () => { + return ( + <> +
+
+ {toolbarButtons.map((toolDef, index) => { + if (!toolDef) { + return null; + } + + const { id, Component, componentProps } = toolDef; + const isLastRow = Math.floor(index / ItemsPerRow) + 1 === numRows; + + const toolClasses = `ml-1 ${isLastRow ? '' : 'mb-2'}`; + + const onInteraction = ({ itemId, id, commands }) => { + const idToUse = itemId || id; + handleToolSelect(idToUse); + props.onInteraction({ + itemId, + commands, + }); + }; + + return ( +
+ +
+ ); + })} +
+
+
+ {activeToolOptions && } +
+ + ); + }; + + return ( + <> + {useCollapsedPanel ? ( + + + {title} + + {render()} + + ) : ( + render() + )} + + ); +} + +export { ToolboxUI }; diff --git a/platform/ui-next/src/components/OHIFToolbox/index.ts b/platform/ui-next/src/components/OHIFToolbox/index.ts new file mode 100644 index 0000000..e5c0d5d --- /dev/null +++ b/platform/ui-next/src/components/OHIFToolbox/index.ts @@ -0,0 +1,3 @@ +import { ToolboxUI } from './ToolboxUI'; +import Toolbox from './Toolbox'; +export { ToolboxUI, Toolbox }; diff --git a/platform/ui-next/src/components/Onboarding/Onboarding.css b/platform/ui-next/src/components/Onboarding/Onboarding.css new file mode 100644 index 0000000..571d49e --- /dev/null +++ b/platform/ui-next/src/components/Onboarding/Onboarding.css @@ -0,0 +1,48 @@ +.shepherd-header { + @apply !bg-popover !w-[100%] !p-0; +} + +.shepherd-title { + @apply !text-highlight !w-[100%] !break-words !text-xl !leading-[1.5]; +} + +.shepherd-content { + @apply flex flex-col gap-[8px] p-[12px]; +} + +.shepherd-element { + @apply !bg-popover !max-w-[260px]; +} + +.shepherd-text { + @apply text-foreground !w-[100%] p-0 text-lg leading-normal; +} + +.shepherd-footer { + @apply !w-[100%] p-0; +} + +.shepherd-button { + @apply !inline-flex !h-[36px] !min-w-[62px] !flex-row !items-center !justify-center !gap-[5px] !whitespace-nowrap !rounded !bg-[#348cfd] !px-[10px] !text-center !font-sans !text-[14px] !leading-[1.2] !text-white !outline-none !transition !duration-300 !ease-in-out focus:!outline-none; +} + +.shepherd-button.shepherd-button-secondary { + @apply !bg-transparent !text-[#348cfd]; +} + +.shepherd-arrow::before { + @apply !bg-popover !h-[30px] !w-[30px]; +} + +.shepherd-element[data-popper-placement^='left'] > .shepherd-arrow { + right: 3px !important; + top: 6px !important; +} + +.shepherd-element[data-popper-placement^='top'] > .shepherd-arrow { + bottom: 2px !important; +} + +.shepherd-modal-overlay-container.shepherd-modal-is-visible { + @apply !opacity-70; +} diff --git a/platform/ui-next/src/components/Onboarding/Onboarding.tsx b/platform/ui-next/src/components/Onboarding/Onboarding.tsx new file mode 100644 index 0000000..a5ecf16 --- /dev/null +++ b/platform/ui-next/src/components/Onboarding/Onboarding.tsx @@ -0,0 +1,60 @@ +import { useEffect } from 'react'; +import { useShepherd } from 'react-shepherd'; +import { StepOptions, TourOptions } from 'shepherd.js'; +import { useLocation } from 'react-router'; +import 'shepherd.js/dist/css/shepherd.css'; +import './Onboarding.css'; + +import { hasTourBeenShown, markTourAsShown, defaultShowHandler, middleware } from './utilities'; + +const Onboarding = ({ + tours = [], +}: { + tours?: Array<{ + id: string; + route: string; + tourOptions: TourOptions; + steps: StepOptions[]; + }>; +}) => { + const Shepherd = useShepherd(); + const location = useLocation(); + + /** + * Show the tour if it hasn't been shown yet based on the current route. + * Constructs a tour instance and adds steps to it based on the matching tour. + */ + useEffect(() => { + if (!tours.length) { + return; + } + + const matchingTour = tours.find(tour => tour.route === location.pathname); + if (!matchingTour || hasTourBeenShown(matchingTour.id)) { + return; + } + + const tourInstance = new Shepherd.Tour({ + ...matchingTour.tourOptions, + defaultStepOptions: { + ...matchingTour.tourOptions?.defaultStepOptions, + floatingUIOptions: matchingTour.tourOptions?.defaultStepOptions?.floatingUIOptions || { + middleware, + }, + when: { + ...matchingTour.tourOptions?.defaultStepOptions?.when, + show: + matchingTour.tourOptions?.defaultStepOptions?.when?.show || + (() => defaultShowHandler(Shepherd)), + }, + }, + }); + matchingTour.steps.forEach(step => tourInstance.addStep(step)); + tourInstance.start(); + markTourAsShown(matchingTour.id); + }, [Shepherd, tours, location.pathname]); + + return null; +}; + +export { Onboarding }; diff --git a/platform/ui-next/src/components/Onboarding/index.ts b/platform/ui-next/src/components/Onboarding/index.ts new file mode 100644 index 0000000..459553f --- /dev/null +++ b/platform/ui-next/src/components/Onboarding/index.ts @@ -0,0 +1,3 @@ +import { Onboarding } from './Onboarding'; + +export { Onboarding }; diff --git a/platform/ui-next/src/components/Onboarding/utilities.ts b/platform/ui-next/src/components/Onboarding/utilities.ts new file mode 100644 index 0000000..8cb0751 --- /dev/null +++ b/platform/ui-next/src/components/Onboarding/utilities.ts @@ -0,0 +1,91 @@ +import { ShepherdBase } from 'shepherd.js'; +import { offset, flip, shift, detectOverflow } from '@floating-ui/dom'; + +/** + * Retrieves the list of tours that have been shown from localStorage. + * @returns {string[]} An array of tour IDs that have been shown. + */ + +const getShownTours = () => JSON.parse(localStorage.getItem('shownTours')) || []; + +/** + * Checks if a specific tour has been shown. + * @param {string} tourId - The ID of the tour to check. + * @returns {boolean} True if the tour has been shown, false otherwise. + */ +const hasTourBeenShown = (tourId: string) => getShownTours().includes(tourId); + +/** + * Marks a specific tour as shown by adding it to localStorage. + * @param {string} tourId - The ID of the tour to mark as shown. + * @returns {void} + */ +const markTourAsShown = (tourId: string) => { + const shownTours = getShownTours(); + if (!shownTours.includes(tourId)) { + shownTours.push(tourId); + localStorage.setItem('shownTours', JSON.stringify(shownTours)); + } +}; + +/** + * Default handler for the 'show' event in Shepherd steps. + * Adds a progress indicator to the footer of the current step. + * + * @param {ShepherdBase} Shepherd - The Shepherd.js instance. + * @returns {void} + */ +const defaultShowHandler = (Shepherd: ShepherdBase) => { + const currentStep = Shepherd.activeTour?.getCurrentStep(); + if (currentStep) { + const progress = document.createElement('span'); + progress.className = 'shepherd-progress text-lg text-muted-foreground'; + progress.innerText = `${Shepherd.activeTour?.steps.indexOf(currentStep) + 1}/${Shepherd.activeTour?.steps.length}`; + progress.style.position = 'absolute'; + progress.style.left = '13px'; + progress.style.bottom = '20px'; + progress.style.zIndex = '1'; + + const footer = currentStep?.getElement()?.querySelector('.shepherd-footer'); + footer?.appendChild(progress); + } +}; + +/** + * Custom middleware for adjusting Shepherd step positioning when overflowing. + * + * @type {object} + * @property {string} name - The name of the middleware. + * @property {function} fn - The function that adjusts the position of the step when overflowing. + */ + +const customMiddleware = { + name: 'customOverflowMiddleware', + async fn(state) { + const overflow = await detectOverflow(state, { + boundary: document.querySelector('body'), + padding: 24, + }); + + const xAdjustment = + overflow.left > 0 ? overflow.left : overflow.right > 0 ? -overflow.right : 0; + const yAdjustment = + overflow.top > 0 ? overflow.top : overflow.bottom > 0 ? -overflow.bottom : 0; + + return { + x: state.x + xAdjustment, + y: state.y + yAdjustment, + }; + }, +}; + +/** + * Default Floating UI middleware for positioning steps in Shepherd.js. + * Includes offset, shift, flip, and custom overflow middleware. + * + * @type {Array} + */ + +const middleware = [offset(15), shift(), flip(), customMiddleware]; + +export { hasTourBeenShown, markTourAsShown, middleware, defaultShowHandler }; diff --git a/platform/ui-next/src/components/PanelSection/PanelSection.tsx b/platform/ui-next/src/components/PanelSection/PanelSection.tsx new file mode 100644 index 0000000..b6c2d8f --- /dev/null +++ b/platform/ui-next/src/components/PanelSection/PanelSection.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { + Accordion, + AccordionItem, + AccordionTrigger, + AccordionContent, +} from '../Accordion/Accordion'; +import { cn } from '../../lib/utils'; +import { Icons } from '../Icons/Icons'; + +interface PanelSectionProps { + children: React.ReactNode; + defaultOpen?: boolean; + className?: string; +} + +interface PanelSectionHeaderProps { + children: React.ReactNode; + className?: string; + showChevron?: boolean; +} + +interface PanelSectionContentProps { + children: React.ReactNode; + className?: string; +} + +export const PanelSection: React.FC & { + Header: React.FC; + Content: React.FC; +} = ({ children, defaultOpen = true, className }) => { + return ( + + + {children} + + + ); +}; + +PanelSection.Header = ({ children, className, showChevron = true }) => ( + + {children} + +); + +PanelSection.Header.displayName = 'PanelSection.Header'; + +PanelSection.Content = ({ children, className }) => ( + +
{children}
+
+); + +PanelSection.Content.displayName = 'PanelSection.Content'; diff --git a/platform/ui-next/src/components/PanelSection/index.ts b/platform/ui-next/src/components/PanelSection/index.ts new file mode 100644 index 0000000..4c1b5ce --- /dev/null +++ b/platform/ui-next/src/components/PanelSection/index.ts @@ -0,0 +1,2 @@ +import { PanelSection } from './PanelSection'; +export { PanelSection }; diff --git a/platform/ui-next/src/components/Popover/Popover.tsx b/platform/ui-next/src/components/Popover/Popover.tsx new file mode 100644 index 0000000..bd5ffcb --- /dev/null +++ b/platform/ui-next/src/components/Popover/Popover.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import * as PopoverPrimitive from '@radix-ui/react-popover'; + +import { cn } from '../../lib/utils'; + +const Popover = PopoverPrimitive.Root; + +const PopoverTrigger = PopoverPrimitive.Trigger; + +const PopoverAnchor = PopoverPrimitive.Anchor; + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; diff --git a/platform/ui-next/src/components/Popover/index.ts b/platform/ui-next/src/components/Popover/index.ts new file mode 100644 index 0000000..a3b9840 --- /dev/null +++ b/platform/ui-next/src/components/Popover/index.ts @@ -0,0 +1,3 @@ +import { Popover, PopoverContent, PopoverTrigger, PopoverAnchor } from './Popover'; + +export { Popover, PopoverContent, PopoverTrigger, PopoverAnchor }; diff --git a/platform/ui-next/src/components/Resizable/Resizable.tsx b/platform/ui-next/src/components/Resizable/Resizable.tsx new file mode 100644 index 0000000..031a0c3 --- /dev/null +++ b/platform/ui-next/src/components/Resizable/Resizable.tsx @@ -0,0 +1,43 @@ +'use client'; +import React from 'react'; + +import { GripVertical } from 'lucide-react'; +import * as ResizablePrimitive from 'react-resizable-panels'; + +import cn from 'classnames'; + +const ResizablePanelGroup = ({ + className, + ...props +}: React.ComponentProps) => ( + +); + +const ResizablePanel = ResizablePrimitive.Panel; + +const ResizableHandle = ({ + withHandle, + className, + ...props +}: React.ComponentProps & { + withHandle?: boolean; +}) => ( + div]:rotate-90", + className + )} + {...props} + > + {withHandle && ( +
+ +
+ )} +
+); + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; diff --git a/platform/ui-next/src/components/Resizable/index.ts b/platform/ui-next/src/components/Resizable/index.ts new file mode 100644 index 0000000..8843278 --- /dev/null +++ b/platform/ui-next/src/components/Resizable/index.ts @@ -0,0 +1,3 @@ +import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from './Resizable'; + +export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; diff --git a/platform/ui-next/src/components/ScrollArea/ScrollArea.tsx b/platform/ui-next/src/components/ScrollArea/ScrollArea.tsx new file mode 100644 index 0000000..4f0a304 --- /dev/null +++ b/platform/ui-next/src/components/ScrollArea/ScrollArea.tsx @@ -0,0 +1,120 @@ +import * as React from 'react'; +import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; + +import { cn } from '../../lib/utils'; +import { Icons } from '../Icons'; + +/** + * Props interface for the ScrollArea component. + * Extends Radix UI ScrollArea root props. + */ +interface ScrollAreaProps extends React.ComponentPropsWithoutRef { + /** Flag to show/hide scroll indicator arrows at top and bottom */ + showArrows?: boolean; +} + +/** + * A custom scroll area component built on top of Radix UI's ScrollArea. + * Provides a scrollable container with custom styling and optional scroll indicators. + * + * @param props - The component props + * @param props.className - Additional CSS classes to apply + * @param props.children - The content to be scrolled + * @param props.showArrows - Whether to show scroll indicator arrows + * @param ref - Forward ref for the root element + * + * @example + * ```tsx + * + *
Scrollable content
+ *
+ * ``` + */ +const ScrollArea = React.forwardRef< + React.ElementRef, + ScrollAreaProps +>(({ className, children, showArrows = false, ...props }, ref) => { + const [showBottomArrow, setShowBottomArrow] = React.useState(false); + const [showTopArrow, setShowTopArrow] = React.useState(false); + const viewportRef = React.useRef(null); + + const checkScroll = React.useCallback(() => { + if (viewportRef.current) { + const { scrollHeight, clientHeight, scrollTop } = viewportRef.current; + setShowBottomArrow(scrollHeight > clientHeight && scrollTop < scrollHeight - clientHeight); + setShowTopArrow(scrollTop > 0); + } + }, []); + + React.useEffect(() => { + checkScroll(); + window.addEventListener('resize', checkScroll); + return () => window.removeEventListener('resize', checkScroll); + }, [checkScroll]); + + return ( + + + {children} + + + + {showArrows && showTopArrow && ( +
+ +
+ )} + {showArrows && showBottomArrow && ( +
+ +
+ )} +
+ ); +}); + +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; + +/** + * Custom scrollbar component for the ScrollArea. + * Provides styled scrollbars that can be either vertical or horizontal. + * + * @param props - The component props + * @param props.className - Additional CSS classes to apply + * @param props.orientation - The scrollbar orientation ('vertical' | 'horizontal') + * @param ref - Forward ref for the scrollbar element + * + * @example + * ```tsx + * + * ``` + */ +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = 'vertical', ...props }, ref) => ( + + + +)); +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; + +export { ScrollArea, ScrollBar }; diff --git a/platform/ui-next/src/components/ScrollArea/index.tsx b/platform/ui-next/src/components/ScrollArea/index.tsx new file mode 100644 index 0000000..feea3d7 --- /dev/null +++ b/platform/ui-next/src/components/ScrollArea/index.tsx @@ -0,0 +1,3 @@ +import { ScrollArea, ScrollBar } from './ScrollArea'; + +export { ScrollArea, ScrollBar }; diff --git a/platform/ui-next/src/components/SegmentationTable/AddSegmentRow.tsx b/platform/ui-next/src/components/SegmentationTable/AddSegmentRow.tsx new file mode 100644 index 0000000..3da4e25 --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/AddSegmentRow.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { Button, Icons } from '@ohif/ui-next'; +import { useSegmentationTableContext } from './SegmentationTableContext'; + +/** + * Props interface for the AddSegmentRow component + */ +interface AddSegmentRowProps { + /** Optional child elements to render within the row */ + children?: React.ReactNode; + /** Optional segmentation object to override the active segmentation */ + segmentation?: unknown; +} + +/** + * A component that renders a row with controls for adding segments and toggling visibility + * in the segmentation table. + * + * @param props - Component properties + * @param props.children - Optional child elements to render within the row + * @param props.segmentation - Optional segmentation object to override the active segmentation + */ +export const AddSegmentRow: React.FC = ({ children = null, segmentation }) => { + const { + activeRepresentation, + disableEditing, + activeSegmentationId, + onSegmentAdd, + onToggleSegmentationRepresentationVisibility, + data, + showAddSegment, + } = useSegmentationTableContext('SegmentationTable'); + + const allSegmentsVisible = Object.values(activeRepresentation?.segments || {}).every( + segment => segment?.visible !== false + ); + + const segmentationIdToUse = segmentation ? segmentation.segmentationId : activeSegmentationId; + + if (!data?.length) { + return null; + } + + const Icon = allSegmentsVisible ? ( + + ) : ( + + ); + + const allowAddSegment = showAddSegment && !disableEditing; + + return ( +
+
+ {allowAddSegment ? ( + + ) : null} +
+ + {children} +
+ ); +}; diff --git a/platform/ui-next/src/components/SegmentationTable/AddSegmentationRow.tsx b/platform/ui-next/src/components/SegmentationTable/AddSegmentationRow.tsx new file mode 100644 index 0000000..2bc56cb --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/AddSegmentationRow.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Icons } from '../Icons'; +import { useTranslation } from 'react-i18next'; +import { useSegmentationTableContext } from './SegmentationTableContext'; + +export const AddSegmentationRow: React.FC<{ children?: React.ReactNode }> = ({ + children = null, +}) => { + const { t } = useTranslation('SegmentationTable'); + + const { onSegmentationAdd, data, disableEditing, mode, disabled } = + useSegmentationTableContext('SegmentationTable'); + + const isEmpty = data.length === 0; + + if (!isEmpty && mode === 'collapsed') { + return null; + } + + if (disableEditing) { + return null; + } + + return ( +
!disabled && onSegmentationAdd('')} + > + {children} +
+
+ {disabled ? : } +
+ + {t(`${disabled ? 'Segmentation Not Supported' : 'Add Segmentation'}`)} + +
+
+ ); +}; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationCollapsed.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationCollapsed.tsx new file mode 100644 index 0000000..bb139dc --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationCollapsed.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export const SegmentationCollapsed: React.FC<{ children?: React.ReactNode }> = ({ + children = null, +}) => { + return
{children}
; +}; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationExpanded.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationExpanded.tsx new file mode 100644 index 0000000..fa14992 --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationExpanded.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { useSegmentationTableContext } from './SegmentationTableContext'; +import { PanelSection } from '../PanelSection'; +import { SegmentationHeader } from './SegmentationHeader'; +import { SegmentationTable } from './SegmentationTable'; + +export const SegmentationExpanded: React.FC<{ children?: React.ReactNode }> = ({ children }) => { + const { data } = useSegmentationTableContext('SegmentationExpanded'); + + // Separate the Header component from other children + const headerComponent = React.Children.toArray(children).find( + child => React.isValidElement(child) && child.type === SegmentationTable.Header + ); + const otherChildren = React.Children.toArray(children).filter( + child => !(React.isValidElement(child) && child.type === SegmentationTable.Header) + ); + + return ( + <> + {data.map(segmentationInfo => ( + + + {headerComponent ? ( + React.cloneElement(headerComponent as React.ReactElement, { + segmentation: segmentationInfo.segmentation, + representation: segmentationInfo.representation, + }) + ) : ( + + )} + + +
+ {React.Children.map(otherChildren, child => + React.isValidElement(child) + ? React.cloneElement(child, { + segmentation: segmentationInfo.segmentation, + }) + : child + )} +
+
+
+ ))} + + ); +}; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationHeader.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationHeader.tsx new file mode 100644 index 0000000..97663d0 --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationHeader.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { Button } from '../Button'; +import { Icons } from '../Icons/Icons'; +import { DropdownMenu, DropdownMenuTrigger } from '../DropdownMenu'; +import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip/Tooltip'; +import { useSegmentationTableContext } from './SegmentationTableContext'; +import { useTranslation } from 'react-i18next'; + +export const SegmentationHeader: React.FC<{ + children?: React.ReactNode; + segmentation?: any; +}> = ({ children, segmentation }) => { + const { t } = useTranslation('SegmentationTable'); + const { ...contextProps } = useSegmentationTableContext('SegmentationHeader'); + + if (!segmentation) { + return null; + } + + const childrenWithProps = React.Children.map(children, child => + React.isValidElement(child) + ? React.cloneElement(child, { + t, + ...contextProps, + }) + : child + ); + + return ( +
+
+ + + + + {childrenWithProps} + +
{segmentation.label}
+
+
+ + + + + +

{segmentation.cachedStats.info}

+
+
+
+
+ ); +}; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationSegments.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationSegments.tsx new file mode 100644 index 0000000..9d2c5f6 --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationSegments.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { ScrollArea, DataRow } from '../../components'; +import { useSegmentationTableContext } from './SegmentationTableContext'; + +export const SegmentationSegments: React.FC<{ + segmentation?: unknown; + representation?: unknown; +}> = ({ segmentation, representation }) => { + const { + activeSegmentationId, + disableEditing, + onSegmentColorClick, + onToggleSegmentVisibility, + onToggleSegmentLock, + onSegmentClick, + mode, + onSegmentEdit, + onSegmentDelete, + data, + } = useSegmentationTableContext('SegmentationTable.Segments'); + + let segmentationToUse = segmentation; + let representationToUse = representation; + let segmentationIdToUse = activeSegmentationId; + if (!segmentationToUse || !representationToUse) { + const entry = data.find(seg => seg.segmentation.segmentationId === activeSegmentationId); + segmentationToUse = entry?.segmentation; + representationToUse = entry?.representation; + segmentationIdToUse = entry?.segmentation.segmentationId; + } + + if (!representationToUse || !segmentationToUse) { + return null; + } + + return ( + + {Object.values(representationToUse.segments).map(segment => { + if (!segment) { + return null; + } + const { segmentIndex, color, visible } = segment; + const segmentFromSegmentation = segmentationToUse.segments[segmentIndex]; + + if (!segmentFromSegmentation) { + return null; + } + + const { locked, active, label, displayText } = segmentFromSegmentation; + const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`; + + return ( + onSegmentColorClick(segmentationIdToUse, segmentIndex)} + onToggleVisibility={() => + onToggleSegmentVisibility(segmentationIdToUse, segmentIndex, representationToUse.type) + } + onToggleLocked={() => onToggleSegmentLock(segmentationIdToUse, segmentIndex)} + onSelect={() => onSegmentClick(segmentationIdToUse, segmentIndex)} + onRename={() => onSegmentEdit(segmentationIdToUse, segmentIndex)} + onDelete={() => onSegmentDelete(segmentationIdToUse, segmentIndex)} + /> + ); + })} + + ); +}; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationSelectorHeader.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationSelectorHeader.tsx new file mode 100644 index 0000000..1d8ca93 --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationSelectorHeader.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { + Icons, + Button, + DropdownMenu, + DropdownMenuTrigger, + Tooltip, + TooltipTrigger, + TooltipContent, +} from '../../components'; + +import { useTranslation } from 'react-i18next'; +import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@ohif/ui-next'; +import { useSegmentationTableContext } from './SegmentationTableContext'; + +export const SegmentationSelectorHeader: React.FC<{ children?: React.ReactNode }> = ({ + children = null, +}) => { + const { t } = useTranslation('SegmentationTable.HeaderCollapsed'); + + const { data, activeSegmentationId, mode, onSegmentationClick, exportOptions, ...contextProps } = + useSegmentationTableContext('SegmentationTable.HeaderCollapsed'); + + if (mode !== 'collapsed' || !data?.length) { + return null; + } + + const activeSegmentationObj = data.find( + seg => seg.segmentation.segmentationId === activeSegmentationId + ); + + const activeSegmentation = { + id: activeSegmentationObj?.segmentation.segmentationId, + label: activeSegmentationObj?.segmentation.label, + info: activeSegmentationObj?.segmentation.cachedStats?.info, + }; + + const segmentations = data.map(seg => ({ + id: seg.segmentation.segmentationId, + label: seg.segmentation.label, + info: seg.segmentation.cachedStats?.info, + })); + + const allowExport = exportOptions?.find( + ({ segmentationId }) => segmentationId === activeSegmentation.id + )?.isExportable; + + const childrenWithProps = React.Children.map(children, child => + React.isValidElement(child) + ? React.cloneElement(child, { + activeSegmentation, + allowExport, + t, + ...contextProps, + }) + : child + ); + + return ( +
+ + + + + {childrenWithProps} + + + + + + + + {activeSegmentation.info} + + +
+ ); +}; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationTable.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationTable.tsx new file mode 100644 index 0000000..4859cfb --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationTable.tsx @@ -0,0 +1,76 @@ +import React, { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PanelSection } from '../PanelSection'; +import { SegmentationTableProvider, SegmentationTableContext } from './SegmentationTableContext'; +import { SegmentationSegments } from './SegmentationSegments'; +import { SegmentationTableConfig } from './SegmentationTableConfig'; +import { AddSegmentRow } from './AddSegmentRow'; +import { AddSegmentationRow } from './AddSegmentationRow'; +import { SegmentationSelectorHeader } from './SegmentationSelectorHeader'; +import { SegmentationHeader } from './SegmentationHeader'; +import { SegmentationCollapsed } from './SegmentationCollapsed'; +import { SegmentationExpanded } from './SegmentationExpanded'; + +interface SegmentationTableProps extends SegmentationTableContext { + disabled?: boolean; + title?: string; + children?: ReactNode; +} + +interface SegmentationTableComponent extends React.FC { + Segments: typeof SegmentationSegments; + Config: typeof SegmentationTableConfig; + AddSegmentRow: typeof AddSegmentRow; + AddSegmentationRow: typeof AddSegmentationRow; + SelectorHeader: typeof SegmentationSelectorHeader; + Header: typeof SegmentationHeader; + Collapsed: typeof SegmentationCollapsed; + Expanded: typeof SegmentationExpanded; +} + +export const SegmentationTable: SegmentationTableComponent = (props: SegmentationTableProps) => { + const { t } = useTranslation('SegmentationTable'); + const { data = [], mode, title, disableEditing, disabled, children, ...contextProps } = props; + + const activeSegmentationInfo = data.find(info => info.representation?.active); + + const activeSegmentationId = activeSegmentationInfo?.segmentation?.segmentationId; + const activeRepresentation = activeSegmentationInfo?.representation; + const activeSegmentation = activeSegmentationInfo?.segmentation; + const { fillAlpha, fillAlphaInactive, outlineWidth, renderFill, renderOutline } = + activeRepresentation?.styles ?? {}; + + return ( + + + + {t(title)} + + {children} + + + ); +}; + +SegmentationTable.Segments = SegmentationSegments; +SegmentationTable.Config = SegmentationTableConfig; +SegmentationTable.AddSegmentRow = AddSegmentRow; +SegmentationTable.AddSegmentationRow = AddSegmentationRow; +SegmentationTable.SelectorHeader = SegmentationSelectorHeader; +SegmentationTable.Header = SegmentationHeader; +SegmentationTable.Collapsed = SegmentationCollapsed; +SegmentationTable.Expanded = SegmentationExpanded; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationTableConfig.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationTableConfig.tsx new file mode 100644 index 0000000..2f5fc22 --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationTableConfig.tsx @@ -0,0 +1,177 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { PanelSection } from '../PanelSection'; +import { Tabs, TabsList, TabsTrigger } from '../Tabs'; +import { Slider } from '../Slider'; +import { Icons } from '../Icons'; +import { Switch } from '../Switch'; +import { Label } from '../Label'; +import { Input } from '../Input'; +import { useSegmentationTableContext } from './SegmentationTableContext'; + +export const SegmentationTableConfig: React.FC<{ children?: React.ReactNode }> = ({ children }) => { + const { t } = useTranslation('SegmentationTable.AppearanceSettings'); + const { + renderFill, + renderOutline, + setRenderFill, + setRenderOutline, + activeRepresentation, + fillAlpha, + fillAlphaInactive, + outlineWidth, + setFillAlpha, + setFillAlphaInactive, + setOutlineWidth, + renderInactiveSegmentations, + toggleRenderInactiveSegmentations, + data, + } = useSegmentationTableContext('styles'); + + if (!data?.length) { + return null; + } + + return ( + + +
+ + {t('Appearance Settings')} +
+
+ +
+
+ + {t('Show')}:{' '} + {renderFill && renderOutline + ? t('Fill & Outline') + : renderOutline + ? t('Outline Only') + : t('Fill Only')} + + { + if (value === 'fill-and-outline') { + setRenderFill({ type: activeRepresentation.type }, true); + setRenderOutline({ type: activeRepresentation.type }, true); + } else if (value === 'outline') { + setRenderFill({ type: activeRepresentation.type }, false); + setRenderOutline({ type: activeRepresentation.type }, true); + } else { + setRenderFill({ type: activeRepresentation.type }, true); + setRenderOutline({ type: activeRepresentation.type }, false); + } + }} + > + + + + + + + + + + + + +
+ +
+
+ + + setFillAlpha({ type: activeRepresentation.type }, value) + } + max={1} + min={0} + step={0.1} + /> + + setFillAlpha({ type: activeRepresentation.type }, Number(e.target.value)) + } + /> +
+ +
+ + + setOutlineWidth({ type: activeRepresentation.type }, value) + } + max={10} + min={0} + step={0.1} + className="mx-1 flex-1" + /> + + setOutlineWidth({ type: activeRepresentation.type }, Number(e.target.value)) + } + className="mx-1 w-10 flex-none text-center" + /> +
+
+ +
+ +
+ + +
+ {renderInactiveSegmentations && ( +
+ + + setFillAlphaInactive({ type: activeRepresentation.type }, value) + } + max={1} + min={0} + step={0.1} + /> + + setFillAlphaInactive({ type: activeRepresentation.type }, Number(e.target.value)) + } + /> +
+ )} +
+ {children} +
+
+ ); +}; diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationTableContext.ts b/platform/ui-next/src/components/SegmentationTable/SegmentationTableContext.ts new file mode 100644 index 0000000..f299b9e --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationTableContext.ts @@ -0,0 +1,80 @@ +import { createContext } from '../../lib/createContext'; + +interface Segmentation { + segmentationId: string; + label: string; + cachedStats: { + info: string; + }; +} + +interface Representation { + active: boolean; + visible: boolean; + type: string; + styles: { + fillAlpha: number; + fillAlphaInactive: number; + outlineWidth: number; + renderFill: boolean; + renderOutline: boolean; + }; +} + +interface ViewportSegmentationInfo { + segmentation: Segmentation; + representation: Representation; +} + +interface SegmentationTableContext { + data: ViewportSegmentationInfo[]; + disabled: boolean; + mode: 'collapsed' | 'expanded'; + fillAlpha: number; + exportOptions: { + segmentationId: string; + isExportable: boolean; + }[]; + fillAlphaInactive: number; + outlineWidth: number; + renderFill: boolean; + renderOutline: boolean; + activeSegmentationId: string; + activeRepresentation: Representation; + activeSegmentation: Segmentation; + setRenderFill: ({ type }, value: boolean) => void; + setRenderOutline: ({ type }, value: boolean) => void; + setOutlineWidth: ({ type }, value: number) => void; + setFillAlpha: ({ type }, value: number) => void; + setFillAlphaInactive: ({ type }, value: number) => void; + renderInactiveSegmentations: boolean; + toggleRenderInactiveSegmentations: () => void; + onSegmentationAdd: (segmentationId: string) => void; + onSegmentationClick: (segmentationId: string) => void; + onSegmentationDelete: (segmentationId: string) => void; + onSegmentAdd: (segmentationId: string) => void; + onSegmentClick: (segmentationId: string, segmentIndex: number) => void; + onSegmentEdit: (segmentationId: string, segmentIndex: number) => void; + onSegmentationEdit: (segmentationId: string) => void; + onSegmentColorClick: (segmentationId: string, segmentIndex: number) => void; + onSegmentDelete: (segmentationId: string, segmentIndex: number) => void; + onToggleSegmentVisibility: (segmentationId: string, segmentIndex: number) => void; + onToggleSegmentLock: (segmentationId: string, segmentIndex: number) => void; + onToggleSegmentationRepresentationVisibility: (segmentationId: string, type: string) => void; + onSegmentationDownload: (segmentationId: string) => void; + storeSegmentation: (segmentationId: string) => void; + onSegmentationDownloadRTSS: (segmentationId: string) => void; + setStyle: ( + segmentationId: string, + representationType: string, + styleKey: string, + value: unknown + ) => void; + onSegmentationRemoveFromViewport: (segmentationId: string) => void; + disableEditing: boolean; +} + +const [SegmentationTableProvider, useSegmentationTableContext] = + createContext('SegmentationTable'); + +export { SegmentationTableProvider, useSegmentationTableContext, SegmentationTableContext }; diff --git a/platform/ui-next/src/components/SegmentationTable/index.ts b/platform/ui-next/src/components/SegmentationTable/index.ts new file mode 100644 index 0000000..b6704de --- /dev/null +++ b/platform/ui-next/src/components/SegmentationTable/index.ts @@ -0,0 +1,22 @@ +import { SegmentationTable } from './SegmentationTable'; +import { SegmentationTableConfig } from './SegmentationTableConfig'; +import { AddSegmentationRow } from './AddSegmentationRow'; +import { AddSegmentRow } from './AddSegmentRow'; +import { SegmentationSegments } from './SegmentationSegments'; +import { SegmentationSelectorHeader } from './SegmentationSelectorHeader'; +import { SegmentationHeader } from './SegmentationHeader'; +import { useSegmentationTableContext } from './SegmentationTableContext'; + +SegmentationTable.Segments = SegmentationSegments; +SegmentationTable.Config = SegmentationTableConfig; +SegmentationTable.AddSegmentRow = AddSegmentRow; +SegmentationTable.AddSegmentationRow = AddSegmentationRow; +SegmentationTable.SelectorHeader = SegmentationSelectorHeader; +SegmentationTable.Header = SegmentationHeader; + +export { + // context + useSegmentationTableContext, + // components + SegmentationTable, +}; diff --git a/platform/ui-next/src/components/Select/Select.tsx b/platform/ui-next/src/components/Select/Select.tsx new file mode 100644 index 0000000..f3f0639 --- /dev/null +++ b/platform/ui-next/src/components/Select/Select.tsx @@ -0,0 +1,150 @@ +import * as React from 'react'; +import { CaretSortIcon, CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'; +import * as SelectPrimitive from '@radix-ui/react-select'; + +import { cn } from '../../lib/utils'; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1 hover:bg-primary/10 flex h-7 w-full items-center justify-between whitespace-nowrap rounded border bg-transparent px-2 py-2 text-base shadow-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50', + className + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = 'popper', ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; diff --git a/platform/ui-next/src/components/Select/index.tsx b/platform/ui-next/src/components/Select/index.tsx new file mode 100644 index 0000000..9610fe2 --- /dev/null +++ b/platform/ui-next/src/components/Select/index.tsx @@ -0,0 +1,40 @@ +import { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} from './Select'; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; + +const SelectComponents = { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; + +export default SelectComponents; diff --git a/platform/ui-next/src/components/Separator/Separator.tsx b/platform/ui-next/src/components/Separator/Separator.tsx new file mode 100644 index 0000000..7c6958c --- /dev/null +++ b/platform/ui-next/src/components/Separator/Separator.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import * as SeparatorPrimitive from '@radix-ui/react-separator'; + +import { cn } from '../../lib/utils'; + +type SeparatorProps = React.ComponentPropsWithoutRef & { + thickness?: string; +}; + +const Separator = React.forwardRef< + React.ElementRef, + SeparatorProps +>( + ( + { className, orientation = 'horizontal', decorative = true, thickness = '1px', ...props }, + ref + ) => ( + + ) +); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/platform/ui-next/src/components/Separator/index.ts b/platform/ui-next/src/components/Separator/index.ts new file mode 100644 index 0000000..6c60264 --- /dev/null +++ b/platform/ui-next/src/components/Separator/index.ts @@ -0,0 +1,3 @@ +import { Separator } from './Separator'; + +export { Separator }; diff --git a/platform/ui-next/src/components/Separator/index.tsx b/platform/ui-next/src/components/Separator/index.tsx new file mode 100644 index 0000000..6c60264 --- /dev/null +++ b/platform/ui-next/src/components/Separator/index.tsx @@ -0,0 +1,3 @@ +import { Separator } from './Separator'; + +export { Separator }; diff --git a/platform/ui-next/src/components/SidePanel/SidePanel.tsx b/platform/ui-next/src/components/SidePanel/SidePanel.tsx new file mode 100644 index 0000000..353b081 --- /dev/null +++ b/platform/ui-next/src/components/SidePanel/SidePanel.tsx @@ -0,0 +1,480 @@ +import classnames from 'classnames'; +import React, { useCallback, useEffect, useState } from 'react'; +import { Icons } from '../Icons'; +import { TooltipTrigger, TooltipContent, Tooltip } from '../Tooltip'; +import { Separator } from '../Separator'; + +/** + * SidePanel component properties. + * Note that the component monitors changes to the various widths and border sizes and will resize dynamically + * @property {boolean} isExpanded - boolean indicating if the side panel is expanded/open or collapsed + * @property {number} expandedWidth - the width of this side panel when expanded not including any borders or margins + * @property {number} collapsedWidth - the width of this side panel when collapsed not including any borders or margins + * @property {number} expandedInsideBorderSize - the width of the space between the expanded side panel content and viewport grid + * @property {number} collapsedInsideBorderSize - the width of the space between the collapsed side panel content and the viewport grid + * @property {number} collapsedOutsideBorderSize - the width of the space between the collapsed side panel content and the edge of the browser window + */ +type SidePanelProps = { + side: 'left' | 'right'; + className: string; + activeTabIndex: number; + onOpen: () => void; + onClose: () => void; + onActiveTabIndexChange: () => void; + isExpanded: boolean; + expandedWidth: number; + collapsedWidth: number; + expandedInsideBorderSize: number; + collapsedInsideBorderSize: number; + collapsedOutsideBorderSize: number; + tabs: any; +}; + +type StyleMap = { + open: { + left: { + marginLeft: string; // the space between the expanded/open left side panel and the browser window left edge + marginRight: string; // the space between the expanded/open left side panel and the viewport grid + }; + right: { + marginLeft: string; // the space between the expanded/open right side panel and the viewport grid + marginRight: string; // the space between the expanded/open right side panel and the browser window right edge + }; + }; + closed: { + left: { + marginLeft: string; // the space between the collapsed/closed left panel and the browser window left edge + marginRight: string; // the space between the collapsed/closed left panel and the viewport grid + alignItems: 'flex-end'; // the flexbox layout align-items property + }; + right: { + marginLeft: string; // the space between the collapsed/closed right panel and the viewport grid + marginRight: string; // the space between the collapsed/closed right panel and the browser window right edge + alignItems: 'flex-start'; // the flexbox layout align-items property + }; + }; +}; +const closeIconWidth = 30; +const gridHorizontalPadding = 10; +const tabSpacerWidth = 2; + +const baseClasses = 'bg-black border-black justify-start box-content flex flex-col'; + +const openStateIconName = { + left: 'SidePanelCloseLeft', + right: 'SidePanelCloseRight', +}; + +const getTabWidth = (numTabs: number) => { + if (numTabs < 3) { + return 68; + } else { + return 40; + } +}; + +const getGridWidth = (numTabs: number, gridAvailableWidth: number) => { + const spacersWidth = (numTabs - 1) * tabSpacerWidth; + const tabsWidth = getTabWidth(numTabs) * numTabs; + + if (gridAvailableWidth > tabsWidth + spacersWidth) { + return tabsWidth + spacersWidth; + } + + return gridAvailableWidth; +}; + +const getNumGridColumns = (numTabs: number, gridWidth: number) => { + if (numTabs === 1) { + return 1; + } + + // Start by calculating the number of tabs assuming each tab was accompanied by a spacer. + const tabWidth = getTabWidth(numTabs); + const numTabsWithOneSpacerEach = Math.floor(gridWidth / (tabWidth + tabSpacerWidth)); + + // But there is always one less spacer than tabs, so now check if an extra tab with one less spacer fits. + if ( + (numTabsWithOneSpacerEach + 1) * tabWidth + numTabsWithOneSpacerEach * tabSpacerWidth <= + gridWidth + ) { + return numTabsWithOneSpacerEach + 1; + } + + return numTabsWithOneSpacerEach; +}; + +const getTabClassNames = ( + numColumns: number, + numTabs: number, + tabIndex: number, + isActiveTab: boolean, + isTabDisabled: boolean +) => + classnames('h-[28px] mb-[2px] cursor-pointer text-white bg-black', { + 'hover:text-primary-active': !isActiveTab && !isTabDisabled, + 'rounded-l': tabIndex % numColumns === 0, + 'rounded-r': (tabIndex + 1) % numColumns === 0 || tabIndex === numTabs - 1, + }); + +const getTabStyle = (numTabs: number) => { + return { + width: `${getTabWidth(numTabs)}px`, + }; +}; + +const getTabIconClassNames = (numTabs: number, isActiveTab: boolean) => { + return classnames('h-full w-full flex items-center justify-center', { + 'bg-customblue-40': isActiveTab, + rounded: isActiveTab, + }); +}; +const createStyleMap = ( + expandedWidth: number, + expandedInsideBorderSize: number, + collapsedWidth: number, + collapsedInsideBorderSize: number, + collapsedOutsideBorderSize: number +): StyleMap => { + const collapsedHideWidth = expandedWidth - collapsedWidth - collapsedOutsideBorderSize; + + return { + open: { + left: { marginLeft: '0px', marginRight: `${expandedInsideBorderSize}px` }, + right: { marginLeft: `${expandedInsideBorderSize}px`, marginRight: '0px' }, + }, + closed: { + left: { + marginLeft: `-${collapsedHideWidth}px`, + marginRight: `${collapsedInsideBorderSize}px`, + alignItems: `flex-end`, + }, + right: { + marginLeft: `${collapsedInsideBorderSize}px`, + marginRight: `-${collapsedHideWidth}px`, + alignItems: `flex-start`, + }, + }, + }; +}; + +const getToolTipContent = (label: string, disabled: boolean) => { + return ( + <> +
{label}
+ {disabled &&
{'Not available based on current context'}
} + + ); +}; + +const createBaseStyle = (expandedWidth: number) => { + return { + maxWidth: `${expandedWidth}px`, + width: `${expandedWidth}px`, + // To align the top of the side panel with the top of the viewport grid, use position relative and offset the + // top by the same top offset as the viewport grid. Also adjust the height so that there is no overflow. + position: 'relative', + top: '0.2%', + height: '99.8%', + }; +}; + +const SidePanel = ({ + side, + className, + activeTabIndex: activeTabIndexProp, + isExpanded, + tabs, + onOpen, + onClose, + onActiveTabIndexChange, + expandedWidth = 280, + collapsedWidth = 25, + expandedInsideBorderSize = 4, + collapsedInsideBorderSize = 8, + collapsedOutsideBorderSize = 4, +}: SidePanelProps) => { + const [panelOpen, setPanelOpen] = useState(isExpanded); + const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp ?? 0); + + const [styleMap, setStyleMap] = useState( + createStyleMap( + expandedWidth, + expandedInsideBorderSize, + collapsedWidth, + collapsedInsideBorderSize, + collapsedOutsideBorderSize + ) + ); + + const [baseStyle, setBaseStyle] = useState(createBaseStyle(expandedWidth)); + + const [gridAvailableWidth, setGridAvailableWidth] = useState( + expandedWidth - closeIconWidth - gridHorizontalPadding + ); + + const [gridWidth, setGridWidth] = useState(getGridWidth(tabs.length, gridAvailableWidth)); + const openStatus = panelOpen ? 'open' : 'closed'; + const style = Object.assign({}, styleMap[openStatus][side], baseStyle); + + const updatePanelOpen = useCallback( + (isOpen: boolean) => { + setPanelOpen(isOpen); + if (isOpen !== panelOpen) { + // only fire events for changes + if (isOpen && onOpen) { + onOpen(); + } else if (onClose && !isOpen) { + onClose(); + } + } + }, + [panelOpen, onOpen, onClose] + ); + + const updateActiveTabIndex = useCallback( + (activeTabIndex: number, forceOpen: boolean = false) => { + if (forceOpen) { + updatePanelOpen(true); + } + + setActiveTabIndex(activeTabIndex); + + if (onActiveTabIndexChange) { + onActiveTabIndexChange({ activeTabIndex }); + } + }, + [onActiveTabIndexChange, updatePanelOpen] + ); + + useEffect(() => { + updatePanelOpen(isExpanded); + }, [isExpanded, updatePanelOpen]); + + useEffect(() => { + setStyleMap( + createStyleMap( + expandedWidth, + expandedInsideBorderSize, + collapsedWidth, + collapsedInsideBorderSize, + collapsedOutsideBorderSize + ) + ); + setBaseStyle(createBaseStyle(expandedWidth)); + + const gridAvailableWidth = expandedWidth - closeIconWidth - gridHorizontalPadding; + setGridAvailableWidth(gridAvailableWidth); + setGridWidth(getGridWidth(tabs.length, gridAvailableWidth)); + }, [ + collapsedInsideBorderSize, + collapsedWidth, + expandedWidth, + expandedInsideBorderSize, + tabs.length, + collapsedOutsideBorderSize, + ]); + + useEffect(() => { + updateActiveTabIndex(activeTabIndexProp ?? 0); + }, [activeTabIndexProp, updateActiveTabIndex]); + + const getCloseStateComponent = () => { + const _childComponents = Array.isArray(tabs) ? tabs : [tabs]; + return ( + <> +
{ + updatePanelOpen(!panelOpen); + }} + data-cy={`side-panel-header-${side}`} + > + +
+
+ {_childComponents.map((childComponent, index) => ( + + +
{ + return childComponent.disabled ? null : updateActiveTabIndex(index, true); + }} + > + {React.createElement(Icons[childComponent.iconName] || Icons.MissingIcon, { + className: classnames({ + 'text-primary-active': true, + 'ohif-disabled': childComponent.disabled, + }), + style: { + width: '22px', + height: '22px', + }, + })} +
+
+ +
+ {getToolTipContent(childComponent.label, childComponent.disabled)} +
+
+
+ ))} +
+ + ); + }; + + const getCloseIcon = () => { + return ( +
{ + updatePanelOpen(!panelOpen); + }} + data-cy={`side-panel-header-${side}`} + > + {React.createElement(Icons[openStateIconName[side]] || Icons.MissingIcon, { + className: 'text-primary-active', + })} +
+ ); + }; + + const getTabGridComponent = () => { + const numCols = getNumGridColumns(tabs.length, gridWidth); + + return ( + <> + {getCloseIcon()} +
+
+ {tabs.map((tab, tabIndex) => { + const { disabled } = tab; + return ( + + {tabIndex % numCols !== 0 && ( +
+
+
+ )} + + +
{ + return disabled ? null : updateActiveTabIndex(tabIndex); + }} + data-cy={`${tab.name}-btn`} + > +
+ {React.createElement(Icons[tab.iconName] || Icons.MissingIcon, { + className: classnames({ + 'text-primary-active': true, + 'ohif-disabled': disabled, + }), + style: { + width: '22px', + height: '22px', + }, + })} +
+
+
+ + {getToolTipContent(tab.label, disabled)} + +
+
+ ); + })} +
+
+ + ); + }; + + const getOneTabComponent = () => { + return ( +
updatePanelOpen(!panelOpen)} + > + {getCloseIcon()} + {tabs[0].label} +
+ ); + }; + + const getOpenStateComponent = () => { + return ( + <> +
+ {tabs.length === 1 ? getOneTabComponent() : getTabGridComponent()} +
+ + + ); + }; + + return ( +
+ {panelOpen ? ( + <> + {getOpenStateComponent()} + {tabs.map((tab, tabIndex) => { + if (tabIndex === activeTabIndex) { + return ; + } + return null; + })} + + ) : ( + {getCloseStateComponent()} + )} +
+ ); +}; + +export { SidePanel }; diff --git a/platform/ui-next/src/components/SidePanel/index.ts b/platform/ui-next/src/components/SidePanel/index.ts new file mode 100644 index 0000000..133f86c --- /dev/null +++ b/platform/ui-next/src/components/SidePanel/index.ts @@ -0,0 +1,2 @@ +import { SidePanel } from './SidePanel'; +export { SidePanel }; diff --git a/platform/ui-next/src/components/Slider/Slider.tsx b/platform/ui-next/src/components/Slider/Slider.tsx new file mode 100644 index 0000000..61729d2 --- /dev/null +++ b/platform/ui-next/src/components/Slider/Slider.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import * as SliderPrimitive from '@radix-ui/react-slider'; + +import { cn } from '../../lib/utils'; + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/platform/ui-next/src/components/Slider/index.tsx b/platform/ui-next/src/components/Slider/index.tsx new file mode 100644 index 0000000..516db3a --- /dev/null +++ b/platform/ui-next/src/components/Slider/index.tsx @@ -0,0 +1,3 @@ +import { Slider } from './Slider'; + +export { Slider }; diff --git a/platform/ui-next/src/components/Sonner/Sonner.tsx b/platform/ui-next/src/components/Sonner/Sonner.tsx new file mode 100644 index 0000000..368e8ed --- /dev/null +++ b/platform/ui-next/src/components/Sonner/Sonner.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Toaster as Sonner } from 'sonner'; +import { Icons } from '../Icons'; + +type ToasterProps = React.ComponentProps; + +const Toaster = ({ ...props }: ToasterProps) => { + return ( + } + icons={{ + warning: , + info: , + success: , + error: , + }} + theme="dark" + richColors="true" + toastOptions={{ + style: { + width: '430px', // Set a maximum width + right: '8px', + }, + }} + {...props} + /> + ); +}; + +export { Toaster }; diff --git a/platform/ui-next/src/components/Sonner/index.ts b/platform/ui-next/src/components/Sonner/index.ts new file mode 100644 index 0000000..3a90950 --- /dev/null +++ b/platform/ui-next/src/components/Sonner/index.ts @@ -0,0 +1,4 @@ +import { Toaster } from './Sonner'; +import { toast } from 'sonner'; + +export { Toaster, toast }; diff --git a/platform/ui-next/src/components/StudyBrowser/StudyBrowser.tsx b/platform/ui-next/src/components/StudyBrowser/StudyBrowser.tsx new file mode 100644 index 0000000..244b895 --- /dev/null +++ b/platform/ui-next/src/components/StudyBrowser/StudyBrowser.tsx @@ -0,0 +1,137 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { StudyItem } from '../StudyItem'; +import { StudyBrowserSort } from '../StudyBrowserSort'; +import { StudyBrowserViewOptions } from '../StudyBrowserViewOptions'; + +const noop = () => {}; + +const StudyBrowser = ({ + tabs, + activeTabName, + expandedStudyInstanceUIDs, + onClickTab = noop, + onClickStudy = noop, + onClickThumbnail = noop, + onDoubleClickThumbnail = noop, + onClickUntrack = noop, + activeDisplaySetInstanceUIDs, + servicesManager, + showSettings, + viewPresets, + ThumbnailMenuItems, + StudyMenuItems, +}: withAppTypes) => { + const getTabContent = () => { + const tabData = tabs.find(tab => tab.name === activeTabName); + const viewPreset = viewPresets + ? viewPresets.filter(preset => preset.selected)[0]?.id + : 'thumbnails'; + return tabData?.studies?.map( + ({ studyInstanceUid, date, description, numInstances, modalities, displaySets }) => { + const isExpanded = expandedStudyInstanceUIDs.includes(studyInstanceUid); + return ( + + onClickStudy(studyInstanceUid)} + onClickThumbnail={onClickThumbnail} + onDoubleClickThumbnail={onDoubleClickThumbnail} + onClickUntrack={onClickUntrack} + activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs} + data-cy="thumbnail-list" + viewPreset={viewPreset} + ThumbnailMenuItems={ThumbnailMenuItems} + StudyMenuItems={StudyMenuItems} + StudyInstanceUID={studyInstanceUid} + /> + + ); + } + ); + }; + + return ( +
+
+ {showSettings && ( +
+ <> + + + +
+ )} + {getTabContent()} +
+
+ ); +}; + +StudyBrowser.propTypes = { + onClickTab: PropTypes.func.isRequired, + onClickStudy: PropTypes.func, + onClickThumbnail: PropTypes.func, + onDoubleClickThumbnail: PropTypes.func, + onClickUntrack: PropTypes.func, + activeTabName: PropTypes.string.isRequired, + expandedStudyInstanceUIDs: PropTypes.arrayOf(PropTypes.string).isRequired, + activeDisplaySetInstanceUIDs: PropTypes.arrayOf(PropTypes.string), + tabs: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + studies: PropTypes.arrayOf( + PropTypes.shape({ + studyInstanceUid: PropTypes.string.isRequired, + date: PropTypes.string, + numInstances: PropTypes.number, + modalities: PropTypes.string, + description: PropTypes.string, + displaySets: PropTypes.arrayOf( + PropTypes.shape({ + displaySetInstanceUID: PropTypes.string.isRequired, + imageSrc: PropTypes.string, + imageAltText: PropTypes.string, + seriesDate: PropTypes.string, + seriesNumber: PropTypes.any, + numInstances: PropTypes.number, + description: PropTypes.string, + componentType: PropTypes.oneOf(['thumbnail', 'thumbnailTracked', 'thumbnailNoImage']) + .isRequired, + isTracked: PropTypes.bool, + /** + * Data the thumbnail should expose to a receiving drop target. Use a matching + * `dragData.type` to identify which targets can receive this draggable item. + * If this is not set, drag-n-drop will be disabled for this thumbnail. + * + * Ref: https://react-dnd.github.io/react-dnd/docs/api/use-drag#specification-object-members + */ + dragData: PropTypes.shape({ + /** Must match the "type" a dropTarget expects */ + type: PropTypes.string.isRequired, + }), + }) + ), + }) + ).isRequired, + }) + ), + StudyMenuItems: PropTypes.func, +}; + +export { StudyBrowser }; diff --git a/platform/ui-next/src/components/StudyBrowser/index.ts b/platform/ui-next/src/components/StudyBrowser/index.ts new file mode 100644 index 0000000..e56fc88 --- /dev/null +++ b/platform/ui-next/src/components/StudyBrowser/index.ts @@ -0,0 +1,2 @@ +import { StudyBrowser } from './StudyBrowser'; +export { StudyBrowser }; diff --git a/platform/ui-next/src/components/StudyBrowserSort/StudyBrowserSort.tsx b/platform/ui-next/src/components/StudyBrowserSort/StudyBrowserSort.tsx new file mode 100644 index 0000000..e9f8717 --- /dev/null +++ b/platform/ui-next/src/components/StudyBrowserSort/StudyBrowserSort.tsx @@ -0,0 +1,93 @@ +import React, { useEffect, useState } from 'react'; +import { Icons } from '../Icons'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from '../DropdownMenu/DropdownMenu'; +import { Tooltip, TooltipContent, TooltipTrigger } from '../Tooltip'; + +export function StudyBrowserSort({ servicesManager }: withAppTypes) { + // Todo: this should not be here, no servicesManager should be in ui-next, only + // customization service + const { customizationService, displaySetService } = servicesManager.services; + const sortFunctions = customizationService.getCustomization('studyBrowser.sortFunctions'); + + const [selectedSort, setSelectedSort] = useState(sortFunctions[0]); + const [sortDirection, setSortDirection] = useState('ascending'); + + const handleSortChange = sortFunction => { + setSelectedSort(sortFunction); + }; + + const toggleSortDirection = e => { + e.stopPropagation(); + setSortDirection(prevDirection => (prevDirection === 'ascending' ? 'descending' : 'ascending')); + }; + + useEffect(() => { + displaySetService.sortDisplaySets(selectedSort.sortFunction, sortDirection); + }, [displaySetService, selectedSort, sortDirection]); + + useEffect(() => { + const SubscriptionDisplaySetsChanged = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SETS_CHANGED, + () => { + displaySetService.sortDisplaySets(selectedSort.sortFunction, sortDirection, true); + } + ); + const SubscriptionDisplaySetMetaDataInvalidated = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, + () => { + displaySetService.sortDisplaySets(selectedSort.sortFunction, sortDirection, true); + } + ); + + return () => { + SubscriptionDisplaySetsChanged.unsubscribe(); + SubscriptionDisplaySetMetaDataInvalidated.unsubscribe(); + }; + }, [displaySetService, selectedSort, sortDirection]); + + return ( +
+ + + + + {selectedSort.label} + + + {selectedSort.label} + + + {sortFunctions.map(sort => ( + handleSortChange(sort)} + > + {sort.label} + + ))} + + + + + + + Sort direction + +
+ ); +} diff --git a/platform/ui-next/src/components/StudyBrowserSort/index.ts b/platform/ui-next/src/components/StudyBrowserSort/index.ts new file mode 100644 index 0000000..ef6fa4a --- /dev/null +++ b/platform/ui-next/src/components/StudyBrowserSort/index.ts @@ -0,0 +1,3 @@ +import { StudyBrowserSort } from './StudyBrowserSort'; + +export { StudyBrowserSort }; diff --git a/platform/ui-next/src/components/StudyBrowserViewOptions/StudyBrowserViewOptions.tsx b/platform/ui-next/src/components/StudyBrowserViewOptions/StudyBrowserViewOptions.tsx new file mode 100644 index 0000000..738834d --- /dev/null +++ b/platform/ui-next/src/components/StudyBrowserViewOptions/StudyBrowserViewOptions.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from '../DropdownMenu/DropdownMenu'; +import { Tooltip, TooltipContent, TooltipTrigger } from '../Tooltip'; + +export function StudyBrowserViewOptions({ tabs, onSelectTab, activeTabName }: withAppTypes) { + const handleTabChange = (tabName: string) => { + onSelectTab(tabName); + }; + + const activeTab = tabs.find(tab => tab.name === activeTabName); + + return ( + + + + + {activeTab?.label} + + + {activeTab?.label} + + + {tabs.map(tab => { + const { name, label, studies } = tab; + const isActive = activeTabName === name; + const isDisabled = !studies.length; + + if (isDisabled) { + return null; + } + + return ( + handleTabChange(name)} + > + {label} + + ); + })} + + + ); +} diff --git a/platform/ui-next/src/components/StudyBrowserViewOptions/index.ts b/platform/ui-next/src/components/StudyBrowserViewOptions/index.ts new file mode 100644 index 0000000..bc14664 --- /dev/null +++ b/platform/ui-next/src/components/StudyBrowserViewOptions/index.ts @@ -0,0 +1,3 @@ +import { StudyBrowserViewOptions } from './StudyBrowserViewOptions'; + +export { StudyBrowserViewOptions }; diff --git a/platform/ui-next/src/components/StudyItem/StudyItem.tsx b/platform/ui-next/src/components/StudyItem/StudyItem.tsx new file mode 100644 index 0000000..20c748b --- /dev/null +++ b/platform/ui-next/src/components/StudyItem/StudyItem.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { ThumbnailList } from '../ThumbnailList'; + +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '../Accordion'; +import { Tooltip, TooltipContent, TooltipTrigger } from '../Tooltip'; + +const StudyItem = ({ + date, + description, + numInstances, + modalities, + isActive, + onClick, + isExpanded, + displaySets, + activeDisplaySetInstanceUIDs, + onClickThumbnail, + onDoubleClickThumbnail, + onClickUntrack, + viewPreset = 'thumbnails', + ThumbnailMenuItems, + StudyMenuItems, + StudyInstanceUID, +}: withAppTypes) => { + return ( + {}} + role="button" + tabIndex={0} + defaultValue={isActive ? 'study-item' : undefined} + > + + +
+
+
+ + {date} + +
+ {date} +
+
+
+ + {description} + +
+ {description} +
+
+
+
+
+
{modalities}
+
{numInstances}
+
+ {StudyMenuItems && ( +
+ +
+ )} +
+
+
+ { + event.stopPropagation(); + }} + > + {isExpanded && displaySets && ( + + )} + +
+
+ ); +}; + +StudyItem.propTypes = { + date: PropTypes.string.isRequired, + description: PropTypes.string, + modalities: PropTypes.string.isRequired, + numInstances: PropTypes.number.isRequired, + isActive: PropTypes.bool, + onClick: PropTypes.func.isRequired, + isExpanded: PropTypes.bool, + displaySets: PropTypes.array, + activeDisplaySetInstanceUIDs: PropTypes.array, + onClickThumbnail: PropTypes.func, + onDoubleClickThumbnail: PropTypes.func, + onClickUntrack: PropTypes.func, + viewPreset: PropTypes.string, + StudyMenuItems: PropTypes.func, + StudyInstanceUID: PropTypes.string, +}; + +export { StudyItem }; diff --git a/platform/ui-next/src/components/StudyItem/index.ts b/platform/ui-next/src/components/StudyItem/index.ts new file mode 100644 index 0000000..39b3b68 --- /dev/null +++ b/platform/ui-next/src/components/StudyItem/index.ts @@ -0,0 +1,2 @@ +import { StudyItem } from './StudyItem'; +export { StudyItem }; diff --git a/platform/ui-next/src/components/StudySummary/StudySummary.tsx b/platform/ui-next/src/components/StudySummary/StudySummary.tsx new file mode 100644 index 0000000..b4bf78f --- /dev/null +++ b/platform/ui-next/src/components/StudySummary/StudySummary.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +interface StudySummaryProps { + date: string; + description: string; +} + +/** + * StudySummary component displays a summary of a study with its date and description. + * + * @param props - The properties for the StudySummary component + * @param props.date - The date of the study + * @param props.description - The description of the study + */ +const StudySummary: React.FC = ({ date, description }) => { + return ( +
+
{date}
+
{description}
+
+ ); +}; + +export { StudySummary }; diff --git a/platform/ui-next/src/components/StudySummary/index.tsx b/platform/ui-next/src/components/StudySummary/index.tsx new file mode 100644 index 0000000..2556564 --- /dev/null +++ b/platform/ui-next/src/components/StudySummary/index.tsx @@ -0,0 +1,3 @@ +import { StudySummary } from './StudySummary'; + +export { StudySummary }; diff --git a/platform/ui-next/src/components/Switch/Switch.tsx b/platform/ui-next/src/components/Switch/Switch.tsx new file mode 100644 index 0000000..3b78ec9 --- /dev/null +++ b/platform/ui-next/src/components/Switch/Switch.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import * as SwitchPrimitives from '@radix-ui/react-switch'; + +import { cn } from '../../lib/utils'; + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +Switch.displayName = SwitchPrimitives.Root.displayName; + +export { Switch }; diff --git a/platform/ui-next/src/components/Switch/index.tsx b/platform/ui-next/src/components/Switch/index.tsx new file mode 100644 index 0000000..cf36910 --- /dev/null +++ b/platform/ui-next/src/components/Switch/index.tsx @@ -0,0 +1,3 @@ +import { Switch } from './Switch'; + +export { Switch }; diff --git a/platform/ui-next/src/components/Tabs/Tabs.tsx b/platform/ui-next/src/components/Tabs/Tabs.tsx new file mode 100644 index 0000000..47b55e5 --- /dev/null +++ b/platform/ui-next/src/components/Tabs/Tabs.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import * as TabsPrimitive from '@radix-ui/react-tabs'; + +import { cn } from '../../lib/utils'; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/platform/ui-next/src/components/Tabs/index.ts b/platform/ui-next/src/components/Tabs/index.ts new file mode 100644 index 0000000..f5be4be --- /dev/null +++ b/platform/ui-next/src/components/Tabs/index.ts @@ -0,0 +1,3 @@ +import { Tabs, TabsList, TabsTrigger, TabsContent } from "./Tabs" + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/platform/ui-next/src/components/Tabs/index.tsx b/platform/ui-next/src/components/Tabs/index.tsx new file mode 100644 index 0000000..ee99e18 --- /dev/null +++ b/platform/ui-next/src/components/Tabs/index.tsx @@ -0,0 +1,3 @@ +import { Tabs, TabsList, TabsTrigger, TabsContent } from './Tabs'; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/platform/ui-next/src/components/ThemeWrapper/ThemeWrapper.tsx b/platform/ui-next/src/components/ThemeWrapper/ThemeWrapper.tsx new file mode 100644 index 0000000..561fcaa --- /dev/null +++ b/platform/ui-next/src/components/ThemeWrapper/ThemeWrapper.tsx @@ -0,0 +1,4 @@ +import React from 'react'; +import '../../tailwind.css'; + +export const ThemeWrapper = ({ children }) => {children}; diff --git a/platform/ui-next/src/components/ThemeWrapper/index.ts b/platform/ui-next/src/components/ThemeWrapper/index.ts new file mode 100644 index 0000000..69f5248 --- /dev/null +++ b/platform/ui-next/src/components/ThemeWrapper/index.ts @@ -0,0 +1 @@ +export { ThemeWrapper } from './ThemeWrapper'; diff --git a/platform/ui-next/src/components/Thumbnail/Thumbnail.tsx b/platform/ui-next/src/components/Thumbnail/Thumbnail.tsx new file mode 100644 index 0000000..a2be669 --- /dev/null +++ b/platform/ui-next/src/components/Thumbnail/Thumbnail.tsx @@ -0,0 +1,316 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { useDrag } from 'react-dnd'; +import { Icons } from '../Icons'; +import { DisplaySetMessageListTooltip } from '../DisplaySetMessageListTooltip'; +import { TooltipTrigger, TooltipContent, Tooltip } from '../Tooltip'; + +/** + * Display a thumbnail for a display set. + */ +const Thumbnail = ({ + displaySetInstanceUID, + className, + imageSrc, + imageAltText, + description, + seriesNumber, + numInstances, + loadingProgress, + countIcon, + messages, + isActive, + onClick, + onDoubleClick, + thumbnailType, + modality, + viewPreset = 'thumbnails', + isHydratedForDerivedDisplaySet = false, + isTracked = false, + canReject = false, + dragData = {}, + onReject = () => {}, + onClickUntrack = () => {}, + ThumbnailMenuItems = () => {}, +}: withAppTypes): React.ReactNode => { + // TODO: We should wrap our thumbnail to create a "DraggableThumbnail", as + // this will still allow for "drag", even if there is no drop target for the + // specified item. + const [collectedProps, drag, dragPreview] = useDrag({ + type: 'displayset', + item: { ...dragData }, + canDrag: function (monitor) { + return Object.keys(dragData).length !== 0; + }, + }); + + const [lastTap, setLastTap] = useState(0); + + const handleTouchEnd = e => { + const currentTime = new Date().getTime(); + const tapLength = currentTime - lastTap; + if (tapLength < 300 && tapLength > 0) { + onDoubleClick(e); + } else { + onClick(e); + } + setLastTap(currentTime); + }; + + const renderThumbnailPreset = () => { + return ( +
+
+
+ {imageSrc ? ( + {imageAltText} + ) : ( +
+ )} + + {/* bottom left */} +
+
+
{modality}
+
+ + {/* top right */} +
+ + {isTracked && ( + + +
+ + +
+
+ +
+
+ +
+
+ + + {isTracked ? 'Series is tracked' : 'Series is untracked'} + + +
+
+
+
+ )} +
+ {/* bottom right */} +
+ +
+
+
+
+ + {description} + +
+ {description} +
+
+
+
+
S:{seriesNumber}
+
+
+ {countIcon ? ( + React.createElement(Icons[countIcon] || Icons.MissingIcon, { className: 'w-3' }) + ) : ( + + )} +
{numInstances}
+
+
+
+
+
+ ); + }; + + const renderListPreset = () => { + return ( +
+
+
+
+
+
{modality}
+ + {description} + +
+ {description} +
+
+
+
+ +
+
S:{seriesNumber}
+
+
+ {' '} + {countIcon ? ( + React.createElement(Icons[countIcon] || Icons.MissingIcon, { className: 'w-3' }) + ) : ( + + )} +
{numInstances}
+
+
+
+
+
+
+ + {isTracked && ( + + +
+ + +
+
+ +
+
+ +
+
+ + + {isTracked ? 'Series is tracked' : 'Series is untracked'} + + +
+
+
+
+ )} + +
+
+ ); + }; + + return ( +
+
+ {viewPreset === 'thumbnails' && renderThumbnailPreset()} + {viewPreset === 'list' && renderListPreset()} +
+
+ ); +}; + +Thumbnail.propTypes = { + displaySetInstanceUID: PropTypes.string.isRequired, + className: PropTypes.string, + imageSrc: PropTypes.string, + /** + * Data the thumbnail should expose to a receiving drop target. Use a matching + * `dragData.type` to identify which targets can receive this draggable item. + * If this is not set, drag-n-drop will be disabled for this thumbnail. + * + * Ref: https://react-dnd.github.io/react-dnd/docs/api/use-drag#specification-object-members + */ + dragData: PropTypes.shape({ + /** Must match the "type" a dropTarget expects */ + type: PropTypes.string.isRequired, + }), + imageAltText: PropTypes.string, + description: PropTypes.string.isRequired, + seriesNumber: PropTypes.any, + numInstances: PropTypes.number.isRequired, + loadingProgress: PropTypes.number, + messages: PropTypes.object, + isActive: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + onDoubleClick: PropTypes.func.isRequired, + viewPreset: PropTypes.string, + modality: PropTypes.string, + isHydratedForDerivedDisplaySet: PropTypes.bool, + isTracked: PropTypes.bool, + onClickUntrack: PropTypes.func, + countIcon: PropTypes.string, + thumbnailType: PropTypes.oneOf(['thumbnail', 'thumbnailTracked', 'thumbnailNoImage']), +}; + +export { Thumbnail }; diff --git a/platform/ui-next/src/components/Thumbnail/index.ts b/platform/ui-next/src/components/Thumbnail/index.ts new file mode 100644 index 0000000..6f55662 --- /dev/null +++ b/platform/ui-next/src/components/Thumbnail/index.ts @@ -0,0 +1,2 @@ +import { Thumbnail } from './Thumbnail'; +export { Thumbnail }; diff --git a/platform/ui-next/src/components/ThumbnailList/ThumbnailList.tsx b/platform/ui-next/src/components/ThumbnailList/ThumbnailList.tsx new file mode 100644 index 0000000..b1a088b --- /dev/null +++ b/platform/ui-next/src/components/ThumbnailList/ThumbnailList.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Thumbnail } from '../Thumbnail'; + +const ThumbnailList = ({ + thumbnails, + onThumbnailClick, + onThumbnailDoubleClick, + onClickUntrack, + activeDisplaySetInstanceUIDs = [], + viewPreset, + ThumbnailMenuItems, +}) => { + // Filter thumbnails into list items and thumbnail items + const listItems = thumbnails?.filter( + ({ componentType }) => componentType === 'thumbnailNoImage' || viewPreset === 'list' + ); + + const thumbnailItems = thumbnails?.filter( + ({ componentType }) => componentType !== 'thumbnailNoImage' && viewPreset === 'thumbnails' + ); + + return ( +
+ {/* Thumbnail Items */} + {thumbnailItems.length > 0 && ( +
+ {thumbnailItems.map(item => { + const { displaySetInstanceUID, componentType, numInstances, ...rest } = item; + + const isActive = activeDisplaySetInstanceUIDs.includes(displaySetInstanceUID); + return ( + + ); + })} +
+ )} + {/* List Items */} + {listItems.length > 0 && ( +
+ {listItems.map(item => { + const { displaySetInstanceUID, componentType, numInstances, ...rest } = item; + const isActive = activeDisplaySetInstanceUIDs.includes(displaySetInstanceUID); + return ( + + ); + })} +
+ )} +
+ ); +}; + +ThumbnailList.propTypes = { + thumbnails: PropTypes.arrayOf( + PropTypes.shape({ + displaySetInstanceUID: PropTypes.string.isRequired, + imageSrc: PropTypes.string, + imageAltText: PropTypes.string, + seriesDate: PropTypes.string, + seriesNumber: PropTypes.any, + numInstances: PropTypes.number, + description: PropTypes.string, + componentType: PropTypes.any, + isTracked: PropTypes.bool, + /** + * Data the thumbnail should expose to a receiving drop target. Use a matching + * `dragData.type` to identify which targets can receive this draggable item. + * If this is not set, drag-n-drop will be disabled for this thumbnail. + * + * Ref: https://react-dnd.github.io/react-dnd/docs/api/use-drag#specification-object-members + */ + dragData: PropTypes.shape({ + /** Must match the "type" a dropTarget expects */ + type: PropTypes.string.isRequired, + }), + }) + ), + activeDisplaySetInstanceUIDs: PropTypes.arrayOf(PropTypes.string), + onThumbnailClick: PropTypes.func.isRequired, + onThumbnailDoubleClick: PropTypes.func.isRequired, + onClickUntrack: PropTypes.func.isRequired, + viewPreset: PropTypes.string, + ThumbnailMenuItems: PropTypes.any, +}; + +export { ThumbnailList }; diff --git a/platform/ui-next/src/components/ThumbnailList/index.ts b/platform/ui-next/src/components/ThumbnailList/index.ts new file mode 100644 index 0000000..c4f5922 --- /dev/null +++ b/platform/ui-next/src/components/ThumbnailList/index.ts @@ -0,0 +1,2 @@ +import { ThumbnailList } from './ThumbnailList'; +export { ThumbnailList }; diff --git a/platform/ui-next/src/components/Toggle/Toggle.tsx b/platform/ui-next/src/components/Toggle/Toggle.tsx new file mode 100644 index 0000000..d6e8d0d --- /dev/null +++ b/platform/ui-next/src/components/Toggle/Toggle.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import * as TogglePrimitive from '@radix-ui/react-toggle'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '../../lib/utils'; + +const toggleVariants = cva( + 'inline-flex items-center justify-center rounded-md text-primary-foreground/80 font-medium transition-colors hover:bg-primary/20 hover:text-primary-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-primary/20 data-[state=on]:text-highlight', + { + variants: { + variant: { + default: 'bg-transparent hover:text-primary', + outline: + 'border border-input bg-transparent shadow-sm hover:bg-primary/20 hover:text-primary-foreground', + }, + size: { + default: 'h-[24px] w-[28px]', + sm: 'h-8 px-2', + lg: 'h-10 px-3', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +); + +const Toggle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & VariantProps +>(({ className, variant, size, ...props }, ref) => ( + +)); + +Toggle.displayName = TogglePrimitive.Root.displayName; + +export { Toggle, toggleVariants }; diff --git a/platform/ui-next/src/components/Toggle/index.ts b/platform/ui-next/src/components/Toggle/index.ts new file mode 100644 index 0000000..e96de61 --- /dev/null +++ b/platform/ui-next/src/components/Toggle/index.ts @@ -0,0 +1,3 @@ +import { Toggle, toggleVariants } from "./Toggle"; + +export { Toggle, toggleVariants }; diff --git a/platform/ui-next/src/components/Toggle/index.tsx b/platform/ui-next/src/components/Toggle/index.tsx new file mode 100644 index 0000000..9fd9a15 --- /dev/null +++ b/platform/ui-next/src/components/Toggle/index.tsx @@ -0,0 +1,3 @@ +import { Toggle, toggleVariants } from './Toggle'; + +export { Toggle, toggleVariants }; diff --git a/platform/ui-next/src/components/ToggleGroup/ToggleGroup.tsx b/platform/ui-next/src/components/ToggleGroup/ToggleGroup.tsx new file mode 100644 index 0000000..b2d8789 --- /dev/null +++ b/platform/ui-next/src/components/ToggleGroup/ToggleGroup.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'; +import { VariantProps } from 'class-variance-authority'; + +import { cn } from '../../lib/utils'; +import { toggleVariants } from '../Toggle'; + +const ToggleGroupContext = React.createContext>({ + size: 'default', + variant: 'default', +}); + +const ToggleGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, size, children, ...props }, ref) => ( + + {children} + +)); + +ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName; + +const ToggleGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, children, variant, size, ...props }, ref) => { + const context = React.useContext(ToggleGroupContext); + + return ( + + {children} + + ); +}); + +ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName; + +export { ToggleGroup, ToggleGroupItem }; diff --git a/platform/ui-next/src/components/ToggleGroup/index.ts b/platform/ui-next/src/components/ToggleGroup/index.ts new file mode 100644 index 0000000..8e54b59 --- /dev/null +++ b/platform/ui-next/src/components/ToggleGroup/index.ts @@ -0,0 +1,3 @@ +import { ToggleGroup, ToggleGroupItem } from "./ToggleGroup"; + +export { ToggleGroup, ToggleGroupItem }; diff --git a/platform/ui-next/src/components/ToolButton/ToolButton.tsx b/platform/ui-next/src/components/ToolButton/ToolButton.tsx new file mode 100644 index 0000000..7d530d5 --- /dev/null +++ b/platform/ui-next/src/components/ToolButton/ToolButton.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip'; +import { Icons } from '../Icons'; +import { Button } from '../Button'; +import { cn } from '../../lib/utils'; + +const baseClasses = '!rounded-lg inline-flex items-center justify-center'; +const defaultClasses = 'bg-transparent text-foreground/80 hover:bg-background hover:text-highlight'; +const activeClasses = 'bg-highlight text-background hover:!bg-highlight/80'; +const disabledClasses = + 'text-common-bright hover:bg-primary-dark hover:text-primary-light opacity-40 cursor-not-allowed'; + +const sizeClasses = { + default: { + buttonSizeClass: 'w-10 h-10', + iconSizeClass: 'h-7 w-7', + }, + small: { + buttonSizeClass: 'w-8 h-8', + iconSizeClass: 'h-6 w-6', + }, +}; + +interface ToolButtonProps { + id: string; + icon?: string; + label?: string; + tooltip?: string; + size?: 'default' | 'small'; + isActive?: boolean; + disabled?: boolean; + disabledText?: string; + commands?: Record; + onInteraction?: (details: { itemId: string; commands?: Record }) => void; + className?: string; +} + +function ToolButton(props: ToolButtonProps) { + const { + id, + icon = 'MissingIcon', + label, + tooltip, + size = 'default', + disabled = false, + isActive = false, + disabledText, + commands, + onInteraction, + className, + } = props; + + const { buttonSizeClass, iconSizeClass } = sizeClasses[size]; + + const buttonClasses = cn( + baseClasses, + buttonSizeClass, + disabled ? disabledClasses : isActive ? activeClasses : defaultClasses, + className + ); + + const defaultTooltip = tooltip || label; + const disabledTooltip = disabled && disabledText ? disabledText : null; + const hasTooltip = defaultTooltip || disabledTooltip; + + return ( + + + {/* TooltipTrigger is a span since a disabled button does not fire events and the tooltip + will not show. */} + + + + + + {hasTooltip && ( + <> +
{defaultTooltip}
+ {disabledTooltip &&
{disabledTooltip}
} + + )} +
+
+ ); +} + +export default ToolButton; diff --git a/platform/ui-next/src/components/ToolButton/ToolButtonList.tsx b/platform/ui-next/src/components/ToolButton/ToolButtonList.tsx new file mode 100644 index 0000000..e2374a4 --- /dev/null +++ b/platform/ui-next/src/components/ToolButton/ToolButtonList.tsx @@ -0,0 +1,207 @@ +import React from 'react'; +import { Button } from '../Button'; +import { Icons } from '../Icons'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '../DropdownMenu'; +import { cn } from '../../lib/utils'; +import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip'; + +/** + * ToolButtonList Component + * Root component that wraps the default and dropdown sections + * ----------------------------------------------- + */ +interface ToolButtonListProps extends React.HTMLAttributes { + children?: React.ReactNode; +} + +const ToolButtonList = React.forwardRef( + ({ className, children, ...props }, ref) => { + return ( +
+ {children} +
+ ); + } +); +ToolButtonList.displayName = 'ToolButtonList'; + +/** + * ToolButtonListDefault Component + * Container for the default/primary tool button + * ----------------------------------------------- + */ +interface ToolButtonListDefaultProps extends React.HTMLAttributes { + children?: React.ReactNode; + tooltip?: string; + disabledText?: string; + disabled?: boolean; +} + +const ToolButtonListDefault = React.forwardRef( + ({ className, children, tooltip, disabledText, disabled, ...props }, ref) => { + const hasTooltip = tooltip || disabledText; + + const defaultContent = ( +
+ {children} +
+ ); + + if (!hasTooltip) { + return defaultContent; + } + + return ( + + + {defaultContent} + + + {tooltip &&
{tooltip}
} + {disabledText && disabled &&
{disabledText}
} +
+
+ ); + } +); +ToolButtonListDefault.displayName = 'ToolButtonListDefault'; + +/** + * ToolButtonListDropDown Component + * Container for the dropdown section with trigger and content + * ----------------------------------------------- + */ +interface ToolButtonListDropDownProps { + children: React.ReactNode; + className?: string; +} + +const ToolButtonListDropDown = React.forwardRef( + ({ children, className, ...props }, ref) => ( + + + + + + {children} + + + ) +); +ToolButtonListDropDown.displayName = 'ToolButtonListDropDown'; + +/** + * ToolButtonListItem Component + * Individual item in the dropdown menu + * ----------------------------------------------- + */ +interface ToolButtonListItemProps extends React.ComponentProps { + icon?: string; + children?: React.ReactNode; + className?: string; + disabledText?: string; + tooltip?: string; +} + +const ToolButtonListItem = React.forwardRef< + React.ElementRef, + ToolButtonListItemProps +>(({ className, children, icon, disabledText, tooltip, disabled, ...props }, ref) => { + const defaultTooltip = tooltip || (typeof children === 'string' ? children : undefined); + const hasTooltip = defaultTooltip || disabledText; + + const menuItem = ( + + {icon && ( + + )} + {children} + + ); + + // Todo: there is a weird issue where i can't control the duration of the delay + // for the items in this list, causing the tooltip to show up too early in the + // dropdown menu. So i'm just removing the tooltip for list items unless the disabledText is set. + if (!disabled) { + return menuItem; + } + + return ( + + + {menuItem} + + + {defaultTooltip &&
{defaultTooltip}
} + {disabledText && disabled &&
{disabledText}
} +
+
+ ); +}); +ToolButtonListItem.displayName = 'ToolButtonListItem'; + +/** + * ToolButtonListDivider Component + * Divider between items in the dropdown menu + * ----------------------------------------------- + */ +const ToolButtonListDivider = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +ToolButtonListDivider.displayName = 'ToolButtonListDivider'; + +export { + ToolButtonList, + ToolButtonListDefault, + ToolButtonListDropDown, + ToolButtonListItem, + ToolButtonListDivider, +}; diff --git a/platform/ui-next/src/components/ToolButton/index.ts b/platform/ui-next/src/components/ToolButton/index.ts new file mode 100644 index 0000000..49dce1d --- /dev/null +++ b/platform/ui-next/src/components/ToolButton/index.ts @@ -0,0 +1,8 @@ +export { default as ToolButton } from './ToolButton'; +export { + ToolButtonList, + ToolButtonListDefault, + ToolButtonListDropDown, + ToolButtonListItem, + ToolButtonListDivider, +} from './ToolButtonList'; diff --git a/platform/ui-next/src/components/Tooltip/Tooltip.tsx b/platform/ui-next/src/components/Tooltip/Tooltip.tsx new file mode 100644 index 0000000..ad8151f --- /dev/null +++ b/platform/ui-next/src/components/Tooltip/Tooltip.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import * as TooltipPrimitive from '@radix-ui/react-tooltip'; + +import { cn } from '../../lib/utils'; + +const TooltipProvider = TooltipPrimitive.Provider; + +const Tooltip = TooltipPrimitive.Root; + +const TooltipTrigger = TooltipPrimitive.Trigger; + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)); +TooltipContent.displayName = TooltipPrimitive.Content.displayName; + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/platform/ui-next/src/components/Tooltip/index.ts b/platform/ui-next/src/components/Tooltip/index.ts new file mode 100644 index 0000000..a775f00 --- /dev/null +++ b/platform/ui-next/src/components/Tooltip/index.ts @@ -0,0 +1,3 @@ +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './Tooltip'; + +export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; diff --git a/platform/ui-next/src/components/Viewport/PatientInfo.tsx b/platform/ui-next/src/components/Viewport/PatientInfo.tsx new file mode 100644 index 0000000..5be1798 --- /dev/null +++ b/platform/ui-next/src/components/Viewport/PatientInfo.tsx @@ -0,0 +1,138 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { useTranslation } from 'react-i18next'; +import { Icons, Tooltip, TooltipTrigger, TooltipContent } from '../../components'; + +const classes = { + infoHeader: 'text-base text-primary-light', + infoText: 'text-base text-white max-w-24 truncate', + firstRow: 'flex flex-col', + row: 'flex flex-col ml-4', +}; + +/** + * A small info icon that, when clicked, can reveal additional patient details in a tooltip overlay. + */ +function PatientInfo({ + patientName, + patientSex, + patientAge, + MRN, + thickness, + thicknessUnits, + spacing, + scanner, + isOpen, + showPatientInfoRef, +}) { + const { t } = useTranslation('PatientInfo'); + + // strip leading '0' from age if present + while (patientAge.charAt(0) === '0') { + patientAge = patientAge.substr(1); + } + + return ( +
+ + + + + {isOpen && ( + +
+
+ +
+
+ + {patientName} + +
+
+ {t('Sex')} + + {patientSex} + +
+
+ {t('Age')} + + {patientAge} + +
+
+ {t('MRN')} + + {MRN} + +
+
+
+
+ {t('Thickness')} + + {thicknessUnits ? `${thickness}${thicknessUnits}` : `${thickness}`} + +
+
+ {t('Spacing')} + + {spacing} + +
+
+ {t('Scanner')} + + {scanner} + +
+
+
+
+
+ )} +
+
+ ); +} + +PatientInfo.propTypes = { + patientName: PropTypes.string, + patientSex: PropTypes.string, + patientAge: PropTypes.string, + MRN: PropTypes.string, + thickness: PropTypes.string, + thicknessUnits: PropTypes.string, + spacing: PropTypes.string, + scanner: PropTypes.string, + isOpen: PropTypes.bool, + showPatientInfoRef: PropTypes.object, +}; + +export { PatientInfo }; diff --git a/platform/ui-next/src/components/Viewport/ViewportActionArrows.tsx b/platform/ui-next/src/components/Viewport/ViewportActionArrows.tsx new file mode 100644 index 0000000..96b41ee --- /dev/null +++ b/platform/ui-next/src/components/Viewport/ViewportActionArrows.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; + +import { Icons } from '@ohif/ui-next'; + +const arrowClasses = + 'cursor-pointer flex items-center justify-center shrink-0 text-primary-light active:text-white hover:bg-secondary-light/60 rounded'; + +/** + * A small set of left/right arrow icons for stepping through slices or series. + */ +function ViewportActionArrows({ onArrowsClick, className }) { + return ( +
+
+ onArrowsClick(-1)} /> +
+
+ onArrowsClick(1)} /> +
+
+ ); +} + +ViewportActionArrows.propTypes = { + onArrowsClick: PropTypes.func.isRequired, + className: PropTypes.string, +}; + +export { ViewportActionArrows }; diff --git a/platform/ui-next/src/components/Viewport/ViewportActionBar.tsx b/platform/ui-next/src/components/Viewport/ViewportActionBar.tsx new file mode 100644 index 0000000..9bba91b --- /dev/null +++ b/platform/ui-next/src/components/Viewport/ViewportActionBar.tsx @@ -0,0 +1,128 @@ +import React, { + MouseEventHandler, + ReactElement, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; +import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; + +import { Icons } from '@ohif/ui-next'; +import { PatientInfo } from './PatientInfo'; + +/** + * This is the modern Viewport Action Bar, showing patient info, series date, + * series description, and optional next/prev arrows if there's enough screen width. + */ +type ViewportActionBarProps = { + studyData: any; + onArrowsClick: (arrow: string) => void; + onDoubleClick: MouseEventHandler; + getStatusComponent: () => ReactElement; +}; + +function ViewportActionBar({ + studyData, + onArrowsClick, + onDoubleClick, + getStatusComponent, +}: ViewportActionBarProps): JSX.Element { + const { label, studyDate, seriesDescription, patientInformation } = studyData; + const { patientName, patientSex, patientAge, MRN, thickness, thicknessUnits, spacing, scanner } = + patientInformation; + + const [showPatientInfo, setShowPatientInfo] = useState(false); + const showPatientInfoElemRef = useRef(null); + const { t } = useTranslation(); + + // handle click outside to close patient info + const handleClickOutside = useCallback( + (evt: MouseEvent) => { + if ( + showPatientInfo && + showPatientInfoElemRef.current && + !showPatientInfoElemRef.current.contains(evt.target as Node) + ) { + setShowPatientInfo(false); + } + }, + [showPatientInfoElemRef, showPatientInfo] + ); + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [handleClickOutside]); + + return ( +
+ {getStatusComponent()} + {!!label?.length && {label}} +
+ + {studyDate} + +
+ {seriesDescription} + {/* Prev/Next icons */} + onArrowsClick('left')} + /> + onArrowsClick('right')} + /> + {/* Patient Info */} +
setShowPatientInfo(!showPatientInfo)}> + +
+
+ ); +} + +ViewportActionBar.propTypes = { + onArrowsClick: PropTypes.func.isRequired, + onDoubleClick: PropTypes.func, + studyData: PropTypes.shape({ + label: PropTypes.string.isRequired, + studyDate: PropTypes.string.isRequired, + seriesDescription: PropTypes.string.isRequired, + patientInformation: PropTypes.shape({ + patientName: PropTypes.string, + patientSex: PropTypes.string, + patientAge: PropTypes.string, + MRN: PropTypes.string, + thickness: PropTypes.string, + thicknessUnits: PropTypes.string, + spacing: PropTypes.string, + scanner: PropTypes.string, + }), + }).isRequired, + getStatusComponent: PropTypes.func.isRequired, +}; + +export { ViewportActionBar }; diff --git a/platform/ui-next/src/components/Viewport/ViewportActionButton.tsx b/platform/ui-next/src/components/Viewport/ViewportActionButton.tsx new file mode 100644 index 0000000..d99421b --- /dev/null +++ b/platform/ui-next/src/components/Viewport/ViewportActionButton.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +/** + * A button that can trigger commands when clicked. + */ +function ViewportActionButton({ onInteraction, commands, id, children }) { + return ( +
{ + onInteraction({ + itemId: id, + commands, + }); + }} + > + {children} +
+ ); +} + +ViewportActionButton.propTypes = { + id: PropTypes.string, + onInteraction: PropTypes.func.isRequired, + commands: PropTypes.array, + children: PropTypes.node, +}; + +export { ViewportActionButton }; diff --git a/platform/ui-next/src/components/Viewport/ViewportActionCorners.tsx b/platform/ui-next/src/components/Viewport/ViewportActionCorners.tsx new file mode 100644 index 0000000..f02d4ff --- /dev/null +++ b/platform/ui-next/src/components/Viewport/ViewportActionCorners.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; + +/** + * A small container that can render multiple "corner" items (like icons, status) + * in each corner of the viewport: top-left, top-right, bottom-left, bottom-right. + */ +export enum ViewportActionCornersLocations { + topLeft, + topRight, + bottomLeft, + bottomRight, +} + +const commonClasses = 'pointer-events-auto flex items-center gap-1'; +const locationClasses = { + [ViewportActionCornersLocations.topLeft]: classNames( + commonClasses, + 'absolute top-[4px] left-[0px] pl-[4px]' + ), + [ViewportActionCornersLocations.topRight]: classNames( + commonClasses, + 'absolute top-[4px] right-[4px] right-viewport-scrollbar' + ), + [ViewportActionCornersLocations.bottomLeft]: classNames( + commonClasses, + 'absolute bottom-[4px] left-[0px] pl-[4px]' + ), + [ViewportActionCornersLocations.bottomRight]: classNames( + commonClasses, + 'absolute bottom-[4px] right-[0px] right-viewport-scrollbar' + ), +}; + +function ViewportActionCorners({ cornerComponents }) { + if (!cornerComponents) { + return null; + } + + return ( +
{ + event.preventDefault(); + event.stopPropagation(); + }} + > + {Object.entries(cornerComponents).map(([location, locationArray]) => ( +
+ {locationArray.map(componentInfo => ( +
{componentInfo.component}
+ ))} +
+ ))} +
+ ); +} + +ViewportActionCorners.propTypes = { + cornerComponents: PropTypes.object.isRequired, +}; + +export { ViewportActionCorners }; diff --git a/platform/ui-next/src/components/Viewport/ViewportGrid.tsx b/platform/ui-next/src/components/Viewport/ViewportGrid.tsx new file mode 100644 index 0000000..4c00a10 --- /dev/null +++ b/platform/ui-next/src/components/Viewport/ViewportGrid.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +/** + * A minimal top-level container that organizes multiple + * children in a grid. Typically driven by a layout config. + */ +function ViewportGrid({ numRows, numCols, layoutType, children }) { + return ( +
+ {children} +
+ ); +} + +ViewportGrid.propTypes = { + numRows: PropTypes.number.isRequired, + numCols: PropTypes.number.isRequired, + layoutType: PropTypes.string, + children: PropTypes.arrayOf(PropTypes.node).isRequired, +}; + +export { ViewportGrid }; diff --git a/platform/ui-next/src/components/Viewport/ViewportOverlay.css b/platform/ui-next/src/components/Viewport/ViewportOverlay.css new file mode 100644 index 0000000..e8aebd0 --- /dev/null +++ b/platform/ui-next/src/components/Viewport/ViewportOverlay.css @@ -0,0 +1,22 @@ +.imageViewerViewport.empty ~ .ViewportOverlay { + display: none; +} +.ViewportOverlay { + color: #9ccef9; +} +.ViewportOverlay .overlay-element { + position: absolute; + font-weight: 400; + text-shadow: 1px 1px #000; + pointer-events: none; +} +.overlay-top { + top: 28px; +} +.overlay-bottom { + bottom: 3px; +} + +.overlay-text { + text-shadow: 0.8px 0.8px 0.5px rgba(0, 0, 0, 0.75); +} diff --git a/platform/ui-next/src/components/Viewport/ViewportOverlay.tsx b/platform/ui-next/src/components/Viewport/ViewportOverlay.tsx new file mode 100644 index 0000000..5164142 --- /dev/null +++ b/platform/ui-next/src/components/Viewport/ViewportOverlay.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; + +import './ViewportOverlay.css'; + +/** + * Renders text overlays (top-left, top-right, bottom-left, bottom-right) + * around the active viewport for metadata or status messages. + * + * The parent is responsible for styling offsets. + */ +const classes = { + topLeft: 'overlay-top left-viewport', + topRight: 'overlay-top right-viewport-scrollbar', + bottomRight: 'overlay-bottom right-viewport-scrollbar', + bottomLeft: 'overlay-bottom left-viewport', +}; + +function ViewportOverlay({ + topLeft, + topRight, + bottomRight, + bottomLeft, + color = 'text-primary-light', +}) { + const overlay = 'absolute pointer-events-none viewport-overlay'; + + return ( +
+
+ {topLeft} +
+
+ {topRight} +
+
+ {bottomRight} +
+
+ {bottomLeft} +
+
+ ); +} + +ViewportOverlay.propTypes = { + topLeft: PropTypes.node, + topRight: PropTypes.node, + bottomRight: PropTypes.node, + bottomLeft: PropTypes.node, + color: PropTypes.string, +}; + +export { ViewportOverlay }; diff --git a/platform/ui-next/src/components/Viewport/ViewportPane.tsx b/platform/ui-next/src/components/Viewport/ViewportPane.tsx new file mode 100644 index 0000000..398b845 --- /dev/null +++ b/platform/ui-next/src/components/Viewport/ViewportPane.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { useDrop } from 'react-dnd'; + +/** + * The "pane" that encloses a Cornerstone or other type of Viewport. This handles + * drag-and-drop for display sets, activation on click, etc. + */ +function ViewportPane({ + children, + className, + customStyle, + isActive, + onDrop, + onDoubleClick, + onInteraction = () => {}, + acceptDropsFor, +}) { + let dropElement = null; + + const [{ isHovered, isHighlighted }, drop] = useDrop({ + accept: acceptDropsFor, + drop: (droppedItem, monitor) => { + if (monitor.canDrop() && monitor.isOver() && onDrop) { + onInteraction(); + onDrop(droppedItem); + } + }, + collect: monitor => ({ + isHighlighted: monitor.canDrop(), + isHovered: monitor.isOver(), + }), + }); + + const focus = () => { + if (dropElement) { + dropElement.focus(); + } + }; + + const onInteractionHandler = event => { + focus(); + onInteraction(event); + }; + + const refHandler = element => { + drop(element); + dropElement = element; + }; + + return ( +
+
+ {children} +
+
+ ); +} + +ViewportPane.propTypes = { + children: PropTypes.node.isRequired, + className: PropTypes.string, + isActive: PropTypes.bool.isRequired, + acceptDropsFor: PropTypes.string.isRequired, + onDrop: PropTypes.func.isRequired, + onInteraction: PropTypes.func.isRequired, + onDoubleClick: PropTypes.func, + customStyle: PropTypes.object, +}; + +export { ViewportPane }; diff --git a/platform/ui-next/src/components/Viewport/index.ts b/platform/ui-next/src/components/Viewport/index.ts new file mode 100644 index 0000000..feb9a0a --- /dev/null +++ b/platform/ui-next/src/components/Viewport/index.ts @@ -0,0 +1,8 @@ +export { ViewportActionButton } from './ViewportActionButton'; +export { PatientInfo } from './PatientInfo'; +export { ViewportActionBar } from './ViewportActionBar'; +export { ViewportActionArrows } from './ViewportActionArrows'; +export { ViewportPane } from './ViewportPane'; +export { ViewportActionCorners, ViewportActionCornersLocations } from './ViewportActionCorners'; +export { ViewportOverlay } from './ViewportOverlay'; +export { ViewportGrid } from './ViewportGrid'; diff --git a/platform/ui-next/src/components/index.ts b/platform/ui-next/src/components/index.ts new file mode 100644 index 0000000..0183ed4 --- /dev/null +++ b/platform/ui-next/src/components/index.ts @@ -0,0 +1,228 @@ +import { Button, buttonVariants } from './Button'; +import { ThemeWrapper } from './ThemeWrapper'; +import { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} from './Command'; +import { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} from './Dialog'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './Select'; +import { Clipboard } from './Clipboard'; +import { Combobox } from './Combobox'; +import { Popover, PopoverContent, PopoverTrigger, PopoverAnchor } from './Popover'; +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from './Resizable'; +import { Calendar } from './Calendar'; +import { DatePickerWithRange } from './DateRange'; +import { Separator } from './Separator'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from './Tabs'; +import { Toggle, toggleVariants } from './Toggle'; +import { ToggleGroup, ToggleGroupItem } from './ToggleGroup'; +import { Input } from './Input'; +import { Label } from './Label'; +import { Switch } from './Switch'; +import { Checkbox } from './Checkbox'; +import { Slider } from './Slider'; +import { ScrollArea, ScrollBar } from './ScrollArea'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './Accordion'; +import { Icons } from './Icons'; +import { SidePanel } from './SidePanel'; +import { StudyItem } from './StudyItem'; +import { StudyBrowser } from './StudyBrowser'; +import { StudyBrowserSort } from './StudyBrowserSort'; +import { StudyBrowserViewOptions } from './StudyBrowserViewOptions'; +import { Thumbnail } from './Thumbnail'; +import { ThumbnailList } from './ThumbnailList'; +import { PanelSection } from './PanelSection'; +import { DisplaySetMessageListTooltip } from './DisplaySetMessageListTooltip'; +import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './Tooltip'; +import { ToolboxUI, Toolbox } from './OHIFToolbox'; +import Numeric from './Numeric'; + +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} from './DropdownMenu'; +import { Onboarding } from './Onboarding'; +import { DoubleSlider } from './DoubleSlider'; +import { DataRow } from './DataRow'; +import { MeasurementTable } from './MeasurementTable'; +import { SegmentationTable, useSegmentationTableContext } from './SegmentationTable'; +import { Toaster, toast } from './Sonner'; +import { StudySummary } from './StudySummary'; +import { ErrorBoundary } from './Errorboundary'; +import { Header } from './Header'; +import { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from './Card'; +import { + ViewportActionButton, + PatientInfo, + ViewportActionBar, + ViewportActionArrows, + ViewportPane, + ViewportActionCorners, + ViewportActionCornersLocations, + ViewportOverlay, + ViewportGrid, +} from './Viewport'; +import { + ToolButton, + ToolButtonList, + ToolButtonListDefault, + ToolButtonListDropDown, + ToolButtonListItem, + ToolButtonListDivider, +} from './ToolButton'; + +export { + Numeric, + ErrorBoundary, + Button, + buttonVariants, + ThemeWrapper, + DoubleSlider, + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, + Combobox, + Popover, + PopoverContent, + PopoverTrigger, + PopoverAnchor, + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, + Calendar, + DatePickerWithRange, + Input, + Label, + Tabs, + TabsList, + TabsTrigger, + TabsContent, + Separator, + Switch, + Checkbox, + Toggle, + toggleVariants, + Slider, + ScrollArea, + ToggleGroup, + ToggleGroupItem, + ScrollBar, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Icons, + SidePanel, + StudyItem, + StudyBrowser, + StudyBrowserSort, + StudyBrowserViewOptions, + Thumbnail, + ThumbnailList, + PanelSection, + DisplaySetMessageListTooltip, + ToolboxUI, + Toolbox, + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, + Onboarding, + Select, + SelectTrigger, + SelectContent, + SelectItem, + SelectValue, + Tooltip, + TooltipTrigger, + TooltipContent, + TooltipProvider, + DataRow, + MeasurementTable, + Toaster, + toast, + SegmentationTable, + useSegmentationTableContext, + StudySummary, + Header, + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, + ViewportActionButton, + PatientInfo, + ViewportActionBar, + ViewportActionArrows, + ViewportPane, + ViewportActionCorners, + ViewportActionCornersLocations, + ViewportOverlay, + ViewportGrid, + Clipboard, + ToolButton, + ToolButtonList, + ToolButtonListDefault, + ToolButtonListDropDown, + ToolButtonListItem, + ToolButtonListDivider, +}; diff --git a/platform/ui-next/src/contextProviders/NotificationProvider.tsx b/platform/ui-next/src/contextProviders/NotificationProvider.tsx new file mode 100644 index 0000000..09e5262 --- /dev/null +++ b/platform/ui-next/src/contextProviders/NotificationProvider.tsx @@ -0,0 +1,88 @@ +import React, { createContext, useContext, useCallback, useEffect, ReactNode } from 'react'; +import PropTypes from 'prop-types'; +import { Toaster, toast } from '../components'; + +const NotificationContext = createContext(null); + +export const useNotification = () => useContext(NotificationContext); + +const NotificationProvider = ({ children, service }) => { + const DEFAULT_OPTIONS = { + title: '', + message: '', + duration: 5000, + position: 'bottom-right', // Aligning to Sonner's positioning system + type: 'info', // info, success, error + }; + + const show = useCallback(options => { + const { title, message, duration, position, type, promise } = { + ...DEFAULT_OPTIONS, + ...options, + }; + + if (promise) { + return toast.promise(promise, { + loading: title || 'Loading...', + success: (data: unknown) => ({ + title: title || 'Success', + description: typeof message === 'function' ? message(data) : message, + }), + error: (err: unknown) => ({ + title: title || 'Error', + description: typeof message === 'function' ? message(err) : message, + }), + }); + } + + return toast[type](title, { + duration, + position, + description: message, + }); + }, []); + + const hide = useCallback(id => { + toast.dismiss(id); + }, []); + + const hideAll = useCallback(() => { + toast.dismiss(); + }, []); + + /** + * Sets the implementation of a notification service that can be used by extensions. + * + * @returns void + */ + useEffect(() => { + if (service) { + service.setServiceImplementation({ hide, show }); + } + }, [service, hide, show]); + + return ( + + + {children} + + ); +}; + +NotificationProvider.propTypes = { + children: PropTypes.node.isRequired, +}; + +export const withNotification = Component => { + return function WrappedComponent(props) { + const notificationContext = useNotification(); + return ( + + ); + }; +}; + +export default NotificationProvider; diff --git a/platform/ui-next/src/contextProviders/ToolboxContext.tsx b/platform/ui-next/src/contextProviders/ToolboxContext.tsx new file mode 100644 index 0000000..23c87f8 --- /dev/null +++ b/platform/ui-next/src/contextProviders/ToolboxContext.tsx @@ -0,0 +1,109 @@ +import React, { createContext, useContext, useReducer } from 'react'; + +export const initialState = {}; + +export const toolboxReducer = (state, action) => { + const { toolbarSectionId } = action.payload; + + if (!state[toolbarSectionId]) { + state[toolbarSectionId] = { activeTool: null, toolOptions: {}, selectedEvent: false }; + } + + switch (action.type) { + case 'SET_ACTIVE_TOOL': + return { + ...state, + [toolbarSectionId]: { + ...state[toolbarSectionId], + activeTool: action.payload.activeTool, + selectedEvent: true, + }, + }; + case 'UPDATE_TOOL_OPTION': + const { toolName, optionName, value } = action.payload; + return { + ...state, + [toolbarSectionId]: { + ...state[toolbarSectionId], + selectedEvent: false, + toolOptions: { + ...state[toolbarSectionId].toolOptions, + [toolName]: state[toolbarSectionId].toolOptions[toolName].map(option => + option.id === optionName ? { ...option, value } : option + ), + }, + }, + }; + case 'INITIALIZE_TOOL_OPTIONS': + // Initialize tool options for each toolbarSectionId + return { + ...state, + selectedEvent: false, + [action.toolbarSectionId]: { + ...state[action.toolbarSectionId], + toolOptions: action.payload, + }, + }; + default: + return state; + } +}; + +const ToolboxContext = createContext(); + +export const ToolboxProvider = ({ children }) => { + const [state, dispatch] = useReducer(toolboxReducer, initialState); + + const handleToolSelect = (toolbarSectionId, toolName) => { + dispatch({ + type: 'SET_ACTIVE_TOOL', + payload: { toolbarSectionId, activeTool: toolName }, + }); + }; + + const handleToolOptionChange = (toolbarSectionId, toolName, optionName, newValue) => { + dispatch({ + type: 'UPDATE_TOOL_OPTION', + payload: { toolbarSectionId, toolName, optionName, value: newValue }, + }); + }; + + const initializeToolOptions = (toolbarSectionId, toolOptions) => { + dispatch({ + type: 'INITIALIZE_TOOL_OPTIONS', + toolbarSectionId, + payload: toolOptions, + }); + }; + + const api = { handleToolSelect, handleToolOptionChange, initializeToolOptions }; + + const value = { state, api }; + + return {children}; +}; + +/** + * Custom hook for accessing toolbox state and actions for a specific toolbar section. + * You can use this hook to access the state and actions for a specific toolbar section ( + * defined by the toolbarSectionId) in your custom toolbar components. This hook + * helps to manage the state and actions for the tools and their options in the toolbar. + */ +export const useToolbox = toolbarSectionId => { + const context = useContext(ToolboxContext); + if (context === undefined) { + throw new Error('useToolbox must be used within a ToolboxProvider'); + } + const { state, api } = context; + + return { + state: state[toolbarSectionId] || { activeTool: null, toolOptions: {} }, + api: { + handleToolSelect: toolName => api.handleToolSelect(toolbarSectionId, toolName), + handleToolOptionChange: (toolName, optionName, value) => + api.handleToolOptionChange(toolbarSectionId, toolName, optionName, value), + initializeToolOptions: toolOptions => + api.initializeToolOptions(toolbarSectionId, toolOptions), + }, + }; +}; diff --git a/platform/ui-next/src/contextProviders/ViewportGridProvider.tsx b/platform/ui-next/src/contextProviders/ViewportGridProvider.tsx new file mode 100644 index 0000000..3a63c96 --- /dev/null +++ b/platform/ui-next/src/contextProviders/ViewportGridProvider.tsx @@ -0,0 +1,522 @@ +import React, { + createContext, + useCallback, + useContext, + useEffect, + useReducer, + ReactNode, +} from 'react'; +import merge from 'lodash.merge'; + +import PropTypes from 'prop-types'; +import { ViewportGridService, utils } from '@ohif/core'; + +const DEFAULT_STATE: AppTypes.ViewportGrid.State = { + activeViewportId: null, + layout: { + numRows: 0, + numCols: 0, + layoutType: 'grid', + }, + // this flag is used to determine if the hanging protocol layout is active + // so that we can inherit the viewport options from the previous state + // otherwise we will not allow that. Basically the issue is that we need + // to be able to come out of the hanging protocol layout and go back to the + // regular layout e.g., if we are in the MPR hanging protocol, and someone use + // 1x1 layout by custom layout selector, there is no way to drag and drop + // a non-reconstructible series to the viewport since it will always + // inherit the hanging protocol layout options (volume viewport), + // so we need to be able to switch back to the regular layout. + isHangingProtocolLayout: false, + // Viewports structure has been changed to Map (previously it was + // tied to the viewportIndex which caused multiple issues. Now we have + // moved completely to viewportId which is unique for each viewport. + viewports: new Map( + Object.entries({ + default: { + viewportId: 'default', + displaySetInstanceUIDs: [], + isReady: false, + viewportOptions: { + viewportId: 'default', + }, + displaySetSelectors: [], + displaySetOptions: [{}], + x: 0, // left + y: 0, // top + width: 100, + height: 100, + viewportLabel: null, + }, + }) + ), +}; + +const determineActiveViewportId = ( + state: AppTypes.ViewportGrid.State, + newViewports: Map +) => { + const { activeViewportId } = state; + const currentActiveViewport = state.viewports.get(activeViewportId); + + if (!currentActiveViewport) { + // if there is no active viewport, we should just return the first viewport + const firstViewport = newViewports.values().next().value; + return firstViewport.viewportOptions.viewportId; + } + + // for the new viewports, we should rank them by the displaySetInstanceUIDs + // they are displaying and the orientation then we can find the active viewport + const currentActiveDisplaySetInstanceUIDs = currentActiveViewport.displaySetInstanceUIDs; + + // This doesn't take into account where stack viewport is converting to volumeViewport + // since in stack viewport we don't have a concept of "orientation" as a string + // maybe we should calculate the orientation based on the active imageId + // so that we can compare it with the new viewports (which might be volume viewports) + // and find the best match + const currentOrientation = currentActiveViewport.viewportOptions.orientation; + + const filteredNewViewports = Array.from(newViewports.values()).filter( + viewport => viewport.displaySetInstanceUIDs?.length > 0 + ); + + const sortedViewports = Array.from(filteredNewViewports.values()).sort((a, b) => { + // Compare orientations + const aOrientationMatch = a.viewportOptions.orientation === currentOrientation; + const bOrientationMatch = b.viewportOptions.orientation === currentOrientation; + if (aOrientationMatch !== bOrientationMatch) { + return bOrientationMatch - aOrientationMatch; + } + + // Compare displaySetInstanceUIDs + const aMatch = a.displaySetInstanceUIDs.some(uid => + currentActiveDisplaySetInstanceUIDs.includes(uid) + ); + const bMatch = b.displaySetInstanceUIDs.some(uid => + currentActiveDisplaySetInstanceUIDs.includes(uid) + ); + if (aMatch !== bMatch) { + return bMatch - aMatch; + } + + return 0; // Return 0 if no differences found + }); + + if (!sortedViewports?.length) { + return null; + } + + return sortedViewports[0].viewportId; +}; + +// Define the API interface +interface ViewportGridApi { + getState: () => AppTypes.ViewportGrid.State; + setActiveViewportId: (index: string) => void; + setDisplaySetsForViewport: (props: any) => void; + setDisplaySetsForViewports: (props: any[]) => void; + setLayout: (layout: AppTypes.ViewportGrid.Layout) => void; + reset: () => void; + set: (gridLayoutState: Partial) => void; + getNumViewportPanes: () => number; + setViewportIsReady: (viewportId: string, isReady: boolean) => void; + getGridViewportsReady: () => boolean; + getActiveViewportOptionByKey: (key: string) => any; + setViewportGridSizeChanged: (props: any) => void; + publishViewportsReady: () => void; +} + +// Update the context type +export const ViewportGridContext = createContext<[AppTypes.ViewportGrid.State, ViewportGridApi]>([ + DEFAULT_STATE, + {} as ViewportGridApi, +]); + +// Update the provider props type +interface ViewportGridProviderProps { + children: ReactNode; + service: ViewportGridService; +} + +export function ViewportGridProvider({ children, service }: ViewportGridProviderProps) { + const viewportGridReducer = (state: AppTypes.ViewportGrid.State, action) => { + switch (action.type) { + case 'SET_ACTIVE_VIEWPORT_ID': { + return { ...state, ...{ activeViewportId: action.payload } }; + } + + /** + * Sets the display sets for multiple viewports. + * This is a replacement for the older set display set for viewport (single) + * because the old one had race conditions wherein the viewports could + * render partially in various ways causing exceptions. + */ + case 'SET_DISPLAYSETS_FOR_VIEWPORTS': { + const { payload } = action; + const viewports = new Map(state.viewports); + + payload.forEach(updatedViewport => { + const { viewportId, displaySetInstanceUIDs } = updatedViewport; + + if (!viewportId) { + throw new Error('ViewportId is required to set display sets for viewport'); + } + + const previousViewport = viewports.get(viewportId); + + // remove options that were meant for one time usage + if (previousViewport?.viewportOptions?.initialImageOptions) { + const { useOnce } = previousViewport.viewportOptions.initialImageOptions; + if (useOnce) { + previousViewport.viewportOptions.initialImageOptions = null; + } + } + + // Use the newly provide viewportOptions and display set options + // when provided, and otherwise fall back to the previous ones. + // That allows for easy updates of just the display set. + let viewportOptions = merge( + {}, + previousViewport?.viewportOptions, + updatedViewport?.viewportOptions + ); + + const displaySetOptions = updatedViewport?.displaySetOptions || []; + if (!displaySetOptions.length) { + // Copy all the display set options, assuming a full set of displaySet UID's is provided. + if (state.isHangingProtocolLayout) { + displaySetOptions.push(...(previousViewport.displaySetOptions || [])); + } + if (!displaySetOptions.length) { + displaySetOptions.push({}); + } + } + + // if it is not part of the hanging protocol layout, we should remove the toolGroupId + // and viewportType from the viewportOptions so that it doesn't + // inherit the hanging protocol layout options, only when + // the viewport options is not provided (e.g., when drag and drop) + // otherwise, programmatically set options should be preserved + if (!updatedViewport.viewportOptions && !state.isHangingProtocolLayout) { + viewportOptions = { + viewportId: viewportOptions.viewportId, + }; + } + + const newViewport = { + ...previousViewport, + displaySetInstanceUIDs, + viewportOptions, + displaySetOptions, + // viewportLabel: getViewportLabel(viewports, viewportId), + }; + + viewportOptions.presentationIds = service.getPresentationIds({ + viewport: newViewport, + viewports, + }); + + viewports.set(viewportId, { + ...viewports.get(viewportId), + ...newViewport, + }); + }); + + return { ...state, viewports }; + } + case 'SET_LAYOUT': { + const { + numCols, + numRows, + layoutOptions, + layoutType = 'grid', + activeViewportId, + findOrCreateViewport, + isHangingProtocolLayout, + } = action.payload; + + // If empty viewportOptions, we use numRow and numCols to calculate number of viewports + const hasOptions = layoutOptions?.length; + const viewports = new Map(); + // Options is a temporary state store which can be used by the + // findOrCreate to store state about already found viewports. Typically, + // it will be used to store the display set UID's which are already + // in view so that the find or create can decide which display sets + // haven't been viewed yet, and add them in the appropriate order. + const options = {}; + + let activeViewportIdToSet = activeViewportId; + for (let row = 0; row < numRows; row++) { + for (let col = 0; col < numCols; col++) { + const position = col + row * numCols; + const layoutOption = layoutOptions[position]; + + let xPos, yPos, w, h; + if (layoutOptions && layoutOptions[position]) { + ({ x: xPos, y: yPos, width: w, height: h } = layoutOptions[position]); + } else { + w = 1 / numCols; + h = 1 / numRows; + xPos = col * w; + yPos = row * h; + } + + const colIndex = Math.round(xPos * numCols); + const rowIndex = Math.round(yPos * numRows); + + const positionId = layoutOption?.positionId || `${colIndex}-${rowIndex}`; + + if (hasOptions && position >= layoutOptions.length) { + continue; + } + + const viewport = findOrCreateViewport(position, positionId, options); + + if (!viewport) { + continue; + } + + viewport.positionId = positionId; + + // If the viewport doesn't have a viewportId, we create one + if (!viewport.viewportOptions?.viewportId) { + const randomUID = utils.uuidv4().substring(0, 8); + viewport.viewportOptions = viewport.viewportOptions || {}; + viewport.viewportOptions.viewportId = `viewport-${randomUID}`; + } + + viewport.viewportId = viewport.viewportOptions.viewportId; + + // Create a new viewport object as it is getting updated here + // and it is part of the read only state + viewports.set(viewport.viewportId, viewport); + + Object.assign(viewport, { + width: w, + height: h, + x: xPos, + y: yPos, + }); + + viewport.isReady = false; + + if (!viewport.viewportOptions.presentationIds) { + const presentationIds = service.getPresentationIds({ + viewport, + viewports, + }); + viewport.viewportOptions.presentationIds = presentationIds; + } + } + } + + activeViewportIdToSet = + activeViewportIdToSet ?? determineActiveViewportId(state, viewports); + + const ret = { + ...state, + activeViewportId: activeViewportIdToSet, + layout: { + ...state.layout, + numCols, + numRows, + layoutType, + }, + viewports, + isHangingProtocolLayout, + }; + return ret; + } + case 'RESET': { + return DEFAULT_STATE; + } + + case 'SET': { + return { + ...state, + ...action.payload, + }; + } + + case 'VIEWPORT_IS_READY': { + const { viewportId, isReady } = action.payload; + const viewports = new Map(state.viewports); + const viewport = viewports.get(viewportId); + if (!viewport) { + return; + } + + viewports.set(viewportId, { + ...viewport, + isReady, + }); + + return { + ...state, + viewports, + }; + } + + default: + return action.payload; + } + }; + + const [viewportGridState, dispatch] = useReducer(viewportGridReducer, DEFAULT_STATE); + + const getState = useCallback(() => { + return viewportGridState; + }, [viewportGridState]); + + const getActiveViewportOptionByKey = (key: string) => { + const { viewports, activeViewportId } = viewportGridState; + return viewports.get(activeViewportId)?.viewportOptions?.[key]; + }; + + const setActiveViewportId = useCallback( + index => dispatch({ type: 'SET_ACTIVE_VIEWPORT_ID', payload: index }), + [dispatch] + ); + + const setDisplaySetsForViewports = useCallback( + viewports => + dispatch({ + type: 'SET_DISPLAYSETS_FOR_VIEWPORTS', + payload: viewports, + }), + [dispatch] + ); + + const setViewportIsReady = useCallback( + (viewportId, isReady) => { + dispatch({ + type: 'VIEWPORT_IS_READY', + payload: { + viewportId, + isReady, + }, + }); + }, + [dispatch, viewportGridState] + ); + + const getGridViewportsReady = useCallback(() => { + const { viewports } = viewportGridState; + const readyViewports = Array.from(viewports.values()).filter(viewport => viewport.isReady); + return readyViewports.length === viewports.size; + }, [viewportGridState]); + + const setLayout = useCallback( + ({ + layoutType, + numRows, + numCols, + layoutOptions = [], + activeViewportId, + findOrCreateViewport, + isHangingProtocolLayout, + }) => + dispatch({ + type: 'SET_LAYOUT', + payload: { + layoutType, + numRows, + numCols, + layoutOptions, + activeViewportId, + findOrCreateViewport, + isHangingProtocolLayout, + }, + }), + [dispatch] + ); + + const reset = useCallback( + () => + dispatch({ + type: 'RESET', + payload: {}, + }), + [dispatch] + ); + + const set = useCallback( + payload => + dispatch({ + type: 'SET', + payload, + }), + [dispatch] + ); + + const getNumViewportPanes = useCallback(() => { + const { layout, viewports } = viewportGridState; + const { numRows, numCols } = layout; + return Math.min(viewports.size, numCols * numRows); + }, [viewportGridState]); + + /** + * Sets the implementation of ViewportGridService that can be used by extensions. + * + * @returns void + */ + useEffect(() => { + if (service) { + service.setServiceImplementation({ + getState, + setActiveViewportId, + setDisplaySetsForViewports, + setLayout, + reset, + onModeExit: reset, + set, + getNumViewportPanes, + setViewportIsReady, + getGridViewportsReady, + }); + } + }, [ + getState, + service, + setActiveViewportId, + setDisplaySetsForViewports, + setLayout, + reset, + set, + getNumViewportPanes, + setViewportIsReady, + getGridViewportsReady, + ]); + + // run many of the calls through the service itself since we want to publish events + const api = { + getState, + setActiveViewportId: index => service.setActiveViewportId(index), + setDisplaySetsForViewport: props => service.setDisplaySetsForViewports([props]), + setDisplaySetsForViewports: props => service.setDisplaySetsForViewports(props), + setLayout: layout => service.setLayout(layout), + reset: () => service.reset(), + set: gridLayoutState => service.setState(gridLayoutState), // run it through the service itself since we want to publish events + getNumViewportPanes, + setViewportIsReady, + getGridViewportsReady, + getActiveViewportOptionByKey, + setViewportGridSizeChanged: props => service.setViewportGridSizeChanged(props), + publishViewportsReady: () => service.publishViewportsReady(), + }; + + return ( + + {children} + + ); +} + +ViewportGridProvider.propTypes = { + children: PropTypes.any, + service: PropTypes.instanceOf(ViewportGridService).isRequired, +}; + +// Update the useViewportGrid hook +export const useViewportGrid = (): [AppTypes.ViewportGrid.State, ViewportGridApi] => + useContext(ViewportGridContext); diff --git a/platform/ui-next/src/contextProviders/index.ts b/platform/ui-next/src/contextProviders/index.ts new file mode 100644 index 0000000..088b514 --- /dev/null +++ b/platform/ui-next/src/contextProviders/index.ts @@ -0,0 +1,7 @@ +import NotificationProvider, { useNotification } from './NotificationProvider'; +import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './ViewportGridProvider'; +import { ToolboxProvider, useToolbox } from './ToolboxContext'; + +export { useNotification, NotificationProvider }; +export { ViewportGridContext, ViewportGridProvider, useViewportGrid }; +export { ToolboxProvider, useToolbox }; diff --git a/platform/ui-next/src/index.ts b/platform/ui-next/src/index.ts new file mode 100644 index 0000000..a53bde4 --- /dev/null +++ b/platform/ui-next/src/index.ts @@ -0,0 +1,228 @@ +import { + Button, + buttonVariants, + ThemeWrapper, + Dialog, + Command, + Popover, + Combobox, + Calendar, + DatePickerWithRange, + Separator, + Tabs, + TabsContent, + TabsList, + Clipboard, + TabsTrigger, + Toggle, + toggleVariants, + ToggleGroup, + ToggleGroupItem, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Icons, + SidePanel, + StudyItem, + StudyBrowser, + StudyBrowserSort, + StudyBrowserViewOptions, + Thumbnail, + ThumbnailList, + PanelSection, + DisplaySetMessageListTooltip, + ToolboxUI, + DoubleSlider, + Label, + Slider, + Input, + Switch, + Checkbox, + Onboarding, + PopoverAnchor, + PopoverContent, + PopoverTrigger, + ResizablePanelGroup, + ResizablePanel, + ResizableHandle, + Select, + SelectTrigger, + SelectContent, + SelectItem, + SelectValue, + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, + ScrollArea, + MeasurementTable, + SegmentationTable, + useSegmentationTableContext, + TooltipProvider, + Tooltip, + TooltipTrigger, + TooltipContent, + StudySummary, + ErrorBoundary, + Header, + ViewportActionButton, + PatientInfo, + ViewportActionBar, + ViewportActionArrows, + ViewportPane, + ViewportActionCorners, + ViewportActionCornersLocations, + ViewportOverlay, + ViewportGrid, + ToolButton, + ToolButtonList, + ToolButtonListDefault, + ToolButtonListDropDown, + ToolButtonListItem, + ToolButtonListDivider, + Toolbox, + Numeric, +} from './components'; +import { DataRow } from './components/DataRow'; + +import { + useNotification, + NotificationProvider, + useToolbox, + ToolboxProvider, +} from './contextProviders'; +import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './contextProviders'; +import * as utils from './utils'; + +export { + ErrorBoundary, + // components + Button, + Dialog, + Command, + Popover, + Combobox, + Checkbox, + DoubleSlider, + buttonVariants, + ThemeWrapper, + Calendar, + DatePickerWithRange, + Clipboard, + // contextProviders + NotificationProvider, + useNotification, + ViewportGridContext, + ViewportGridProvider, + useViewportGrid, + Separator, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + Toggle, + toggleVariants, + ToggleGroup, + ToggleGroupItem, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Icons, + SidePanel, + StudyItem, + StudyBrowser, + StudyBrowserSort, + StudyBrowserViewOptions, + Thumbnail, + ThumbnailList, + PanelSection, + DisplaySetMessageListTooltip, + ToolboxUI, + Label, + Slider, + Input, + Switch, + Onboarding, + PopoverAnchor, + PopoverContent, + PopoverTrigger, + ResizablePanelGroup, + ResizablePanel, + ResizableHandle, + Select, + SelectTrigger, + SelectContent, + SelectItem, + SelectValue, + DataRow, + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, + ScrollArea, + MeasurementTable, + SegmentationTable, + useSegmentationTableContext, + TooltipProvider, + Tooltip, + TooltipTrigger, + TooltipContent, + StudySummary, + Header, + ViewportActionButton, + PatientInfo, + ViewportActionBar, + ViewportActionArrows, + ViewportPane, + ViewportActionCorners, + ViewportActionCornersLocations, + ViewportOverlay, + ViewportGrid, + ToolButton, + ToolButtonList, + ToolButtonListDefault, + ToolButtonListDropDown, + ToolButtonListItem, + ToolButtonListDivider, + ToolboxProvider, + Toolbox, + useToolbox, + utils, + Numeric, +}; diff --git a/platform/ui-next/src/lib/createContext.tsx b/platform/ui-next/src/lib/createContext.tsx new file mode 100644 index 0000000..ef3cb31 --- /dev/null +++ b/platform/ui-next/src/lib/createContext.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; + +// https://github.com/reach/reach-ui/blob/dev/packages/utils/src/context.tsx +type ContextProvider = React.FC>; + +export function createContext( + rootComponentName: string, + defaultContext?: ContextValueType +): [ContextProvider, (callerComponentName: string) => ContextValueType] { + const Ctx = React.createContext(defaultContext); + + function Provider(props: React.PropsWithChildren) { + const { children, ...context } = props; + const value = React.useMemo( + () => context, + // eslint-disable-next-line react-hooks/exhaustive-deps + Object.values(context) + ) as ContextValueType; + return {children}; + } + + function useContext(callerComponentName: string) { + const context = React.useContext(Ctx); + if (context) { + return context; + } + if (defaultContext) { + return defaultContext; + } + throw Error( + `${callerComponentName} must be rendered inside of a ${rootComponentName} component.` + ); + } + + Ctx.displayName = `${rootComponentName}Context`; + Provider.displayName = `${rootComponentName}Provider`; + return [Provider, useContext]; +} diff --git a/platform/ui-next/src/lib/utils.ts b/platform/ui-next/src/lib/utils.ts new file mode 100644 index 0000000..9ad0df4 --- /dev/null +++ b/platform/ui-next/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/platform/ui-next/src/tailwind.css b/platform/ui-next/src/tailwind.css new file mode 100644 index 0000000..07987ee --- /dev/null +++ b/platform/ui-next/src/tailwind.css @@ -0,0 +1,255 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap'); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* OHIF Theme */ + +@layer base { + :root { + --highlight: 191 74% 63%; + --background: 236 62% 5%; + --foreground: 0 0% 98%; + --card: 234 64% 10%; + --card-foreground: 0 0% 98%; + --popover: 219 90% 15%; + --popover-foreground: 0 0% 98%; + --primary: 214 98% 60%; + --primary-foreground: 0 0% 98%; + --secondary: 214 66% 48%; + --secondary-foreground: 200 50% 84%; + --muted: 234 64% 10%; + --muted-foreground: 200 46% 65%; + --accent: 217 79% 24%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 236 45% 21%; + --ring: 214 98% 60%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --radius: 0.5rem; + --success-bg: hsl(143, 85%, 96%); + --success-border: hsl(145, 92%, 91%); + --success-text: hsl(140, 100%, 27%); + + --info-bg: hsl(208, 100%, 97%); + --info-border: hsl(221, 91%, 91%); + --info-text: hsl(210, 92%, 45%); + + --warning-bg: hsl(49, 100%, 97%); + --warning-border: hsl(49, 91%, 91%); + --warning-text: hsl(31, 92%, 45%); + + --error-bg: hsl(359, 100%, 97%); + --error-border: hsl(359, 100%, 94%); + --error-text: hsl(360, 100%, 45%); + } + + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 234 64% 10%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 214 98% 60%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 236 45% 21%; + --ring: 214 98% 60%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +/* ORIGINAL THEME for comparison and testing + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 72.22% 50.59%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 5% 64.9%; + --radius: 0.5rem; + + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + } + + + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 85.7% 97.3%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +*/ + +/* Theme Copy Example + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 224 71.4% 4.1%; + --card: 0 0% 100%; + --card-foreground: 224 71.4% 4.1%; + --popover: 0 0% 100%; + --popover-foreground: 224 71.4% 4.1%; + --primary: 262.1 83.3% 57.8%; + --primary-foreground: 210 20% 98%; + --secondary: 220 14.3% 95.9%; + --secondary-foreground: 220.9 39.3% 11%; + --muted: 220 14.3% 95.9%; + --muted-foreground: 220 8.9% 46.1%; + --accent: 220 14.3% 95.9%; + --accent-foreground: 220.9 39.3% 11%; + --destructive: 360 84.2% 60.2%; + --destructive-foreground: 210 20% 98%; + --border: 220 13% 91%; + --input: 220 13% 91%; + --ring: 262.1 83.3% 57.8%; + --radius: 0.5rem; + --chart-1: ; + --chart-2: ; + --chart-3: ; + --chart-4: ; + --chart-5: ; + } + + .dark { + --background: 224 71.4% 4.1%; + --foreground: 210 20% 98%; + --card: 224 71.4% 4.1%; + --card-foreground: 210 20% 98%; + --popover: 224 71.4% 4.1%; + --popover-foreground: 210 20% 98%; + --primary: 263.4 70% 50.4%; + --primary-foreground: 210 20% 98%; + --secondary: 215 27.9% 16.9%; + --secondary-foreground: 210 20% 98%; + --muted: 215 27.9% 16.9%; + --muted-foreground: 217.9 10.6% 64.9%; + --accent: 215 27.9% 16.9%; + --accent-foreground: 210 20% 98%; + --destructive: 360 84.2% 60.2%; + --destructive-foreground: 210 20% 98%; + --border: 215 27.9% 16.9%; + --input: 215 27.9% 16.9%; + --ring: 263.4 70% 50.4%; + --chart-1: ; + --chart-2: ; + --chart-3: ; + --chart-4: ; + --chart-5: ; + } +} + +*/ + +/* Updated Playground page styles */ + +@layer base { + html { + font-family: 'Inter', sans-serif; + } + + body { + @apply !bg-black; + } +} + +h2.section-header { + @apply py-4 text-2xl font-normal text-white; +} + +h3.section-header { + @apply py-3 text-xl text-white; +} + +.row { + @apply mb-6 flex flex-row flex-wrap rounded-md border py-10; +} + +.example { + @apply flex-initial px-6; +} + +.example2 { + @apply flex-initial px-4; +} + +/* Additional CSS edits to components */ + +/* Tooltip */ + +.TooltipContent[data-side='bottom'] { + animation-name: slideDown; +} + +/* Custom CSS to hide default number input arrows */ +input[type='number']::-webkit-inner-spin-button, +input[type='number']::-webkit-outer-spin-button { + @apply appearance-none; +} + +input[type='number'] { + -moz-appearance: textfield; /* For Firefox */ +} diff --git a/platform/ui-next/src/types/ContextMenuItem.ts b/platform/ui-next/src/types/ContextMenuItem.ts new file mode 100644 index 0000000..5e856b0 --- /dev/null +++ b/platform/ui-next/src/types/ContextMenuItem.ts @@ -0,0 +1,9 @@ +export type ContextMenuItem = { + // A label to show for the item. + label: string; + // An icon to show the on right of the text - typically used for submenus + iconRight?: string; + // item is the menu item (eg the instance of this that is clicked on) + // props is the remaining properties passed to the context menu + action: (item, props) => void; +}; diff --git a/platform/ui-next/src/types/Predicate.ts b/platform/ui-next/src/types/Predicate.ts new file mode 100644 index 0000000..a14c760 --- /dev/null +++ b/platform/ui-next/src/types/Predicate.ts @@ -0,0 +1 @@ +export type Predicate = (props: Record) => boolean; diff --git a/platform/ui-next/src/types/ThumbnailType.ts b/platform/ui-next/src/types/ThumbnailType.ts new file mode 100644 index 0000000..df7199d --- /dev/null +++ b/platform/ui-next/src/types/ThumbnailType.ts @@ -0,0 +1,3 @@ +type ThumbnailType = 'thumbnail' | 'thumbnailTracked' | 'thumbnailNoImage'; + +export default ThumbnailType; diff --git a/platform/ui-next/src/types/ViewportActionCornersTypes.ts b/platform/ui-next/src/types/ViewportActionCornersTypes.ts new file mode 100644 index 0000000..f5f2f69 --- /dev/null +++ b/platform/ui-next/src/types/ViewportActionCornersTypes.ts @@ -0,0 +1,6 @@ +import { ReactNode } from 'react'; + +export interface ViewportActionCornersComponentInfo { + id: string; + component: ReactNode; +} diff --git a/platform/ui-next/src/types/global.d.ts b/platform/ui-next/src/types/global.d.ts new file mode 100644 index 0000000..403b544 --- /dev/null +++ b/platform/ui-next/src/types/global.d.ts @@ -0,0 +1,4 @@ +declare module '*.png' { + const content: string; + export default content; +} diff --git a/platform/ui-next/src/types/index.ts b/platform/ui-next/src/types/index.ts new file mode 100644 index 0000000..1405338 --- /dev/null +++ b/platform/ui-next/src/types/index.ts @@ -0,0 +1,20 @@ +import ThumbnailType from './ThumbnailType'; + +// A few miscellaneous types declared inline here. + +export * from './Predicate'; +export * from './ContextMenuItem'; +export * from './ViewportActionCornersTypes'; + +/** + * StringNumber often comes back from DICOMweb for integer valued items. + */ +type StringNumber = string | number; + +/** + * StringArray often comes back from dcmjs for single valued strings that + * might have multiple values such as window level descriptions. + */ +type StringArray = string | string[]; + +export type { StringNumber, StringArray, ThumbnailType }; diff --git a/platform/ui-next/src/utils/getToggledClassName.tsx b/platform/ui-next/src/utils/getToggledClassName.tsx new file mode 100644 index 0000000..9d18de0 --- /dev/null +++ b/platform/ui-next/src/utils/getToggledClassName.tsx @@ -0,0 +1,7 @@ +const getToggledClassName = isToggled => { + return isToggled + ? '!text-primary-active' + : '!text-common-bright hover:!bg-primary-dark hover:text-primary-light'; +}; + +export { getToggledClassName }; diff --git a/platform/ui-next/src/utils/index.ts b/platform/ui-next/src/utils/index.ts new file mode 100644 index 0000000..b842eb1 --- /dev/null +++ b/platform/ui-next/src/utils/index.ts @@ -0,0 +1,3 @@ +import { getToggledClassName } from './getToggledClassName'; + +export { getToggledClassName }; diff --git a/platform/ui-next/tailwind.config.js b/platform/ui-next/tailwind.config.js new file mode 100644 index 0000000..7d5d028 --- /dev/null +++ b/platform/ui-next/tailwind.config.js @@ -0,0 +1,117 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + presets: [require('../ui/tailwind.config.js')], + content: [ + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + './src/**/*.{ts,tsx}', + ], + prefix: '', + theme: { + fontFamily: { + inter: ['Inter', 'sans-serif'], + }, + fontSize: { + xxs: '0.625rem', // 10px + xs: '0.6875rem', // 11px + sm: '0.75rem', // 12px + base: '0.8125rem', // 13px + lg: '0.875rem', // 14px + xl: '1rem', // 16px + // 2xl and above will be updated in an upcoming version + '2xl': '1.5rem', + '3xl': '1.875rem', + '4xl': '2.25rem', + '5xl': '3rem', + '6xl': '4rem', + // '2xl': '1.125rem', // 18px + // '3xl': '1.375rem', // 22px + // '4xl': '1.5rem', // 24px + // '5xl': '1.875rem', // 30px + }, + fontWeight: { + hairline: '100', + thin: '200', + light: '300', + normal: '400', + medium: '500', + semibold: '600', + bold: '700', + extrabold: '800', + black: '900', + }, + extend: { + colors: { + highlight: 'hsl(var(--highlight))', + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + keyframes: { + 'accordion-down': { + from: { height: '0' }, + to: { height: 'var(--radix-accordion-content-height)' }, + }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: '0' }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + }, + bkg: { + low: '#050615', + med: '#090C29', + full: '#041C4A', + }, + info: { + primary: '#FFFFFF', + secondary: '#7BB2CE', + }, + actions: { + primary: '#348CFD', + highlight: '#5ACCE6', + hover: 'rgba(52, 140, 253, 0.2)', + }, + }, + }, + plugins: [require('tailwindcss-animate')], +}; diff --git a/platform/ui-next/tsconfig.json b/platform/ui-next/tsconfig.json new file mode 100644 index 0000000..adea64e --- /dev/null +++ b/platform/ui-next/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@ui/*": ["./src/*"], + "@ohif/core": ["../core/src"], + "@ohif/ui": ["../ui/src"] + } + }, + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/platform/ui/.gitignore b/platform/ui/.gitignore new file mode 100644 index 0000000..590af21 --- /dev/null +++ b/platform/ui/.gitignore @@ -0,0 +1,2 @@ +node_modules +storybook-static diff --git a/platform/ui/.storybook/OHIFTheme.js b/platform/ui/.storybook/OHIFTheme.js new file mode 100644 index 0000000..6be91b6 --- /dev/null +++ b/platform/ui/.storybook/OHIFTheme.js @@ -0,0 +1,8 @@ +import { create } from '@storybook/theming'; + +export default create({ + base: 'light', + brandTitle: 'OHIF', + brandUrl: 'https://ohif.org', + brandImage: 'ohif-logo-light.svg', +}); diff --git a/platform/ui/.storybook/custom.css b/platform/ui/.storybook/custom.css new file mode 100644 index 0000000..ec7b686 --- /dev/null +++ b/platform/ui/.storybook/custom.css @@ -0,0 +1,3 @@ +#storybook-explorer-menu svg { + color: #5034ff; +} diff --git a/platform/ui/.storybook/main.ts b/platform/ui/.storybook/main.ts new file mode 100644 index 0000000..6261239 --- /dev/null +++ b/platform/ui/.storybook/main.ts @@ -0,0 +1,105 @@ +import path, { dirname, join } from 'path'; +import remarkGfm from 'remark-gfm'; +import type { StorybookConfig } from '@storybook/react-webpack5'; + +const config: StorybookConfig = { + stories: ['../src/**/*.stories.@(mdx)'], + addons: [ + getAbsolutePath('@storybook/addon-links'), + getAbsolutePath('@storybook/addon-essentials'), + // Other addons go here + { + name: '@storybook/addon-docs', + options: { + mdxPluginOptions: { + mdxCompileOptions: { + remarkPlugins: [remarkGfm], + }, + }, + }, + }, + ], + core: {}, + framework: { + name: getAbsolutePath('@storybook/react-webpack5'), + options: {}, + }, + docs: { + autodocs: true, // see below for alternatives + defaultName: 'Docs', // set to change the name of generated docs entries + }, + staticDirs: ['../static'], + webpackFinal: async (config: any, { configType }) => { + // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION' + // You can change the configuration based on that. + // 'PRODUCTION' is used when building the static version of storybook. + + // config.module.rules[0].use[0].options.plugins[1] = [ + // '@babel/plugin-proposal-class-properties', + // { loose: true }, + // ]; + + // config.module.rules[0].use[0].options.plugins[3] = [ + // '@babel/plugin-proposal-private-methods', + // { loose: true }, + // ]; + + // config.module.rules[0].use[0].options.plugins[4] = [ + // '@babel/plugin-proposal-private-property-in-object', + // { loose: true }, + // ]; + + // Make whatever fine-grained changes you need + config.module.rules.push({ + test: /\.m?js/, + resolve: { + fullySpecified: false, + }, + }); + + // Default rule for images /\.(svg|ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/ + const fileLoaderRule = config.module.rules.find(rule => rule.test && rule.test.test('.svg')); + fileLoaderRule.exclude = /\.svg$/; + + config.module.rules.push({ + test: /\.svg$/, + use: [ + { loader: require.resolve('babel-loader') }, + // { loader: 'svg-inline-loader' }, + ], + }); + + config.module.rules.push({ + test: /\.css$/, + use: [ + { + loader: 'postcss-loader', + options: { + // HERE: OPTIONS + postcssOptions: { + plugins: [require('tailwindcss'), require('autoprefixer')], + }, + }, + }, + ], + include: path.resolve(__dirname, '../'), + }); + + // ignore the file @icr/polyseg-wasm during the build as it is a wasm file and + // we don't need that for ui + config.module.rules.push({ + test: /@icr\/polyseg-wasm/, + type: 'javascript/auto', + loader: 'file-loader', + }); + + // Return the altered config + return config; + }, +}; + +export default config; + +function getAbsolutePath(value: string): any { + return dirname(require.resolve(join(value, 'package.json'))); +} diff --git a/platform/ui/.storybook/manager-head.html b/platform/ui/.storybook/manager-head.html new file mode 100644 index 0000000..9bca7f1 --- /dev/null +++ b/platform/ui/.storybook/manager-head.html @@ -0,0 +1,30 @@ + +OHIF UI Component + + + + diff --git a/platform/ui/.storybook/manager.js b/platform/ui/.storybook/manager.js new file mode 100644 index 0000000..8ca5f10 --- /dev/null +++ b/platform/ui/.storybook/manager.js @@ -0,0 +1,15 @@ +// .storybook/manager.js + +import { addons } from '@storybook/addons'; +import ohifTheme from './OHIFTheme'; + +const link = document.createElement('link'); +link.setAttribute('rel', 'shortcut icon'); +document.head.appendChild(link); + +addons.setConfig({ + theme: ohifTheme, +}); + +window.STORYBOOK_GA_ID = 'G-3S63CTHNP6'; +window.STORYBOOK_REACT_GA_OPTIONS = {}; diff --git a/platform/ui/.storybook/preview.tsx b/platform/ui/.storybook/preview.tsx new file mode 100644 index 0000000..6a6ad43 --- /dev/null +++ b/platform/ui/.storybook/preview.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { DocsPage, DocsContainer } from '@storybook/addon-docs'; +import { + Heading, + SectionName, + Footer, + AnchorListItem, + LinkComponent, +} from '../src/storybook/components'; + +import '../src/tailwind.css'; +import './custom.css'; + +// https://github.com/mondaycom/monday-ui-react-core/tree/master/.storybook + +export const parameters = { + docs: { + inlineStories: true, + container: ({ children, context }) => ( + {children} + ), + page: DocsPage, + components: { + Heading, + Footer, + h2: SectionName, + h3: ({ children }) =>

{children}

, + li: AnchorListItem, + a: LinkComponent, + p: ({ children }) =>

{children}

, + // todo: add pre and code + }, + }, + viewMode: 'docs', + previewTabs: { + 'storybook/docs/panel': { + index: -1, + }, + canvas: { title: 'Sandbox' }, + }, + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + viewport: { + disable: true, + }, + backgrounds: { + default: 'OHIF-v3', + values: [ + { + name: 'White', + value: '#FFFFFF', + }, + { + name: 'OHIF-v3', + value: '#090C29', + }, + { + name: 'Light', + value: '#F8F8F8', + }, + { + name: 'Dark', + value: '#333333', + }, + ], + }, + options: { + storySort: { + order: ['Welcome', 'Contribute', 'Foundations', 'Modals', '*'], + }, + }, +}; + +export const decorators = []; diff --git a/platform/ui/.webpack/webpack.dev.js b/platform/ui/.webpack/webpack.dev.js new file mode 100644 index 0000000..f51b5ac --- /dev/null +++ b/platform/ui/.webpack/webpack.dev.js @@ -0,0 +1,11 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); +const ENTRY = { + app: `${SRC_DIR}/index.tsx`, +}; + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); +}; diff --git a/platform/ui/.webpack/webpack.prod.js b/platform/ui/.webpack/webpack.prod.js new file mode 100644 index 0000000..0f2e00b --- /dev/null +++ b/platform/ui/.webpack/webpack.prod.js @@ -0,0 +1,60 @@ +const { merge } = require('webpack-merge'); +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; + +const webpackCommon = require('./../../../.webpack/webpack.base.js'); +const pkg = require('./../package.json'); + +const ROOT_DIR = path.join(__dirname, './..'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +const ENTRY = { + app: `${SRC_DIR}/index.js`, +}; + +const outputName = `ohif-${pkg.name.split('/').pop()}`; + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY }); + + return merge(commonConfig, { + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: false, + }, + output: { + path: ROOT_DIR, + library: 'ohif-ui', + libraryTarget: 'umd', + filename: pkg.main, + }, + externals: [ + /\b(dcmjs)/, + /\b(gl-matrix)/, + { + react: 'React', + 'react-dom': 'ReactDOM', + }, + ], + plugins: [ + new MiniCssExtractPlugin({ + filename: `./dist/${outputName}.css`, + chunkFilename: `./dist/${outputName}.css`, + }), + // new BundleAnalyzerPlugin({}), + ], + }); +}; diff --git a/platform/ui/17dd54813d5acc10bf8f.wasm b/platform/ui/17dd54813d5acc10bf8f.wasm new file mode 100644 index 0000000..e0b9922 Binary files /dev/null and b/platform/ui/17dd54813d5acc10bf8f.wasm differ diff --git a/platform/ui/CHANGELOG.md b/platform/ui/CHANGELOG.md new file mode 100644 index 0000000..d7fc382 --- /dev/null +++ b/platform/ui/CHANGELOG.md @@ -0,0 +1,3850 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.10.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.110...v3.10.0-beta.111) (2025-02-26) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.109...v3.10.0-beta.110) (2025-02-26) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.108...v3.10.0-beta.109) (2025-02-25) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.107...v3.10.0-beta.108) (2025-02-25) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.106...v3.10.0-beta.107) (2025-02-25) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.105...v3.10.0-beta.106) (2025-02-25) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.104...v3.10.0-beta.105) (2025-02-25) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.103...v3.10.0-beta.104) (2025-02-25) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.102...v3.10.0-beta.103) (2025-02-23) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.101...v3.10.0-beta.102) (2025-02-20) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.100...v3.10.0-beta.101) (2025-02-20) + + +### Bug Fixes + +* icon is not defined ([#4794](https://github.com/OHIF/Viewers/issues/4794)) ([b7cd0c6](https://github.com/OHIF/Viewers/commit/b7cd0c6027debcbfa573bc8068bc2e87928af9a5)) + + + + + +# [3.10.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.99...v3.10.0-beta.100) (2025-02-19) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.98...v3.10.0-beta.99) (2025-02-18) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.97...v3.10.0-beta.98) (2025-02-18) + + +### Bug Fixes + +* lodash dependencies ([#4791](https://github.com/OHIF/Viewers/issues/4791)) ([4e16099](https://github.com/OHIF/Viewers/commit/4e16099ad3ab777b09f6ac8f181025cfd656ab6b)) + + + + + +# [3.10.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.96...v3.10.0-beta.97) (2025-02-18) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.95...v3.10.0-beta.96) (2025-02-18) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.94...v3.10.0-beta.95) (2025-02-13) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.93...v3.10.0-beta.94) (2025-02-11) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.92...v3.10.0-beta.93) (2025-02-04) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.91...v3.10.0-beta.92) (2025-02-04) + + +### Bug Fixes + +* **core:** Address 3D reconstruction and Android compatibility issues and clean up 4D data mode ([#4762](https://github.com/OHIF/Viewers/issues/4762)) ([149d6d0](https://github.com/OHIF/Viewers/commit/149d6d049cd333b9e5846576b403ff387558a66f)) + + + + + +# [3.10.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.90...v3.10.0-beta.91) (2025-02-04) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.89...v3.10.0-beta.90) (2025-02-04) + + +### Features + +* **ui:** Add support for Custom Modal component in Modal Service ([#4752](https://github.com/OHIF/Viewers/issues/4752)) ([2c183aa](https://github.com/OHIF/Viewers/commit/2c183aa4a777d7b5a0417ebcc8576a0fc2631ad2)) + + + + + +# [3.10.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.88...v3.10.0-beta.89) (2025-02-04) + + +### Features + +* **ui:** customization option for viewport notification ([#4638](https://github.com/OHIF/Viewers/issues/4638)) ([8acbd76](https://github.com/OHIF/Viewers/commit/8acbd760d801dcaf624c5d9fb636a029201b91e1)) + + + + + +# [3.10.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.87...v3.10.0-beta.88) (2025-02-04) + + +### Bug Fixes + +* **context-menu:** Fixing regression introduced by PR [#4727](https://github.com/OHIF/Viewers/issues/4727) ([#4760](https://github.com/OHIF/Viewers/issues/4760)) ([12d3db2](https://github.com/OHIF/Viewers/commit/12d3db2dbc80438df60139c67e9bcf0a610532d6)) + + + + + +# [3.10.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.86...v3.10.0-beta.87) (2025-02-03) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.85...v3.10.0-beta.86) (2025-02-03) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.84...v3.10.0-beta.85) (2025-02-03) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.83...v3.10.0-beta.84) (2025-01-31) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.82...v3.10.0-beta.83) (2025-01-31) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.81...v3.10.0-beta.82) (2025-01-31) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.80...v3.10.0-beta.81) (2025-01-29) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.79...v3.10.0-beta.80) (2025-01-29) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.78...v3.10.0-beta.79) (2025-01-28) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.77...v3.10.0-beta.78) (2025-01-28) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.76...v3.10.0-beta.77) (2025-01-28) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.75...v3.10.0-beta.76) (2025-01-27) + + +### Bug Fixes + +* **context menu:** Context menus for edge-proximate measurements are partially obscured. ([#4727](https://github.com/OHIF/Viewers/issues/4727)) ([61699d0](https://github.com/OHIF/Viewers/commit/61699d00b6ce1e53631fd8c01e783701e01a7373)) + + + + + +# [3.10.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.74...v3.10.0-beta.75) (2025-01-27) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.73...v3.10.0-beta.74) (2025-01-27) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.72...v3.10.0-beta.73) (2025-01-24) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.71...v3.10.0-beta.72) (2025-01-24) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.70...v3.10.0-beta.71) (2025-01-23) + + +### Features + +* **customization:** new customization service api ([#4688](https://github.com/OHIF/Viewers/issues/4688)) ([55ad8ef](https://github.com/OHIF/Viewers/commit/55ad8efbabc3fabd8031fc08927b2f92ae5aec69)) + + + + + +# [3.10.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.69...v3.10.0-beta.70) (2025-01-23) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.68...v3.10.0-beta.69) (2025-01-22) + + +### Bug Fixes + +* **seg:** sphere scissor on stack and cpu rendering reset properties was broken ([#4721](https://github.com/OHIF/Viewers/issues/4721)) ([f00d182](https://github.com/OHIF/Viewers/commit/f00d18292f02e8910215d913edfc994850a68d88)) + + + + + +# [3.10.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.67...v3.10.0-beta.68) (2025-01-21) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.66...v3.10.0-beta.67) (2025-01-21) + + +### Bug Fixes + +* **ui:** Update dependencies and add missing icons ([#4699](https://github.com/OHIF/Viewers/issues/4699)) ([cf97fa9](https://github.com/OHIF/Viewers/commit/cf97fa9b7b9687a9b73c1cf6926bc9fbc39b6512)) + + + + + +# [3.10.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.65...v3.10.0-beta.66) (2025-01-21) + + +### Bug Fixes + +* Inconsistent Handling of Patient Name Tag ([#4703](https://github.com/OHIF/Viewers/issues/4703)) ([8aedb2e](https://github.com/OHIF/Viewers/commit/8aedb2ec54a0ccf2550f745fed6f0b8aa184a860)) + + + + + +# [3.10.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.64...v3.10.0-beta.65) (2025-01-20) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.63...v3.10.0-beta.64) (2025-01-20) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.62...v3.10.0-beta.63) (2025-01-17) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.61...v3.10.0-beta.62) (2025-01-16) + + +### Bug Fixes + +* context menu icon ([#4696](https://github.com/OHIF/Viewers/issues/4696)) ([1993161](https://github.com/OHIF/Viewers/commit/19931614dc53da440718e512d39a87ca9118b96e)) + + + + + +# [3.10.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.60...v3.10.0-beta.61) (2025-01-16) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.59...v3.10.0-beta.60) (2025-01-15) + + +### Bug Fixes + +* Having sop instance in a per-frame or shared attribute breaks load ([#4560](https://github.com/OHIF/Viewers/issues/4560)) ([cded082](https://github.com/OHIF/Viewers/commit/cded08261788143e0d5be57a55c927fd96aafb22)) + + + + + +# [3.10.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.58...v3.10.0-beta.59) (2025-01-14) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.57...v3.10.0-beta.58) (2025-01-13) + + +### Features + +* **multimonitor:** Add simple multi-monitor support to open another study([#4178](https://github.com/OHIF/Viewers/issues/4178)) ([07c628e](https://github.com/OHIF/Viewers/commit/07c628e689b28f831317a7c28d712509b69c6b13)) + + + + + +# [3.10.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.56...v3.10.0-beta.57) (2025-01-13) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.55...v3.10.0-beta.56) (2025-01-13) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.54...v3.10.0-beta.55) (2025-01-10) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.53...v3.10.0-beta.54) (2025-01-10) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.52...v3.10.0-beta.53) (2025-01-10) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.51...v3.10.0-beta.52) (2025-01-10) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.50...v3.10.0-beta.51) (2025-01-10) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.49...v3.10.0-beta.50) (2025-01-10) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.48...v3.10.0-beta.49) (2025-01-09) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.47...v3.10.0-beta.48) (2025-01-09) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.46...v3.10.0-beta.47) (2025-01-09) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.45...v3.10.0-beta.46) (2025-01-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.44...v3.10.0-beta.45) (2025-01-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.43...v3.10.0-beta.44) (2025-01-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.42...v3.10.0-beta.43) (2025-01-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.41...v3.10.0-beta.42) (2025-01-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.40...v3.10.0-beta.41) (2025-01-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.39...v3.10.0-beta.40) (2025-01-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.38...v3.10.0-beta.39) (2025-01-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.37...v3.10.0-beta.38) (2025-01-07) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.36...v3.10.0-beta.37) (2025-01-06) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.35...v3.10.0-beta.36) (2025-01-03) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.34...v3.10.0-beta.35) (2025-01-03) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.33...v3.10.0-beta.34) (2025-01-02) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.32...v3.10.0-beta.33) (2024-12-20) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.31...v3.10.0-beta.32) (2024-12-20) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.30...v3.10.0-beta.31) (2024-12-20) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.29...v3.10.0-beta.30) (2024-12-18) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.28...v3.10.0-beta.29) (2024-12-18) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.27...v3.10.0-beta.28) (2024-12-18) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.26...v3.10.0-beta.27) (2024-12-18) + + +### Features + +* **measurements:** Provide for the Load (SR) measurements button to optionally clear existing measurements prior to loading the SR. ([#4586](https://github.com/OHIF/Viewers/issues/4586)) ([4d3d5e7](https://github.com/OHIF/Viewers/commit/4d3d5e794cb99212eba06bf91dbb30a258725efe)) + + + + + +# [3.10.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.25...v3.10.0-beta.26) (2024-12-18) + + +### Bug Fixes + +* ohif icons in Header ([#4611](https://github.com/OHIF/Viewers/issues/4611)) ([52cf9b1](https://github.com/OHIF/Viewers/commit/52cf9b1e0398f966d4498dda83fd5ceae69262c6)) + + + + + +# [3.10.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.24...v3.10.0-beta.25) (2024-12-17) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.23...v3.10.0-beta.24) (2024-12-17) + + +### Features + +* migrate icons to ui-next ([#4606](https://github.com/OHIF/Viewers/issues/4606)) ([4e2ae32](https://github.com/OHIF/Viewers/commit/4e2ae328744ed95589c2cdf7a531454a25bf88b5)) + + + + + +# [3.10.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.22...v3.10.0-beta.23) (2024-12-17) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.21...v3.10.0-beta.22) (2024-12-13) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.20...v3.10.0-beta.21) (2024-12-11) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.19...v3.10.0-beta.20) (2024-12-11) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.18...v3.10.0-beta.19) (2024-12-11) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.17...v3.10.0-beta.18) (2024-12-06) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.16...v3.10.0-beta.17) (2024-12-06) + + +### Bug Fixes + +* **touch:** For viewport interactions use onPointerDown. ([#4572](https://github.com/OHIF/Viewers/issues/4572)) ([6160718](https://github.com/OHIF/Viewers/commit/6160718fd20db6bac6dd511183a30359d9420140)) + + + + + +# [3.10.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.15...v3.10.0-beta.16) (2024-12-05) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.14...v3.10.0-beta.15) (2024-12-05) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.13...v3.10.0-beta.14) (2024-12-03) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.12...v3.10.0-beta.13) (2024-12-02) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.11...v3.10.0-beta.12) (2024-11-29) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.10...v3.10.0-beta.11) (2024-11-28) + + +### Bug Fixes + +* **multiframe:** metadata handling of NM studies and loading order ([#4554](https://github.com/OHIF/Viewers/issues/4554)) ([7624ccb](https://github.com/OHIF/Viewers/commit/7624ccb5e495c0a151227a458d8d5bfb8babb22c)) + + + + + +# [3.10.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.9...v3.10.0-beta.10) (2024-11-28) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.8...v3.10.0-beta.9) (2024-11-22) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.7...v3.10.0-beta.8) (2024-11-22) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.6...v3.10.0-beta.7) (2024-11-22) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.5...v3.10.0-beta.6) (2024-11-22) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.4...v3.10.0-beta.5) (2024-11-15) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.3...v3.10.0-beta.4) (2024-11-15) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.2...v3.10.0-beta.3) (2024-11-14) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.1...v3.10.0-beta.2) (2024-11-13) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.10.0-beta.0...v3.10.0-beta.1) (2024-11-12) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.10.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.111...v3.10.0-beta.0) (2024-11-12) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.111](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.110...v3.9.0-beta.111) (2024-11-12) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.109...v3.9.0-beta.110) (2024-11-11) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.108...v3.9.0-beta.109) (2024-11-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.107...v3.9.0-beta.108) (2024-11-07) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.106...v3.9.0-beta.107) (2024-11-06) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.105...v3.9.0-beta.106) (2024-11-06) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.104...v3.9.0-beta.105) (2024-11-05) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.103...v3.9.0-beta.104) (2024-10-30) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.102...v3.9.0-beta.103) (2024-10-29) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.101...v3.9.0-beta.102) (2024-10-29) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.100...v3.9.0-beta.101) (2024-10-18) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.99...v3.9.0-beta.100) (2024-10-17) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.98...v3.9.0-beta.99) (2024-10-17) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.97...v3.9.0-beta.98) (2024-10-15) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.96...v3.9.0-beta.97) (2024-10-11) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.95...v3.9.0-beta.96) (2024-10-10) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.94...v3.9.0-beta.95) (2024-10-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.93...v3.9.0-beta.94) (2024-10-04) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.92...v3.9.0-beta.93) (2024-10-04) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.91...v3.9.0-beta.92) (2024-10-01) + + +### Bug Fixes + +* **Select:** select clear button ([#4398](https://github.com/OHIF/Viewers/issues/4398)) ([a11cd6d](https://github.com/OHIF/Viewers/commit/a11cd6d6cbe20d7d986430befb3398f910a03ada)) + + + + + +# [3.9.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.90...v3.9.0-beta.91) (2024-10-01) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.89...v3.9.0-beta.90) (2024-09-30) + + +### Bug Fixes + +* ๐Ÿ› Fix imports for ui-next ([#4394](https://github.com/OHIF/Viewers/issues/4394)) ([43efed2](https://github.com/OHIF/Viewers/commit/43efed207e0d8d13bcbf52fab14c1be034d22d0c)) + + + + + +# [3.9.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.88...v3.9.0-beta.89) (2024-09-27) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.87...v3.9.0-beta.88) (2024-09-24) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.86...v3.9.0-beta.87) (2024-09-19) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.85...v3.9.0-beta.86) (2024-09-19) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.84...v3.9.0-beta.85) (2024-09-17) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.83...v3.9.0-beta.84) (2024-09-12) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.82...v3.9.0-beta.83) (2024-09-11) + + +### Features + +* **studies-panel:** New OHIF study panel - under experimental flag ([#4254](https://github.com/OHIF/Viewers/issues/4254)) ([7a96406](https://github.com/OHIF/Viewers/commit/7a96406a116e46e62c396855fa64f434e2984b58)) + + + + + +# [3.9.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.81...v3.9.0-beta.82) (2024-09-05) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.80...v3.9.0-beta.81) (2024-08-27) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.79...v3.9.0-beta.80) (2024-08-16) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.78...v3.9.0-beta.79) (2024-08-16) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.77...v3.9.0-beta.78) (2024-08-15) + + +### Features + +* Add CS3D WSI and Video Viewports and add annotation navigation for MPR ([#4182](https://github.com/OHIF/Viewers/issues/4182)) ([7599ec9](https://github.com/OHIF/Viewers/commit/7599ec9421129dcade94e6fa6ec7908424ab3134)) + + + + + +# [3.9.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.76...v3.9.0-beta.77) (2024-08-15) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.75...v3.9.0-beta.76) (2024-08-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.74...v3.9.0-beta.75) (2024-08-07) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.73...v3.9.0-beta.74) (2024-08-06) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.72...v3.9.0-beta.73) (2024-08-02) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.71...v3.9.0-beta.72) (2024-07-31) + + +### Bug Fixes + +* customization types ([#4321](https://github.com/OHIF/Viewers/issues/4321)) ([72bef63](https://github.com/OHIF/Viewers/commit/72bef63ef6e63395ba18ff91a39294913966e9db)) + + + + + +# [3.9.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.70...v3.9.0-beta.71) (2024-07-30) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.69...v3.9.0-beta.70) (2024-07-30) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.68...v3.9.0-beta.69) (2024-07-27) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.67...v3.9.0-beta.68) (2024-07-26) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.66...v3.9.0-beta.67) (2024-07-26) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.65...v3.9.0-beta.66) (2024-07-24) + + +### Features + +* **pmap:** added support for parametric map ([#4284](https://github.com/OHIF/Viewers/issues/4284)) ([fc0064f](https://github.com/OHIF/Viewers/commit/fc0064fd9d8cdc8fde81b81f0e71fd5d077ca22b)) + + + + + +# [3.9.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.64...v3.9.0-beta.65) (2024-07-23) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.63...v3.9.0-beta.64) (2024-07-19) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.62...v3.9.0-beta.63) (2024-07-10) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.61...v3.9.0-beta.62) (2024-07-09) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.60...v3.9.0-beta.61) (2024-07-09) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.59...v3.9.0-beta.60) (2024-07-09) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.58...v3.9.0-beta.59) (2024-07-05) + + +### Bug Fixes + +* Cobb angle not working in basic-test mode and open contour ([#4280](https://github.com/OHIF/Viewers/issues/4280)) ([6fd3c7e](https://github.com/OHIF/Viewers/commit/6fd3c7e293fec851dd30e650c1347cc0bc7a99ee)) + + + + + +# [3.9.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.57...v3.9.0-beta.58) (2024-07-04) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.56...v3.9.0-beta.57) (2024-07-02) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.55...v3.9.0-beta.56) (2024-07-02) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.54...v3.9.0-beta.55) (2024-06-28) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.53...v3.9.0-beta.54) (2024-06-28) + + +### Features + +* **studyPrefetcher:** Study Prefetcher ([#4206](https://github.com/OHIF/Viewers/issues/4206)) ([2048b19](https://github.com/OHIF/Viewers/commit/2048b19484c0b1fae73f993cfaa814f861bbd230)) + + + + + +# [3.9.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.52...v3.9.0-beta.53) (2024-06-28) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.51...v3.9.0-beta.52) (2024-06-27) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.50...v3.9.0-beta.51) (2024-06-27) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.49...v3.9.0-beta.50) (2024-06-26) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.48...v3.9.0-beta.49) (2024-06-26) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.47...v3.9.0-beta.48) (2024-06-25) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.46...v3.9.0-beta.47) (2024-06-21) + + +### Bug Fixes + +* Allow the mode setup/creation to be async, and provide a few more values to extension/app config/mode setup. ([#4016](https://github.com/OHIF/Viewers/issues/4016)) ([88575c6](https://github.com/OHIF/Viewers/commit/88575c6c09fd778a31b2f91524163ce65d1639dd)) + + +### Features + +* customization service append and customize functionality should run once ([#4238](https://github.com/OHIF/Viewers/issues/4238)) ([e462fd3](https://github.com/OHIF/Viewers/commit/e462fd31f7944acfee34f08cfbc28cfd9de16169)) +* **sort:** custom series sort in study panel ([#4214](https://github.com/OHIF/Viewers/issues/4214)) ([a433d40](https://github.com/OHIF/Viewers/commit/a433d406e2cac13f644203996c682260b54e8865)) + + + + + +# [3.9.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.45...v3.9.0-beta.46) (2024-06-18) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.44...v3.9.0-beta.45) (2024-06-18) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.43...v3.9.0-beta.44) (2024-06-17) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.42...v3.9.0-beta.43) (2024-06-12) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.41...v3.9.0-beta.42) (2024-06-12) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.40...v3.9.0-beta.41) (2024-06-12) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.39...v3.9.0-beta.40) (2024-06-12) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.38...v3.9.0-beta.39) (2024-06-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.37...v3.9.0-beta.38) (2024-06-07) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.36...v3.9.0-beta.37) (2024-06-05) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.35...v3.9.0-beta.36) (2024-06-05) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.34...v3.9.0-beta.35) (2024-06-05) + + +### Bug Fixes + +* **seg:** maintain algorithm name and algorithm type when DICOM seg is exported or downloaded ([#4203](https://github.com/OHIF/Viewers/issues/4203)) ([a29e94d](https://github.com/OHIF/Viewers/commit/a29e94de803f79bbb3372d00ad8eb14b4224edc2)) + + + + + +# [3.9.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.33...v3.9.0-beta.34) (2024-06-05) + + +### Bug Fixes + +* **hydration:** Maintain the same slice that the user was on pre hydration in post hydration for SR and SEG. ([#4200](https://github.com/OHIF/Viewers/issues/4200)) ([430330f](https://github.com/OHIF/Viewers/commit/430330f7e384d503cb6fc695a7a9642ddfaac313)) + + + + + +# [3.9.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.32...v3.9.0-beta.33) (2024-06-05) + + +### Features + +* **window-level-region:** add window level region tool ([#4127](https://github.com/OHIF/Viewers/issues/4127)) ([ab1a18a](https://github.com/OHIF/Viewers/commit/ab1a18af5a5b0f9086c080ed81c8fda9bfaa975b)) + + + + + +# [3.9.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.31...v3.9.0-beta.32) (2024-05-31) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.30...v3.9.0-beta.31) (2024-05-30) + + +### Bug Fixes + +* **seg:** should be able to navigate outside toolbox and come back later ([#4196](https://github.com/OHIF/Viewers/issues/4196)) ([93e7609](https://github.com/OHIF/Viewers/commit/93e760937f6587ba7481fcf3484ba9004ba49a62)) + + + + + +# [3.9.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.29...v3.9.0-beta.30) (2024-05-30) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.28...v3.9.0-beta.29) (2024-05-30) + + +### Bug Fixes + +* **tmtv:** side panel crashing when activeToolOptions is not an array ([#4189](https://github.com/OHIF/Viewers/issues/4189)) ([19b5b1c](https://github.com/OHIF/Viewers/commit/19b5b1c15cb29ddf1cfd9b608815199bc838f8b2)) + + + + + +# [3.9.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.27...v3.9.0-beta.28) (2024-05-30) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.26...v3.9.0-beta.27) (2024-05-29) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.25...v3.9.0-beta.26) (2024-05-29) + + +### Features + +* **hp:** Add displayArea option for Hanging protocols and example with Mamo([#3808](https://github.com/OHIF/Viewers/issues/3808)) ([18ac08e](https://github.com/OHIF/Viewers/commit/18ac08ed860d119721c52e4ffc270332259100b6)) + + + + + +# [3.9.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.24...v3.9.0-beta.25) (2024-05-29) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.23...v3.9.0-beta.24) (2024-05-29) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.22...v3.9.0-beta.23) (2024-05-28) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.21...v3.9.0-beta.22) (2024-05-27) + + +### Features + +* **ui:** move to React 18 and base for using shadcn/ui ([#4174](https://github.com/OHIF/Viewers/issues/4174)) ([70f2c79](https://github.com/OHIF/Viewers/commit/70f2c797f42af603d7ea0eb8d23b4103aba66f77)) + + + + + +# [3.9.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.20...v3.9.0-beta.21) (2024-05-24) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.19...v3.9.0-beta.20) (2024-05-24) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.18...v3.9.0-beta.19) (2024-05-24) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.17...v3.9.0-beta.18) (2024-05-24) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.16...v3.9.0-beta.17) (2024-05-23) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.15...v3.9.0-beta.16) (2024-05-23) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.14...v3.9.0-beta.15) (2024-05-22) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.13...v3.9.0-beta.14) (2024-05-21) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.12...v3.9.0-beta.13) (2024-05-21) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.11...v3.9.0-beta.12) (2024-05-21) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.10...v3.9.0-beta.11) (2024-05-21) + + +### Features + +* **test:** Playwright testing integration ([#4146](https://github.com/OHIF/Viewers/issues/4146)) ([fe1a706](https://github.com/OHIF/Viewers/commit/fe1a706446cc33670bf5fab8451e8281b487fcd6)) + + + + + +# [3.9.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.9...v3.9.0-beta.10) (2024-05-21) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.8...v3.9.0-beta.9) (2024-05-17) + + +### Bug Fixes + +* **select:** utilize react portals for select component ([#4144](https://github.com/OHIF/Viewers/issues/4144)) ([dce1e7d](https://github.com/OHIF/Viewers/commit/dce1e7d423cb64ec0d4be7362ecbfd52db47ef36)) + + + + + +# [3.9.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.7...v3.9.0-beta.8) (2024-05-16) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.6...v3.9.0-beta.7) (2024-05-15) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.5...v3.9.0-beta.6) (2024-05-15) + + +### Bug Fixes + +* ๐Ÿ› Overflow scroll list menu based on screen hight ([#4123](https://github.com/OHIF/Viewers/issues/4123)) ([6bba2e7](https://github.com/OHIF/Viewers/commit/6bba2e70f80d8eacc57c0e765013d9c10adf5413)) + + + + + +# [3.9.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.4...v3.9.0-beta.5) (2024-05-14) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.3...v3.9.0-beta.4) (2024-05-14) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.2...v3.9.0-beta.3) (2024-05-08) + + +### Features + +* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([374065b](https://github.com/OHIF/Viewers/commit/374065bc3bad9d212f9817a8d41546cc64cfabfb)) + + + + + +# [3.9.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.1...v3.9.0-beta.2) (2024-05-06) + + +### Bug Fixes + +* **bugs:** enhancements and bugs in several areas ([#4086](https://github.com/OHIF/Viewers/issues/4086)) ([730f434](https://github.com/OHIF/Viewers/commit/730f4349100f21b4489a21707dbb2dca9dbfbba2)) + + + + + +# [3.9.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.9.0-beta.0...v3.9.0-beta.1) (2024-05-06) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.9.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.94...v3.9.0-beta.0) (2024-04-29) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0-beta.94) (2024-04-29) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29) + + +### Bug Fixes + +* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f)) + + + + + +# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28) + + +### Bug Fixes + +* **bugs:** fix patient header for doc, track ball rotate resize observer and add segmentation button not being enabled on viewport data change ([#4068](https://github.com/OHIF/Viewers/issues/4068)) ([c09311d](https://github.com/OHIF/Viewers/commit/c09311d3b7df05fcd00a9f36a7233e9d7e5589d0)) + + + + + +# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22) + + +### Bug Fixes + +* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951)) + + + + + +# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19) + + +### Features + +* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb)) + + + + + +# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19) + + +### Bug Fixes + +* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af)) + + + + + +# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes - final ([#4048](https://github.com/OHIF/Viewers/issues/4048)) ([170bb96](https://github.com/OHIF/Viewers/commit/170bb96983082c39b22b7352e0c54aacf3e73b02)) + + + + + +# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes - more ([#4043](https://github.com/OHIF/Viewers/issues/4043)) ([3754c22](https://github.com/OHIF/Viewers/commit/3754c224b4dab28182adb0a41e37d890942144d8)) + + + + + +# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16) + + +### Bug Fixes + +* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025)) + + + + + +# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16) + + +### Bug Fixes + +* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637)) + + + + + +# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10) + + +### Features + +* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13)) + + + + + +# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10) + + +### Bug Fixes + +* **general:** enhancements and bug fixes ([#4018](https://github.com/OHIF/Viewers/issues/4018)) ([2b83393](https://github.com/OHIF/Viewers/commit/2b83393f91cb16ea06821d79d14ff60f80c29c90)) + + + + + +# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10) + + +### Bug Fixes + +* **dicom-video:** Update get direct func for dicom json to use url if present and fix config argument ([#4017](https://github.com/OHIF/Viewers/issues/4017)) ([4f99244](https://github.com/OHIF/Viewers/commit/4f99244d864427d69be6f863cb7a6a78411adb12)) + + + + + +# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10) + + +### Features + +* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2)) + + + + + +# [3.8.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.72...v3.8.0-beta.73) (2024-04-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.71...v3.8.0-beta.72) (2024-04-05) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.70...v3.8.0-beta.71) (2024-04-05) + + +### Features + +* **advanced-roi-tools:** new tools and icon updates and overlay bug fixes ([#4014](https://github.com/OHIF/Viewers/issues/4014)) ([cea27d4](https://github.com/OHIF/Viewers/commit/cea27d438d1de2c1ec90cbaefdc2b31a1d9980a1)) + + + + + +# [3.8.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.69...v3.8.0-beta.70) (2024-04-05) + + +### Features + +* **measurement:** Add support measurement label autocompletion ([#3855](https://github.com/OHIF/Viewers/issues/3855)) ([56b1eae](https://github.com/OHIF/Viewers/commit/56b1eae6356a6534960df1196bdd1e95b0a9a470)) + + + + + +# [3.8.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.68...v3.8.0-beta.69) (2024-04-03) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.67...v3.8.0-beta.68) (2024-04-03) + + +### Features + +* **segmentation:** Enhanced segmentation panel design for TMTV ([#3988](https://github.com/OHIF/Viewers/issues/3988)) ([9f3235f](https://github.com/OHIF/Viewers/commit/9f3235ff096636aafa88d8a42859e8dc85d9036d)) + + + + + +# [3.8.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.66...v3.8.0-beta.67) (2024-04-02) + + +### Features + +* **ViewportActionMenu:** window level per viewport / new patient info / colorbars/ 3D presets and 3D volume rendering ([#3963](https://github.com/OHIF/Viewers/issues/3963)) ([b7f90e3](https://github.com/OHIF/Viewers/commit/b7f90e3951845396f99b69f0a74fc56b2ffeada1)) + + + + + +# [3.8.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.65...v3.8.0-beta.66) (2024-03-28) + + +### Bug Fixes + +* **new layout:** address black screen bugs ([#4008](https://github.com/OHIF/Viewers/issues/4008)) ([158a181](https://github.com/OHIF/Viewers/commit/158a1816703e0ad66cae08cb9bd1ffb93bbd8d43)) + + + + + +# [3.8.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.64...v3.8.0-beta.65) (2024-03-28) + + +### Features + +* **layout:** new layout selector with 3D volume rendering ([#3923](https://github.com/OHIF/Viewers/issues/3923)) ([617043f](https://github.com/OHIF/Viewers/commit/617043fe0da5de91fbea4ac33a27f1df16ae1ca6)) + + + + + +# [3.8.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.63...v3.8.0-beta.64) (2024-03-27) + + +### Features + +* **toolbar:** new Toolbar to enable reactive state synchronization ([#3983](https://github.com/OHIF/Viewers/issues/3983)) ([566b25a](https://github.com/OHIF/Viewers/commit/566b25a54425399096864bd263193646556011a5)) + + + + + +# [3.8.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.62...v3.8.0-beta.63) (2024-03-25) + + +### Features + +* **worklist:** new investigational use text ([#3999](https://github.com/OHIF/Viewers/issues/3999)) ([45b68e8](https://github.com/OHIF/Viewers/commit/45b68e841dcb9e28a2ea991c37ee7ac4a8c5b71e)) + + + + + +# [3.8.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.61...v3.8.0-beta.62) (2024-03-19) + + +### Features + +* **worklist:** New worklist buttons and tooltips ([#3989](https://github.com/OHIF/Viewers/issues/3989)) ([9bcd1ae](https://github.com/OHIF/Viewers/commit/9bcd1ae6f51d61786cc1e99624f396b56a47cd69)) + + + + + +# [3.8.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.60...v3.8.0-beta.61) (2024-03-18) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.59...v3.8.0-beta.60) (2024-03-15) + + +### Features + +* **delete measurement:** icon for measurement table ([#3775](https://github.com/OHIF/Viewers/issues/3775)) ([f7fe91c](https://github.com/OHIF/Viewers/commit/f7fe91c5f6c4f05f3f3f5f640d3a119bd40a5870)) + + + + + +# [3.8.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.58...v3.8.0-beta.59) (2024-03-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.57...v3.8.0-beta.58) (2024-03-05) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.56...v3.8.0-beta.57) (2024-02-28) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.55...v3.8.0-beta.56) (2024-02-22) + + +### Bug Fixes + +* **demo:** Deploy issue ([#3951](https://github.com/OHIF/Viewers/issues/3951)) ([21e8a2b](https://github.com/OHIF/Viewers/commit/21e8a2bd0b7cc72f90a31e472d285d761be15d30)) + + + + + +# [3.8.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.54...v3.8.0-beta.55) (2024-02-21) + + +### Features + +* **resize:** Optimize resizing process and maintain zoom level ([#3889](https://github.com/OHIF/Viewers/issues/3889)) ([b3a0faf](https://github.com/OHIF/Viewers/commit/b3a0faf5f5f0a1993b2b017eb4cc1216164ea2c6)) + + + + + +# [3.8.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.53...v3.8.0-beta.54) (2024-02-14) + + +### Features + +* **errorboundary:** format stack trace properly ([#3931](https://github.com/OHIF/Viewers/issues/3931)) ([0eac386](https://github.com/OHIF/Viewers/commit/0eac386a31a5d6965536360aa65a44769c1a5740)) + + + + + +# [3.8.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.52...v3.8.0-beta.53) (2024-02-05) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.51...v3.8.0-beta.52) (2024-01-22) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.50...v3.8.0-beta.51) (2024-01-22) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.49...v3.8.0-beta.50) (2024-01-22) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.48...v3.8.0-beta.49) (2024-01-19) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.47...v3.8.0-beta.48) (2024-01-17) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.46...v3.8.0-beta.47) (2024-01-12) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.45...v3.8.0-beta.46) (2024-01-12) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.44...v3.8.0-beta.45) (2024-01-09) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.43...v3.8.0-beta.44) (2024-01-09) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.42...v3.8.0-beta.43) (2024-01-09) + + +### Bug Fixes + +* **segmentation:** upgrade cs3d to fix various segmentation bugs ([#3885](https://github.com/OHIF/Viewers/issues/3885)) ([b1efe40](https://github.com/OHIF/Viewers/commit/b1efe40aa146e4052cc47b3f774cabbb47a8d1a6)) + + + + + +# [3.8.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.41...v3.8.0-beta.42) (2024-01-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.40...v3.8.0-beta.41) (2024-01-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.39...v3.8.0-beta.40) (2024-01-08) + + +### Features + +* **ui:** sidePanel expandedWidth ([#3728](https://github.com/OHIF/Viewers/issues/3728)) ([61bf22c](https://github.com/OHIF/Viewers/commit/61bf22c6f80e764bdf5c3b56bb0124a95aa0f793)) + + + + + +# [3.8.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.38...v3.8.0-beta.39) (2024-01-08) + + +### Features + +* improve disableEditing flag ([#3875](https://github.com/OHIF/Viewers/issues/3875)) ([2049c09](https://github.com/OHIF/Viewers/commit/2049c0936c86f819604c243d3dc7b3fe971b5b2c)) + + + + + +# [3.8.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.37...v3.8.0-beta.38) (2024-01-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.36...v3.8.0-beta.37) (2024-01-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.35...v3.8.0-beta.36) (2023-12-15) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.34...v3.8.0-beta.35) (2023-12-14) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.33...v3.8.0-beta.34) (2023-12-13) + + +### Bug Fixes + +* **icon-style:** Ensure consistent icon dimensions ([#3727](https://github.com/OHIF/Viewers/issues/3727)) ([6ca13c0](https://github.com/OHIF/Viewers/commit/6ca13c0a4cb5a95bbb52b0db902b5dbf72f8aa6e)) + + + + + +# [3.8.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.32...v3.8.0-beta.33) (2023-12-13) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.31...v3.8.0-beta.32) (2023-12-13) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.30...v3.8.0-beta.31) (2023-12-13) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.29...v3.8.0-beta.30) (2023-12-13) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.28...v3.8.0-beta.29) (2023-12-13) + + +### Bug Fixes + +* address and improve system vulnerabilities ([#3851](https://github.com/OHIF/Viewers/issues/3851)) ([805c532](https://github.com/OHIF/Viewers/commit/805c53270f243ec61f142a3ffa0af500021cd5ec)) + + +### Features + +* **i18n:** enhanced i18n support ([#3761](https://github.com/OHIF/Viewers/issues/3761)) ([d14a8f0](https://github.com/OHIF/Viewers/commit/d14a8f0199db95cd9e85866a011b64d6bf830d57)) + + + + + +# [3.8.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.27...v3.8.0-beta.28) (2023-12-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.26...v3.8.0-beta.27) (2023-12-06) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.25...v3.8.0-beta.26) (2023-11-28) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.24...v3.8.0-beta.25) (2023-11-27) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.23...v3.8.0-beta.24) (2023-11-24) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.22...v3.8.0-beta.23) (2023-11-24) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.21...v3.8.0-beta.22) (2023-11-21) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.20...v3.8.0-beta.21) (2023-11-21) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.19...v3.8.0-beta.20) (2023-11-21) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.18...v3.8.0-beta.19) (2023-11-18) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.17...v3.8.0-beta.18) (2023-11-15) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.16...v3.8.0-beta.17) (2023-11-13) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.15...v3.8.0-beta.16) (2023-11-13) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.14...v3.8.0-beta.15) (2023-11-10) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.14](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.13...v3.8.0-beta.14) (2023-11-10) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.13](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.12...v3.8.0-beta.13) (2023-11-09) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.12](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.11...v3.8.0-beta.12) (2023-11-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.11](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.10...v3.8.0-beta.11) (2023-11-08) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.10](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.9...v3.8.0-beta.10) (2023-11-03) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.9](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.8...v3.8.0-beta.9) (2023-11-02) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.8](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.7...v3.8.0-beta.8) (2023-10-31) + + +### Features + +* **i18n:** enhanced i18n support ([#3730](https://github.com/OHIF/Viewers/issues/3730)) ([330e11c](https://github.com/OHIF/Viewers/commit/330e11c7ff0151e1096e19b8ffdae7d64cae280e)) + + + + + +# [3.8.0-beta.7](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.6...v3.8.0-beta.7) (2023-10-30) + + +### Features + +* **filters:** save worklist query filters to session storage so that they persist between navigation to the viewer and back ([#3749](https://github.com/OHIF/Viewers/issues/3749)) ([2a15ef0](https://github.com/OHIF/Viewers/commit/2a15ef0e44b7b4d8bbf5cb9363db6e523201c681)) + + + + + +# [3.8.0-beta.6](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.5...v3.8.0-beta.6) (2023-10-25) + + +### Bug Fixes + +* **toolbar:** allow customizable toolbar for active viewport and allow active tool to be deactivated via a click ([#3608](https://github.com/OHIF/Viewers/issues/3608)) ([dd6d976](https://github.com/OHIF/Viewers/commit/dd6d9768bbca1d3cc472e8c1e6d85822500b96ef)) + + + + + +# [3.8.0-beta.5](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.4...v3.8.0-beta.5) (2023-10-24) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.4](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.3...v3.8.0-beta.4) (2023-10-23) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.3](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.2...v3.8.0-beta.3) (2023-10-23) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.2](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.1...v3.8.0-beta.2) (2023-10-19) + + +### Bug Fixes + +* **cine:** Use the frame rate specified in DICOM and optionally auto play cine ([#3735](https://github.com/OHIF/Viewers/issues/3735)) ([d9258ec](https://github.com/OHIF/Viewers/commit/d9258eca70587cf4dc18be4e56c79b16bae73d6d)) + + + + + +# [3.8.0-beta.1](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.0...v3.8.0-beta.1) (2023-10-19) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.8.0-beta.0](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.110...v3.8.0-beta.0) (2023-10-12) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.110](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.109...v3.7.0-beta.110) (2023-10-11) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.109](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.108...v3.7.0-beta.109) (2023-10-11) + + +### Bug Fixes + +* **export:** wrong export for the tmtv RT function ([#3715](https://github.com/OHIF/Viewers/issues/3715)) ([a3f2a1a](https://github.com/OHIF/Viewers/commit/a3f2a1a7b0d16bfcc0ecddc2ab731e54c5e377c8)) + + + + + +# [3.7.0-beta.108](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.107...v3.7.0-beta.108) (2023-10-10) + + +### Bug Fixes + +* **i18n:** display set(s) are two words for English messages ([#3711](https://github.com/OHIF/Viewers/issues/3711)) ([c3a5847](https://github.com/OHIF/Viewers/commit/c3a5847dcd3dce4f1c8d8b11af95f79e3f93f70d)) + + + + + +# [3.7.0-beta.107](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.106...v3.7.0-beta.107) (2023-10-10) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.106](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.105...v3.7.0-beta.106) (2023-10-10) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.105](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.104...v3.7.0-beta.105) (2023-10-10) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.104](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.103...v3.7.0-beta.104) (2023-10-09) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.103](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.102...v3.7.0-beta.103) (2023-10-09) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.102](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.101...v3.7.0-beta.102) (2023-10-06) + + +### Features + +* **Segmentation:** download RTSS from Labelmap([#3692](https://github.com/OHIF/Viewers/issues/3692)) ([40673f6](https://github.com/OHIF/Viewers/commit/40673f64b36b1150149c55632aa1825178a39e65)) + + + + + +# [3.7.0-beta.101](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.100...v3.7.0-beta.101) (2023-10-06) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.100](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.99...v3.7.0-beta.100) (2023-10-06) + + +### Bug Fixes + +* **segmentation scroll:** and hydration bugs ([#3701](https://github.com/OHIF/Viewers/issues/3701)) ([1fd98d9](https://github.com/OHIF/Viewers/commit/1fd98d922094d10fe0c6e9df726314ec9fce49e8)) + + + + + +# [3.7.0-beta.99](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.98...v3.7.0-beta.99) (2023-10-04) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.98](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.97...v3.7.0-beta.98) (2023-10-04) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.97](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.96...v3.7.0-beta.97) (2023-10-04) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.96](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.95...v3.7.0-beta.96) (2023-10-04) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.95](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.94...v3.7.0-beta.95) (2023-10-04) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.94](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.93...v3.7.0-beta.94) (2023-10-03) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.92...v3.7.0-beta.93) (2023-10-03) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.91...v3.7.0-beta.92) (2023-10-03) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.90...v3.7.0-beta.91) (2023-10-03) + + +### Bug Fixes + +* **editing:** regression bug in disable editing ([#3687](https://github.com/OHIF/Viewers/issues/3687)) ([4dc2acd](https://github.com/OHIF/Viewers/commit/4dc2acdefa872dd1d8df47f465e9e9656f95f67f)) + + + + + +# [3.7.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.89...v3.7.0-beta.90) (2023-10-03) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.88...v3.7.0-beta.89) (2023-10-03) + + +### Bug Fixes + +* **StackSync:** Miscellaneous fixes for stack image sync ([#3663](https://github.com/OHIF/Viewers/issues/3663)) ([8a335bd](https://github.com/OHIF/Viewers/commit/8a335bd03d14ba87d65d7468d93f74040aa828d9)) + + + + + +# [3.7.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.87...v3.7.0-beta.88) (2023-10-03) + + +### Bug Fixes + +* **config:** support more values for the useSharedArrayBuffer ([#3688](https://github.com/OHIF/Viewers/issues/3688)) ([1129c15](https://github.com/OHIF/Viewers/commit/1129c155d2c7d46c98a5df7c09879aa3d459fa7e)) + + + + + +# [3.7.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.86...v3.7.0-beta.87) (2023-09-29) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.85...v3.7.0-beta.86) (2023-09-29) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.84...v3.7.0-beta.85) (2023-09-26) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.83...v3.7.0-beta.84) (2023-09-26) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.82...v3.7.0-beta.83) (2023-09-26) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.81...v3.7.0-beta.82) (2023-09-26) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.80...v3.7.0-beta.81) (2023-09-26) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.79...v3.7.0-beta.80) (2023-09-22) + + +### Bug Fixes + +* **react-select:** update react select package ([#3622](https://github.com/OHIF/Viewers/issues/3622)) ([04ca10d](https://github.com/OHIF/Viewers/commit/04ca10d8779dd15454920002f3d48afa8830de8a)) + + +### Features + +* **segmentation mode:** Add create, and export SEG with Brushes ([#3632](https://github.com/OHIF/Viewers/issues/3632)) ([48bbd62](https://github.com/OHIF/Viewers/commit/48bbd6281a497ea68670239f5426a10ee6c56dc1)) +* **SidePanel:** new side panel tab look-and-feel ([#3657](https://github.com/OHIF/Viewers/issues/3657)) ([85c899b](https://github.com/OHIF/Viewers/commit/85c899b399e2521480724be145538993721b9378)) + + + + + +# [3.7.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.78...v3.7.0-beta.79) (2023-09-22) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.77...v3.7.0-beta.78) (2023-09-21) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.76...v3.7.0-beta.77) (2023-09-21) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.75...v3.7.0-beta.76) (2023-09-19) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.74...v3.7.0-beta.75) (2023-09-18) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.73...v3.7.0-beta.74) (2023-09-15) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12) + +**Note:** Version bump only for package @ohif/ui + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/ui + + + +# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11) + +**Note:** Version bump only for package @ohif/ui + + + + + +# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11) + +**Note:** Version bump only for package @ohif/ui + + +**Note:** Version bump only for package @ohif/ui + + + +# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06) + +**Note:** Version bump only for package @ohif/ui + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + + + +* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd)) + +# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06) + +**Note:** Version bump only for package @ohif/ui + + + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06) + +**Note:** Version bump only for package @ohif/ui + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f)) + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/ui + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/ui + +# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05) + +**Note:** Version bump only for package @ohif/ui + + + +**Note:** Version bump only for package @ohif/ui + +## [1.4.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.3...@ohif/ui@1.4.4) (2020-05-04) + +# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01) + +- ๐Ÿ› Proper error handling for derived display sets + ([#1708](https://github.com/OHIF/Viewers/issues/1708)) + ([5b20d8f](https://github.com/OHIF/Viewers/commit/5b20d8f323e4b3ef9988f2f2ab672d697b6da409)) + +### Features + +* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd)) + + + +## [1.4.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.1...@ohif/ui@1.4.2) (2020-04-06) + +**Note:** Version bump only for package @ohif/ui + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30) + +# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.3.3...@ohif/ui@1.4.0) (2020-03-13) + +# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29) + +**Note:** Version bump only for package @ohif/ui + +# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29) + +**Note:** Version bump only for package @ohif/ui + +# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29) + +**Note:** Version bump only for package @ohif/ui + +# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25) + +### Features + +- **cloud data source config:** GUI and API for configuring a cloud data source + with Google cloud healthcare implementation + ([#3589](https://github.com/OHIF/Viewers/issues/3589)) + ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1)) + +# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23) + +**Note:** Version bump only for package @ohif/ui + +## [1.4.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.3...@ohif/ui@1.4.4) (2020-05-04) + +### Bug Fixes + +- ๐Ÿ› Proper error handling for derived display sets + ([#1708](https://github.com/OHIF/Viewers/issues/1708)) + ([5b20d8f](https://github.com/OHIF/Viewers/commit/5b20d8f323e4b3ef9988f2f2ab672d697b6da409)) + +## [1.4.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.2...@ohif/ui@1.4.3) (2020-04-09) + +**Note:** Version bump only for package @ohif/ui + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.4.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.1...@ohif/ui@1.4.2) (2020-04-06) + +- Combined Hotkeys for special characters + ([#1233](https://github.com/OHIF/Viewers/issues/1233)) + ([2f30e7a](https://github.com/OHIF/Viewers/commit/2f30e7a821a238144c49c56f37d8e5565540b4bd)) + +## [1.4.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.0...@ohif/ui@1.4.1) (2020-03-17) + +### Bug Fixes + +- rendering delete button when text is too wide for parent div + ([#1526](https://github.com/OHIF/Viewers/issues/1526)) + ([b269415](https://github.com/OHIF/Viewers/commit/b269415048dfec50e2abb3dd4f4355a23d6ad75a)) + +# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.3.3...@ohif/ui@1.4.0) (2020-03-13) + +### Features + +- Segmentations Settings UI - Phase 1 + [#1391](https://github.com/OHIF/Viewers/issues/1391) + ([#1392](https://github.com/OHIF/Viewers/issues/1392)) + ([e8842cf](https://github.com/OHIF/Viewers/commit/e8842cf8aebde98db7fc123e4867c8288552331f)), + closes [#1423](https://github.com/OHIF/Viewers/issues/1423) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.3.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.3.2...@ohif/ui@1.3.3) (2020-03-09) + +**Note:** Version bump only for package @ohif/ui + +## [1.3.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.3.1...@ohif/ui@1.3.2) (2020-03-09) + +### Bug Fixes + +- Remove Eraser and ROI Window + ([6c950a9](https://github.com/OHIF/Viewers/commit/6c950a9669f7fbf3c46e48679fa26ee514824156)) + +## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.3.0...@ohif/ui@1.3.1) (2020-02-29) + +### Bug Fixes + +- prevent the native context menu from appearing when right-clicking on a + measurement or angle (https://github.com/OHIF/Viewers/issues/1406) + ([#1469](https://github.com/OHIF/Viewers/issues/1469)) + ([9b3be9b](https://github.com/OHIF/Viewers/commit/9b3be9b0c082c9a5b62f2a40f42e59381860fe73)) + +# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.2.1...@ohif/ui@1.3.0) (2020-02-20) + +### Features + +- [#1342](https://github.com/OHIF/Viewers/issues/1342) - Window level tab + ([#1429](https://github.com/OHIF/Viewers/issues/1429)) + ([ebc01a8](https://github.com/OHIF/Viewers/commit/ebc01a8ca238d5a3437b44d81f75aa8a5e8d0574)) + +## [1.2.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.2.0...@ohif/ui@1.2.1) (2020-02-12) + +### Bug Fixes + +- Combined Hotkeys for special characters + ([#1233](https://github.com/OHIF/Viewers/issues/1233)) + ([2f30e7a](https://github.com/OHIF/Viewers/commit/2f30e7a821a238144c49c56f37d8e5565540b4bd)) + +# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.9...@ohif/ui@1.2.0) (2020-02-10) + +### Features + +- Lesion tracker right panel + ([#1428](https://github.com/OHIF/Viewers/issues/1428)) + ([98a649b](https://github.com/OHIF/Viewers/commit/98a649b455ffc712938fc5035cdef40695e58440)) + +## [1.1.9](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.8...@ohif/ui@1.1.9) (2020-01-30) + +### Bug Fixes + +- download tool fixes & improvements + ([#1235](https://github.com/OHIF/Viewers/issues/1235)) + ([b9574b6](https://github.com/OHIF/Viewers/commit/b9574b6efcfeb85cde35b5cae63282f8e1b35be6)) + +## [1.1.8](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.7...@ohif/ui@1.1.8) (2020-01-08) + +### Bug Fixes + +- measurements panel css and delete button visibility + ([#1352](https://github.com/OHIF/Viewers/issues/1352)) + ([7ab0bbb](https://github.com/OHIF/Viewers/commit/7ab0bbb32581dcba16ee16b49b92406e2856ac76)) + +## [1.1.7](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.6...@ohif/ui@1.1.7) (2019-12-20) + +**Note:** Version bump only for package @ohif/ui + +## [1.1.6](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.5...@ohif/ui@1.1.6) (2019-12-20) + +**Note:** Version bump only for package @ohif/ui + +## [1.1.5](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.4...@ohif/ui@1.1.5) (2019-12-19) + +### Bug Fixes + +- ๐Ÿ› Fix drag-n-drop of local files into OHIF + ([#1319](https://github.com/OHIF/Viewers/issues/1319)) + ([23305ce](https://github.com/OHIF/Viewers/commit/23305cec9c0f514e73a8dd17f984ffc87ad8d131)), + closes [#1307](https://github.com/OHIF/Viewers/issues/1307) + +## [1.1.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.3...@ohif/ui@1.1.4) (2019-12-16) + +**Note:** Version bump only for package @ohif/ui + +## [1.1.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.2...@ohif/ui@1.1.3) (2019-12-13) + +### Bug Fixes + +- allow empty values for dimensions + ([#1295](https://github.com/OHIF/Viewers/issues/1295)) + ([cd2da34](https://github.com/OHIF/Viewers/commit/cd2da349e5212cccdd8e65ffa3f7fdc6bad1057c)) + +## [1.1.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.1...@ohif/ui@1.1.2) (2019-12-12) + +### Bug Fixes + +- translations ([#1234](https://github.com/OHIF/Viewers/issues/1234)) + ([30b9e44](https://github.com/OHIF/Viewers/commit/30b9e4422073557287ef26a80b38eeb3f3fcff4c)) + +## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.0...@ohif/ui@1.1.1) (2019-12-11) + +**Note:** Version bump only for package @ohif/ui + +# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.0.1...@ohif/ui@1.1.0) (2019-12-11) + +### Features + +- ๐ŸŽธ DICOM SR STOW on MeasurementAPI + ([#954](https://github.com/OHIF/Viewers/issues/954)) + ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), + closes [#758](https://github.com/OHIF/Viewers/issues/758) + +## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.0.0...@ohif/ui@1.0.1) (2019-12-09) + +**Note:** Version bump only for package @ohif/ui + +# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.65.4...@ohif/ui@1.0.0) (2019-12-09) + +- feat!: Ability to configure cornerstone tools via extension configuration + (#1229) + ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), + closes [#1229](https://github.com/OHIF/Viewers/issues/1229) + +### BREAKING CHANGES + +- modifies the exposed react components props. The contract for + providing configuration for the app has changed. Please reference updated + documentation for guidance. + +## [0.65.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.65.3...@ohif/ui@0.65.4) (2019-12-07) + +**Note:** Version bump only for package @ohif/ui + +## [0.65.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.65.2...@ohif/ui@0.65.3) (2019-12-07) + +**Note:** Version bump only for package @ohif/ui + +## [0.65.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.65.1...@ohif/ui@0.65.2) (2019-12-07) + +**Note:** Version bump only for package @ohif/ui + +## [0.65.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.65.0...@ohif/ui@0.65.1) (2019-11-28) + +### Bug Fixes + +- User Preferences Issues ([#1207](https://github.com/OHIF/Viewers/issues/1207)) + ([1df21a9](https://github.com/OHIF/Viewers/commit/1df21a9e075b5e6dfc10a429ae825826f46c71b8)), + closes [#1161](https://github.com/OHIF/Viewers/issues/1161) + [#1164](https://github.com/OHIF/Viewers/issues/1164) + [#1177](https://github.com/OHIF/Viewers/issues/1177) + [#1179](https://github.com/OHIF/Viewers/issues/1179) + [#1180](https://github.com/OHIF/Viewers/issues/1180) + [#1181](https://github.com/OHIF/Viewers/issues/1181) + [#1182](https://github.com/OHIF/Viewers/issues/1182) + [#1183](https://github.com/OHIF/Viewers/issues/1183) + [#1184](https://github.com/OHIF/Viewers/issues/1184) + [#1185](https://github.com/OHIF/Viewers/issues/1185) + +# [0.65.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.64.2...@ohif/ui@0.65.0) (2019-11-25) + +### Features + +- Add new annotate tool using new dialog service + ([#1211](https://github.com/OHIF/Viewers/issues/1211)) + ([8fd3af1](https://github.com/OHIF/Viewers/commit/8fd3af1e137e793f1b482760a22591c64a072047)) + +## [0.64.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.64.1...@ohif/ui@0.64.2) (2019-11-25) + +### Bug Fixes + +- Issue branch from danny experimental changes pr 1128 + ([#1150](https://github.com/OHIF/Viewers/issues/1150)) + ([a870b3c](https://github.com/OHIF/Viewers/commit/a870b3cc6056cf824af422e46f1ad674910b534e)), + closes [#1161](https://github.com/OHIF/Viewers/issues/1161) + [#1164](https://github.com/OHIF/Viewers/issues/1164) + [#1177](https://github.com/OHIF/Viewers/issues/1177) + [#1179](https://github.com/OHIF/Viewers/issues/1179) + [#1180](https://github.com/OHIF/Viewers/issues/1180) + [#1181](https://github.com/OHIF/Viewers/issues/1181) + [#1182](https://github.com/OHIF/Viewers/issues/1182) + [#1183](https://github.com/OHIF/Viewers/issues/1183) + [#1184](https://github.com/OHIF/Viewers/issues/1184) + [#1185](https://github.com/OHIF/Viewers/issues/1185) + +## [0.64.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.64.0...@ohif/ui@0.64.1) (2019-11-20) + +**Note:** Version bump only for package @ohif/ui + +# [0.64.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.63.0...@ohif/ui@0.64.0) (2019-11-19) + +### Features + +- New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) + ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37)) + +# [0.63.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.4...@ohif/ui@0.63.0) (2019-11-19) + +### Features + +- Issue 879 viewer route query param not filtering but promoting + ([#1141](https://github.com/OHIF/Viewers/issues/1141)) + ([b17f753](https://github.com/OHIF/Viewers/commit/b17f753e6222045252ef885e40233681541a32e1)), + closes [#1118](https://github.com/OHIF/Viewers/issues/1118) + +## [0.62.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.3...@ohif/ui@0.62.4) (2019-11-18) + +### Bug Fixes + +- minor date picker UX improvements + ([813ee5e](https://github.com/OHIF/Viewers/commit/813ee5ed4d78b7bda234922d5f3389efe346451c)) + +## [0.62.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.2...@ohif/ui@0.62.3) (2019-11-15) + +**Note:** Version bump only for package @ohif/ui + +## [0.62.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.1...@ohif/ui@0.62.2) (2019-11-15) + +**Note:** Version bump only for package @ohif/ui + +## [0.62.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.0...@ohif/ui@0.62.1) (2019-11-14) + +**Note:** Version bump only for package @ohif/ui + +# [0.62.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.61.0...@ohif/ui@0.62.0) (2019-11-13) + +### Features + +- expose UiNotifications service + ([#1172](https://github.com/OHIF/Viewers/issues/1172)) + ([5c04e34](https://github.com/OHIF/Viewers/commit/5c04e34c8fb2394ab7acd9eb4f2ab12afeb2f255)) + +# [0.61.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.60.1...@ohif/ui@0.61.0) (2019-11-12) + +### Features + +- ๐ŸŽธ Update hotkeys and user preferences modal + ([#1135](https://github.com/OHIF/Viewers/issues/1135)) + ([e62f5f8](https://github.com/OHIF/Viewers/commit/e62f5f8dd28ab363f23671cd21cee115abb870ff)), + closes [#923](https://github.com/OHIF/Viewers/issues/923) + +## [0.60.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.60.0...@ohif/ui@0.60.1) (2019-11-08) + +### Bug Fixes + +- Fix display issues with incorrect thumbnails. Change ImageThumb to functional + component. ([#1148](https://github.com/OHIF/Viewers/issues/1148)) + ([d70eae3](https://github.com/OHIF/Viewers/commit/d70eae3eb04fe854464f3e62316df8869bba6f11)) + +# [0.60.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.59.1...@ohif/ui@0.60.0) (2019-11-06) + +### Features + +- modal provider ([#1151](https://github.com/OHIF/Viewers/issues/1151)) + ([75d88bc](https://github.com/OHIF/Viewers/commit/75d88bc454710d2dcdbc7d68c4d9df041159c840)), + closes [#1086](https://github.com/OHIF/Viewers/issues/1086) + [#1116](https://github.com/OHIF/Viewers/issues/1116) + [#1116](https://github.com/OHIF/Viewers/issues/1116) + [#1146](https://github.com/OHIF/Viewers/issues/1146) + [#1142](https://github.com/OHIF/Viewers/issues/1142) + [#1143](https://github.com/OHIF/Viewers/issues/1143) + [#1110](https://github.com/OHIF/Viewers/issues/1110) + [#1086](https://github.com/OHIF/Viewers/issues/1086) + [#1116](https://github.com/OHIF/Viewers/issues/1116) + [#1119](https://github.com/OHIF/Viewers/issues/1119) + +## [0.59.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.59.0...@ohif/ui@0.59.1) (2019-11-05) + +### Bug Fixes + +- [#1075](https://github.com/OHIF/Viewers/issues/1075) Returning to the Study + List before all series have finisheโ€ฆ + ([#1090](https://github.com/OHIF/Viewers/issues/1090)) + ([ecaf578](https://github.com/OHIF/Viewers/commit/ecaf578f92dc40294cec7ff9b272fb432dec4125)) + +# [0.59.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.58.5...@ohif/ui@0.59.0) (2019-11-04) + +### Features + +- ๐ŸŽธ New modal provider ([#1110](https://github.com/OHIF/Viewers/issues/1110)) + ([5ee832b](https://github.com/OHIF/Viewers/commit/5ee832b19505a4e8e5756660ce6ed03a7f18dec3)), + closes [#1086](https://github.com/OHIF/Viewers/issues/1086) + [#1116](https://github.com/OHIF/Viewers/issues/1116) + +## [0.58.5](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.58.4...@ohif/ui@0.58.5) (2019-11-04) + +### Bug Fixes + +- ๐Ÿ› Minor issues measurement panel related to description + ([#1142](https://github.com/OHIF/Viewers/issues/1142)) + ([681384b](https://github.com/OHIF/Viewers/commit/681384b7425c83b02a0ed83371ca92d78ca7838c)) + +## [0.58.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.58.3...@ohif/ui@0.58.4) (2019-10-31) + +### Bug Fixes + +- application crash if patientName is an object + ([#1138](https://github.com/OHIF/Viewers/issues/1138)) + ([64cf3b3](https://github.com/OHIF/Viewers/commit/64cf3b324da2383a927af1df2d46db2fca5318aa)) + +## [0.58.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.58.2...@ohif/ui@0.58.3) (2019-10-30) + +### Bug Fixes + +- ๐Ÿ› Fix ghost shadow on thumb + ([#1113](https://github.com/OHIF/Viewers/issues/1113)) + ([caaa032](https://github.com/OHIF/Viewers/commit/caaa032c4bc24fd69fdb01a15a8feb2721c321db)) + +## [0.58.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.58.1...@ohif/ui@0.58.2) (2019-10-29) + +### Bug Fixes + +- ๐Ÿ› Limit image download size to avoid browser issues + ([#1112](https://github.com/OHIF/Viewers/issues/1112)) + ([5716b71](https://github.com/OHIF/Viewers/commit/5716b71d409ee1c6f13393c8cb7f50222415e198)), + closes [#1099](https://github.com/OHIF/Viewers/issues/1099) + +## [0.58.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.58.0...@ohif/ui@0.58.1) (2019-10-29) + +### Bug Fixes + +- **StudyList:** camel case colSpan + ([#1123](https://github.com/OHIF/Viewers/issues/1123)) + ([0d498ba](https://github.com/OHIF/Viewers/commit/0d498ba17ddde8d8f0c51d742770e1574041eec0)) + +# [0.58.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.57.1...@ohif/ui@0.58.0) (2019-10-28) + +### Features + +- responsive study list ([#1068](https://github.com/OHIF/Viewers/issues/1068)) + ([2cdef4b](https://github.com/OHIF/Viewers/commit/2cdef4b9844cc2ce61e9ce76b5a942ba7051fe16)) + +## [0.57.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.57.0...@ohif/ui@0.57.1) (2019-10-28) + +### Bug Fixes + +- MIP styling ([#1109](https://github.com/OHIF/Viewers/issues/1109)) + ([0d21cc6](https://github.com/OHIF/Viewers/commit/0d21cc6ad0c47706b9e62e05fe2a0f1d86339760)) + +# [0.57.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.56.1...@ohif/ui@0.57.0) (2019-10-26) + +### Features + +- Snapshot Download Tool ([#840](https://github.com/OHIF/Viewers/issues/840)) + ([450e098](https://github.com/OHIF/Viewers/commit/450e0981a5ba054fcfcb85eeaeb18371af9088f8)) + +## [0.56.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.56.0...@ohif/ui@0.56.1) (2019-10-26) + +**Note:** Version bump only for package @ohif/ui + +# [0.56.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.55.0...@ohif/ui@0.56.0) (2019-10-22) + +### Features + +- ๐ŸŽธ Load spinner when selecting gcloud store. Add key on td + ([#1034](https://github.com/OHIF/Viewers/issues/1034)) + ([e62f403](https://github.com/OHIF/Viewers/commit/e62f403fe9e3df56713128e3d59045824b086d8d)), + closes [#1057](https://github.com/OHIF/Viewers/issues/1057) + +# [0.55.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.54.0...@ohif/ui@0.55.0) (2019-10-15) + +### Features + +- Add browser info and app version + ([#1046](https://github.com/OHIF/Viewers/issues/1046)) + ([c217b8b](https://github.com/OHIF/Viewers/commit/c217b8b)) + +# [0.54.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.53.4...@ohif/ui@0.54.0) (2019-10-14) + +### Features + +- Notification Service ([#1011](https://github.com/OHIF/Viewers/issues/1011)) + ([92c8996](https://github.com/OHIF/Viewers/commit/92c8996)) + +## [0.53.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.53.3...@ohif/ui@0.53.4) (2019-10-11) + +**Note:** Version bump only for package @ohif/ui + +## [0.53.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.53.2...@ohif/ui@0.53.3) (2019-10-10) + +### Bug Fixes + +- ๐ŸŽธ switch ohif logo from text + font to SVG + ([#1021](https://github.com/OHIF/Viewers/issues/1021)) + ([e7de8be](https://github.com/OHIF/Viewers/commit/e7de8be)) + +## [0.53.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.53.1...@ohif/ui@0.53.2) (2019-10-09) + +**Note:** Version bump only for package @ohif/ui + +## [0.53.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.53.0...@ohif/ui@0.53.1) (2019-10-04) + +### Bug Fixes + +- Move Series Information to Separate Row + ([#990](https://github.com/OHIF/Viewers/issues/990)) + ([458d310](https://github.com/OHIF/Viewers/commit/458d310)) + +# [0.53.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.52.0...@ohif/ui@0.53.0) (2019-10-03) + +### Features + +- Use QIDO + WADO to load series metadata individually rather than the entire + study metadata at once ([#953](https://github.com/OHIF/Viewers/issues/953)) + ([9e10c2b](https://github.com/OHIF/Viewers/commit/9e10c2b)) + +# [0.52.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.51.3...@ohif/ui@0.52.0) (2019-10-01) + +### Features + +- ๐ŸŽธ MPR UI improvements. Added MinIP, AvgIP, slab thickness slider and mode + toggle ([#947](https://github.com/OHIF/Viewers/issues/947)) + ([c79c0c3](https://github.com/OHIF/Viewers/commit/c79c0c3)) + +## [0.51.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.51.2...@ohif/ui@0.51.3) (2019-10-01) + +### Bug Fixes + +- Address issues with touch devices and drag/drop causing crashes + ([#982](https://github.com/OHIF/Viewers/issues/982)) + ([cf40a83](https://github.com/OHIF/Viewers/commit/cf40a83)) + +## [0.51.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.51.1...@ohif/ui@0.51.2) (2019-09-26) + +### Bug Fixes + +- ๐Ÿ› Set series into active viewport by clicking on thumbnail + ([#945](https://github.com/OHIF/Viewers/issues/945)) + ([5551f81](https://github.com/OHIF/Viewers/commit/5551f81)), closes + [#895](https://github.com/OHIF/Viewers/issues/895) + [#895](https://github.com/OHIF/Viewers/issues/895) + +## [0.51.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.51.0...@ohif/ui@0.51.1) (2019-09-19) + +### Bug Fixes + +- Use HTML5Backend for drag-drop if the device does not support touch + ([#927](https://github.com/OHIF/Viewers/issues/927)) + ([6fdac4d](https://github.com/OHIF/Viewers/commit/6fdac4d)) + +# [0.51.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.50.4...@ohif/ui@0.51.0) (2019-09-12) + +### Features + +- **EraserTool:** add eraserTool to @ohif/extension-cornerstone + ([#912](https://github.com/OHIF/Viewers/issues/912)) + ([698d274](https://github.com/OHIF/Viewers/commit/698d274)) + +## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.50.3...@ohif/ui@0.50.4) (2019-09-06) + +**Note:** Version bump only for package @ohif/ui + +## [0.50.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.50.2...@ohif/ui@0.50.3) (2019-09-04) + +**Note:** Version bump only for package @ohif/ui + +## [0.50.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.50.1...@ohif/ui@0.50.2) (2019-09-04) + +### Bug Fixes + +- measurementsAPI issue caused by production build + ([#842](https://github.com/OHIF/Viewers/issues/842)) + ([49d3439](https://github.com/OHIF/Viewers/commit/49d3439)) + +## [0.50.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.50.0-alpha.10...@ohif/ui@0.50.1) (2019-08-14) + +**Note:** Version bump only for package @ohif/ui + +# [0.50.0-alpha.10](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.2.18-alpha.9...@ohif/ui@0.50.0-alpha.10) (2019-08-14) + +**Note:** Version bump only for package @ohif/ui + +## [0.2.18-alpha.9](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.2.18-alpha.8...@ohif/ui@0.2.18-alpha.9) (2019-08-14) + +**Note:** Version bump only for package @ohif/ui + +## 0.2.18-alpha.8 (2019-08-14) + +**Note:** Version bump only for package @ohif/ui + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.2.18-alpha.7](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.2.18-alpha.6...@ohif/ui@0.2.18-alpha.7) (2019-08-08) + +**Note:** Version bump only for package @ohif/ui + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.2.18-alpha.6](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.2.18-alpha.5...@ohif/ui@0.2.18-alpha.6) (2019-08-08) + +**Note:** Version bump only for package @ohif/ui + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.2.18-alpha.5](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.2.18-alpha.4...@ohif/ui@0.2.18-alpha.5) (2019-08-08) + +**Note:** Version bump only for package @ohif/ui + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.2.18-alpha.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.2.18-alpha.3...@ohif/ui@0.2.18-alpha.4) (2019-08-08) + +**Note:** Version bump only for package @ohif/ui + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.2.18-alpha.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.2.18-alpha.2...@ohif/ui@0.2.18-alpha.3) (2019-08-08) + +**Note:** Version bump only for package @ohif/ui + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.2.18-alpha.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.2.18-alpha.1...@ohif/ui@0.2.18-alpha.2) (2019-08-07) + +**Note:** Version bump only for package @ohif/ui + +## [0.2.18-alpha.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.2.18-alpha.0...@ohif/ui@0.2.18-alpha.1) (2019-08-07) + +**Note:** Version bump only for package @ohif/ui + +## 0.2.18-alpha.0 (2019-08-05) + +**Note:** Version bump only for package @ohif/ui diff --git a/platform/ui/README.md b/platform/ui/README.md new file mode 100644 index 0000000..2dcfda1 --- /dev/null +++ b/platform/ui/README.md @@ -0,0 +1,14 @@ +# @ohif/ui + +## For Running Storybook + +``` +cd platform/ui + +yarn install + +yarn storybook + +``` + +Stories are available default at `http://localhost:6006/` diff --git a/platform/ui/assets/images/CT-AAA.png b/platform/ui/assets/images/CT-AAA.png new file mode 100644 index 0000000..67c6bf7 Binary files /dev/null and b/platform/ui/assets/images/CT-AAA.png differ diff --git a/platform/ui/assets/images/CT-AAA2.png b/platform/ui/assets/images/CT-AAA2.png new file mode 100644 index 0000000..4c51a6c Binary files /dev/null and b/platform/ui/assets/images/CT-AAA2.png differ diff --git a/platform/ui/assets/images/CT-Air.png b/platform/ui/assets/images/CT-Air.png new file mode 100644 index 0000000..a65680a Binary files /dev/null and b/platform/ui/assets/images/CT-Air.png differ diff --git a/platform/ui/assets/images/CT-Bone.png b/platform/ui/assets/images/CT-Bone.png new file mode 100644 index 0000000..7c3f8c9 Binary files /dev/null and b/platform/ui/assets/images/CT-Bone.png differ diff --git a/platform/ui/assets/images/CT-Bones.png b/platform/ui/assets/images/CT-Bones.png new file mode 100644 index 0000000..441d6bf Binary files /dev/null and b/platform/ui/assets/images/CT-Bones.png differ diff --git a/platform/ui/assets/images/CT-Cardiac.png b/platform/ui/assets/images/CT-Cardiac.png new file mode 100644 index 0000000..3f9daad Binary files /dev/null and b/platform/ui/assets/images/CT-Cardiac.png differ diff --git a/platform/ui/assets/images/CT-Cardiac2.png b/platform/ui/assets/images/CT-Cardiac2.png new file mode 100644 index 0000000..a281b24 Binary files /dev/null and b/platform/ui/assets/images/CT-Cardiac2.png differ diff --git a/platform/ui/assets/images/CT-Cardiac3.png b/platform/ui/assets/images/CT-Cardiac3.png new file mode 100644 index 0000000..0b8773e Binary files /dev/null and b/platform/ui/assets/images/CT-Cardiac3.png differ diff --git a/platform/ui/assets/images/CT-Chest-Contrast-Enhanced.png b/platform/ui/assets/images/CT-Chest-Contrast-Enhanced.png new file mode 100644 index 0000000..be165b4 Binary files /dev/null and b/platform/ui/assets/images/CT-Chest-Contrast-Enhanced.png differ diff --git a/platform/ui/assets/images/CT-Chest-Vessels.png b/platform/ui/assets/images/CT-Chest-Vessels.png new file mode 100644 index 0000000..23f8732 Binary files /dev/null and b/platform/ui/assets/images/CT-Chest-Vessels.png differ diff --git a/platform/ui/assets/images/CT-Coronary-Arteries-2.png b/platform/ui/assets/images/CT-Coronary-Arteries-2.png new file mode 100644 index 0000000..1b6b161 Binary files /dev/null and b/platform/ui/assets/images/CT-Coronary-Arteries-2.png differ diff --git a/platform/ui/assets/images/CT-Coronary-Arteries-3.png b/platform/ui/assets/images/CT-Coronary-Arteries-3.png new file mode 100644 index 0000000..088a286 Binary files /dev/null and b/platform/ui/assets/images/CT-Coronary-Arteries-3.png differ diff --git a/platform/ui/assets/images/CT-Coronary-Arteries.png b/platform/ui/assets/images/CT-Coronary-Arteries.png new file mode 100644 index 0000000..3b32f1b Binary files /dev/null and b/platform/ui/assets/images/CT-Coronary-Arteries.png differ diff --git a/platform/ui/assets/images/CT-Cropped-Volume-Bone.png b/platform/ui/assets/images/CT-Cropped-Volume-Bone.png new file mode 100644 index 0000000..13c0922 Binary files /dev/null and b/platform/ui/assets/images/CT-Cropped-Volume-Bone.png differ diff --git a/platform/ui/assets/images/CT-Fat.png b/platform/ui/assets/images/CT-Fat.png new file mode 100644 index 0000000..9cdd78a Binary files /dev/null and b/platform/ui/assets/images/CT-Fat.png differ diff --git a/platform/ui/assets/images/CT-Liver-Vasculature.png b/platform/ui/assets/images/CT-Liver-Vasculature.png new file mode 100644 index 0000000..b33856d Binary files /dev/null and b/platform/ui/assets/images/CT-Liver-Vasculature.png differ diff --git a/platform/ui/assets/images/CT-Lung.png b/platform/ui/assets/images/CT-Lung.png new file mode 100644 index 0000000..158f3d7 Binary files /dev/null and b/platform/ui/assets/images/CT-Lung.png differ diff --git a/platform/ui/assets/images/CT-MIP.png b/platform/ui/assets/images/CT-MIP.png new file mode 100644 index 0000000..30a9356 Binary files /dev/null and b/platform/ui/assets/images/CT-MIP.png differ diff --git a/platform/ui/assets/images/CT-Muscle.png b/platform/ui/assets/images/CT-Muscle.png new file mode 100644 index 0000000..76ecdc4 Binary files /dev/null and b/platform/ui/assets/images/CT-Muscle.png differ diff --git a/platform/ui/assets/images/CT-Pulmonary-Arteries.png b/platform/ui/assets/images/CT-Pulmonary-Arteries.png new file mode 100644 index 0000000..4558000 Binary files /dev/null and b/platform/ui/assets/images/CT-Pulmonary-Arteries.png differ diff --git a/platform/ui/assets/images/CT-Soft-Tissue.png b/platform/ui/assets/images/CT-Soft-Tissue.png new file mode 100644 index 0000000..f036900 Binary files /dev/null and b/platform/ui/assets/images/CT-Soft-Tissue.png differ diff --git a/platform/ui/assets/images/DTI-FA-Brain.png b/platform/ui/assets/images/DTI-FA-Brain.png new file mode 100644 index 0000000..9643546 Binary files /dev/null and b/platform/ui/assets/images/DTI-FA-Brain.png differ diff --git a/platform/ui/assets/images/MR-Angio.png b/platform/ui/assets/images/MR-Angio.png new file mode 100644 index 0000000..f54d6fa Binary files /dev/null and b/platform/ui/assets/images/MR-Angio.png differ diff --git a/platform/ui/assets/images/MR-Default.png b/platform/ui/assets/images/MR-Default.png new file mode 100644 index 0000000..f8bf302 Binary files /dev/null and b/platform/ui/assets/images/MR-Default.png differ diff --git a/platform/ui/assets/images/MR-MIP.png b/platform/ui/assets/images/MR-MIP.png new file mode 100644 index 0000000..8b3e91a Binary files /dev/null and b/platform/ui/assets/images/MR-MIP.png differ diff --git a/platform/ui/assets/images/MR-T2-Brain.png b/platform/ui/assets/images/MR-T2-Brain.png new file mode 100644 index 0000000..8b1f7a5 Binary files /dev/null and b/platform/ui/assets/images/MR-T2-Brain.png differ diff --git a/platform/ui/assets/images/VolumeRendering.png b/platform/ui/assets/images/VolumeRendering.png new file mode 100644 index 0000000..8d7313e Binary files /dev/null and b/platform/ui/assets/images/VolumeRendering.png differ diff --git a/platform/ui/babel.config.js b/platform/ui/babel.config.js new file mode 100644 index 0000000..325ca2a --- /dev/null +++ b/platform/ui/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/platform/ui/jest.config.js b/platform/ui/jest.config.js new file mode 100644 index 0000000..f57711b --- /dev/null +++ b/platform/ui/jest.config.js @@ -0,0 +1,12 @@ +const base = require('../../jest.config.base.js'); +const pkg = require('./package'); + +module.exports = { + ...base, + displayName: pkg.name, + // rootDir: "../.." + // testMatch: [ + // //`/platform/${pack.name}/**/*.spec.js` + // "/platform/app/**/*.test.js" + // ] +}; diff --git a/platform/ui/package.json b/platform/ui/package.json new file mode 100644 index 0000000..aa10a1c --- /dev/null +++ b/platform/ui/package.json @@ -0,0 +1,87 @@ +{ + "name": "@ohif/ui", + "version": "3.10.0-beta.111", + "description": "A set of React components for Medical Imaging Viewers", + "author": "OHIF Contributors", + "license": "MIT", + "main": "dist/ohif-ui.umd.js", + "module": "src/index.js", + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "clean": "rm -rf node_modules/.cache/storybook && shx rm -rf dist", + "clean:deep": "yarn run clean && shx rm -rf node_modules", + "start": "yarn run build --watch", + "build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", + "build:package": "yarn run build", + "storybook": "storybook dev -p 6006", + "dev": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "peerDependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "dependencies": { + "@testing-library/react": "^13.1.0", + "browser-detect": "^0.2.28", + "classnames": "^2.3.2", + "d3-array": "3", + "d3-axis": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-zoom": "3", + "lodash.clonedeep": "^4.5.0", + "lodash.debounce": "^4.0.8", + "lodash.merge": "^4.6.1", + "moment": "*", + "mousetrap": "^1.6.5", + "react": "^18.3.1", + "react-dates": "^21.8.0", + "react-dnd": "14.0.2", + "react-dnd-html5-backend": "14.0.0", + "react-dom": "^18.3.1", + "react-draggable": "^4.4.6", + "react-error-boundary": "^3.1.3", + "react-modal": "3.11.2", + "react-outside-click-handler": "^1.3.0", + "react-select": "5.7.4", + "react-test-renderer": "^18.3.1", + "react-window": "^1.8.9", + "react-with-direction": "^1.3.1", + "swiper": "^8.4.2", + "webpack": "5.94.0" + }, + "devDependencies": { + "@babel/core": "7.24.7", + "@storybook/addon-actions": "^7.6.10", + "@storybook/addon-docs": "^7.6.10", + "@storybook/addon-essentials": "^7.6.10", + "@storybook/addon-links": "^7.6.10", + "@storybook/cli": "^7.6.10", + "@storybook/react": "^7.6.10", + "@storybook/react-webpack5": "^7.6.10", + "@storybook/source-loader": "^7.6.10", + "autoprefixer": "^10.4.14", + "babel-loader": "^9.1.2", + "dotenv-webpack": "^8.0.1", + "postcss": "^8.4.23", + "postcss-loader": "^7.2.4", + "prop-types": "^15.8.1", + "remark-gfm": "^3.0.1", + "storybook": "^7.6.10", + "tailwindcss": "3.2.4" + } +} diff --git a/platform/ui/src/assets/styles/styles.css b/platform/ui/src/assets/styles/styles.css new file mode 100644 index 0000000..357f8a5 --- /dev/null +++ b/platform/ui/src/assets/styles/styles.css @@ -0,0 +1,57 @@ +/* CUSTOM OHIF SCROLLBAR */ +.ohif-scrollbar { + scrollbar-color: #173239 transparent; + overflow-y: auto; +} + +.ohif-scrollbar-stable-gutter { + scrollbar-gutter: stable; +} + +.study-min-height { + min-height: 450px; +} + +.ohif-scrollbar:hover { + overflow-y: auto; +} + +.ohif-scrollbar::-webkit-scrollbar { + scrollbar-width: thin; +} + +.ohif-scrollbar::-webkit-scrollbar-track { + @apply rounded; +} + +.ohif-scrollbar::-webkit-scrollbar-thumb { + @apply rounded; + @apply bg-secondary-dark; + background-color: #041c4a; +} + +.ohif-scrollbar::-webkit-scrollbar-thumb:window-inactive { + @apply bg-secondary-dark; + background-color: #041c4a; +} + +/* INVISIBLE SCROLLBAR */ +.invisible-scrollbar { + scrollbar-width: none; +} + +.invisible-scrollbar::-webkit-scrollbar { + @apply hidden; +} + +.invisible-scrollbar::-webkit-scrollbar-track { + @apply hidden; +} + +.invisible-scrollbar::-webkit-scrollbar-thumb { + @apply hidden; +} + +.invisible-scrollbar::-webkit-scrollbar-thumb:window-inactive { + @apply hidden; +} diff --git a/platform/ui/src/components/AboutModal/AboutModal.tsx b/platform/ui/src/components/AboutModal/AboutModal.tsx new file mode 100644 index 0000000..898120f --- /dev/null +++ b/platform/ui/src/components/AboutModal/AboutModal.tsx @@ -0,0 +1,150 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import detect from 'browser-detect'; +import { useTranslation } from 'react-i18next'; + +import Typography from '../Typography'; +import { Icons } from '@ohif/ui-next'; + +const Link = ({ href, children, showIcon = false }) => { + return ( + + + {children} + {!!showIcon && } + + + ); +}; + +const Row = ({ title, value, link }) => { + return ( +
+ + {title} + + + {link ? ( + {value} + ) : ( + + {value} + + )} +
+ ); +}; + +const AboutModal = ({ buildNumber, versionNumber, commitHash }) => { + const { os, version, name } = detect(); + const browser = `${name[0].toUpperCase()}${name.substr(1)} ${version}`; + const { t } = useTranslation('AboutModal'); + + const renderRowTitle = title => ( +
+ + {title} + +
+ ); + return ( +
+ {renderRowTitle(t('Important links'))} +
+ + {t('Visit the forum')} + + + + {t('Report an issue')} + + + + + {t('More details')} + + +
+ + {renderRowTitle(t('Version information'))} +
+ + + {/* */} + + {buildNumber && ( + + )} + {commitHash && ( + + )} + + +
+
+ ); +}; + +AboutModal.propTypes = { + buildNumber: PropTypes.string, + versionNumber: PropTypes.string, +}; + +export default AboutModal; diff --git a/platform/ui/src/components/AboutModal/__stories__/aboutModal.stories.mdx b/platform/ui/src/components/AboutModal/__stories__/aboutModal.stories.mdx new file mode 100644 index 0000000..51e4c83 --- /dev/null +++ b/platform/ui/src/components/AboutModal/__stories__/aboutModal.stories.mdx @@ -0,0 +1,50 @@ +import AboutModal from '../../AboutModal'; +import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs'; +import { + createComponentTemplate, + createStoryMetaSettings, +} from '../../../storybook/functions/create-component-story'; + +export const argTypes = { + component: AboutModal, + title: 'Modals/About', +}; + + + +export const aboutTemplate = args => ( +
+ +
+); + + + +- [Overview](#overview) +- [Props](#props) +- [Usage](#usage) +- [Contribute](#contribute) + +## Overview + +OHIF about modal component provides information about the application version and build number + + + {aboutTemplate.bind({})} + + +## Props + + + +## Usage + +## Contribute + +
diff --git a/platform/ui/src/components/AboutModal/index.js b/platform/ui/src/components/AboutModal/index.js new file mode 100644 index 0000000..a325d75 --- /dev/null +++ b/platform/ui/src/components/AboutModal/index.js @@ -0,0 +1,2 @@ +import AboutModal from './AboutModal'; +export default AboutModal; diff --git a/platform/ui/src/components/ActionButtons/ActionButtons.tsx b/platform/ui/src/components/ActionButtons/ActionButtons.tsx new file mode 100644 index 0000000..0e7bd33 --- /dev/null +++ b/platform/ui/src/components/ActionButtons/ActionButtons.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Button, ButtonEnums } from '../../components'; + +function ActionButtons({ actions, disabled = false, t }) { + return ( + + {actions.map((action, index) => ( + + ))} + + ); +} + +ActionButtons.propTypes = { + actions: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + disabled: PropTypes.bool, + }) + ).isRequired, + disabled: PropTypes.bool, +}; + +export default ActionButtons; diff --git a/platform/ui/src/components/ActionButtons/index.ts b/platform/ui/src/components/ActionButtons/index.ts new file mode 100644 index 0000000..d3cc681 --- /dev/null +++ b/platform/ui/src/components/ActionButtons/index.ts @@ -0,0 +1,3 @@ +import ActionButtons from './ActionButtons'; + +export default ActionButtons; diff --git a/platform/ui/src/components/AdvancedToolbox/AdvancedToolbox.tsx b/platform/ui/src/components/AdvancedToolbox/AdvancedToolbox.tsx new file mode 100644 index 0000000..aed13ed --- /dev/null +++ b/platform/ui/src/components/AdvancedToolbox/AdvancedToolbox.tsx @@ -0,0 +1,72 @@ +import React, { useState, useEffect } from 'react'; +import classnames from 'classnames'; +import { PanelSection, Tooltip } from '../../components'; +import ToolSettings from './ToolSettings'; +import { Icons } from '@ohif/ui-next'; + +/** + * Use Toolbox component instead of this although it doesn't have "Advanced" in its name + * it is better to use it instead of this one + */ +const AdvancedToolbox = ({ title, items }) => { + const [activeItemName, setActiveItemName] = useState(null); + + useEffect(() => { + // see if any of the items are active from the outside + const activeItem = items?.find(item => item.active); + setActiveItemName(activeItem ? activeItem.name : null); + }, [items]); + + const activeItemOptions = items?.find(item => item.name === activeItemName)?.options; + + return ( + +
+
+ {items?.map(item => { + return ( + {item.name}} + key={item.name} + > +
{ + if (item.disabled) { + return; + } + setActiveItemName(item.name); + item.onClick(item.name); + }} + > +
+ +
+
+
+ ); + })} +
+
+ +
+
+
+ ); +}; + +AdvancedToolbox.propTypes = {}; + +export default AdvancedToolbox; diff --git a/platform/ui/src/components/AdvancedToolbox/ToolSettings.tsx b/platform/ui/src/components/AdvancedToolbox/ToolSettings.tsx new file mode 100644 index 0000000..0d5d339 --- /dev/null +++ b/platform/ui/src/components/AdvancedToolbox/ToolSettings.tsx @@ -0,0 +1,128 @@ +import React from 'react'; +import { ButtonGroup, InputDoubleRange, InputRange } from '../../components'; + +const SETTING_TYPES = { + RANGE: 'range', + RADIO: 'radio', + CUSTOM: 'custom', + DOUBLE_RANGE: 'double-range', +}; + +function ToolSettings({ options }) { + if (!options) { + return null; + } + + if (typeof options === 'function') { + return options(); + } + + return ( +
+ {options?.map(option => { + if (option.condition && option.condition?.({ options }) === false) { + return null; + } + + switch (option.type) { + case SETTING_TYPES.RANGE: + return renderRangeSetting(option); + case SETTING_TYPES.RADIO: + return renderRadioSetting(option); + case SETTING_TYPES.DOUBLE_RANGE: + return renderDoubleRangeSetting(option); + case SETTING_TYPES.CUSTOM: + return renderCustomSetting(option); + default: + return null; + } + })} +
+ ); +} + +const renderRangeSetting = option => { + return ( +
+
{option.name}
+
+ option.commands?.(value)} + allowNumberEdit={true} + showAdjustmentArrows={false} + inputClassName="ml-1 w-4/5 cursor-pointer" + /> +
+
+ ); +}; + +const renderRadioSetting = option => { + const renderButtons = option => { + return option.values?.map(({ label, value: optionValue }, index) => ( + + )); + }; + + return ( +
+ {option.name} +
+ value === option.value) || 0} + > + {renderButtons(option)} + +
+
+ ); +}; + +const renderDoubleRangeSetting = option => { + return ( +
+ +
+ ); +}; + +const renderCustomSetting = option => { + return ( +
+ {typeof option.children === 'function' ? option.children() : option.children} +
+ ); +}; + +export default ToolSettings; diff --git a/platform/ui/src/components/AdvancedToolbox/__stories__/advancedToolbox.stories.mdx b/platform/ui/src/components/AdvancedToolbox/__stories__/advancedToolbox.stories.mdx new file mode 100644 index 0000000..5517913 --- /dev/null +++ b/platform/ui/src/components/AdvancedToolbox/__stories__/advancedToolbox.stories.mdx @@ -0,0 +1,144 @@ +import AdvancedToolbox from '../../AdvancedToolbox'; +import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs'; +import { + createComponentTemplate, + createStoryMetaSettings, +} from '../../../storybook/functions/create-component-story'; + +export const argTypes = { + component: AdvancedToolbox, + title: 'Components/AdvancedToolbox', +}; + + + +export const advancedToolboxTemplate = args => ( +
+ +
+); + + + +- [Overview](#overview) +- [Props](#props) +- [Usage](#usage) +- [Contribute](#contribute) + +## Overview + +OHIF advanced toolbox which can host set of tools that require more space for customization. + + + console.log('Brush clicked'), + options: [ + { + name: 'Radius (mm)', + type: 'range', + min: 1, + max: 10, + value: 5, + step: 1, + onChange: value => console.log('Brush size changed', value), + }, + { + name: 'Mode', + type: 'radio', + value: 'Circle', + values: [ + { value: 'Circle', label: 'Circle' }, + { value: 'Sphere', label: 'Sphere' }, + { value: 'Rectangle', label: 'Rectangle' }, + ], + onChange: value => console.log('Brush mode changed', value), + }, + ], + }, + { + name: 'Eraser', + icon: 'icon-tool-eraser', + onClick: () => console.log('eraser clicked'), + options: [ + { + name: 'Mode', + type: 'radio', + value: 'EraserSphere', + values: [ + { value: 'EraserCircle', label: 'Circle' }, + { value: 'EraserSphere', label: 'Sphere' }, + ], + onChange: value => console.log('Brush mode changed', value), + }, + ], + }, + { + name: 'Threshold', + icon: 'icon-tool-threshold', + active: true, + onClick: () => console.log('eraser clicked'), + options: [ + { + name: 'Radius (mm)', + type: 'range', + min: 1, + max: 10, + value: 5, + step: 1, + onChange: value => console.log('Brush size changed', value), + }, + { + name: 'Mode', + type: 'radio', + value: 'Circle', + values: [ + { value: 'Circle', label: 'Circle' }, + { value: 'Sphere', label: 'Sphere' }, + { value: 'Rectangle', label: 'Rectangle' }, + ], + onChange: value => console.log('Brush mode changed', value), + }, + { + name: 'custom', + type: 'custom', + children: () => { + return ( +
+
Custom
+ +
+ ); + }, + }, + ], + }, + ], + }} + > + {advancedToolboxTemplate.bind({})} +
+
+ +## Props + + + +## Usage + +## Contribute + +
diff --git a/platform/ui/src/components/AdvancedToolbox/index.js b/platform/ui/src/components/AdvancedToolbox/index.js new file mode 100644 index 0000000..450d4f5 --- /dev/null +++ b/platform/ui/src/components/AdvancedToolbox/index.js @@ -0,0 +1,4 @@ +import AdvancedToolbox from './AdvancedToolbox'; +import ToolSettings from './ToolSettings'; +export default AdvancedToolbox; +export { ToolSettings }; diff --git a/platform/ui/src/components/AllInOneMenu/BackItem.tsx b/platform/ui/src/components/AllInOneMenu/BackItem.tsx new file mode 100644 index 0000000..e8c9f16 --- /dev/null +++ b/platform/ui/src/components/AllInOneMenu/BackItem.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import { Icons } from '@ohif/ui-next'; +import DividerItem from './DividerItem'; + +type BackItemProps = { + backLabel?: string; + onBackClick: () => void; +}; + +const BackItem = ({ backLabel, onBackClick }: BackItemProps) => { + return ( + <> +
+ + +
{backLabel || 'Back to Display Options'}
+
+ + + ); +}; + +export default BackItem; diff --git a/platform/ui/src/components/AllInOneMenu/DividerItem.tsx b/platform/ui/src/components/AllInOneMenu/DividerItem.tsx new file mode 100644 index 0000000..056d68e --- /dev/null +++ b/platform/ui/src/components/AllInOneMenu/DividerItem.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const DividerItem = () => { + return ( +
+
+
+ ); +}; + +export default DividerItem; diff --git a/platform/ui/src/components/AllInOneMenu/HeaderItem.tsx b/platform/ui/src/components/AllInOneMenu/HeaderItem.tsx new file mode 100644 index 0000000..43ba183 --- /dev/null +++ b/platform/ui/src/components/AllInOneMenu/HeaderItem.tsx @@ -0,0 +1,13 @@ +import React, { ReactNode } from 'react'; + +type HeaderItemProps = { + children: ReactNode; +}; + +const HeaderItem = ({ children }: HeaderItemProps) => { + return ( +
{children}
+ ); +}; + +export default HeaderItem; diff --git a/platform/ui/src/components/AllInOneMenu/IconMenu.tsx b/platform/ui/src/components/AllInOneMenu/IconMenu.tsx new file mode 100644 index 0000000..b166239 --- /dev/null +++ b/platform/ui/src/components/AllInOneMenu/IconMenu.tsx @@ -0,0 +1,83 @@ +import React, { useCallback, useState } from 'react'; +import OutsideClickHandler from 'react-outside-click-handler'; +import { MenuProps } from './Menu'; +import classNames from 'classnames'; +import { AllInOneMenu } from '..'; +import { Icons } from '@ohif/ui-next'; + +export interface IconMenuProps extends MenuProps { + icon: string; + iconClassName?: string; + horizontalDirection?: AllInOneMenu.HorizontalDirection; + verticalDirection?: AllInOneMenu.VerticalDirection; + menuKey?: number | string; +} + +/** + * An IconMenu allows for a div wrapped icon to be clicked to show and hide + * an AllInOneMenu.Menu. Based on the direction(s) specified, the menu is + * positioned relative to the icon. + * + * HorizontalDirection.LeftToRight - the left edges of the icon and menu are aligned + * HorizontalDirection.RightRoLeft - the right edges of the icon and menu are aligned + * VerticalDirection.TopToBottom - the top edge of the menu appears directly below the bottom edge of the icon + * VerticalDirection.BottomToTop - the bottom edge of the menu appears directly above the top edge of the icon + * + * For example, if an IconMenu were situated in the bottom-left corner of a container, + * it would be best to use BottomToTop and LeftToRight directions for it. + */ +export default function IconMenu({ + icon, + iconClassName, + horizontalDirection, + verticalDirection, + children, + backLabel, + menuClassName, + menuStyle, + onVisibilityChange, + menuKey, +}: IconMenuProps) { + const [isMenuVisible, setIsMenuVisible] = useState(false); + + const toggleMenuVisibility = useCallback(() => setIsMenuVisible(isVisible => !isVisible), []); + + return ( + +
+
+ +
+ { + setIsMenuVisible(isVis); + onVisibilityChange?.(isVis); + }} + horizontalDirection={horizontalDirection} + > + {children} + +
+
+ ); +} diff --git a/platform/ui/src/components/AllInOneMenu/Item.tsx b/platform/ui/src/components/AllInOneMenu/Item.tsx new file mode 100644 index 0000000..7ad4511 --- /dev/null +++ b/platform/ui/src/components/AllInOneMenu/Item.tsx @@ -0,0 +1,45 @@ +import React, { ReactNode, useCallback, useContext } from 'react'; +import { MenuContext } from './Menu'; + +type ItemProps = { + label: string; + secondaryLabel?: string; + icon?: ReactNode; + onClick?: () => void; + onMouseEnter?: () => void; + onMouseLeave?: () => void; + rightIcon?: ReactNode; +}; + +const Item = ({ + label, + secondaryLabel, + icon, + rightIcon, + onClick, + onMouseEnter, + onMouseLeave, +}: ItemProps) => { + const { hideMenu } = useContext(MenuContext); + + const onClickHandler = useCallback(() => { + hideMenu(); + onClick?.(); + }, [hideMenu, onClick]); + + return ( +
+ {icon &&
{icon}
} + {label} + {secondaryLabel != null && {secondaryLabel}} + {rightIcon &&
{rightIcon}
} +
+ ); +}; + +export default Item; diff --git a/platform/ui/src/components/AllInOneMenu/ItemPanel.tsx b/platform/ui/src/components/AllInOneMenu/ItemPanel.tsx new file mode 100644 index 0000000..abe407c --- /dev/null +++ b/platform/ui/src/components/AllInOneMenu/ItemPanel.tsx @@ -0,0 +1,29 @@ +import React, { ReactNode, useContext, useEffect } from 'react'; +import { MenuContext } from './Menu'; + +type ItemPanelProps = { + label?: string; + index?: number; + children: ReactNode; +}; + +const ItemPanel = ({ label, index = 0, children }: ItemPanelProps) => { + const { addItemPanel, activePanelIndex } = useContext(MenuContext); + + useEffect(() => { + addItemPanel(index, label); + }, []); + + return ( + activePanelIndex === index && ( +
+ {children} +
+ ) + ); +}; + +export default ItemPanel; diff --git a/platform/ui/src/components/AllInOneMenu/Menu.tsx b/platform/ui/src/components/AllInOneMenu/Menu.tsx new file mode 100644 index 0000000..7e50649 --- /dev/null +++ b/platform/ui/src/components/AllInOneMenu/Menu.tsx @@ -0,0 +1,188 @@ +import React, { createContext, ReactNode, useCallback, useEffect, useState } from 'react'; + +import './allInOneMenu.css'; +import DividerItem from './DividerItem'; +import PanelSelector from './PanelSelector'; +import classNames from 'classnames'; +import BackItem from './BackItem'; + +/** + * The vertical direction that the menu will be opened/used with. + * + * A TopToBottom menu would be used for cases where the menu is opened "near" + * the top edge of its container. Likewise a BottomToTop menu would be used + * for cases where the menu is opened "near" the bottom edge of its container. + * + * See IconMenu for more information. + */ +export enum VerticalDirection { + TopToBottom, + BottomToTop, +} + +/** + * The horizontal direction that the menu is opened/used with. + * This direction dictates the general direction sub-menus and + * back-to-menus are opened with. For example, a RightToLeft menu + * will have sub-menu items indicated with a left pointing chevron + * and aligned with the left edge of the menu. Similarly back-to items of a + * RightToLeft menu are indicated with a right pointing chevron and + * aligned with the right edge of the menu. + * + * It is also worth noting that a LeftToRight menu would be used for + * cases where a menu is opened "near" the left edge of its container. + * Likewise, a RightToLeft menu would be used for cases where a menu is opened + * "near" the right edge of its container. + * + * See IconMenu for more information. + */ +export enum HorizontalDirection { + LeftToRight, + RightToLeft, +} + +export interface MenuProps { + menuStyle?: unknown; + menuClassName?: string; + isVisible?: boolean; + preventHideMenu?: boolean; + backLabel?: string; + headerComponent?: ReactNode; + showHeaderDivider?: boolean; + activePanelIndex?: number; + onVisibilityChange?: (isVisible: boolean) => void; + horizontalDirection?: HorizontalDirection; + children: ReactNode; +} +type MenuContextProps = { + showSubMenu: (subMenuProps: MenuProps) => void; + hideMenu: () => void; + addItemPanel: (index: number, label: string) => void; + horizontalDirection: HorizontalDirection; + activePanelIndex: number; +}; + +type MenuPathState = { + props: MenuProps; + activePanelIndex: number; +}; + +export const MenuContext = createContext(null); + +const Menu = (props: MenuProps) => { + const { + isVisible, + onVisibilityChange, + activePanelIndex, + preventHideMenu, + menuClassName, + menuStyle, + horizontalDirection = HorizontalDirection.LeftToRight, + } = props; + + const [isMenuVisible, setIsMenuVisible] = useState(isVisible); + + // The menuPath is an array consisting of this top Menu and every SubMenu + // that has been traversed/opened by the user with the last item in the array + // being the current (sub)menu that is currently visible. This allows for the previously + // viewed menus to be returned to via the Back button at the top of the menu. + const [menuPath, setMenuPath] = useState>([ + { props, activePanelIndex: activePanelIndex || 0 }, + ]); + const [itemPanelLabels, setItemPanelLabels] = useState>([]); + + const hideMenu = useCallback(() => { + if (preventHideMenu) { + return; + } + setMenuPath(path => [path[0]]); + setItemPanelLabels([]); + setIsMenuVisible(false); + onVisibilityChange?.(false); + }, [preventHideMenu, onVisibilityChange]); + + useEffect(() => { + if (isVisible) { + setIsMenuVisible(isVisible); + onVisibilityChange?.(isVisible); + } else { + hideMenu(); + } + }, [hideMenu, isVisible, onVisibilityChange]); + + const showSubMenu = useCallback((subMenuProps: MenuProps) => { + setMenuPath(path => { + return [ + ...path, + { props: subMenuProps, activePanelIndex: subMenuProps.activePanelIndex || 0 }, + ]; + }); + setItemPanelLabels([]); + }, []); + + const addItemPanel = useCallback((index, label) => { + setItemPanelLabels(labels => { + return [...labels.slice(0, index), label, ...labels.slice(index + 1, labels.length)]; + }); + }, []); + + const onActivePanelIndexChange = useCallback(index => { + setMenuPath(path => { + return [ + ...path.slice(0, path.length - 1), + { ...path[path.length - 1], activePanelIndex: index }, + ]; + }); + }, []); + + const onBackClick = useCallback(() => { + setMenuPath(path => [...path.slice(0, path.length - 1)]); + setItemPanelLabels([]); + }, []); + + const { props: currentMenuProps, activePanelIndex: currentMenuActivePanelIndex } = + menuPath[menuPath.length - 1]; + + return ( + <> + + {isMenuVisible && ( +
+ {menuPath.length > 1 && ( + + )} + {itemPanelLabels.length > 1 && ( + + )} + {currentMenuProps.headerComponent} + {currentMenuProps.showHeaderDivider && } + {currentMenuProps.children} +
+ )} +
+ + ); +}; + +export default Menu; diff --git a/platform/ui/src/components/AllInOneMenu/PanelSelector.tsx b/platform/ui/src/components/AllInOneMenu/PanelSelector.tsx new file mode 100644 index 0000000..c46207b --- /dev/null +++ b/platform/ui/src/components/AllInOneMenu/PanelSelector.tsx @@ -0,0 +1,31 @@ +import React, { ReactNode } from 'react'; +import { ButtonGroup } from '../../components'; + +type PanelSelectorProps = { + panelLabels: Array; + onActiveIndexChange: (index: number) => void; + activeIndex: number; +}; + +const PanelSelector = ({ panelLabels, onActiveIndexChange, activeIndex }: PanelSelectorProps) => { + const getButtons = () => { + return panelLabels.map((panelLabel, index) => { + return { + children: panelLabel, + key: index, + }; + }); + }; + + return ( +
+ +
+ ); +}; + +export default PanelSelector; diff --git a/platform/ui/src/components/AllInOneMenu/SubMenu.tsx b/platform/ui/src/components/AllInOneMenu/SubMenu.tsx new file mode 100644 index 0000000..b9628db --- /dev/null +++ b/platform/ui/src/components/AllInOneMenu/SubMenu.tsx @@ -0,0 +1,35 @@ +import React, { useCallback, useContext } from 'react'; +import { MenuContext, MenuProps } from './Menu'; +import { Icons } from '@ohif/ui-next'; +export interface SubMenuProps extends MenuProps { + itemLabel: string; + onClick?: () => void; + itemIcon?: string; +} + +const SubMenu = (props: SubMenuProps) => { + const { showSubMenu } = useContext(MenuContext); + + const onClickHandler = useCallback(() => { + showSubMenu(props); + props.onClick?.(); + }, [showSubMenu, props]); + + return ( +
+ {props.itemIcon && ( + + )} +
{props.itemLabel}
+ +
+ ); +}; + +export default SubMenu; diff --git a/platform/ui/src/components/AllInOneMenu/__stories__/allInOneMenu.stories.mdx b/platform/ui/src/components/AllInOneMenu/__stories__/allInOneMenu.stories.mdx new file mode 100644 index 0000000..a513dc6 --- /dev/null +++ b/platform/ui/src/components/AllInOneMenu/__stories__/allInOneMenu.stories.mdx @@ -0,0 +1,129 @@ +import { DividerItem, HeaderItem, Item, ItemPanel, Menu, SubMenu } from '..'; +import InputRange from '../../InputRange/index.js'; +import SwitchButton from '../../SwitchButton/index.js'; + +import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs'; + +export const argTypes = { + component: Menu, + title: 'Components/AllInOneMenu', +}; + + + +export const AllInOneMenuTemplate = args => ( +
+ + + console.info('Item 1 clicked.')} + > + +
+
Arbitrary item component:
+ {}} + > +
+ + console.info('Sub menu item clicked.')} + showHeaderDivider={true} + headerComponent={ +
+ +
+ } + > + + + + + + Header for scrolling list of items} + > + + + + + + + + + + +
+
+
+
+); + + + +- [Overview](#overview) +- [Props](#props) +- [Contribute](#contribute) + +## Overview + +AllInOneMenu is a component that renders a menu with various menu items, sub-menus and +sub-components. The particular feature of the AllInOneMenu is its ability to render a sub-menu by +replacing the parent menu on screen. It also provides the ability to return to the parent menu with +a back menu item from the sub-menu. Furthermore, each menu level can be split into item panes - that +is several panes of menu items that are switched in and out of view like a tabbed pane. + + + console.info(`The menu visibility: ${isVisible}`), + }} +> + + {AllInOneMenuTemplate.bind({})} + + + + +## Props + + + +## Usage + +## Contribute + +
diff --git a/platform/ui/src/components/AllInOneMenu/allInOneMenu.css b/platform/ui/src/components/AllInOneMenu/allInOneMenu.css new file mode 100644 index 0000000..ce8ea95 --- /dev/null +++ b/platform/ui/src/components/AllInOneMenu/allInOneMenu.css @@ -0,0 +1,11 @@ +.all-in-one-menu-item { + @apply h-8 px-2 text-[14px] w-full; + display: flex; + align-items: center; + flex-shrink: 0; + line-height: 18px; +} + +.all-in-one-menu-item-effects { + @apply cursor-pointer hover:bg-primary-dark hover:rounded; +} diff --git a/platform/ui/src/components/AllInOneMenu/index.tsx b/platform/ui/src/components/AllInOneMenu/index.tsx new file mode 100644 index 0000000..f0522cf --- /dev/null +++ b/platform/ui/src/components/AllInOneMenu/index.tsx @@ -0,0 +1,19 @@ +import Menu, { HorizontalDirection, VerticalDirection } from './Menu'; +import DividerItem from './DividerItem'; +import HeaderItem from './HeaderItem'; +import IconMenu from './IconMenu'; +import Item from './Item'; +import ItemPanel from './ItemPanel'; +import SubMenu from './SubMenu'; + +export { + Menu, + DividerItem, + HeaderItem, + IconMenu, + Item, + ItemPanel, + SubMenu, + HorizontalDirection, + VerticalDirection, +}; diff --git a/platform/ui/src/components/Button/Button.tsx b/platform/ui/src/components/Button/Button.tsx new file mode 100644 index 0000000..f3ba159 --- /dev/null +++ b/platform/ui/src/components/Button/Button.tsx @@ -0,0 +1,150 @@ +import React, { useRef } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import * as ButtonEnums from './ButtonEnums'; +import Tooltip from '../Tooltip/Tooltip'; + +const sizeClasses = { + [ButtonEnums.size.small]: 'h-[26px] text-[13px]', + [ButtonEnums.size.medium]: 'h-[32px] text-[14px]', +}; + +const layoutClasses = + 'box-content inline-flex flex-row items-center justify-center gap-[5px] justify center px-[10px] outline-none rounded'; + +const baseFontTextClasses = 'leading-[1.2] font-sans text-center whitespace-nowrap'; + +const fontTextClasses = { + [ButtonEnums.type.primary]: classnames(baseFontTextClasses, 'font-semibold'), + [ButtonEnums.type.secondary]: classnames(baseFontTextClasses, 'font-400'), +}; + +const baseEnabledEffectClasses = 'transition duration-300 ease-in-out focus:outline-none'; + +const enabledEffectClasses = { + [ButtonEnums.type.primary]: classnames( + baseEnabledEffectClasses, + 'hover:bg-customblue-80 active:bg-customblue-40' + ), + [ButtonEnums.type.secondary]: classnames( + baseEnabledEffectClasses, + 'hover:bg-customblue-50 active:bg-customblue-20' + ), +}; + +const baseEnabledClasses = 'text-white'; + +const enabledClasses = { + [ButtonEnums.type.primary]: classnames( + 'bg-primary-main', + baseEnabledClasses, + enabledEffectClasses[ButtonEnums.type.primary] + ), + [ButtonEnums.type.secondary]: classnames( + 'bg-customblue-30', + baseEnabledClasses, + enabledEffectClasses[ButtonEnums.type.secondary] + ), +}; + +const disabledClasses = 'bg-inputfield-placeholder text-common-light cursor-default'; + +const defaults = { + color: 'default', + disabled: false, + rounded: 'small', + size: ButtonEnums.size.medium, + type: ButtonEnums.type.primary, +}; + +const Button = ({ + children = '', + size = defaults.size, + disabled = defaults.disabled, + type = defaults.type, + startIcon: startIconProp, + endIcon: endIconProp, + name, + className, + onClick = () => {}, + dataCY, + startIconTooltip = null, + endIconTooltip = null, +}) => { + dataCY = dataCY || `${name}-btn`; + + const startIcon = startIconProp && ( + <> + {React.cloneElement(startIconProp, { + className: classnames('w-4 h-4 fill-current', startIconProp?.props?.className), + })} + + ); + + const endIcon = endIconProp && ( + <> + {React.cloneElement(endIconProp, { + className: classnames('w-4 h-4 fill-current', endIconProp?.props?.className), + })} + + ); + const buttonElement = useRef(null); + + const handleOnClick = e => { + buttonElement.current.blur(); + if (!disabled) { + onClick(e); + } + }; + + const finalClassName = classnames( + layoutClasses, + fontTextClasses[type], + disabled ? disabledClasses : enabledClasses[type], + sizeClasses[size], + children ? 'min-w-[32px]' : '', // minimum width for buttons with text; icon only button does NOT get a minimum width + className + ); + + return ( + + ); +}; + +Button.propTypes = { + /** What is inside the button, can be text or react component */ + children: PropTypes.node, + /** Callback to be called when the button is clicked */ + onClick: PropTypes.func.isRequired, + /** Button size */ + size: PropTypes.oneOf([ButtonEnums.size.medium, ButtonEnums.size.small]), + /** Whether the button should be disabled */ + disabled: PropTypes.bool, + /** Button type */ + type: PropTypes.oneOf([ButtonEnums.type.primary, ButtonEnums.type.secondary]), + name: PropTypes.string, + /** Button start icon name - if any icon is specified */ + startIcon: PropTypes.node, + /** Button end icon name - if any icon is specified */ + endIcon: PropTypes.node, + /** Additional TailwindCSS classnames */ + className: PropTypes.string, + /** Tooltip for the start icon */ + startIconTooltip: PropTypes.node, + /** Tooltip for the end icon */ + endIconTooltip: PropTypes.node, + /** Data attribute for testing */ + dataCY: PropTypes.string, +}; + +export default Button; diff --git a/platform/ui/src/components/Button/ButtonEnums.ts b/platform/ui/src/components/Button/ButtonEnums.ts new file mode 100644 index 0000000..1fe82c0 --- /dev/null +++ b/platform/ui/src/components/Button/ButtonEnums.ts @@ -0,0 +1,15 @@ +enum type { + primary = 'primary', + secondary = 'secondary', +} +enum size { + medium = 'medium', + small = 'small', +} + +enum orientation { + horizontal = 'horizontal', + vertical = 'vertical', +} + +export { type, size, orientation }; diff --git a/platform/ui/src/components/Button/__stories__/button.stories.mdx b/platform/ui/src/components/Button/__stories__/button.stories.mdx new file mode 100644 index 0000000..a773741 --- /dev/null +++ b/platform/ui/src/components/Button/__stories__/button.stories.mdx @@ -0,0 +1,171 @@ +import { Button, ButtonEnums } from '../../../components'; +import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs'; +import { + createComponentTemplate, + createStoryMetaSettings, +} from '../../../storybook/functions/create-component-story'; + +export const argTypes = { + component: Button, + title: 'Components/Button', +}; + + + +export const buttonTemplate = createComponentTemplate(Button); + + + +- [Overview](#overview) +- [Props](#props) +- [Usage](#usage) +- [Contribute](#contribute) + +## Overview + +You can use the button component to create a button. It can be used in different ways, the default +button is a simple button with text. + + + + {buttonTemplate.bind({})} + + + +## Props + + + +## Usage + +### Types + +There can be different types of buttons: `primary`, and `secondary`. + + + +
+ + +
+
+
+ +### Sizes + +There are different sizes for the button: `small`, and `medium`. The size refers to the button's +height. + + + +
+ + +
+
+
+ +### Mixing props + +You can mix different props together to create a button. + + + + + + + +### Disabled + +You can disable the button by setting the `disabled` property to true. + + + + + + + +### Start/End Icons + +You can add an icon to the start of the button. It accepts an icon component. + + + + {() => { + // svg icon for github + const Github = () => { + return ( + + + + ); + }; + return ; + }} + + + +End Icon is the same as start icon, but for the end of the button. + + + + {() => { + // svg icon for github + const Github = () => { + return ( + + + + ); + }; + return ( + + ); + }} + + + +## Contribute + +
diff --git a/platform/ui/src/components/Button/index.js b/platform/ui/src/components/Button/index.js new file mode 100644 index 0000000..70c4390 --- /dev/null +++ b/platform/ui/src/components/Button/index.js @@ -0,0 +1,5 @@ +import Button from './Button'; +import * as ButtonEnums from './ButtonEnums'; + +export { ButtonEnums }; +export default Button; diff --git a/platform/ui/src/components/ButtonGroup/ButtonGroup.tsx b/platform/ui/src/components/ButtonGroup/ButtonGroup.tsx new file mode 100644 index 0000000..f7cc41e --- /dev/null +++ b/platform/ui/src/components/ButtonGroup/ButtonGroup.tsx @@ -0,0 +1,105 @@ +import React, { useState, useEffect, cloneElement, Children } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { ButtonEnums } from '../../components'; + +const ButtonGroup = ({ + children, + className, + orientation = ButtonEnums.orientation.horizontal, + activeIndex: defaultActiveIndex = 0, + onActiveIndexChange, + separated = false, + disabled = false, +}) => { + const [activeIndex, setActiveIndex] = useState(defaultActiveIndex); + + useEffect(() => { + setActiveIndex(defaultActiveIndex); + }, [defaultActiveIndex]); + + const handleButtonClick = index => { + setActiveIndex(index); + onActiveIndexChange && onActiveIndexChange(index); + }; + + const orientationClasses = { + horizontal: 'flex-row', + vertical: 'flex-col', + }; + + const wrapperClasses = classnames( + `${separated ? '' : 'inline-flex'}`, + orientationClasses[orientation], + className + ); + + return ( +
+ {!separated && ( +
+ {Children.map(children, (child, index) => { + if (React.isValidElement(child)) { + return cloneElement(child, { + key: index, + className: classnames( + 'rounded-[4px] px-2 py-1', + index === activeIndex + ? 'bg-customblue-40 text-white' + : 'text-primary-active bg-black', + child.props.className, + child.props.disabled ? 'ohif-disabled' : '' + ), + onClick: e => { + child.props.onClick && child.props.onClick(e); + handleButtonClick(index); + }, + }); + } + return child; + })} +
+ )} + {separated && ( +
+ {Children.map(children, (child, index) => { + if (React.isValidElement(child)) { + return cloneElement(child, { + key: index, + className: classnames( + 'rounded-[4px] px-2 py-1', + index === activeIndex + ? 'bg-customblue-40 text-white' + : 'text-primary-active bg-black border-secondary-light rounded-[5px] border', + child.props.className, + child.props.disabled ? 'ohif-disabled' : '' + ), + onClick: e => { + child.props.onClick && child.props.onClick(e); + handleButtonClick(index); + }, + }); + } + return child; + })} +
+ )} +
+ ); +}; + +ButtonGroup.propTypes = { + children: PropTypes.node.isRequired, + orientation: PropTypes.oneOf(Object.values(ButtonEnums.orientation)), + activeIndex: PropTypes.number, + onActiveIndexChange: PropTypes.func, + className: PropTypes.string, + disabled: PropTypes.bool, + separated: PropTypes.bool, +}; + +export default ButtonGroup; diff --git a/platform/ui/src/components/ButtonGroup/__stories__/buttonGroup.stories.mdx b/platform/ui/src/components/ButtonGroup/__stories__/buttonGroup.stories.mdx new file mode 100644 index 0000000..cb5f00d --- /dev/null +++ b/platform/ui/src/components/ButtonGroup/__stories__/buttonGroup.stories.mdx @@ -0,0 +1,55 @@ +import { Button, ButtonGroup, ButtonEnums } from '../../../components'; + +import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs'; +import { + createComponentTemplate, + createStoryMetaSettings, +} from '../../../storybook/functions/create-component-story'; + +export const argTypes = { + component: ButtonGroup, + title: 'Components/ButtonGroup', +}; + + + + + +- [Overview](#overview) +- [Props](#props) +- [Usage](#usage) +- [Contribute](#contribute) + +## Overview + +ButtonGroup is a container for a group of buttons. + + + + console.log(index)} + > + + + + + + + +## Props + + + +## Usage + +## Contribute + +
diff --git a/platform/ui/src/components/ButtonGroup/index.js b/platform/ui/src/components/ButtonGroup/index.js new file mode 100644 index 0000000..14bddfe --- /dev/null +++ b/platform/ui/src/components/ButtonGroup/index.js @@ -0,0 +1,2 @@ +import ButtonGroup from './ButtonGroup'; +export default ButtonGroup; diff --git a/platform/ui/src/components/CheckBox/CheckBox.tsx b/platform/ui/src/components/CheckBox/CheckBox.tsx new file mode 100644 index 0000000..78ce843 --- /dev/null +++ b/platform/ui/src/components/CheckBox/CheckBox.tsx @@ -0,0 +1,59 @@ +import React, { useState, useCallback } from 'react'; +import PropTypes from 'prop-types'; +import { Typography } from '../../'; +import { Icons } from '@ohif/ui-next'; + +/** + * REACT CheckBox component + * it has two props, checked and onChange + * checked is a boolean value + * onChange is a function that will be called when the checkbox is clicked + * + * CheckBox is a component that allows you to use as a boolean + */ + +const CheckBox: React.FC<{ + checked: boolean; + onChange: (state) => void; + className?: string; + label: string; + labelClassName?: string; + labelVariant?: string; +}> = ({ checked, onChange, label, labelClassName, labelVariant = 'body', className }) => { + const [isChecked, setIsChecked] = useState(checked); + + const handleClick = useCallback(() => { + setIsChecked(!isChecked); + onChange(!isChecked); + }, [isChecked, onChange]); + + return ( +
+ {isChecked ? ( + + ) : ( + + )} + + {label} + +
+ ); +}; + +CheckBox.propTypes = { + checked: PropTypes.bool, + onChange: PropTypes.func, + label: PropTypes.string, + labelClassName: PropTypes.string, + labelVariant: PropTypes.string, +}; + +export default CheckBox; diff --git a/platform/ui/src/components/CheckBox/__stories__/checkBox.stories.mdx b/platform/ui/src/components/CheckBox/__stories__/checkBox.stories.mdx new file mode 100644 index 0000000..ef4162c --- /dev/null +++ b/platform/ui/src/components/CheckBox/__stories__/checkBox.stories.mdx @@ -0,0 +1,49 @@ +import CheckBox from '../CheckBox'; +import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs'; + +export const argTypes = { + component: CheckBox, + title: 'Components/CheckBox', +}; + + + +export const CheckBoxTemplate = args => ; + + + +- [Overview](#overview) +- [Props](#props) +- [Contribute](#contribute) + +## Overview + +CheckBox is a component that allows you to use as a boolean value + + + console.log('on change callback'), + label: 'checkBoxLabel', + labelClassName: 'text-black text-sm', + }} + > + {CheckBoxTemplate.bind({})} + + + +## Props + + + +## Contribute + +
diff --git a/platform/ui/src/components/CheckBox/index.js b/platform/ui/src/components/CheckBox/index.js new file mode 100644 index 0000000..33b5f5f --- /dev/null +++ b/platform/ui/src/components/CheckBox/index.js @@ -0,0 +1,3 @@ +import CheckBox from './CheckBox'; + +export default CheckBox; diff --git a/platform/ui/src/components/CinePlayer/CinePlayer.css b/platform/ui/src/components/CinePlayer/CinePlayer.css new file mode 100644 index 0000000..ffa0a52 --- /dev/null +++ b/platform/ui/src/components/CinePlayer/CinePlayer.css @@ -0,0 +1,3 @@ +.cine-fps-range-tooltip .tooltip.tooltip-top { + bottom: 85% !important; +} diff --git a/platform/ui/src/components/CinePlayer/CinePlayer.tsx b/platform/ui/src/components/CinePlayer/CinePlayer.tsx new file mode 100644 index 0000000..7993934 --- /dev/null +++ b/platform/ui/src/components/CinePlayer/CinePlayer.tsx @@ -0,0 +1,189 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import PropTypes from 'prop-types'; +import debounce from 'lodash.debounce'; + +import Tooltip from '../Tooltip'; +import InputRange from '../InputRange'; +import { Icons } from '@ohif/ui-next'; +import './CinePlayer.css'; + +export type CinePlayerProps = { + className: string; + isPlaying: boolean; + minFrameRate?: number; + maxFrameRate?: number; + stepFrameRate?: number; + frameRate?: number; + onFrameRateChange: (value: number) => void; + onPlayPauseChange: (value: boolean) => void; + onClose: () => void; + updateDynamicInfo?: () => void; + dynamicInfo?: { + dimensionGroupNumber: number; + numDimensionGroups: number; + label?: string; + }; +}; + +const fpsButtonClassNames = + 'cursor-pointer text-primary-active active:text-primary-light hover:bg-customblue-300 w-4 flex items-center justify-center'; + +const CinePlayer: React.FC = ({ + className, + isPlaying = false, + minFrameRate = 1, + maxFrameRate = 90, + stepFrameRate = 1, + frameRate: defaultFrameRate = 24, + onFrameRateChange = () => {}, + onPlayPauseChange = () => {}, + onClose = () => {}, + dynamicInfo = {}, + updateDynamicInfo, +}) => { + const isDynamic = !!dynamicInfo?.numDimensionGroups; + const [frameRate, setFrameRate] = useState(defaultFrameRate); + const debouncedSetFrameRate = useCallback(debounce(onFrameRateChange, 100), [onFrameRateChange]); + + const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play'); + + const handleSetFrameRate = (frameRate: number) => { + if (frameRate < minFrameRate || frameRate > maxFrameRate) { + return; + } + setFrameRate(frameRate); + debouncedSetFrameRate(frameRate); + }; + + useEffect(() => { + setFrameRate(defaultFrameRate); + }, [defaultFrameRate]); + + const handleDimensionGroupNumberChange = useCallback( + (newGroupNumber: number) => { + if (isDynamic && dynamicInfo) { + // Here, you would update the component's state or context that controls the current time point index + // For demonstration, assuming a hypothetical function that updates the time point index + updateDynamicInfo({ + ...dynamicInfo, + dimensionGroupNumber: newGroupNumber, + }); + } + }, + [isDynamic, dynamicInfo] + ); + + return ( +
+ {isDynamic && dynamicInfo && ( + + )} +
+ onPlayPauseChange(!isPlaying)} + data-cy={'cine-player-play-pause'} + /> + {isDynamic && dynamicInfo && ( +
+ {/* Add Tailwind classes for monospace font and center alignment */} +
+ {dynamicInfo.dimensionGroupNumber}{' '} + {`/${dynamicInfo.numDimensionGroups}`} +
+
{dynamicInfo.label}
+
+ )} + +
+
handleSetFrameRate(frameRate - 1)} + data-cy={'cine-player-left-arrow'} + > + +
+ + } + > +
+
+ {`${frameRate} `} + {' FPS'} +
+
+
+ +
handleSetFrameRate(frameRate + 1)} + data-cy={'cine-player-right-arrow'} + > + +
+
+ +
+
+ ); +}; + +CinePlayer.propTypes = { + /** Minimum value for range slider */ + minFrameRate: PropTypes.number, + /** Maximum value for range slider */ + maxFrameRate: PropTypes.number, + /** Increment range slider can "step" in either direction */ + stepFrameRate: PropTypes.number, + frameRate: PropTypes.number, + /** 'true' if playing, 'false' if paused */ + isPlaying: PropTypes.bool.isRequired, + onPlayPauseChange: PropTypes.func, + onFrameRateChange: PropTypes.func, + onClose: PropTypes.func, + isDynamic: PropTypes.bool, + dynamicInfo: PropTypes.shape({ + dimensionGroupNumber: PropTypes.number, + numDimensionGroups: PropTypes.number, + label: PropTypes.string, + }), +}; + +export default CinePlayer; diff --git a/platform/ui/src/components/CinePlayer/__stories__/cinePlayer.stories.mdx b/platform/ui/src/components/CinePlayer/__stories__/cinePlayer.stories.mdx new file mode 100644 index 0000000..9f2ea52 --- /dev/null +++ b/platform/ui/src/components/CinePlayer/__stories__/cinePlayer.stories.mdx @@ -0,0 +1,57 @@ +import { CinePlayer } from '../../../components'; +import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs'; + +export const argTypes = { + component: CinePlayer, + title: 'Components/CinePlayer', +}; + + + +export const CinePlayerTemplate = args => ( +
+
+ +
+
+); + + + +- [Overview](#overview) +- [Props](#props) +- [Contribute](#contribute) + +## Overview + +CinePlayer is a component that allows you to use as a boolean value + + + + {CinePlayerTemplate.bind({})} + + + +## Props + + + +## Contribute + +
diff --git a/platform/ui/src/components/CinePlayer/index.js b/platform/ui/src/components/CinePlayer/index.js new file mode 100644 index 0000000..3a0fa6f --- /dev/null +++ b/platform/ui/src/components/CinePlayer/index.js @@ -0,0 +1,2 @@ +import CinePlayer from './CinePlayer'; +export default CinePlayer; diff --git a/platform/ui/src/components/ContextMenu/ContextMenu.tsx b/platform/ui/src/components/ContextMenu/ContextMenu.tsx new file mode 100644 index 0000000..dcacb92 --- /dev/null +++ b/platform/ui/src/components/ContextMenu/ContextMenu.tsx @@ -0,0 +1,69 @@ +import React, { useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import Typography from '../Typography'; +import { Icons } from '@ohif/ui-next'; + +const ContextMenu = ({ items, ...props }) => { + const contextMenuRef = useRef(null); + useEffect(() => { + if (!contextMenuRef?.current) { + return; + } + + const contextMenu = contextMenuRef.current; + + const boundingClientRect = contextMenu.getBoundingClientRect(); + if (boundingClientRect.bottom > window.innerHeight) { + props.defaultPosition.y = props.defaultPosition.y - boundingClientRect.height; + } + if (boundingClientRect.right > window.innerWidth) { + props.defaultPosition.x = props.defaultPosition.x - boundingClientRect.width; + } + }, [props.defaultPosition]); + + if (!items) { + return null; + } + + return ( +
e.preventDefault()} + > + {items.map((item, index) => ( +
item.action(item, props)} + style={{ justifyContent: 'space-between' }} + className="hover:bg-primary-dark border-primary-dark flex cursor-pointer items-center border-b px-4 py-3 transition duration-300 last:border-b-0" + > + {item.label} + {item.iconRight && ( + + )} +
+ ))} +
+ ); +}; + +ContextMenu.propTypes = { + defaultPosition: PropTypes.shape({ + x: PropTypes.number, + y: PropTypes.number, + }), + items: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string.isRequired, + action: PropTypes.func.isRequired, + }) + ), +}; + +export default ContextMenu; diff --git a/platform/ui/src/components/ContextMenu/__stories__/contextMenu.stories.mdx b/platform/ui/src/components/ContextMenu/__stories__/contextMenu.stories.mdx new file mode 100644 index 0000000..93a92c4 --- /dev/null +++ b/platform/ui/src/components/ContextMenu/__stories__/contextMenu.stories.mdx @@ -0,0 +1,65 @@ +import ContextMenu from '../ContextMenu'; +import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs'; +import { createComponentTemplate } from '../../../storybook/functions/create-component-story'; + +export const argTypes = { + component: ContextMenu, + title: 'Modals/ContextMenu', +}; + + + +export const contextMenueTemplate = createComponentTemplate(ContextMenu); + + + +- [Overview](#overview) +- [Props](#props) +- [Contribute](#contribute) + +## Overview + +Context Menu is a component that is used to display a list of options to the user. This component +can be used for use cases such as opening a list of options on user right click. + + + { + window.alert(`${item.label} clicked`); + }, + value: {}, + }, + { + label: 'Add Label', + actionType: 'setLabel', + action: item => { + window.alert(`${item.label} clicked`); + }, + value: {}, + }, + ], + }} + > + {contextMenueTemplate.bind({})} + + + +## Props + + + +## Contribute + +
diff --git a/platform/ui/src/components/ContextMenu/index.ts b/platform/ui/src/components/ContextMenu/index.ts new file mode 100644 index 0000000..1bf5f07 --- /dev/null +++ b/platform/ui/src/components/ContextMenu/index.ts @@ -0,0 +1 @@ +export { default } from './ContextMenu'; diff --git a/platform/ui/src/components/DateRange/DateRange.css b/platform/ui/src/components/DateRange/DateRange.css new file mode 100644 index 0000000..5bdc29d --- /dev/null +++ b/platform/ui/src/components/DateRange/DateRange.css @@ -0,0 +1,99 @@ +/** CONTAINER STYLES **/ + +.DateRangePickerInput { + @apply flex border-0 bg-transparent; +} + +.DateRangePicker_picker { + @apply -mt-1; +} + +/** INPUT DIV STYLES **/ + +.DateInput { + background: transparent; + @apply flex w-auto flex-1; +} + +/** INPUT FIELD COMMON STYLES **/ + +.DateInput_input { + /* used data:image as background-image because svg import with relative url didn't work */ + background-image: url('data:image/svg+xml;utf8,'); + + background-size: 14px; + background-position: 10px center; + @apply bg-no-repeat; +} +.DateInput_input { + @apply border-primary-main mt-2 w-full cursor-pointer appearance-none rounded border-t border-l border-r border-b border-solid bg-black py-2 px-3 pl-8 text-sm font-light leading-tight text-white shadow transition duration-300; +} +.DateInput_input:hover { + @apply border-gray-500; +} +.DateInput_input:focus { + @apply border-gray-500 outline-none; +} +/** FIRST INPUT STYLES **/ +.DateInput:first-child .DateInput_input { + @apply rounded-r-none; +} + +.DateInput:first-child .DateInput_input:hover, +.DateInput:first-child .DateInput_input:focus { + @apply relative z-10; +} + +/** SECOND INPUT STYLES **/ +.DateInput:last-child .DateInput_input { + @apply rounded-l-none; + margin-left: -1px; +} +/** ARROW STYLES **/ +.DateRangePickerInput_arrow { + @apply hidden; +} + +/* SELECT MONTH PICKER */ +.DateRangePicker_select { + @apply text-secondary-active border-common-dark cursor-pointer appearance-none rounded border bg-white py-1 pl-2 pr-5 text-base; /* NEEDED FOR ARROW DOWN */ + background-image: linear-gradient(45deg, transparent 50%, gray 50%), + linear-gradient(135deg, gray 50%, transparent 50%); + background-position: + calc(100% - 11px) 11px, + calc(100% - 6px) calc(11px); + background-size: + 5px 5px, + 5px 5px; + background-repeat: no-repeat; +} +/* CALENDAR DAYS */ +.CalendarDay { + @apply rounded-full border-0; +} + +.CalendarDay:hover, +.CalendarDay__selected, +.CalendarDay__selected:active, +.CalendarDay__selected:hover { + @apply bg-primary-main border-primary-main border-0 text-white; +} + +.CalendarDay__blocked_out_of_range:hover { + @apply text-common-dark cursor-not-allowed border-0 bg-white; +} + +.CalendarDay__selected_span { + @apply bg-primary-light border-0; +} + +/* MONTH NAVIGATION BUTTONS */ +.DayPickerNavigation_button__horizontalDefault, +.DayPickerNavigation_button__horizontalDefault:hover { + @apply border-common-dark text-common-dark; + top: 24px; + padding: 3px 9px; +} +.DayPickerNavigation_svg__horizontal { + @apply fill-current; +} diff --git a/platform/ui/src/components/DateRange/DateRange.tsx b/platform/ui/src/components/DateRange/DateRange.tsx new file mode 100644 index 0000000..bfca06a --- /dev/null +++ b/platform/ui/src/components/DateRange/DateRange.tsx @@ -0,0 +1,178 @@ +import React, { useState, useCallback } from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import { useTranslation } from 'react-i18next'; + +/** REACT DATES */ +import { DateRangePicker, isInclusivelyBeforeDay } from 'react-dates'; +import 'react-dates/initialize'; +import 'react-dates/lib/css/_datepicker.css'; +import './DateRange.css'; + +const renderYearsOptions = () => { + const currentYear = moment().year(); + const options = []; + + for (let i = 0; i < 20; i++) { + const year = currentYear - i; + options.push( + + ); + } + + return options; +}; + +const DateRange = props => { + const { id = '', onChange, startDate = null, endDate = null } = props; + const [focusedInput, setFocusedInput] = useState(null); + const renderYearsOptionsCallback = useCallback(renderYearsOptions, []); + const { t } = useTranslation('DatePicker'); + const today = moment(); + const lastWeek = moment().subtract(7, 'day'); + const lastMonth = moment().subtract(1, 'month'); + const studyDatePresets = [ + { + text: t('Today'), + start: today, + end: today, + }, + { + text: t('Last 7 days'), + start: lastWeek, + end: today, + }, + { + text: t('Last 30 days'), + start: lastMonth, + end: today, + }, + ]; + + const renderDatePresets = () => { + return ( +
+ {studyDatePresets.map(({ text, start, end }) => { + return ( + + ); + })} +
+ ); + }; + const renderMonthElement = ({ month, onMonthSelect, onYearSelect }) => { + renderMonthElement.propTypes = { + month: PropTypes.object, + onMonthSelect: PropTypes.func, + onYearSelect: PropTypes.func, + }; + + const handleMonthChange = event => { + onMonthSelect(month, event.target.value); + }; + + const handleYearChange = event => { + onYearSelect(month, event.target.value); + }; + + const handleOnBlur = () => {}; + + return ( +
+
+ +
+
+ +
+
+ ); + }; + + // Moment + const parsedStartDate = startDate ? moment(startDate, 'YYYYMMDD') : null; + const parsedEndDate = endDate ? moment(endDate, 'YYYYMMDD') : null; + + return ( + { + onChange({ + startDate: newStartDate ? newStartDate.format('YYYYMMDD') : undefined, + endDate: newEndDate ? newEndDate.format('YYYYMMDD') : undefined, + }); + }} + focusedInput={focusedInput} + onFocusChange={updatedVal => setFocusedInput(updatedVal)} + /** OPTIONAL */ + renderCalendarInfo={renderDatePresets} + renderMonthElement={renderMonthElement} + startDatePlaceholderText={t('Start Date')} + endDatePlaceholderText={t('End Date')} + phrases={{ + closeDatePicker: t('Close'), + clearDates: t('Clear dates'), + }} + isOutsideRange={day => !isInclusivelyBeforeDay(day, moment())} + hideKeyboardShortcutsPanel={true} + numberOfMonths={1} + showClearDates={false} + anchorDirection="left" + /> + ); +}; + +DateRange.propTypes = { + id: PropTypes.string, + /** YYYYMMDD (19921022) */ + startDate: PropTypes.string, + /** YYYYMMDD (19921022) */ + endDate: PropTypes.string, + /** Callback that received { startDate: string(YYYYMMDD), endDate: string(YYYYMMDD)} */ + onChange: PropTypes.func.isRequired, +}; + +export default DateRange; diff --git a/platform/ui/src/components/DateRange/__stories__/dateRange.stories.mdx b/platform/ui/src/components/DateRange/__stories__/dateRange.stories.mdx new file mode 100644 index 0000000..1db197a --- /dev/null +++ b/platform/ui/src/components/DateRange/__stories__/dateRange.stories.mdx @@ -0,0 +1,52 @@ +import DateRange from '../DateRange'; +import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs'; + +export const argTypes = { + component: DateRange, + title: 'Components/DateRange', +}; + + + +export const DateRangeTemplate = args => ( +
+ +
+); + + + +- [Overview](#overview) +- [Props](#props) +- [Contribute](#contribute) + +## Overview + +DateRange is a component that allows you to select a range of dates. + + + + {DateRangeTemplate.bind({})} + + + +## Props + + + +## Contribute + +
diff --git a/platform/ui/src/components/DateRange/index.js b/platform/ui/src/components/DateRange/index.js new file mode 100644 index 0000000..58cda0a --- /dev/null +++ b/platform/ui/src/components/DateRange/index.js @@ -0,0 +1,3 @@ +import DateRange from './DateRange'; + +export default DateRange; diff --git a/platform/ui/src/components/Dialog/Body.tsx b/platform/ui/src/components/Dialog/Body.tsx new file mode 100644 index 0000000..ae2865e --- /dev/null +++ b/platform/ui/src/components/Dialog/Body.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; + +import Typography from '../Typography'; + +const Body = ({ text, className }) => { + const theme = 'bg-primary-dark'; + return ( +
+ + {text} + +
+ ); +}; + +Body.propTypes = { + text: PropTypes.string, + className: PropTypes.string, +}; + +export default Body; diff --git a/platform/ui/src/components/Dialog/Dialog.tsx b/platform/ui/src/components/Dialog/Dialog.tsx new file mode 100644 index 0000000..faa2bd5 --- /dev/null +++ b/platform/ui/src/components/Dialog/Dialog.tsx @@ -0,0 +1,84 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import Footer from './Footer'; +import Body from './Body'; +import Header from './Header'; +import { useEffect } from 'react'; + +const Dialog = ({ + title, + text, + onClose, + noCloseButton, + actions, + onShow, + onSubmit, + header: HeaderComponent = Header, + body: BodyComponent = Body, + footer: FooterComponent = Footer, + value: defaultValue = {}, +}) => { + const [value, setValue] = useState(defaultValue); + + const theme = 'bg-primary-dark'; + const flex = 'flex flex-col'; + const border = 'border-0 rounded'; + const outline = 'outline-none focus:outline-none'; + const position = 'relative'; + const width = 'w-full'; + const padding = 'px-[20px] pb-[20px] pt-[13px]'; + + useEffect(() => { + if (onShow) { + onShow(); + } + }, [onShow]); + + return ( +
+ + + +
+ ); +}; + +Dialog.propTypes = { + title: PropTypes.string, + text: PropTypes.string, + onClose: PropTypes.func, + noCloseButton: PropTypes.bool, + header: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + body: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + footer: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + onSubmit: PropTypes.func.isRequired, + value: PropTypes.object, + actions: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + text: PropTypes.string.isRequired, + value: PropTypes.any, + type: PropTypes.oneOf(['primary', 'secondary', 'cancel']).isRequired, + }) + ).isRequired, + onShow: PropTypes.func, +}; + +export default Dialog; diff --git a/platform/ui/src/components/Dialog/Footer.tsx b/platform/ui/src/components/Dialog/Footer.tsx new file mode 100644 index 0000000..c369df9 --- /dev/null +++ b/platform/ui/src/components/Dialog/Footer.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; + +import Button, { ButtonEnums } from '../Button'; + +const Footer = ({ actions = [], className, onSubmit = () => {}, value }) => { + const flex = 'flex items-center justify-end'; + const padding = 'pt-[20px]'; + + return ( +
+ {actions?.map((action, index) => { + const isFirst = index === 0; + + const onClickHandler = event => onSubmit({ action, value, event }); + + return ( + + ); + })} +
+ ); +}; + +const noop = () => {}; + +Footer.propTypes = { + className: PropTypes.string, + onSubmit: PropTypes.func.isRequired, + actions: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + text: PropTypes.string.isRequired, + value: PropTypes.any, + type: PropTypes.oneOf([ButtonEnums.type.primary, ButtonEnums.type.secondary]).isRequired, + classes: PropTypes.arrayOf(PropTypes.string), + }) + ).isRequired, +}; + +export default Footer; diff --git a/platform/ui/src/components/Dialog/Header.tsx b/platform/ui/src/components/Dialog/Header.tsx new file mode 100644 index 0000000..fda7094 --- /dev/null +++ b/platform/ui/src/components/Dialog/Header.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; + +import Typography from '../Typography'; +import { Icons } from '@ohif/ui-next'; + +const CloseButton = ({ onClick }) => { + return ( + + ); +}; + +CloseButton.propTypes = { + onClick: PropTypes.func, +}; + +const Header = ({ title, noCloseButton = false, onClose }) => { + const theme = 'bg-primary-dark'; + const flex = 'flex items-center justify-between'; + const padding = 'pb-[20px]'; + + return ( +
+ + {title} + + {!noCloseButton && } +
+ ); +}; + +Header.propTypes = { + className: PropTypes.string, + title: PropTypes.string, + noCloseButton: PropTypes.bool, + onClose: PropTypes.func, +}; + +export default Header; diff --git a/platform/ui/src/components/Dialog/__stories__/dialog.stories.mdx b/platform/ui/src/components/Dialog/__stories__/dialog.stories.mdx new file mode 100644 index 0000000..65b2c19 --- /dev/null +++ b/platform/ui/src/components/Dialog/__stories__/dialog.stories.mdx @@ -0,0 +1,73 @@ +import Dialog from '../Dialog'; +import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs'; +import { createComponentTemplate } from '../../../storybook/functions/create-component-story'; + +export const argTypes = { + component: Dialog, + title: 'Modals/Dialog', +}; + + + +export const DialogTemplate = createComponentTemplate(Dialog); + + + +- [Overview](#overview) +- [Props](#props) +- [Usage](#usage) +- [Contribute](#contribute) + +## Overview + +Dialog is a modal dialog component which enabled to show a modal dialog. + + + {DialogTemplate.bind({})} + + +## Props + + + +## Usage + +It can be used to show a modal dialog to submit a form or show a message. + + + { + window.alert('Dialog closed'); + }, + noCloseButton: false, + actions: [ + { + id: 'cancel', + text: 'Cancel', + type: 'cancel', + }, + { + id: 'submit', + text: 'Submit', + type: 'primary', + }, + ], + }} + > + {DialogTemplate.bind({})} + + + +## Contribute + +
diff --git a/platform/ui/src/components/Dialog/index.js b/platform/ui/src/components/Dialog/index.js new file mode 100644 index 0000000..42ce789 --- /dev/null +++ b/platform/ui/src/components/Dialog/index.js @@ -0,0 +1,2 @@ +import Dialog from './Dialog'; +export default Dialog; diff --git a/platform/ui/src/components/DisplaySetMessageListTooltip/DisplaySetMessageListTooltip.tsx b/platform/ui/src/components/DisplaySetMessageListTooltip/DisplaySetMessageListTooltip.tsx new file mode 100644 index 0000000..3f1ed3c --- /dev/null +++ b/platform/ui/src/components/DisplaySetMessageListTooltip/DisplaySetMessageListTooltip.tsx @@ -0,0 +1,71 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import PortalTooltip from '../Tooltip/PortalTooltip'; +import { useTranslation } from 'react-i18next'; +import { Icons } from '@ohif/ui-next'; +/** + * Displays a tooltip with a list of messages of a displaySet + * @param param0 + * @returns + */ +const DisplaySetMessageListTooltip = ({ messages, id }): React.ReactNode => { + const { t } = useTranslation('Messages'); + const [isOpen, setIsOpen] = useState(false); + if (messages?.size()) { + return ( + <> + setIsOpen(true)} + onFocus={() => setIsOpen(true)} + onMouseOut={() => setIsOpen(false)} + onBlur={() => setIsOpen(false)} + name="status-alert-warning" + /> + +
+
+ {t('Display Set Messages')} +
+
    + {messages.messages.map((message, index) => ( +
  1. + {index + 1}. {t(message.id)} +
  2. + ))} +
+
+
+ + ); + } + return <>; +}; + +DisplaySetMessageListTooltip.propTypes = { + messages: PropTypes.object, +}; + +export default DisplaySetMessageListTooltip; diff --git a/platform/ui/src/components/DisplaySetMessageListTooltip/index.js b/platform/ui/src/components/DisplaySetMessageListTooltip/index.js new file mode 100644 index 0000000..f8e139c --- /dev/null +++ b/platform/ui/src/components/DisplaySetMessageListTooltip/index.js @@ -0,0 +1,2 @@ +import DisplaySetMessageListTooltip from './DisplaySetMessageListTooltip'; +export default DisplaySetMessageListTooltip; diff --git a/platform/ui/src/components/Dropdown/Dropdown.tsx b/platform/ui/src/components/Dropdown/Dropdown.tsx new file mode 100644 index 0000000..46a9584 --- /dev/null +++ b/platform/ui/src/components/Dropdown/Dropdown.tsx @@ -0,0 +1,221 @@ +import React, { useEffect, useCallback, useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import ReactDOM from 'react-dom'; + +import Typography from '../Typography'; +import { Icons } from '@ohif/ui-next'; + +const borderStyle = 'border-b last:border-b-0 border-secondary-main'; + +const Dropdown = ({ + id, + children, + showDropdownIcon = true, + list, + itemsClassName, + titleClassName, + showBorders = true, + alignment, + // By default the max characters per line is the longest title + // if you wish to override this, you can pass in a number + maxCharactersPerLine = 20, +}) => { + const [open, setOpen] = useState(false); + const elementRef = useRef(null); + const dropdownRef = useRef(null); + const [coords, setCoords] = useState({ x: 0, y: 0 }); + + // choose the max characters per line based on the longest title + const longestTitle = list.reduce((acc, item) => { + if (item.title.length > acc) { + return item.title.length; + } + return acc; + }, 0); + + maxCharactersPerLine = maxCharactersPerLine ?? longestTitle; + + const DropdownItem = useCallback( + ({ id, title, icon, onClick }) => { + // Split the title into lines of length maxCharactersPerLine + const lines = []; + for (let i = 0; i < title.length; i += maxCharactersPerLine) { + lines.push(title.substring(i, i + maxCharactersPerLine)); + } + + return ( +
{ + setOpen(false); + onClick(); + }} + data-cy={id} + > + {!!icon && ( + + )} +
+ {title.length > maxCharactersPerLine && ( +
+ {lines.map((line, index) => ( + + {line} + + ))} +
+ )} + {title.length <= maxCharactersPerLine && ( + {title} + )} +
+
+ ); + }, + [maxCharactersPerLine, itemsClassName, titleClassName, showBorders] + ); + + const renderTitleElement = () => { + return ( +
+ {children} + {showDropdownIcon && ( + + )} +
+ ); + }; + + const toggleList = () => { + setOpen(s => !s); + }; + + const handleClick = e => { + if (elementRef.current && !elementRef.current.contains(e.target)) { + setOpen(false); + } + }; + + useEffect(() => { + if (elementRef.current && dropdownRef.current) { + const triggerRect = elementRef.current.getBoundingClientRect(); + const dropdownRect = dropdownRef.current.getBoundingClientRect(); + let x, y; + + switch (alignment) { + case 'right': + x = triggerRect.right + window.scrollX - dropdownRect.width; + y = triggerRect.bottom + window.scrollY; + break; + case 'left': + x = triggerRect.left + window.scrollX; + y = triggerRect.bottom + window.scrollY; + break; + default: + x = triggerRect.left + window.scrollX; + y = triggerRect.bottom + window.scrollY; + break; + } + setCoords({ x, y }); + } + }, [open, alignment, elementRef.current, dropdownRef.current]); + + const renderList = () => { + const portalElement = document.getElementById('react-portal'); + + const listElement = ( +
+ {list.map((item, idx) => ( + + ))} +
+ ); + return ReactDOM.createPortal(listElement, portalElement); + }; + + useEffect(() => { + document.addEventListener('click', handleClick); + + if (!open) { + document.removeEventListener('click', handleClick); + } + }, [open]); + + return ( +
+
+ {renderTitleElement()} +
+ + {renderList()} +
+ ); +}; + +Dropdown.propTypes = { + id: PropTypes.string, + children: PropTypes.node.isRequired, + showDropdownIcon: PropTypes.bool, + titleClassName: PropTypes.string, + /** Items to render in the select's drop down */ + list: PropTypes.arrayOf( + PropTypes.shape({ + title: PropTypes.string.isRequired, + icon: PropTypes.string, + onClick: PropTypes.func.isRequired, + }) + ).isRequired, + alignment: PropTypes.oneOf(['left', 'right']), + maxCharactersPerLine: PropTypes.number, + showBorders: PropTypes.bool, +}; + +export default Dropdown; diff --git a/platform/ui/src/components/Dropdown/__stories__/dropdown.stories.mdx b/platform/ui/src/components/Dropdown/__stories__/dropdown.stories.mdx new file mode 100644 index 0000000..34a6824 --- /dev/null +++ b/platform/ui/src/components/Dropdown/__stories__/dropdown.stories.mdx @@ -0,0 +1,84 @@ +import Dropdown from '../Dropdown'; +import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs'; +import { createComponentTemplate } from '../../../storybook/functions/create-component-story'; + +export const argTypes = { + component: Dropdown, + title: 'Components/Dropdown', +}; + + + +export const DropdownTemplate = args => ( + // Todo: this should not set a background +
+ +
+); + + + +- [Overview](#overview) +- [Props](#props) +- [Usage](#usage) +- [Contribute](#contribute) + +## Overview + +Dropdown is a modal Dropdown component which enabled to show a modal Dropdown. + + + + {DropdownTemplate.bind({})} + + + +## Props + + + +## Usage + +You can list the items in the dropdown. + + + Drop Down
, + showDropdownIcon: true, + list: [ + { + title: 'Item 1', + icon: 'clipboard', + onClick: () => { + alert('Item 1 clicked'); + }, + }, + { + title: 'Item 2', + icon: 'tracked', + onClick: () => { + alert('Item 2 clicked'); + }, + }, + ], + }} + > + {DropdownTemplate.bind({})} + + + +## Contribute + +